From 0e536eb9fe4b15eb22ea62c0652f0851b9580026 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Wed, 28 May 2025 08:31:30 -0700 Subject: [PATCH] Introduces FlutterPluginRegistrant protocol. (#169399) design doc: https://docs.google.com/document/d/1ZfcQOs-UKRa9jsFG84-MTFeibZTLKCvPQLxF2eskx44/edit?tab=t.0 issue: https://github.com/flutter/flutter/issues/167267 This provides the proper long term API for registering plugins in lieu of `application:didFinishLaunching:withOptions:` no longer being a viable place. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> --- .../framework/Headers/FlutterAppDelegate.h | 15 ++++++++++- .../ios/framework/Headers/FlutterPlugin.h | 21 ++++++++++++++++ .../framework/Headers/FlutterViewController.h | 10 ++++++++ .../framework/Source/FlutterAppDelegate.mm | 25 ++++++++++++++++++- .../Source/FlutterAppDelegateTest.mm | 16 ++++++++++++ .../framework/Source/FlutterViewController.mm | 10 ++++++++ .../Source/FlutterViewControllerTest.mm | 19 ++++++++++++++ .../ios/IosUnitTests/App/AppDelegate.h | 7 +++++- 8 files changed, 120 insertions(+), 3 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h index 0c427c247d4..126fb00bc6a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h @@ -27,7 +27,20 @@ FLUTTER_DARWIN_EXPORT @interface FlutterAppDelegate : UIResponder -@property(strong, nonatomic) UIWindow* window; +@property(nonatomic, strong, nullable) UIWindow* window; + +/** + * The `FlutterPluginRegistrant` that will be used when FlutterViewControllers + * are instantiated from nibs. + * + * The `FlutterAppDelegate` itself can be passed in without creating a retain + * cycle. + * + * This was introduced to help users migrate code from the FlutterAppDelegate + * when UISceneDelegate was adopted. Using + * FlutterViewController.pluginRegistrant should be preferred. + */ +@property(nonatomic, strong, nullable) NSObject* pluginRegistrant; @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h index 8ab46cfe80a..532d5262d60 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h @@ -425,6 +425,27 @@ typedef enum { - (nullable NSObject*)valuePublishedByPlugin:(NSString*)pluginKey; @end +#pragma mark - +/** + * The target of registration of plugins. + * + * This often is hooked up to the GeneratedPluginRegistrant which is + * automatically generated by Flutter for the dependencies listed in the + * project. + */ +@protocol FlutterPluginRegistrant +@required +/** + * Register all the plugins for the registrant. + * + * This will be called after a FlutterEngine has been instantiated, the registry + * will connect any plugins to that engine. + * + * @param registry The registry where plugins will be registered. + */ +- (void)registerWithRegistry:(NSObject*)registry; +@end + #pragma mark - /** * Implement this in the `UIAppDelegate` of your app to enable Flutter plugins to register diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index e41565c3b1f..ede5b1aea84 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -254,6 +254,16 @@ FLUTTER_DARWIN_EXPORT */ @property(nonatomic, readonly) BOOL engineAllowHeadlessExecution; +/** + * The plugin registrant that will be executed when the FlutterViewController is + * created with a NIB. + * + * This is only necessary when working with NIBs (XIBs and Storyboards). When + * programatically creating FlutterViewControllers, plugins can be registered + * directly. + */ +@property(nonatomic, weak) IBOutlet NSObject* pluginRegistrant; + @end NS_ASSUME_NONNULL_END diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index a30dd9a6b19..e15a0a059fc 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -20,7 +20,10 @@ static NSString* const kRemoteNotificationCapabitiliy = @"remote-notification"; static NSString* const kBackgroundFetchCapatibility = @"fetch"; static NSString* const kRestorationStateAppModificationKey = @"mod-date"; -@interface FlutterAppDelegate () +@interface FlutterAppDelegate () { + __weak NSObject* _weakRegistrant; + NSObject* _strongRegistrant; +} @property(nonatomic, copy) FlutterViewController* (^rootFlutterViewControllerGetter)(void); @property(nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate; @end @@ -228,6 +231,26 @@ static NSString* const kRestorationStateAppModificationKey = @"mod-date"; #pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController +- (NSObject*)pluginRegistrant { + if (_weakRegistrant) { + return _weakRegistrant; + } + if (_strongRegistrant) { + return _strongRegistrant; + } + return nil; +} + +- (void)setPluginRegistrant:(NSObject*)pluginRegistrant { + if (pluginRegistrant == (id)self) { + _weakRegistrant = pluginRegistrant; + _strongRegistrant = nil; + } else { + _weakRegistrant = nil; + _strongRegistrant = pluginRegistrant; + } +} + - (NSObject*)registrarForPlugin:(NSString*)pluginKey { FlutterViewController* flutterRootViewController = [self rootFlutterViewController]; if (flutterRootViewController) { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm index df9e7c9b43a..184c1c6010d 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm @@ -198,4 +198,20 @@ FLUTTER_ASSERT_ARC completionHandler:[OCMArg any]]); } +- (void)testSetGetPluginRegistrant { + id mockRegistrant = OCMProtocolMock(@protocol(FlutterPluginRegistrant)); + self.appDelegate.pluginRegistrant = mockRegistrant; + XCTAssertEqual(self.appDelegate.pluginRegistrant, mockRegistrant); +} + +- (void)testSetGetPluginRegistrantSelf { + __weak FlutterAppDelegate* appDelegate = self.appDelegate; + @autoreleasepool { + appDelegate.pluginRegistrant = (id)appDelegate; + self.appDelegate = nil; + } + // A retain cycle would keep this alive. + XCTAssertNil(appDelegate); +} + @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 7139e13983c..71aca23a407 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -247,6 +247,9 @@ typedef struct MouseState { if (!self.engine) { [self sharedSetupWithProject:nil initialRoute:nil]; } + if (self.pluginRegistrant) { + [self.pluginRegistrant registerWithRegistry:self]; + } } - (instancetype)init { @@ -281,6 +284,13 @@ typedef struct MouseState { // Eliminate method calls in initializers and dealloc. [self loadDefaultSplashScreenView]; [self performCommonViewControllerInitialization]; + + if ([FlutterSharedApplication.application.delegate + respondsToSelector:@selector(pluginRegistrant)]) { + NSObject* pluginRegistrant = + [FlutterSharedApplication.application.delegate performSelector:@selector(pluginRegistrant)]; + [pluginRegistrant registerWithRegistry:self]; + } } - (BOOL)isViewOpaque { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 2849be7c5f3..ac92f4bddef 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -22,6 +22,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h" #import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h" #import "flutter/shell/platform/embedder/embedder.h" +#import "flutter/testing/ios/IosUnitTests/App/AppDelegate.h" #import "flutter/third_party/spring_animation/spring_animation.h" FLUTTER_ASSERT_ARC @@ -2471,4 +2472,22 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; [mockVC stopMocking]; } +- (void)testPluginRegistrant { + id mockRegistrant = OCMProtocolMock(@protocol(FlutterPluginRegistrant)); + FlutterViewController* viewController = [[FlutterViewController alloc] init]; + viewController.pluginRegistrant = mockRegistrant; + [viewController awakeFromNib]; + OCMVerify([mockRegistrant registerWithRegistry:viewController]); +} + +- (void)testAppDelegatePluginRegistrant { + id mockRegistrant = OCMProtocolMock(@protocol(FlutterPluginRegistrant)); + id appDelegate = [[UIApplication sharedApplication] delegate]; + XCTAssertTrue([appDelegate respondsToSelector:@selector(setPluginRegistrant:)]); + [appDelegate setPluginRegistrant:mockRegistrant]; + FlutterViewController* viewController = [[FlutterViewController alloc] init]; + [appDelegate setPluginRegistrant:nil]; + OCMVerify([mockRegistrant registerWithRegistry:viewController]); +} + @end diff --git a/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.h b/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.h index c987bb4da9e..530b65ecb9a 100644 --- a/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.h +++ b/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.h @@ -7,9 +7,14 @@ #import +@protocol FlutterPluginRegistrant; + @interface AppDelegate : UIResponder -@property(strong, nonatomic) UIWindow* window; +@property(nonatomic, strong, nullable) UIWindow* window; + +// A mirror of the FlutterAppDelegate API for integration testing. +@property(nonatomic, strong, nullable) NSObject* pluginRegistrant; @end