在 iOS 应用中添加 Flutter 页面
This guide describes how to add a single Flutter screen to an existing iOS app.
Start a FlutterEngine and FlutterViewController
To launch a Flutter screen from an existing iOS, you start aFlutterEngine
and a FlutterViewController
.
The FlutterEngine
serves as a host to the Dart VM and your Flutter runtime, and the FlutterViewController
attaches to a FlutterEngine
to pass UIKit input events into Flutter and to display frames rendered by the FlutterEngine
.
The FlutterEngine
may have the same lifespan as your FlutterViewController
or outlive your FlutterViewController
.
小提示It’s generally recommended to pre-warm a long-lived FlutterEngine
for yourapplication because:
- The first frame will appear faster when showing the
FlutterViewController
. - Your Flutter and Dart state will outlive one
FlutterViewController
. - Your application and your plugins can interact with Flutter and your Dartlogic before showing the UI.
See Loading sequence and performancefor more analysis on the latency and memory trade-offs of pre-warming an engine.
Create a FlutterEngine
The proper place to create a FlutterEngine
is specific to your host app. As an example, wedemonstrate creating a FlutterEngine
, exposed as a property, on app startup inthe app delegate.
In AppDelegate.h
:
AppDelegate.h
- @import UIKit;@import Flutter;
@interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.@property (nonatomic,strong) FlutterEngine *flutterEngine;@end
In AppDelegate.m
:
AppDelegate.m
- #import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Used to connect plugins.
- #import "AppDelegate.h"
- @implementation AppDelegate
- - (BOOL)application:(UIApplication *)application
- didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
- self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
- // Runs the default Dart entrypoint with a default Flutter route.
- [self.flutterEngine run];
- [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
- return [super application:application didFinishLaunchingWithOptions:launchOptions];
- }
- @end
In AppDelegate.swift
:
AppDelegate.swift
- import UIKit
- import Flutter
- import FlutterPluginRegistrant // Used to connect plugins.
- @UIApplicationMain
- class AppDelegate: FlutterAppDelegate { // More on the FlutterAppDelegate.
- lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
- override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- // Runs the default Dart entrypoint with a default Flutter route.
- flutterEngine.run();
- GeneratedPluginRegistrant.register(with: self.flutterEngine);
- return super.application(application, didFinishLaunchingWithOptions: launchOptions);
- }
- }
Show a FlutterViewController with your FlutterEngine
The following example shows a generic ViewController with a UIButton hooked topresent a FlutterViewController
.The FlutterViewController
uses the FlutterEngine
instance created in theAppDelegate
.
ViewController.m
- @import Flutter;
import "AppDelegate.h"
import "ViewController.h"
@implementation ViewController
(void)viewDidLoad { [super viewDidLoad];
// Make a button to call the showFlutter function when pressed. UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; [button addTarget:self
action:@selector(showFlutter)
forControlEvents:UIControlEventTouchUpInside]; [button setTitle:@"Show Flutter!" forState:UIControlStateNormal]; button.backgroundColor = UIColor.blueColor; button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0); [self.view addSubview:button];}
(void)showFlutter { FlutterEngine *flutterEngine =
((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
FlutterViewController *flutterViewController =
[[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
[self presentViewController:flutterViewController animated:YES completion:nil];}@end
ViewController.swift
- import UIKit
- import Flutter
- class ViewController: UIViewController {
- override func viewDidLoad() {
- super.viewDidLoad()
- // Make a button to call the showFlutter function when pressed.
- let button = UIButton(type:UIButton.ButtonType.custom)
- button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
- button.setTitle("Show Flutter!", for: UIControl.State.normal)
- button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
- button.backgroundColor = UIColor.blue
- self.view.addSubview(button)
- }
- @objc func showFlutter() {
- let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
- let flutterViewController =
- FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
- present(flutterViewController, animated: true, completion: nil)
- }
- }
Now, you have a Flutter screen embedded in your iOS app.
备忘Using the previous example, the default main()
entrypoint function of yourdefault Dart library would run when calling run
on theFlutterEngine
created in the AppDelegate
.
Alternatively - Create a FlutterViewController with an implicit FlutterEngine
As an alternative to the previous example, you can let theFlutterViewController
implicitly create its own FlutterEngine
withoutpre-warming one ahead of time.
This is not recommended because creating a FlutterEngine
on-demand couldintroduce a noticeable latency between when the FlutterViewController
ispresented and when it renders its first frame. This could, however, beuseful if the Flutter screen is rarely shown, when there are no goodheuristics to determine when the Dart VM should be started, and when Flutterdoesn’t need to persist state between view controllers.
To let the FlutterViewController
present without an existing FlutterEngine
,omit the FlutterEngine
construction, and create theFlutterViewController
without an engine reference.
ViewController.m
- // Existing code omitted.
- - (void)showFlutter {
- FlutterViewController *flutterViewController =
- [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
- [self presentViewController:flutterViewController animated:YES completion:nil];
- }
- @end
ViewController.swift
- // Existing code omitted.
- func showFlutter() {
- let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
- present(flutterViewController, animated: true, completion: nil)
- }
See Loading sequence and performancefor more explorations on latency and memory usage.
Using the FlutterAppDelegate
Letting your application’s UIApplicationDelegate
subclass FlutterAppDelegate
is recommended but not required.
The FlutterAppDelegate
performs functions such as:
- Forwarding application callbacks such as
openURL
to plugins such as local_auth. - Forwarding status bar taps (which can only be detected in the AppDelegate) toFlutter for scroll-to-top behavior.
If your app delegate can’t directly make FlutterAppDelegate
a subclass,make your app delegate implement the FlutterAppLifeCycleProvider
protocol inorder to make sure your plugins receive the necessary callbacks. Otherwise,plugins that depend on these events may have undefined behavior.
For instance:
AppDelegate.h
- @import Flutter;@import UIKit;@import FlutterPluginRegistrant;
@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>@property (strong, nonatomic) UIWindow window;@property (nonatomic,strong) FlutterEngine flutterEngine;@end
The implementation should delegate mostly to a FlutterPluginAppLifeCycleDelegate
:
AppDelegate.m
- @interface AppDelegate ()@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;@end
@implementation AppDelegate
(instancetype)init { if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
} return self;}
(BOOL)application:(UIApplication)applicationdidFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id>))launchOptions { self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil]; [self.flutterEngine runWithEntrypoint:nil]; [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]; return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];}
// Returns the key window's rootViewController, if it's a FlutterViewController.// Otherwise, returns nil.
(FlutterViewController)rootFlutterViewController { UIViewController viewController = [UIApplication sharedApplication].keyWindow.rootViewController; if ([viewController isKindOfClass:[FlutterViewController class]]) {
return (FlutterViewController*)viewController;
} return nil;}
(void)touchesBegan:(NSSet)touches withEvent:(UIEvent)event { [super touchesBegan:touches withEvent:event];
// Pass status bar taps to key window Flutter rootViewController. if (self.rootFlutterViewController != nil) {
[self.rootFlutterViewController handleStatusBarTouches:event];
}}
(void)application:(UIApplication)applicationdidRegisterUserNotificationSettings:(UIUserNotificationSettings)notificationSettings { [_lifeCycleDelegate application:applicationdidRegisterUserNotificationSettings:notificationSettings];}
(void)application:(UIApplication)applicationdidRegisterForRemoteNotificationsWithDeviceToken:(NSData)deviceToken { [_lifeCycleDelegate application:applicationdidRegisterForRemoteNotificationsWithDeviceToken:deviceToken];}
(void)application:(UIApplication)applicationdidReceiveRemoteNotification:(NSDictionary)userInfofetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { [_lifeCycleDelegate application:application
didReceiveRemoteNotification:userInfo
fetchCompletionHandler:completionHandler];
}
(BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
return [_lifeCycleDelegate application:application openURL:url options:options];}
(BOOL)application:(UIApplication)application handleOpenURL:(NSURL)url { return [_lifeCycleDelegate application:application handleOpenURL:url];}
(BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
sourceApplication:(NSString*)sourceApplication
annotation:(id)annotation {
return [_lifeCycleDelegate application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}
(void)application:(UIApplication)applicationperformActionForShortcutItem:(UIApplicationShortcutItem)shortcutItemcompletionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) { [_lifeCycleDelegate application:application
performActionForShortcutItem:shortcutItem
completionHandler:completionHandler];
}
(void)application:(UIApplication)applicationhandleEventsForBackgroundURLSession:(nonnull NSString)identifiercompletionHandler:(nonnull void (^)(void))completionHandler { [_lifeCycleDelegate application:applicationhandleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
}
(void)application:(UIApplication*)applicationperformFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];}
(void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate { [_lifeCycleDelegate addDelegate:delegate];}@end
Launch options
The examples demonstrate running Flutter using the default launch settings.
In order to customize your Flutter runtime, you can also specify the Dart entrypoint, library, and route.
Dart entrypoint
Calling run
on a FlutterEngine
, by default, runs the main()
Dart functionof your lib/main.dart
file.
You can also run a different entrypoint function by using runWithEntrypoint
FlutterEngine(im)runWithEntrypoint:)with an NSString
specifying a different Dart function.
备忘Dart entrypoint functions other than main()
must be annotated with the following in order to not be tree-shaken away when compiling:
main.dart
- @pragma('vm:entry-point')void myOtherEntrypoint() { … };
Dart library
In addition to specifying a Dart function, you can specify an entrypointfunction in a specific file.
For instance the following runs myOtherEntrypoint()
in lib/other_file.dart
instead of main()
inlib/main.dart
:
Objective-C
- [flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];
Swift
- flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart")
Route
An initial route can be set for your Flutter WidgetsApp
when constructing the engine.
Creating engine
- FlutterEngine *flutterEngine =
- [[FlutterEngine alloc] initWithName:@"my flutter engine"];
- [[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute"
- arguments:@"/onboarding"];
- [flutterEngine run];
Creating engine
- let flutterEngine = FlutterEngine(name: "my flutter engine")
- flutterEngine.navigationChannel.invokeMethod("setInitialRoute", arguments:"/onboarding")
- flutterEngine.run()
This code sets your dart:ui
’s window.defaultRouteName
to "/onboarding"
instead of "/"
.
请注意"setInitialRoute"
on the navigationChannel
must be called before running yourFlutterEngine
in order for Flutter’s first frame to use the desiredroute.
Specifically, this must be called before running the Dart entrypoint. Theentrypoint may lead to a series of events whererunApp
builds aMaterial/Cupertino/WidgetsApp which implicitly creates aNavigator whichmay read window.defaultRouteName
when theNavigatorState
isfirst initialized.
Setting the initial route after running the engine doesn’t have an effect.
小提示In order to imperatively change your current Flutter route from the platformside after the FlutterEngine
is already running, use pushRouteFlutterViewController(im)pushRoute:)or popRouteFlutterViewController(im)popRoute)on the FlutterViewController
.
To pop the iOS route from the Flutter side, call SystemNavigator.pop()
.
See Navigation and routing for more about Flutter’s routes.
Other
The preious example only illustrates a few ways to customize how a Flutterinstance is initiated. Using platform channels,you’re free to push data or prepare your Flutter environment in any way you’dlike, before presenting the Flutter UI via a FlutterViewController
.