Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.1.0

* Upgrades to using Pigeon.

## 2.0.10

* Switches to an in-package method channel implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,7 @@
// found in the LICENSE file.

#import "FLTSharedPreferencesPlugin.h"

static NSString *const CHANNEL_NAME = @"plugins.flutter.io/shared_preferences_ios";

@implementation FLTSharedPreferencesPlugin

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:CHANNEL_NAME
binaryMessenger:registrar.messenger];
[channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
NSString *method = [call method];
NSDictionary *arguments = [call arguments];

if ([method isEqualToString:@"getAll"]) {
result(getAllPrefs());
} else if ([method isEqualToString:@"setBool"]) {
NSString *key = arguments[@"key"];
NSNumber *value = arguments[@"value"];
[[NSUserDefaults standardUserDefaults] setBool:value.boolValue forKey:key];
result(@YES);
} else if ([method isEqualToString:@"setInt"]) {
NSString *key = arguments[@"key"];
NSNumber *value = arguments[@"value"];
// int type in Dart can come to native side in a variety of forms
// It is best to store it as is and send it back when needed.
// Platform channel will handle the conversion.
[[NSUserDefaults standardUserDefaults] setValue:value forKey:key];
result(@YES);
} else if ([method isEqualToString:@"setDouble"]) {
NSString *key = arguments[@"key"];
NSNumber *value = arguments[@"value"];
[[NSUserDefaults standardUserDefaults] setDouble:value.doubleValue forKey:key];
result(@YES);
} else if ([method isEqualToString:@"setString"]) {
NSString *key = arguments[@"key"];
NSString *value = arguments[@"value"];
[[NSUserDefaults standardUserDefaults] setValue:value forKey:key];
result(@YES);
} else if ([method isEqualToString:@"setStringList"]) {
NSString *key = arguments[@"key"];
NSArray *value = arguments[@"value"];
[[NSUserDefaults standardUserDefaults] setValue:value forKey:key];
result(@YES);
} else if ([method isEqualToString:@"commit"]) {
// synchronize is deprecated.
// "this method is unnecessary and shouldn't be used."
result(@YES);
} else if ([method isEqualToString:@"remove"]) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:arguments[@"key"]];
result(@YES);
} else if ([method isEqualToString:@"clear"]) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
for (NSString *key in getAllPrefs()) {
[defaults removeObjectForKey:key];
}
result(@YES);
} else {
result(FlutterMethodNotImplemented);
}
}];
}

#pragma mark - Private
#import "messages.g.h"

static NSMutableDictionary *getAllPrefs() {
NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
Expand All @@ -80,4 +19,50 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
return filteredPrefs;
}

@interface FLTSharedPreferencesPlugin () <UserDefaultsApi>
@end

@implementation FLTSharedPreferencesPlugin

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FLTSharedPreferencesPlugin *plugin = [[FLTSharedPreferencesPlugin alloc] init];
UserDefaultsApiSetup(registrar.messenger, plugin);
}

// Must not return nil unless "error" is set.
- (nullable NSDictionary<NSString *, id> *)getAllWithError:
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add a:

// Must not return nil unless "error" is set.

here, so that we have an explicit flag at the implementation point.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
return getAllPrefs();
}

- (void)clearWithError:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
for (NSString *key in getAllPrefs()) {
[defaults removeObjectForKey:key];
}
}

- (void)removeKey:(nonnull NSString *)key
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
}

- (void)setBoolKey:(nonnull NSString *)key
value:(nonnull NSNumber *)value
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
[[NSUserDefaults standardUserDefaults] setBool:value.boolValue forKey:key];
}

- (void)setDoubleKey:(nonnull NSString *)key
value:(nonnull NSNumber *)value
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
[[NSUserDefaults standardUserDefaults] setDouble:value.doubleValue forKey:key];
}

- (void)setValueKey:(nonnull NSString *)key
value:(nonnull NSString *)value
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
[[NSUserDefaults standardUserDefaults] setValue:value forKey:key];
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Pigeon note: I think there's a potential red flag here that we've created over two hundred lines of generated code to make an 80-line implementation file 10 lines longer.

Strong typing is definitely good, but the tradeoff here is getting to be somewhat questionable. I think it's worth thinking about ways to make pigeon feel lighter weight for simple plugins.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think lines of code is the correct way to measure the change because of objc formatting and the need to distinguish between compile time verified lines and runtime verified lines. Here's instead how the changes should be measured:

  1. Every method went from ~6 operations to ~2.
  2. Every method removed 2 casts
  3. Conformance to the API is enforced at compile time
  4. Error handling has been made available to the objc handlers

Copy link
Member Author

Choose a reason for hiding this comment

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

