在 iOS 应用中添加 Flutter 页面
本指南描述了怎样在既有 iOS 应用中添加单个 Flutter 页面。
启动 FlutterEngine 和 FlutterViewController
#为了在既有 iOS 应用中展示 Flutter 页面,请启动 FlutterEngine
和 FlutterViewController
。
FlutterEngine
的寿命可能与 FlutterViewController
相同,也可能超过 FlutterViewController
。
加载顺序和性能 里有更多关于预热 engine 的延迟和内存取舍的分析。
创建一个 FlutterEngine
#在哪创建 FlutterEngine
取决于你要用的宿主类型。
在这个例子中,我们在名为 FlutterDependencies
的 SwiftUI Observable
对象中创建了一个 FlutterEngine
对象。调用 run()
为引擎预热,然后使用 environment()
view modifier 将此对象传递给了 ContentView
。
import SwiftUI
import Flutter
// The following library connects plugins with iOS platform code to this app.
import FlutterPluginRegistrant
@Observable
class FlutterDependencies {
let flutterEngine = FlutterEngine(name: "my flutter engine")
init() {
// Runs the default Dart entrypoint with a default Flutter route.
flutterEngine.run()
// Connects plugins with iOS platform code to this app.
GeneratedPluginRegistrant.register(with: self.flutterEngine);
}
}
@main
struct MyApp: App {
// flutterDependencies will be injected through the view environment.
@State var flutterDependencies = FlutterDependencies()
var body: some Scene {
WindowGroup {
ContentView()
.environment(flutterDependencies)
}
}
}
这个例子中,我们在应用启动时的 App Delegate 中创建了一个 FlutterEngine
并作为属性暴露给外界。
import UIKit
import Flutter
// The following library connects plugins with iOS platform code to this app.
import FlutterPluginRegistrant
@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();
// Connects plugins with iOS platform code to this app.
GeneratedPluginRegistrant.register(with: self.flutterEngine);
return super.application(application, didFinishLaunchingWithOptions: launchOptions);
}
}
下面的示例演示了,我们在应用启动时的 App Delegate 中创建了一个 FlutterEngine
并作为属性暴露给外界。
@import UIKit;
@import Flutter;
@interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
// The following library connects plugins with iOS platform code to this app.
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>
#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];
// Connects plugins with iOS platform code to this app.
[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
使用 FlutterEngine 展示 FlutterViewController
#下面的例子中展示了一个带有 NavigationLink
的 ContentView
连接到一个 Flutter 屏幕。首先,创建一个 FlutterViewControllerRepresentable
来代表 FlutterViewController
。
FlutterViewController
的构造函数会接收一个预热过的
FlutterEngine
作为参数,并通过视图环境 (view environment) 注入。
import SwiftUI
import Flutter
struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
// Flutter dependencies are passed in through the view environment.
@Environment(FlutterDependencies.self) var flutterDependencies
func makeUIViewController(context: Context) -> some UIViewController {
return FlutterViewController(
engine: flutterDependencies.flutterEngine,
nibName: nil,
bundle: nil)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("My Flutter Feature") {
FlutterViewControllerRepresentable()
}
}
}
}
现在,你的 iOS 应用中集成了一个 Flutter 页面。
下面的例子展示了一个普通的 ViewController
,包含一个能跳转到 FlutterViewController
的 UIButton
,这个
FlutterViewController
使用在 AppDelegate
中创建的 Flutter 引擎 (FlutterEngine
)。
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 页面。
下面的例子中展示了在一个常见的 ViewController
,包含一个能跳转到 FlutterViewController
的 UIButton
,
FlutterViewController
会使用在 AppDelegate
中创建的 FlutterEngine
实例。
@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
现在,你的 iOS 应用中集成了一个 Flutter 页面。
或者 —— 使用隐式 FlutterEngine 创建 FlutterViewController
#上一个示例还有另一个选择,你可以让 FlutterViewController
隐式创建它自己的 FlutterEngine
,而不用提前预热 engine。
不过不建议这样做,因为按需创建FlutterEngine
的话,在 FlutterViewController
被 present 出来之后,第一帧图像渲染完之前,将会引入明显的延迟。但是当 Flutter 页面很少被展示时,当对决定何时启动 Dart VM 没有好的启发时,当 Flutter 无需在页面(view controller)之间保持状态时,此方式可能会有用。
为了不使用已经存在的 FlutterEngine
来展现 FlutterViewController
,省略 FlutterEngine
的创建步骤,并且在创建 FlutterViewController
时,去掉 engine 的引用。
import SwiftUI
import Flutter
struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
return FlutterViewController(
project: nil,
nibName: nil,
bundle: nil)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("My Flutter Feature") {
FlutterViewControllerRepresentable()
}
}
}
}
// Existing code omitted.
func showFlutter() {
let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
present(flutterViewController, animated: true, completion: nil)
}
// Existing code omitted.
- (void)showFlutter {
FlutterViewController *flutterViewController =
[[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
[self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
查看 加载顺序和性能 了解更多关于延迟和内存使用的探索。
使用 FlutterAppDelegate
#推荐让你应用的 UIApplicationDelegate
继承 FlutterAppDelegate
,但不是必须的。
FlutterAppDelegate
有这些功能:
-
传递应用的回调,例如
openURL
到 Flutter 的插件 —— local_auth。 -
当手机屏幕锁定时,在调试模式下保持 Flutter 连接处于开启状态。
创建 FlutterAppDelegate 子类
#启动 FlutterEngine 和 FlutterViewController
文档中展示了如何在使用 UIKit 的应用中创建 FlutterAppDelegate
子类。在使用 SwiftUI 的应用中,你可以创建一个 FlutterAppDelegate
的子类,并使用 Observable()
宏 (macro) 对其进行注解,如下所示:
import SwiftUI
import Flutter
import FlutterPluginRegistrant
@Observable
class AppDelegate: FlutterAppDelegate {
let 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();
// Used to connect plugins (only if you have plugins with iOS platform code).
GeneratedPluginRegistrant.register(with: self.flutterEngine);
return true;
}
}
@main
struct MyApp: App {
// Use this property wrapper to tell SwiftUI
// it should use the AppDelegate class for the application delegate
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
然后,在视图中,可以通过视图环境 (view environment) 访问 AppDelegate
。
import SwiftUI
import Flutter
struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
// Access the AppDelegate through the view environment.
@Environment(AppDelegate.self) var appDelegate
func makeUIViewController(context: Context) -> some UIViewController {
return FlutterViewController(
engine: appDelegate.flutterEngine,
nibName: nil,
bundle: nil)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("My Flutter Feature") {
FlutterViewControllerRepresentable()
}
}
}
}
如果不能直接让 FlutterAppDelegate 成为子类
#如果你的 app delegate 不能直接继承 FlutterAppDelegate
,让你的 app delegate 实现 FlutterAppLifeCycleProvider
协议,来确保 Flutter plugins 接收到必要的回调。否则,依赖这些事件的 plugins 将会有无法预估的行为。
例如:
import Foundation
import Flutter
@Observable
class AppDelegate: UIResponder, UIApplicationDelegate, FlutterAppLifeCycleProvider {
private let lifecycleDelegate = FlutterPluginAppLifeCycleDelegate()
let flutterEngine = FlutterEngine(name: "my flutter engine")
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
flutterEngine.run()
return lifecycleDelegate.application(application, didFinishLaunchingWithOptions: launchOptions ?? [:])
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
lifecycleDelegate.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
lifecycleDelegate.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
lifecycleDelegate.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return lifecycleDelegate.application(app, open: url, options: options)
}
func application(_ application: UIApplication, handleOpen url: URL) -> Bool {
return lifecycleDelegate.application(application, handleOpen: url)
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
return lifecycleDelegate.application(application, open: url, sourceApplication: sourceApplication ?? "", annotation: annotation)
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
lifecycleDelegate.application(application, performActionFor: shortcutItem, completionHandler: completionHandler)
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
lifecycleDelegate.application(application, handleEventsForBackgroundURLSession: identifier, completionHandler: completionHandler)
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
lifecycleDelegate.application(application, performFetchWithCompletionHandler: completionHandler)
}
func add(_ delegate: FlutterApplicationLifeCycleDelegate) {
lifecycleDelegate.add(delegate)
}
}
@import Flutter;
@import UIKit;
@import FlutterPluginRegistrant;
@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
在具体实现中,应该最大化地委托给
FlutterPluginAppLifeCycleDelegate
:
@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)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 {
[_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
启动选项
#例子中展示了使用默认启动选项运行 Flutter。
为了定制化你的 Flutter 运行时,你也可以置顶 Dart 入口、库和路由。
Dart 入口
#在 FlutterEngine
上调用 run
,默认将会调用你的 lib/main.dart
文件里的 main()
函数。
你也可以使用另一个入口方法 runWithEntrypoint
,并使用 NSString
字符串指定一个不同的 Dart 入口。
Dart 库
#另外,在指定 Dart 函数时,你可以指定特定文件的特定函数。
下面的例子使用 lib/other_file.dart
文件的
myOtherEntrypoint()
函数取代 lib/main.dart
的 main()
函数:
flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart")
[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];
路由
#当构建 engine 时,可以为你的 Flutter WidgetsApp
设置一个初始路由。
let flutterEngine = FlutterEngine()
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
engine.run(
withEntrypoint: "main", initialRoute: "/onboarding")
FlutterEngine *flutterEngine = [[FlutterEngine alloc] init];
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
[flutterEngine runWithEntrypoint:FlutterDefaultDartEntrypoint
initialRoute:@"/onboarding"];
这段代码使用 "/onboarding"
取代 "/"
,作为你的 dart:ui
的 PlatformDispatcher.defaultRouteName
你也可以直接构造 FlutterViewController 而不用提前初始化 FlutterEngine:
let flutterViewController = FlutterViewController(
project: nil, initialRoute: "/onboarding", nibName: nil, bundle: nil)
FlutterViewController* flutterViewController =
[[FlutterViewController alloc] initWithProject:nil
initialRoute:@"/onboarding"
nibName:nil
bundle:nil];
查看文档:路由和导航 了解更多 Flutter 路由的内容。
其它
#之前的例子仅仅展示了怎样定制 Flutter 实例初始化的几种方式,通过 撰写双端平台代码,你可以在 FlutterViewController
展示 Flutter UI 之前,自由地选择你喜欢的,推入数据和准备 Flutter 环境的方式。
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-12-02。 查看文档源码 或者 为本页面内容提出建议。