Thanks to visit codestin.com
Credit goes to github.com

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

[shared_preferences] upgraded ios to using pigeon #4732

Merged
merged 7 commits into from
Feb 17, 2022

Conversation

gaaclarke
Copy link
Member

@gaaclarke gaaclarke commented Feb 4, 2022

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/plugins repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.
  • I updated CHANGELOG.md to add a description of the change, following repository CHANGELOG style.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Comment on lines 14 to 28
'Bool': (String key, Object value) {
return _api.setBool(key, value as bool);
},
'Double': (String key, Object value) {
return _api.setDouble(key, value as double);
},
'Int': (String key, Object value) {
return _api.setInt(key, value as int);
},
'String': (String key, Object value) {
return _api.setString(key, value as String);
},
'StringList': (String key, Object value) {
return _api.setStringList(key, value as List<String?>);
},
Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately the interface has strings built into it, so I used them as the source of truth.

@gaaclarke gaaclarke changed the title [shared_preferences] upgraded to using pigeon [shared_preferences] upgraded ios to using pigeon Feb 4, 2022
@gaaclarke gaaclarke force-pushed the shared_preferences_pigeon branch 2 times, most recently from f69ab4b to 10569d7 Compare February 4, 2022 18:35
@gaaclarke gaaclarke marked this pull request as ready for review February 4, 2022 18:43
@gaaclarke
Copy link
Member Author

@stuartmorgan I decided to split up iOS and Android implementations so we can touch base on a smaller codebase before jumping off.

@stuartmorgan-g
Copy link
Contributor

Unfortunately the interface has strings built into it, so I used them as the source of truth.

Yes, we should really consider adding new methods to the platform interface and deprecating the string-based one.

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

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

High level notes:

  • I left some comments on the API itself; if we're making a new platform-specific Dart/native API boundary from scratch that's iOS-specific, there's no reason we need to recreate the old, platform-agnostic decisions in the Pigeon version.
  • Please file issues for the pigeon notes so we can track them. They aren't blockers here, but I don't want to lose them.

}

void SharedPreferencesApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
NSObject<SharedPreferencesApi> *api) {
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: ObjC naming would be SharedPreferencesAPI.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you think Pigeon should mangle the names? That probably isn't possible. We could provide an option to override the generated name if they want something different that what is provided in the input.

} 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.

- (FlutterStandardReader *)readerWithData:(NSData *)data {
return [[SharedPreferencesApiCodecReader 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

@@ -3,69 +3,10 @@
// found in the LICENSE file.

#import "FLTSharedPreferencesPlugin.h"
#import "messages.g.h"

static NSString *const CHANNEL_NAME = @"plugins.flutter.io/shared_preferences";
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 dead code.

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

[[NSUserDefaults standardUserDefaults] setValue:value forKey:key];
return @YES;
}

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.

SharedPreferencesIos.registerWith();
expect(
SharedPreferencesStorePlatform.instance, isA<SharedPreferencesIos>());
});
Copy link
Contributor

Choose a reason for hiding this comment

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

The actual API passthroughs should be tested as well.

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

bool setInt(String key, int value);
bool setString(String key, String value);
bool setStringList(String key, List<String> value);
bool clear();
Copy link
Contributor

Choose a reason for hiding this comment

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

None of these messages ever returns anything other than YES, so they should all just be void.

Copy link
Member Author

@gaaclarke gaaclarke Feb 4, 2022

Choose a reason for hiding this comment

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

I don't think so, the platform_interface states that a bool should be returned from these methods. It should be the host platform that decides the value, not the glue code between the platform_interface and the host code.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think so, the platform_interface states that a bool should be returned from these methods.

I'm not suggesting that the implementation of the platform interface—which is Dart code—violate that. This structure is not the implementation of the platform interface, it's a completely internal API boundary.

It should be the host platform that decides the value, not the glue code between the platform_interface and the host code.

shared_preferences_ios is "the host platform". It's all iOS code. Which parts of that iOS code are written in Dart and which parts in ObjC are up to us, as is defining the API between those layers.

There is no reason to put useless return value on an internal API boundary.

Copy link
Member Author

Choose a reason for hiding this comment

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

There is no reason to put useless return value on an internal API boundary.

Here's how you are looking at it:

(dart_platform_interface)->(dart_host_implementation)->(objc_host_implementation)

Here's how I think you should be thinking about it:

(dart_platform_interface)->(dart_glue)->(host_implementation)

It makes maintaining much easier since logic for implementing dart_platform_interface isn't spread across multiple files.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is no reason to put useless return value on an internal API boundary.

Here's how you are looking at it:

(dart_platform_interface)->(dart_host_implementation)->(objc_host_implementation)

Not really, no.

I look it at as:

Dart platform interface
Adapter code
System API

More specifically:

Dart platform interface
Adapter code (Dart)
Method channel/pigeon API
Adapter code (Native language)
System API

