在 iOS 应用中添加 Flutter 页面

本指南描述了怎样在既有 iOS 应用中添加单个 Flutter 页面。

This guide describes how to add a single Flutter screen to an existing iOS app.

启动 FlutterEngine 和 FlutterViewController

Start a FlutterEngine and FlutterViewController

为了在既有 iOS 应用中展示 Flutter 页面,请启动 FlutterEngineFlutterViewController

To launch a Flutter screen from an existing iOS, you start a FlutterEngine and a FlutterViewController.

FlutterEngine 的寿命可能与 FlutterViewController 相同,也可能超过 FlutterViewController

The FlutterEngine may have the same lifespan as your FlutterViewController or outlive your FlutterViewController.

加载顺序和性能 里有更多关于预热 engine 的延迟和内存取舍的分析。

See Loading sequence and performance for more analysis on the latency and memory trade-offs of pre-warming an engine.

创建一个 FlutterEngine

Create a FlutterEngine

创建 FlutterEngine 的合适位置取决于您的应用。作为示例,我们将在应用启动的 app delegate 中创建一个 FlutterEngine,并作为属性暴露给外界。

The proper place to create a FlutterEngine is specific to your host app. As an example, we demonstrate creating a FlutterEngine, exposed as a property, on app startup in the app delegate.

AppDelegate.h:

In AppDelegate.h:

AppDelegate.h
@import UIKit;
@import Flutter;

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

AppDelegate.m:

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

AppDelegate.swift:

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);
  }
}

使用 FlutterEngine 展示 FlutterViewController

Show a FlutterViewController with your FlutterEngine

下面的例子展示了一个普通的 ViewController,包含一个 present FlutterViewController 的按钮。

The following example shows a generic ViewController with a UIButton hooked to present a FlutterViewController. The FlutterViewController uses the FlutterEngine instance created in the AppDelegate.

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

现在,你的 iOS 应用中集成了一个 Flutter 页面。

Now, you have a Flutter screen embedded in your iOS app.

或者 —— 使用隐式 FlutterEngine 创建 FlutterViewController

Alternatively - Create a FlutterViewController with an implicit FlutterEngine

上一个示例还有另一个选择,你可以让 FlutterViewController 隐式创建它自己的 FlutterEngine,而不用提前预热 engine。

As an alternative to the previous example, you can let the FlutterViewController implicitly create its own FlutterEngine without pre-warming one ahead of time.

不过不建议这样做,因为按需创建FlutterEngine 的话,在 FlutterViewController 被 present 出来之后,第一帧图像渲染完之前,将会引入明显的延迟。但是当 Flutter 页面很少被展示时,当对决定何时启动 Dart VM 没有好的启发时,当 Flutter 无需在页面(view controller)之间保持状态时,此方式可能会有用。

This is not recommended because creating a FlutterEngine on-demand could introduce a noticeable latency between when the FlutterViewController is presented and when it renders its first frame. This could, however, be useful if the Flutter screen is rarely shown, when there are no good heuristics to determine when the Dart VM should be started, and when Flutter doesn’t need to persist state between view controllers.

为了不使用已经存在的 FlutterEngine 来展现 FlutterViewController,省略 FlutterEngine 的创建步骤,并且在创建 FlutterViewController 时,去掉 engine 的引用。

To let the FlutterViewController present without an existing FlutterEngine, omit the FlutterEngine construction, and create the FlutterViewController 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 performance for more explorations on latency and memory usage.

使用 FlutterAppDelegate

Using the FlutterAppDelegate

推荐让你应用的 UIApplicationDelegate 继承 FlutterAppDelegate,但不是必须的。

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

FlutterAppDelegate 有这些功能:

The FlutterAppDelegate performs functions such as:

  • 传递应用的回调,例如 openURL,到 Flutter plugins,例如 local_auth

    Forwarding application callbacks such as openURL to plugins such as local_auth.

  • 传递状态栏点击(这只能在 AppDelegate 中检测)到 Flutter 的点击置顶行为。

    Forwarding status bar taps (which can only be detected in the AppDelegate) to Flutter for scroll-to-top behavior.

如果你的 app delegate 不能直接继承 FlutterAppDelegate,让你的 app delegate 实现 FlutterAppLifeCycleProvider 协议,来确保 Flutter plugins 接收到必要的回调。否则,依赖这些事件的 plugins 将会有无法预估的行为。

If your app delegate can’t directly make FlutterAppDelegate a subclass, make your app delegate implement the FlutterAppLifeCycleProvider protocol in order 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

App delegate 的实现中,应该最大化地委托给 FlutterPluginAppLifeCycleDelegate

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*)application
didFinishLaunchingWithOptions:(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*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
    [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}

- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
    [_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(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*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
  completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
    [_lifeCycleDelegate application:application
       performActionForShortcutItem:shortcutItem
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
  completionHandler:(nonnull void (^)(void))completionHandler {
    [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}

- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
    [_lifeCycleDelegate addDelegate:delegate];
}
@end

启动选项

Launch options

例子中展示了使用默认启动选项运行 Flutter。

The examples demonstrate running Flutter using the default launch settings.

为了定制化你的 Flutter 运行时,你也可以置顶 Dart 入口、库和路由。

In order to customize your Flutter runtime, you can also specify the Dart entrypoint, library, and route.

Dart 入口

Dart entrypoint

FlutterEngine 上调用 run,默认将会调用你的 lib/main.dart 文件里的 main() 函数。

Calling run on a FlutterEngine, by default, runs the main() Dart function of your lib/main.dart file.

你也可以使用另一个入口方法 runWithEntrypoint,并使用 NSString 字符串指定一个不同的 Dart 入口。

You can also run a different entrypoint function by using runWithEntrypoint with an NSString specifying a different Dart function.

Dart 库

Dart library

另外,在指定 Dart 函数时,你可以指定特定文件的特定函数。

In addition to specifying a Dart function, you can specify an entrypoint function in a specific file.

下面的例子使用 lib/other_file.dart 文件的 myOtherEntrypoint() 函数取代 lib/main.dartmain() 函数:

For instance the following runs myOtherEntrypoint() in lib/other_file.dart instead of main() in lib/main.dart:

Objective-C
[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];
Swift
flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart")

路由

Route

当构建 engine 时,可以为你的 Flutter WidgetsApp 设置一个初始路由。

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

这段代码使用 "/onboarding" 取代 "/",作为你的 dart:uiwindow.defaultRouteName

This code sets your dart:ui’s window.defaultRouteName to "/onboarding" instead of "/".

查看 路由和导航 了解更多 Flutter 路由的内容。

See Navigation and routing for more about Flutter’s routes.

其它

Other

之前的例子仅仅展示了怎样定制 Flutter 实例初始化的几种方式,通过 撰写双端平台代码,你可以在 FlutterViewController 展示 Flutter UI 之前,自由地选择你喜欢的,推入数据和准备 Flutter 环境的方式。

The preious example only illustrates a few ways to customize how a Flutter instance is initiated. Using platform channels, you’re free to push data or prepare your Flutter environment in any way you’d like, before presenting the Flutter UI via a FlutterViewController.