flutterfire icon indicating copy to clipboard operation
flutterfire copied to clipboard

[Firebase_remote_config] not calling addOnConfigUpdateListener after network toggle

Open hnim3002 opened this issue 4 months ago • 4 comments

Is there an existing issue for this?

  • [x] I have searched the existing issues.

Which plugins are affected?

Remote Config

Which platforms are affected?

Android, iOS

Description

When the device network is toggled off and then back on, the Firebase Remote Config event stream stops delivering onConfigUpdated events to the app. This happens every time. The event stream eventually resumes by itself after about 3–5 minutes

Reproducing the issue

  1. Launch app on device
  2. Listener will start
  3. Update remote config on console. app will receive update event
  4. Turn off wifi on device.
  5. Update remote config on console.
  6. Turn wifi on.
  7. Many time app receive event after 3-5 minutes.
  8. But if it stop receiving event. even after changing config multiple times app didn't receive event.
  9. We have waited for 5-10 min as well, same result.

Firebase Core version

3.8.1

Flutter Version

3.29.3

Relevant Log Output


Flutter dependencies

Dart SDK 3.7.2 Flutter SDK 3.29.3 remote_config_test 1.0.0+1

dependencies:

  • cupertino_icons 1.0.8
  • firebase_core 3.15.2 [firebase_core_platform_interface firebase_core_web flutter meta]
  • firebase_remote_config 5.5.0 [firebase_core firebase_core_platform_interface firebase_remote_config_platform_interface firebase_remote_config_web flutter]
  • flutter 0.0.0 [characters collection material_color_utilities meta vector_math sky_engine]
  • flutter_bloc 8.1.6 [bloc flutter provider]

dev dependencies:

  • flutter_lints 5.0.0 [lints]
  • flutter_test 0.0.0 [flutter test_api matcher path fake_async clock stack_trace vector_math leak_tracker_flutter_testing async boolean_selector characters collection leak_tracker leak_tracker_testing material_color_utilities meta source_span stream_channel string_scanner term_glyph vm_service]

transitive dependencies:

  • _flutterfire_internals 1.3.59 [collection firebase_core firebase_core_platform_interface flutter meta]
  • async 2.12.0 [collection meta]
  • bloc 8.1.4 [meta]
  • boolean_selector 2.1.2 [source_span string_scanner]
  • characters 1.4.0
  • clock 1.1.2
  • collection 1.19.1
  • fake_async 1.3.2 [clock collection]
  • firebase_core_platform_interface 6.0.2 [collection flutter flutter_test meta plugin_platform_interface]
  • firebase_core_web 2.24.1 [firebase_core_platform_interface flutter flutter_web_plugins meta web]
  • firebase_remote_config_platform_interface 2.0.0 [_flutterfire_internals firebase_core flutter meta plugin_platform_interface]
  • firebase_remote_config_web 1.8.9 [_flutterfire_internals firebase_core firebase_core_web firebase_remote_config_platform_interface flutter flutter_web_plugins]
  • flutter_web_plugins 0.0.0 [flutter characters collection material_color_utilities meta vector_math]
  • leak_tracker 10.0.8 [clock collection meta path vm_service]
  • leak_tracker_flutter_testing 3.0.9 [flutter leak_tracker leak_tracker_testing matcher meta]
  • leak_tracker_testing 3.0.1 [leak_tracker matcher meta]
  • lints 5.1.1
  • matcher 0.12.17 [async meta stack_trace term_glyph test_api]
  • material_color_utilities 0.11.1 [collection]
  • meta 1.16.0
  • nested 1.0.0 [flutter]
  • path 1.9.1
  • plugin_platform_interface 2.1.8 [meta]
  • provider 6.1.5+1 [collection flutter nested]
  • sky_engine 0.0.0
  • source_span 1.10.1 [collection path term_glyph]
  • stack_trace 1.12.1 [path]
  • stream_channel 2.1.4 [async]
  • string_scanner 1.4.1 [source_span]
  • term_glyph 1.2.2
  • test_api 0.7.4 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph]
  • vector_math 2.1.4
  • vm_service 14.3.1
  • web 1.1.1

Additional context and comments

No response

hnim3002 avatar Dec 05 '25 04:12 hnim3002

This is my current code

import 'dart:async';