Here's how I think you should be thinking about it:

(dart_platform_interface)->(dart_glue)->(host_implementation)

To the extent that I think we should have an abstract goal of minimizing the amount of adapter code on one side of the language boundary, I think it should be the native side, favoring writing adapter code in Dart, for the reasons laid out in https://docs.flutter.dev/go/platform-channels-in-federated-plugins (search for "Allows moving adapter code from native code to Dart")

Copy link
Member Author

Choose a reason for hiding this comment

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

Well it doesn't help that what those return values mean is never defined, so I can't even tell you if the values are correct or if they need to be generated from the native code. Imagine those return values actually meant something, they would have to be forwarded from the host platform to the plugin. It seems a bit silly to make a special case about where the return values come from in the case where return values mean nothing. I think it's just setting up the wrong example.

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g Feb 5, 2022

Choose a reason for hiding this comment

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

Well it doesn't help that what those return values mean is never defined

On Android the setters can fail, and the return values is whether setting succeeded.

A PR fixing the docs to say that sounds like a good idea.

so I can't even tell you if the values are correct

Setting a value can't fail on iOS, so unconditionally returning true is the correct implementation on iOS.

or if they need to be generated from the native code.

We know that they don't, because we know what the APIs on iOS are.

Imagine those return values actually meant something, they would have to be forwarded from the host platform to the plugin.

If there were meaningful return values that we needed from the native side on iOS, I wouldn't have made that review comment in the first place.

It seems a bit silly to make a special case about where the return values come from in the case where return values mean nothing.

There is, and will only ever be, one implementation of the API you are defining with this pigeon file. When there is only one case, "special case" isn't meaningful.

I think it's just setting up the wrong example.

This isn't an example. It's a specific, concrete case.

bool setDouble(String key, double value);
bool setInt(String key, int value);
bool setString(String key, String value);
bool setStringList(String key, List<String> value);
Copy link
Contributor

Choose a reason for hiding this comment

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

The methods that just use setValueForKey can all be collapsed together.

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 think it's better to match the Dart interface as closely as possible instead instead of trying to be smart in the glue code (as per other comment).

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

@stuartmorgan-g stuartmorgan-g Feb 5, 2022

Choose a reason for hiding this comment

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

And specifically, my point here is not just that you are putting logic in the Dart code there, but the purpose of that logic is to maximally fragment the calls by value type. You've created an API layer that not only has more calls than the underlying host API (which is what I suggest matching), but far more than the Dart interface. Your argument against reducing the number from five setters to three is that you want to better match the side that only has one...

Copy link
Member Author

@gaaclarke gaaclarke Feb 5, 2022

Choose a reason for hiding this comment

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

Yep, I'd say that's matching the Dart interface. The interface is weird in that case because it's defined in the comments and in the code not just in code because Dart lacks the ability to express it in code. There is no computation or branching in the dart file, just dispatching.

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 know it's late there, we can hash out the last mile of this conversation on Monday, over vc if we have to.

