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

<!-- Links -->
[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>
This commit is contained in:
gaaclarke 2025-05-28 08:31:30 -07:00 committed by GitHub
parent 5d013c73ba
commit 0e536eb9fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 120 additions and 3 deletions

View File

@ -27,7 +27,20 @@ FLUTTER_DARWIN_EXPORT
@interface FlutterAppDelegate
: UIResponder <UIApplicationDelegate, FlutterPluginRegistry, FlutterAppLifeCycleProvider>
@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<FlutterPluginRegistrant>* pluginRegistrant;
@end

View File

@ -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 <NSObject>
@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<FlutterPluginRegistry>*)registry;
@end
#pragma mark -
/**
* Implement this in the `UIAppDelegate` of your app to enable Flutter plugins to register

View File

@ -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<FlutterPluginRegistrant>* pluginRegistrant;
@end
NS_ASSUME_NONNULL_END

View File

@ -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<FlutterPluginRegistrant>* _weakRegistrant;
NSObject<FlutterPluginRegistrant>* _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<FlutterPluginRegistrant>*)pluginRegistrant {
if (_weakRegistrant) {
return _weakRegistrant;
}
if (_strongRegistrant) {
return _strongRegistrant;
}
return nil;
}
- (void)setPluginRegistrant:(NSObject<FlutterPluginRegistrant>*)pluginRegistrant {
if (pluginRegistrant == (id)self) {
_weakRegistrant = pluginRegistrant;
_strongRegistrant = nil;
} else {
_weakRegistrant = nil;
_strongRegistrant = pluginRegistrant;
}
}
- (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
FlutterViewController* flutterRootViewController = [self rootFlutterViewController];
if (flutterRootViewController) {

View File

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

View File

@ -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<FlutterPluginRegistrant>* pluginRegistrant =
[FlutterSharedApplication.application.delegate performSelector:@selector(pluginRegistrant)];
[pluginRegistrant registerWithRegistry:self];
}
}
- (BOOL)isViewOpaque {

View File

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

View File

@ -7,9 +7,14 @@
#import <UIKit/UIKit.h>
@protocol FlutterPluginRegistrant;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@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<FlutterPluginRegistrant>* pluginRegistrant;
@end