import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  StreamSubscription? subscription;
  RemoteConfigUpdate? update;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Remote Config Example')),
      body: Column(
        children: [
          _ButtonAndText(
            defaultText: 'Not initialized',
            buttonText: 'Initialize',
            onPressed: () async {
              final FirebaseRemoteConfig remoteConfig =
                  FirebaseRemoteConfig.instance;
              await remoteConfig.setConfigSettings(
                RemoteConfigSettings(
                  fetchTimeout: const Duration(seconds: 10),
                  minimumFetchInterval: const Duration(hours: 1),
                ),
              );
              RemoteConfigValue(null, ValueSource.valueStatic);
              return 'Initialized';
            },
          ),
          _ButtonAndText(
            defaultText: 'No data',
            onPressed: () async {
              try {
                final FirebaseRemoteConfig remoteConfig =
                    FirebaseRemoteConfig.instance;
                // Using zero duration to force fetching from remote server.
                await remoteConfig.setConfigSettings(
                  RemoteConfigSettings(
                    fetchTimeout: const Duration(seconds: 10),
                    minimumFetchInterval: Duration.zero,
                  ),
                );
                await remoteConfig.fetchAndActivate();
                return 'Fetched: ${remoteConfig.getBool('maintanceForUAT')}';
              } on PlatformException catch (exception) {
                // Fetch exception.
                print(exception);
                return 'Exception: $exception';
              } catch (exception) {
                print(exception);
                return 'Unable to fetch remote config. Cached or default values will be '
                    'used';
              }
            },
            buttonText: 'Fetch Activate',
          ),
          _ButtonAndText(
            defaultText:
                update != null
                    ? 'Updated keys: ${update?.updatedKeys}'
                    : 'No data',
            onPressed: () async {
              try {
                final FirebaseRemoteConfig remoteConfig =
                    FirebaseRemoteConfig.instance;
                if (subscription != null) {
                  await subscription!.cancel();
                  setState(() {
                    subscription = null;
                  });
                  return 'Listening cancelled';
                }
                setState(() {
                  subscription = remoteConfig.onConfigUpdated.listen((
                    event,
                  ) async {
                    // Make new values available to the app.
                    await remoteConfig.activate();
                    print(
                      'Remote config updated: ${remoteConfig.getBool('maintanceForUAT')}',
                    );

                    setState(() {
                      update = event;
                    });
                  });
                });

                return 'Listening, waiting for update...';
              } on PlatformException catch (exception) {
                // Fetch exception.
                print(exception);
                return 'Exception: $exception';
              } catch (exception) {
                print(exception);
                return 'Unable to listen to remote config. Cached or default values will be '
                    'used';
              }
            },
            buttonText: subscription != null ? 'Cancel' : 'Listen',
          ),
        ],
      ),
    );
  }
}

class _ButtonAndText extends StatefulWidget {
  const _ButtonAndText({
    Key? key,
    required this.defaultText,
    required this.onPressed,
    required this.buttonText,
  }) : super(key: key);

  final String defaultText;
  final String buttonText;
  final Future<String> Function() onPressed;

  @override
  State<_ButtonAndText> createState() => _ButtonAndTextState();
}

class _ButtonAndTextState extends State<_ButtonAndText> {
  String? _text;

  // Update text when widget is updated.
  @override
  void didUpdateWidget(covariant _ButtonAndText oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.defaultText != oldWidget.defaultText) {
      setState(() {
        _text = widget.defaultText;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Row(
        children: [
          Text(_text ?? widget.defaultText),
          const Spacer(),
          ElevatedButton(
            onPressed: () async {
              final result = await widget.onPressed();
              setState(() {
                _text = result;
              });
            },
            child: Text(widget.buttonText),
          ),
        ],
      ),
    );
  }
}

hnim3002 avatar Dec 05 '25 04:12 hnim3002

Judging from what you are saying, this looks like intended behaviour. Are you saying you expect the event streaming to continue much quicker after a network toggle?

MichaelVerdon avatar Dec 05 '25 13:12 MichaelVerdon

Hi @MichaelVerdon, based on further testing I found that the reconnection time can sometimes take more than 10 minutes. I’m not sure whether this is considered a bug or not, but for a feature that is intended to provide real-time updates, being disconnected for that long after simply toggling Wi-Fi while not being able to manually trigger a reconnect doesn’t seem like expected behavior to me. There’s also a similar reported issue here: firebase/firebase-ios-sdk#15490.

hnim3002 avatar Dec 05 '25 15:12 hnim3002

Hi @hnim3002, thanks for the additional insight. Since a similar issue has been reported on the native Firebase repository, this appears to be a native-side problem and will require an upstream fix.

SelaseKay avatar Dec 08 '25 08:12 SelaseKay