Another thing to note is that xcode generated most of this code so it didn't feel heavy weight when writing it. Once you implement a protocol xcode offers to fill it in.

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v1.0.16), do not edit directly.
// See also: https://pub.dev/packages/pigeon
#import <Foundation/Foundation.h>
@protocol FlutterBinaryMessenger;
@protocol FlutterMessageCodec;
@class FlutterError;
@class FlutterStandardTypedData;

NS_ASSUME_NONNULL_BEGIN

/// The codec used by UserDefaultsApi.
NSObject<FlutterMessageCodec> *UserDefaultsApiGetCodec(void);

@protocol UserDefaultsApi
- (void)removeKey:(NSString *)key error:(FlutterError *_Nullable *_Nonnull)error;
- (void)setBoolKey:(NSString *)key
value:(NSNumber *)value
error:(FlutterError *_Nullable *_Nonnull)error;
- (void)setDoubleKey:(NSString *)key
value:(NSNumber *)value
error:(FlutterError *_Nullable *_Nonnull)error;
- (void)setValueKey:(NSString *)key value:(id)value error:(FlutterError *_Nullable *_Nonnull)error;
- (nullable NSDictionary<NSString *, id> *)getAllWithError:(FlutterError *_Nullable *_Nonnull)error;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is Pigeon annotating this as nullable? It's non-nullable in the Dart specification; that seems like a significant Pigeon bug. (Or is this stale?)

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not stale. It was probably done because of error handling. If there is an error you don't want people to artificially return some value.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea, reading through the code, that's what it's for. If you return nil and set an error, it will throw a PlatformException on the dart side. That's what we want. If you return nil and don't set an error, you'll get an exception when it is cast to a non-null type before returning. I think that's what we want as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

That means that it's currently impossible to distinguish in the context of the native code between a nullable and a non-nullable return value, which is definitely not great. We can't do anything compiler-enforceable I guess, which is unfortunate, but it seems like we should at least have pigeon put a comment on methods like this that says:

/// This may only return nil if 'error' is set.

so that there's clear information within the context of the generated native code what the required behavior is.

If you return nil and don't set an error, you'll get an exception when it is cast to a non-null type before returning. I think that's what we want as well.

It looks like that's only on the Dart side. Shouldn't we be validating in ObjC code that either error or the return value is non-nil, in the case of non-nullable return values? Otherwise mistakes will look like they are on the Dart side rather than in the native implementation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea, those are improvements we can do when we implement nullable return types. I'll add a note to that issue: flutter/flutter#98452 (comment)

Shouldn't we be validating in ObjC code that either error or the return value is non-nil, in the case of non-nullable return values? Otherwise mistakes will look like they are on the Dart side rather than in the native implementation.

It doesn't necessarily have to happen on the objc side. The end result will look the same to a user, an exception on the Dart side. I think generating a PlatformException about a null value instead of a null value exception will be more clear though.

it seems like we should at least have pigeon put a comment on methods like this that says:

Yea, it's a good idea. I don't know how helpful it will be in practice because it won't show up where people are actually implementing the logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

The end result will look the same to a user, an exception on the Dart side.

Right, I wasn't thinking of the end user, but the plugin developer(s). Especially if multiple platforms share method channels (which is still overwhelmingly the common case in the ecosystem). If the exception is in the Dart code, it points someone investigating in the wrong direction; probably not a huge effect, but anything that makes it harder for people to root-cause bugs isn't ideal.

- (void)clearWithError:(FlutterError *_Nullable *_Nonnull)error;
@end

extern void UserDefaultsApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
NSObject<UserDefaultsApi> *_Nullable api);

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v1.0.16), do not edit directly.
// See also: https://pub.dev/packages/pigeon
#import "messages.g.h"
#import <Flutter/Flutter.h>

#if !__has_feature(objc_arc)
#error File requires ARC to be enabled.
#endif

static NSDictionary<NSString *, id> *wrapResult(id result, FlutterError *error) {
NSDictionary *errorDict = (NSDictionary *)[NSNull null];
if (error) {
errorDict = @{
@"code" : (error.code ? error.code : [NSNull null]),
@"message" : (error.message ? error.message : [NSNull null]),
@"details" : (error.details ? error.details : [NSNull null]),
};
}
return @{
@"result" : (result ? result : [NSNull null]),
@"error" : errorDict,
};
}

@interface UserDefaultsApiCodecReader : FlutterStandardReader
@end
@implementation UserDefaultsApiCodecReader
@end

@interface UserDefaultsApiCodecWriter : FlutterStandardWriter
@end
@implementation UserDefaultsApiCodecWriter
@end

