Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Add Firebase Remote Config support #391

Merged
merged 22 commits into from
Apr 30, 2018
Merged

Conversation

kroikie
Copy link
Contributor

@kroikie kroikie commented Feb 27, 2018

Add plugin to support Firebase Remote Config.

Fixes flutter/flutter#9815

@kroikie
Copy link
Contributor Author

kroikie commented Feb 27, 2018

This plugin is incomplete, creating this PR for general design review. Will add tests and clean up formatting once design is finalized.

/// Gets the instance of RemoteConfig for the default Firebase app.
static RemoteConfig get instance => _instance;

Future<Map<String, dynamic>> fetch({int expiration: 43200}) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs dartdoc. It looks like this method activates in addition to fetching, and I'm not sure why that is. can they be separate methods to match the native API?

static const MethodChannel _channel =
const MethodChannel('firebase_remote_config');

Map<String, dynamic> _parameters;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name this _remoteValues or something

const MethodChannel('firebase_remote_config');

Map<String, dynamic> _parameters;
Map<String, dynamic> _defaults;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps _defaultValues to match the above

int getInt(String key) {
final dynamic value = _parameters[key];
if (value != null) {
final String strValue = UTF8.decode(value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit surprising but it works because when we ask for the value as a byte array on the native side we get a byte array containing a UTF8-encoded stringified int.

@@ -0,0 +1,10 @@
# firebase_remote_config
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should write a real README based on one of the other FlutterFire packages

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/** FirebaseRemoteConfigPlugin */
public class FirebaseRemoteConfigPlugin implements MethodCallHandler {

public static final String TAG = "FirebbaseRCPlugin";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo, and I would just use the class name probably

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

private static SharedPreferences sharedPreferences;
private final MethodChannel channel;

/** Plugin registration. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems unnecessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

new MethodChannel(registrar.messenger(), "plugins.flutter.io/firebase_remote_config");
channel.setMethodCallHandler(new FirebaseRemoteConfigPlugin(channel));
sharedPreferences =
registrar.context().getSharedPreferences("FirebaseRCPlugin", Context.MODE_PRIVATE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference is to move this string constant into a public static final and use the full class name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return valueMap;
}

private int mapLastFetchStatus(int status) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we're not using the same int mappings as iOS or Android, perhaps we should match one or the other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks they should be. Done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like these don't match either platform quite yet. After thinking about this some more I think should convert to String instead of int. For example "success" or "no_fetch_yet". It's slightly less efficient but way easier to follow because of the platform differences.

);
}

Future<void> setupRemoteConfig() async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this refreshRemoteConfig or something

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to rename, but there is a different method that is doing the fetching. This one is used to set the defaults and debug mode, feels like setup stuff. WDYT?

resultDict[@"IN_DEBUG_MODE"] =
[[NSNumber alloc] initWithBool:[firRemoteConfigSettings isDeveloperModeEnabled]];

result(resultDict);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also put the current remote config values in here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

mapLastFetchStatus(firebaseRemoteConfigInfo.getLastFetchStatus()));
if (!task.isSuccessful()) {
final Exception exception = task.getException();
channel.invokeMethod(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we are using invokeMethod to return to Dart. It seems like we could return these properties on the method invocation that called "RemoteConfig#fetch". This would make awaiting a fetch() in Dart actually wait for the fetch to happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was used to updated some RemoteConfig properties on the Dart side, however this can be done in handling of the exception so removed this call to invokeMethod.

/// LastFetchStatus defines the possible status values of the last fetch.
enum LastFetchStatus { success, failure, throttled, noFetchYet }

class FetchThrottledException implements Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My vote is to split each class into a separate file in src/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

RemoteConfig._() {
_channel.setMethodCallHandler((MethodCall call) async {
switch (call.method) {
case 'UpdateFetch':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you might be able to remove this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -0,0 +1 @@
TODO: Add your license here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the same license as other plugins

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


Initialize `RemoteConfig`:
```
final RemoteConfig _remoteConfig = await RemoteConfig.instance;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave off the _ for this variable since the context is somewhat ambiguous

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

You can now use the Firebase `_remoteConfig` to fetch remote configurations in your Dart code, e.g.
```
final defaults = <String, dynamic>{'welcome': 'default welcome'};
await _remoteConfig.setDefaults();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure you actually need to await here, but you definitely want to pass defaults as an argument

Copy link
Contributor Author

@kroikie kroikie Mar 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that fetch/activate retrieves default values from the native side I think it best to ensure that the setting of defaults completes before doing so. WDYT?

:) forgot the parameter, thanks!

final defaults = <String, dynamic>{'welcome': 'default welcome'};
await _remoteConfig.setDefaults();

await _remoteConfig.fetch(expiration: new Duration(hours: 5));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe const instead of new?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

case "RemoteConfig#fetch":
{
long expiration =
call.argument("expiration") instanceof Integer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would cast call.argument("expiration") as a Number and call its longValue() method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

_parameters[key] = remoteConfigValue;
}
});
return new Future<void>.value();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to just leave this off I think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
}
print('fetch succeeded');
return new Future<void>.value();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to just leave this off I think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

throw new Exception('Unable to fetch remote config');
}
}
print('fetch succeeded');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be printing here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

final int fetchThrottleEnd = e.details['FETCH_THROTTLED_END'];
throw new FetchThrottledException._(endTimeInMills: fetchThrottleEnd);
} else {
print('fetch failed unknown');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be printing here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

new DateTime.fromMillisecondsSinceEpoch(e.details[lastFetchTimeKey]);
_lastFetchStatus = LastFetchStatus.values[e.details[lastFetchStatusKey]];
if (e.code == RemoteConfig.fetchFailedThrottledKey) {
print('fetch failed throttled');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be printing here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

_remoteConfigSettings = remoteConfigSettings;
}

/// Fetches parameter values for your app. Parameter values may be from
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first sentence of Dartdoc documentation should be a standalone paragraph.

This applies to the other methods in this file as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/// Activates the fetched config. This makes fetched key-values take effect.
///
/// The returned Future contains true if the fetched config is different
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that a Future "completes" to true rather than "contains" true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
}

/// Gets the RemoteConfigValue corresponding to the key. If there is no
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

_lastFetchTime =
new DateTime.fromMillisecondsSinceEpoch(e.details[_lastFetchTimeKey]);
_lastFetchStatus = _parseLastFetchStatus(e.details[_lastFetchStatusKey]);
if (e.code == RemoteConfig._fetchFailedThrottledKey) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can leave off the RemoteConfig. here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

class FetchThrottledException implements Exception {
DateTime _throttleEnd;

FetchThrottledException._({int endTimeInMills = 43200}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why there's a default value of 43200 here. It seems like we are already specifying a value in the one place where this exception is constructed, so you can just remove this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, Done

RemoteConfigSettings _remoteConfigSettings;

RemoteConfig._() {
_channel.setMethodCallHandler((MethodCall call) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't doing anything any more and can probably be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

RemoteConfigSettings get remoteConfigSettings => _remoteConfigSettings;

/// Gets the instance of RemoteConfig for the default Firebase app.
static Future<RemoteConfig> get instance async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to make the RemoteConfig instance a singleton stored in a private static member. Otherwise you could have multiple RemoteConfig objects with different default values because they don't know how to synchronize their copies of _parameters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

parameterDict[key] = [self createRemoteConfigValueDict:[remoteConfig configValueForKey:key]];
}
// Add default parameters if missing since `keysWithPrefix` does not return default keys.
NSArray *defaultKeys = [[NSUserDefaults standardUserDefaults] arrayForKey:DEFAULT_KEYS];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the allKeysFromSource: API instead of saving these in NSUserDefaults

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


@implementation FirebaseRemoteConfigPlugin

static NSString *DEFAULT_KEYS = @"default_keys";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can get rid of this and use allKeysFromSource instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

FIRRemoteConfig *remoteConfig = [FIRRemoteConfig remoteConfig];
NSDictionary *defaults = call.arguments[@"defaults"];
[remoteConfig setDefaults:defaults];
[[NSUserDefaults standardUserDefaults] setValue:[defaults allKeys] forKey:DEFAULT_KEYS];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed using allKeysFromSource

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/// You can get an instance by calling [RemoteConfig.instance]. Note
/// [RemoteConfig.instance] is async.
class RemoteConfig extends ChangeNotifier {
static const MethodChannel _channel =
Copy link
Contributor

@collinjackson collinjackson Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other plugins make this public and @visibleForTesting to avoid duplicating the channel name in the test, perhaps we should do the same here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


RemoteConfigValue._(this._value, this._source);

ValueSource get source => _source == ValueSource.valueDefault
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a dartdoc comment here, e.g. "Indicates at which source this value came from."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/// Gets the instance of RemoteConfig for the default Firebase app.
static Future<RemoteConfig> get instance async {
if (_instance != null) {
Copy link
Contributor

@collinjackson collinjackson Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a race condition here where it's possible to have two instance objects outstanding if you read RemoteConfig.instance twice in a row before the first invokeMethod returns. Consider using a Completer to avoid this. (We should probably have a test to ensure that reading instance twice gives you the same singleton.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/// mode is disabled. When developer mode is enabled fetch throttling is
/// relaxed to allow many more fetch calls per hour to the remote server than
/// the 5 per hour that is enforced when developer mode is disabled.
bool debugMode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be final (readonly) to match the native SDKs (e.g. iOS)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

test('setConfigSettings', () async {
expect(remoteConfig.remoteConfigSettings.debugMode, true);
final RemoteConfigSettings remoteConfigSettings =
new RemoteConfigSettings();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the constructor to initialize debugMode

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

_instance._lastFetchStatus =
_parseLastFetchStatus(properties[_lastFetchStatusKey]);
final RemoteConfigSettings remoteConfigSettings =
new RemoteConfigSettings();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the constructor to initialize debugMode

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/// When set to true developer mode is enabled, when set to false developer
/// mode is disabled. When developer mode is enabled fetch throttling is
/// relaxed to allow many more fetch calls per hour to the remote server than
/// the 5 per hour that is enforced when developer mode is disabled.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first sentence of Dartdoc comments should be its own paragraph.

Consider adding a sentence at the end about what the default is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

static const bool defaultValueForBool = false;

static const String _fetchFailedThrottledKey = 'FETCH_FAILED_THROTTLED';
static const String _lastFetchTimeKey = 'LAST_FETCH_TIME';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should either use constants for all the keys (e.g. 'IN_DEBUG_MODE') or none of them. Also I think the keys should be lower case to match other first-party plugins.

Copy link
Contributor Author

@kroikie kroikie Apr 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, removed constants for keys, made keys lower case.

s.ios.deployment_target = '6.0'
s.dependency 'Flutter'
s.dependency 'Firebase/RemoteConfig'
s.pod_target_xcconfig = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Format has changed and this should be static_framework = true now, see 57f8977

Copy link
Contributor Author

@kroikie kroikie Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@implementation FirebaseRemoteConfigPlugin

static NSString *LAST_FETCH_TIME_KEY = @"LAST_FETCH_TIME";
static NSString *LAST_FETCH_STATUS_KEY = @"LAST_FETCH_STATUS";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use constants for all of the keys or none of them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, removed key constants.

- Used completer to avoid race conditions for config instance
- Removed key constants and switched keys to camel case
static const double defaultValueForDouble = 0.0;
static const bool defaultValueForBool = false;

static RemoteConfig _instance;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make this a local variable in _getRemoteConfigInstance() now that you have _instanceCompleter

@collinjackson collinjackson self-requested a review April 27, 2018 18:44
Copy link
Contributor

@collinjackson collinjackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm once the above comment is addressed

@ozexpert
Copy link

hi all. when is this coming?

@kroikie kroikie merged commit c0e9db5 into flutter:master Apr 30, 2018
haydenflinner pushed a commit to haydenflinner/plugins that referenced this pull request May 4, 2018
slightfoot pushed a commit to slightfoot/plugins that referenced this pull request Jun 5, 2018
julianscheel pushed a commit to jusst-engineering/plugins that referenced this pull request Mar 11, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
4 participants