在 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 aFlutterEngineand 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 FlutterViewControlleror 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

  1. @import UIKit;@import Flutter;

  2. @interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.@property (nonatomic,strong) FlutterEngine *flutterEngine;@end

In AppDelegate.m:

AppDelegate.m

  1. #import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Used to connect plugins.
  2.  
  3. #import "AppDelegate.h"
  4.  
  5. @implementation AppDelegate
  6.  
  7. - (BOOL)application:(UIApplication *)application
  8. didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
  9. self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
  10. // Runs the default Dart entrypoint with a default Flutter route.
  11. [self.flutterEngine run];
  12. [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  13. return [super application:application didFinishLaunchingWithOptions:launchOptions];
  14. }
  15.  
  16. @end

In AppDelegate.swift:

AppDelegate.swift

  1. import UIKit
  2. import Flutter
  3. import FlutterPluginRegistrant // Used to connect plugins.
  4.  
  5. @UIApplicationMain
  6. class AppDelegate: FlutterAppDelegate { // More on the FlutterAppDelegate.
  7. lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
  8.  
  9. override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  10. // Runs the default Dart entrypoint with a default Flutter route.
  11. flutterEngine.run();
  12. GeneratedPluginRegistrant.register(with: self.flutterEngine);
  13. return super.application(application, didFinishLaunchingWithOptions: launchOptions);
  14. }
  15. }

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

  1. @import Flutter;

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

    1. import UIKit
    2. import Flutter
    3.  
    4. class ViewController: UIViewController {
    5. override func viewDidLoad() {
    6. super.viewDidLoad()
    7.  
    8. // Make a button to call the showFlutter function when pressed.
    9. let button = UIButton(type:UIButton.ButtonType.custom)
    10. button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
    11. button.setTitle("Show Flutter!", for: UIControl.State.normal)
    12. button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
    13. button.backgroundColor = UIColor.blue
    14. self.view.addSubview(button)
    15. }
    16.  
    17. @objc func showFlutter() {
    18. let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
    19. let flutterViewController =
    20. FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
    21. present(flutterViewController, animated: true, completion: nil)
    22. }
    23. }

    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

    1. // Existing code omitted.
    2. - (void)showFlutter {
    3. FlutterViewController *flutterViewController =
    4. [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
    5. [self presentViewController:flutterViewController animated:YES completion:nil];
    6. }
    7. @end

    ViewController.swift

    1. // Existing code omitted.
    2. func showFlutter() {
    3. let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
    4. present(flutterViewController, animated: true, completion: nil)
    5. }

    See Loading sequence and performancefor more explorations on latency and memory usage.

    Using the FlutterAppDelegate

    Letting your application’s UIApplicationDelegate subclass FlutterAppDelegateis recommended but not required.

    The FlutterAppDelegate performs functions such as:

    • Forwarding application callbacks such as openURLto 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

    1. @import Flutter;@import UIKit;@import FlutterPluginRegistrant;

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

    1. @interface AppDelegate ()@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;@end

    2. @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&lt;UIApplicationOpenURLOptionsKey, id&gt;*)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 runWithEntrypointFlutterEngine(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

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

        1. [flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];

        Swift

        1. flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart")

        Route

        An initial route can be set for your Flutter WidgetsAppwhen constructing the engine.

        Creating engine

        1. FlutterEngine *flutterEngine =
        2. [[FlutterEngine alloc] initWithName:@"my flutter engine"];
        3. [[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute"
        4. arguments:@"/onboarding"];
        5. [flutterEngine run];

        Creating engine

        1. let flutterEngine = FlutterEngine(name: "my flutter engine")
        2. flutterEngine.navigationChannel.invokeMethod("setInitialRoute", arguments:"/onboarding")
        3. flutterEngine.run()

        This code sets your dart:ui’s window.defaultRouteNameto "/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.