Skip to content

Commit

Permalink
feat(remote-config)!: add support for onConfigUpdated (#10647)
Browse files Browse the repository at this point in the history
This PR is a breaking change for Remote Config since we're removing the ChangeNotifier mixin that came with FirebaseRemoteConfig. You should handle the state of the RemoteConfig using your own state provider.
  • Loading branch information
Lyokone committed Mar 28, 2023
1 parent 531ce04 commit f702869
Show file tree
Hide file tree
Showing 16 changed files with 375 additions and 113 deletions.
18 changes: 18 additions & 0 deletions docs/remote-config/_get-started.md
Expand Up @@ -133,6 +133,24 @@ smooth experience for your user, such as the next time that the user opens your
app. See [Remote Config loading strategies](/docs/remote-config/loading)
for more information and examples.

## Step 7: Listen for updates in real time
After you fetch parameter values, you can use real-time Remote Config to listen for updates from the Remote Config backend.
Real-time Remote Config signals to connected devices when updates are available and automatically
fetches the changes after you publish a new Remote Config version.

Please note that real-time Remote Config is not available for Web.

1. In your app, use `onConfigUpdated` to start listening for updates and automatically fetch any new parameter values. Implement the onUpdate() callback to activate the updated config.
```dart
remoteConfig.onConfigUpdated.listen((event) async {
await remoteConfig.activate();
// Use the new config values here.
});
```

2. The next time you publish a new version of your Remote Config, devices that are running your app and listening for changes will call the callback.

## Throttling {: #throttling }

If an app fetches too many times in a short time period, fetch calls will be
Expand Down
Expand Up @@ -6,36 +6,52 @@

import static io.flutter.plugins.firebase.core.FlutterFirebasePluginRegistry.registerPlugin;

import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.remoteconfig.ConfigUpdate;
import com.google.firebase.remoteconfig.ConfigUpdateListener;
import com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigClientException;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigFetchThrottledException;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigServerException;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue;
import io.flutter.Log;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.firebase.core.FlutterFirebasePlugin;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/** FirebaseRemoteConfigPlugin */
public class FirebaseRemoteConfigPlugin
implements FlutterFirebasePlugin, MethodChannel.MethodCallHandler, FlutterPlugin {
implements FlutterFirebasePlugin,
MethodChannel.MethodCallHandler,
FlutterPlugin,
EventChannel.StreamHandler {

static final String TAG = "FRCPlugin";
static final String METHOD_CHANNEL = "plugins.flutter.io/firebase_remote_config";
static final String EVENT_CHANNEL = "plugins.flutter.io/firebase_remote_config_updated";

private MethodChannel channel;

private final Map<String, ConfigUpdateListenerRegistration> listenersMap = new HashMap<>();
private EventChannel eventChannel;
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());

@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
setupChannel(binding.getBinaryMessenger());
Expand Down Expand Up @@ -101,11 +117,16 @@ private void setupChannel(BinaryMessenger messenger) {
registerPlugin(METHOD_CHANNEL, this);
channel = new MethodChannel(messenger, METHOD_CHANNEL);
channel.setMethodCallHandler(this);

eventChannel = new EventChannel(messenger, EVENT_CHANNEL);
eventChannel.setStreamHandler(this);
}

private void tearDownChannel() {
channel.setMethodCallHandler(null);
channel = null;
eventChannel.setStreamHandler(null);
eventChannel = null;
}

private FirebaseRemoteConfig getRemoteConfig(Map<String, Object> arguments) {
Expand Down Expand Up @@ -216,7 +237,8 @@ public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result re
private Map<String, Object> parseParameters(Map<String, FirebaseRemoteConfigValue> parameters) {
Map<String, Object> parsedParameters = new HashMap<>();
for (String key : parameters.keySet()) {
parsedParameters.put(key, createRemoteConfigValueMap(parameters.get(key)));
parsedParameters.put(
key, createRemoteConfigValueMap(Objects.requireNonNull(parameters.get(key))));
}
return parsedParameters;
}
Expand Down Expand Up @@ -254,4 +276,41 @@ private String mapValueSource(int source) {
return "static";
}
}

@SuppressWarnings("unchecked")
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
Map<String, Object> argumentsMap = (Map<String, Object>) arguments;
FirebaseRemoteConfig remoteConfig = getRemoteConfig(argumentsMap);
String appName = (String) Objects.requireNonNull(argumentsMap.get("appName"));

listenersMap.put(
appName,
remoteConfig.addOnConfigUpdateListener(
new ConfigUpdateListener() {
@Override
public void onUpdate(@NonNull ConfigUpdate configUpdate) {
ArrayList<String> updatedKeys = new ArrayList<>(configUpdate.getUpdatedKeys());
mainThreadHandler.post(() -> events.success(updatedKeys));
}

@Override
public void onError(@NonNull FirebaseRemoteConfigException error) {
events.error("firebase_remote_config", error.getMessage(), null);
}
}));
}

@SuppressWarnings("unchecked")
@Override
public void onCancel(Object arguments) {
Map<String, Object> argumentsMap = (Map<String, Object>) arguments;
String appName = (String) Objects.requireNonNull(argumentsMap.get("appName"));

ConfigUpdateListenerRegistration listener = listenersMap.get(appName);
if (listener != null) {
listener.remove();
listenersMap.remove(appName);
}
}
}
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -216,6 +216,7 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down Expand Up @@ -248,6 +249,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down
Expand Up @@ -47,5 +47,7 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,169 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
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),
),
);
await remoteConfig.setDefaults(<String, dynamic>{
'welcome': 'default welcome',
'hello': 'default hello',
});
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.getString('welcome')}';
} 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) {
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),
),
],
),
);
}
}

0 comments on commit f702869

Please sign in to comment.