@override
Future<Map<String, Object>> getAll() async {
final Map<String?, Object?> result = await _api.getAll();
return result.cast<String, Object>();
Copy link
Contributor

Choose a reason for hiding this comment

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

We definitely need non-null generics; this is ugly.

Copy link
Member Author

Choose a reason for hiding this comment

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

if (setter != null) {
return setter(key, value);
} else {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

You've changed the semantics; this used to be a platform error, and now it just returns false. I think the error is a much better option here.

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.

@gaaclarke gaaclarke force-pushed the shared_preferences_pigeon branch from 81a81e0 to edb08fd Compare February 4, 2022 23:11
@gaaclarke
Copy link
Member Author

Talked with stuart offline. The thesis is that we should be moving as much as logic as possible to the Dart layer and making the pigeon files match host API's as closely as possible, maybe opening up the door for some tool that will make host apis accessible by Dart more directly. I'll rework this PR to make the pigeon interface look more like that.

- started throwing an exception for invalid types
- removed dead code
- added unit tests
@gaaclarke gaaclarke force-pushed the shared_preferences_pigeon branch from 19fec11 to bb7285f Compare February 14, 2022 23:14
@gaaclarke gaaclarke force-pushed the shared_preferences_pigeon branch from bb7285f to e9ddd40 Compare February 14, 2022 23:17
@gaaclarke
Copy link
Member Author

gaaclarke commented Feb 14, 2022

@stuartmorgan I made the changes we discussed last week. This now matches our goal of removing logic from the host platform and putting it into Dart, also making the pigeon abstraction match the host api. Give it a look to make sure this is the path we want to take. We can't fully migrate yet if we want to go this route since it requires returning a nullable map for getAll() ( flutter/flutter#98452 )

If this is where we want to be with our plugins I think we should divide the work up into 2 different PR since I ended up having to migrate to Pigeon and also migrate the abstraction to be closer to the host platform in the same PR. Doing both things in the same PR is a bit dicey.

In the first PR, the migration to Pigeon should attempt to minimize changes to the host platforms code, then in the second PR can migrate code from the host platform to Dart.

@gaaclarke
Copy link
Member Author

Actually we can move forward now if we just make that return value nonnull. It is a slight deviation from the API, but I made that change. PTAL.

@gaaclarke gaaclarke force-pushed the shared_preferences_pigeon branch from 59fe511 to 48993a6 Compare February 15, 2022 00:22
@gaaclarke gaaclarke force-pushed the shared_preferences_pigeon branch from 48993a6 to 502a358 Compare February 15, 2022 00:25
Future<bool> clear() async {
final Map<String, Object> all = await getAll();
for (final String key in all.keys) {
await _api.remove(key);
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 incredibly slow and inefficient relative to doing it on the native side. Moving things to Dart is a general goal, but we shouldn't do it at the expense of the quality of the plugin.

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, I wanted to take the code to the logical extreme of what we are saying we wanted to see how it looked. If our goal is to move logic from host to dart that's often going to incur a performance cost.

I moved it back to the native side.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fortunately logical extremes are not the only options available to us. As with all code, we can balance performance against other design goals, favoring the general goal in the general case, and adjusting for performance in specific cases where it will obviously have a significant impact (like here, where moving a few lines of code puts a potentially large number of cross-boundary calls in a tight loop).

final Map<String, Object> result =
(await _api.getAll()).cast<String, Object>();
result
.removeWhere((String key, Object value) => !key.startsWith('flutter.'));
Copy link
Contributor

Choose a reason for hiding this comment

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

The filtering logic has to stay on the native side. It's there in case people want to store non-Flutter prefs (e.g., in add-to-app) and there's no guarantee that the things with other keys can be handled by our codec since we didn't write them.

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. That one didn't occur to me, but raising the idea it would be nice if we did lazy serialization / deserialization. That could help with some of the performance issues we are seeing.

/// The macOS implementation of [SharedPreferencesStorePlatform].
///
/// This class implements the `package:shared_preferences` functionality for iOS.
class SharedPreferencesIOS extends SharedPreferencesStorePlatform {
Copy link
Contributor

Choose a reason for hiding this comment

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

Renaming the class is a breaking change.

It's also still not actually clear to me what the correct capitalization is. The relevant Dart rule says that two-letter acronyms are capitalized in Dart. "i" isn't part of the acronym; the acronym itself, OS, is two letters. So there are two possible interpretations:

  • It's a single letter ("i"), which is capitalized because it's the first letter, followed by a two-letter acronym, which is also capitalized, giving IOS.
  • We pretend it's a normal three-letter word (or a three-letter acronym-like thing?), giving Ios.

Dart appears to use the former (ref), and I only just realized that that there's an explicit mention in the Flutter style guide saying it should be the latter (although that section is itself somewhat wrong since it says Dart rules would give macOs, which isn't correct). Given the existing inconsistency in the ecosystem, I haven't caught one vs. the other in reviews.

I'll make sure to apply the Flutter rule going forward now that I'm aware of it, but it's not worth a breaking change here to fix.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, done.

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.

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

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

LGTM with nits.

UserDefaultsApiSetup(registrar.messenger, plugin);
}

- (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

@@ -24,3 +24,5 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
pedantic: ^1.10.0
Copy link
Contributor

Choose a reason for hiding this comment

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

pedantic should not be re-added.

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

});
test('setValueMap', () {
expect(() async {
await plugin.setValue('flutter.Map', 'key', <String, String>{});
Copy link
Contributor

Choose a reason for hiding this comment

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

The flutter. prefix should be on the key here, not the type string.

While in practice it doesn't matter since both are unknown values, having it be wrong is needlessly confusing.

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

expect(await testData.getAll(), isEmpty);
expect(log.single.method, 'clear');
});
test('setValueMap', () {
Copy link
Contributor

Choose a reason for hiding this comment

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

This naming doesn't make it clear why it expects to throw. How about 'setValue with unsupported type'?

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

@gaaclarke gaaclarke added the waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land. label Feb 17, 2022
@fluttergithubbot fluttergithubbot merged commit 3fd2e5d into flutter:main Feb 17, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Feb 17, 2022
debokarmakar pushed a commit to nurture-farm/plugins that referenced this pull request Feb 18, 2022
* google/master:
  [webview_flutter_wkwebview] Add support for `WKUIDelegate` (flutter#4809)
  [camera] Restore compatibility with older Flutter (flutter#4885)
  [ci.yaml] Migrate to Cocoon scheduler (flutter#4884)
  Roll Flutter from adafd66 to 93c0c04 (6 revisions) (flutter#4880)
  [shared_preferences] upgraded ios to using pigeon (flutter#4732)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
p: shared_preferences platform-ios waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants