-
Notifications
You must be signed in to change notification settings - Fork 9.8k
Conversation
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 { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
ea90262
to
54cb972
Compare
@@ -0,0 +1,10 @@ | |||
# firebase_remote_config |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment seems unnecessary
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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/
There was a problem hiding this comment.
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': |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
5480934
to
1dcec0e
Compare
@@ -0,0 +1 @@ | |||
TODO: Add your license here. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
f3ca9ac
to
d8e1c5e
Compare
_remoteConfigSettings = remoteConfigSettings; | ||
} | ||
|
||
/// Fetches parameter values for your app. Parameter values may be from |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use square brackets for in-scope identifiers like RemoteConfigValue
and key
.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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}) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
6f423df
to
8df64fe
Compare
/// You can get an instance by calling [RemoteConfig.instance]. Note | ||
/// [RemoteConfig.instance] is async. | ||
class RemoteConfig extends ChangeNotifier { | ||
static const MethodChannel _channel = |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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."
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
b95f88d
to
e5e86ca
Compare
static const double defaultValueForDouble = 0.0; | ||
static const bool defaultValueForBool = false; | ||
|
||
static RemoteConfig _instance; |
There was a problem hiding this comment.
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
There was a problem hiding this 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
hi all. when is this coming? |
* Add Remote Config support
* Add Remote Config support
* Add Remote Config support
Add plugin to support Firebase Remote Config.
Fixes flutter/flutter#9815