@interface UserDefaultsApiCodecReaderWriter : FlutterStandardReaderWriter
@end
@implementation UserDefaultsApiCodecReaderWriter
- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
return [[UserDefaultsApiCodecWriter alloc] initWithData:data];
}
- (FlutterStandardReader *)readerWithData:(NSData *)data {
return [[UserDefaultsApiCodecReader alloc] initWithData:data];
}
@end
Copy link
Contributor

Choose a reason for hiding this comment

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

Pigeon note: It would be nice to optimize the generator to not create these at all when there are no custom types.

Copy link
Member Author

Choose a reason for hiding this comment

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

Filed issue: flutter/flutter#97845


NSObject<FlutterMessageCodec> *UserDefaultsApiGetCodec() {
static dispatch_once_t s_pred = 0;
static FlutterStandardMessageCodec *s_sharedObject = nil;
dispatch_once(&s_pred, ^{
UserDefaultsApiCodecReaderWriter *readerWriter =
[[UserDefaultsApiCodecReaderWriter alloc] init];
s_sharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];
});
return s_sharedObject;
}

void UserDefaultsApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
NSObject<UserDefaultsApi> *api) {
{
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.remove"
binaryMessenger:binaryMessenger
codec:UserDefaultsApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(removeKey:error:)],
@"UserDefaultsApi api (%@) doesn't respond to @selector(removeKey:error:)", api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSString *arg_key = args[0];
FlutterError *error;
[api removeKey:arg_key error:&error];
callback(wrapResult(nil, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Pigeon note: Couldn't we reduce the amount of generated boilerplate code by making this whole block a helper function, and only generate the block and name each time? (And the assertion I guess).

static void SomeHelper(
    id<FlutterBinaryMessenger> binaryMessenger,
    NSObject<SharedPreferencesApi *api,
    NSString *channelName,
    <insert correct block syntax here> methodHandler) {
  FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
      messageChannelWithName:channelName
             binaryMessenger:binaryMessenger
                       codec:SharedPreferencesApiGetCodec()];
  if (api) {
    [channel setMessageHandler:messageHandler];
  } else {
    [channel setMessageHandler:nil];
  }
}

Then each code block here becomes just:

  NSCAssert(!api || [api respondsToSelector:@selector(removeKey:error:)],
            @"SharedPreferencesApi api (%@) doesn't respond to @selector(removeKey:error:)",
            api);
  SomeHelper(binaryMessenger, api, @"dev.flutter.pigeon.SharedPreferencesApi.remove",
      ^(id _Nullable message, FlutterReply callback) {
        NSArray *args = message;
        NSString *arg_key = args[0];
        FlutterError *error;
        NSNumber *output = [api removeKey:arg_key error:&error];
        callback(wrapResult(output, error));
      });

It wouldn't even need the scoping block.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea, that could be something we look into. It isn't boilerplate since it is generated and can be treated as a black box by the user. Boilerplate to me means code a user has to manage that has semantic meaning that is small compared to its verbosity.

{
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setBool"
binaryMessenger:binaryMessenger
codec:UserDefaultsApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(setBoolKey:value:error:)],
@"UserDefaultsApi api (%@) doesn't respond to @selector(setBoolKey:value:error:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSString *arg_key = args[0];
NSNumber *arg_value = args[1];
FlutterError *error;
[api setBoolKey:arg_key value:arg_value error:&error];
callback(wrapResult(nil, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
{
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setDouble"
binaryMessenger:binaryMessenger
codec:UserDefaultsApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(setDoubleKey:value:error:)],
@"UserDefaultsApi api (%@) doesn't respond to @selector(setDoubleKey:value:error:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSString *arg_key = args[0];
NSNumber *arg_value = args[1];
FlutterError *error;
[api setDoubleKey:arg_key value:arg_value error:&error];
callback(wrapResult(nil, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
{
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setValue"
binaryMessenger:binaryMessenger
codec:UserDefaultsApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(setValueKey:value:error:)],
@"UserDefaultsApi api (%@) doesn't respond to @selector(setValueKey:value:error:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSString *arg_key = args[0];
id arg_value = args[1];
FlutterError *error;
[api setValueKey:arg_key value:arg_value error:&error];
callback(wrapResult(nil, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
{
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.getAll"
binaryMessenger:binaryMessenger
codec:UserDefaultsApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(getAllWithError:)],
@"UserDefaultsApi api (%@) doesn't respond to @selector(getAllWithError:)", api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
FlutterError *error;
NSDictionary<NSString *, id> *output = [api getAllWithError:&error];
callback(wrapResult(output, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
{
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.clear"
binaryMessenger:binaryMessenger
codec:UserDefaultsApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(clearWithError:)],
@"UserDefaultsApi api (%@) doesn't respond to @selector(clearWithError:)", api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
FlutterError *error;
[api clearWithError:&error];
callback(wrapResult(nil, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
}
Loading