跳转至正文

向 macOS 应用添加 Flutter 屏幕

了解如何向你现有的 macOS 应用添加单个 Flutter 屏幕。

本指南介绍如何向现有 macOS 应用添加单个 Flutter 屏幕。

启动 FlutterEngine 与 FlutterViewController

#

要从现有 macOS 应用启动 Flutter 屏幕,需要启动 FlutterEngineFlutterViewController

FlutterEngine 的生命周期可能与 FlutterViewController 相同,也可能更长。

有关预热引擎在延迟与内存方面的权衡,请参阅 加载顺序与性能

创建 FlutterEngine

#

创建 FlutterEngine 的位置取决于宿主应用。

本示例在名为 FlutterDependencies 的 SwiftUI Observable 对象内创建 FlutterEngine,通过调用 run() 预热引擎,再使用 environment() 视图修饰符将其注入 ContentView

MyApp.swift
swift
import SwiftUI
import FlutterMacOS
// The following library connects plugins with macOS platform code to this app.
import FlutterPluginRegistrant

@Observable
class FlutterDependencies {
 let flutterEngine = FlutterEngine(name: "my flutter engine", project: nil)
 init() {
   // Runs the default Dart entrypoint with a default Flutter route.
   flutterEngine.run(withEntrypoint: nil)
   // Connects plugins with macOS platform code to this app.
   RegisterGeneratedPlugins(registry: 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)
     }
   }
}

作为示例,我们在应用委托的应用启动时创建并暴露为属性的 FlutterEngine

AppDelegate.swift
swift
import Cocoa
import FlutterMacOS
// The following library connects plugins with macOS platform code to this app.
import FlutterPluginRegistrant

@main
class AppDelegate: FlutterAppDelegate {
  lazy var flutterEngine = FlutterEngine(name: "my flutter engine", project: nil)

  override func applicationDidFinishLaunching(_ aNotification: Notification) {
    flutterEngine.run(withEntrypoint: nil)
    RegisterGeneratedPlugins(registry: self.flutterEngine)
  }
}

使用 FlutterEngine 显示 FlutterViewController

#

以下示例展示带有连接到 Flutter 屏幕的 NavigationLink 的通用 ContentView。首先创建 FlutterViewControllerRepresentable 表示 FlutterViewControllerFlutterViewController 构造函数接收预热的 FlutterEngine 作为参数,通过视图环境注入。

ContentView.swift
swift
import SwiftUI
import FlutterMacOS

struct FlutterViewControllerRepresentable: NSViewControllerRepresentable {
  // Flutter dependencies are passed in through the view environment.
  @Environment(FlutterDependencies.self) var flutterDependencies

  func makeNSViewController(context: Context) -> FlutterViewController {
    return FlutterViewController(
      engine: flutterDependencies.flutterEngine,
      nibName: nil,
      bundle: nil
    )
  }

  func updateNSViewController(_ nsViewController: FlutterViewController, context: Context) {}
}

struct ContentView: View {
  var body: some View {
    NavigationStack {
      NavigationLink("My Flutter Feature") {
        FlutterViewControllerRepresentable()
      }
    }
  }
}

现在,你已在 macOS 应用中嵌入了 Flutter 屏幕。

以下示例展示带有 NSButton 的通用 ViewController,用于呈现 FlutterViewControllerFlutterViewController 使用在 AppDelegate 中创建的 FlutterEngine 实例。

ViewController.swift
swift
import Cocoa
import FlutterMacOS

class ViewController: NSViewController {

  override func viewDidLoad() {
    super.viewDidLoad()

    // Make a button to call the showFlutter function when pressed.
    let button =  NSButton(title: "Show Flutter!", target: self, action: #selector(showFlutter))
    button.frame = CGRect(x: 202, y: 187, width: 160.0, height: 40.0)
    self.view.addSubview(button)
  }

  @objc func showFlutter() {
    let flutterEngine = (NSApplication.shared.delegate as! AppDelegate).flutterEngine
    let flutterViewController =
        FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
    self.addChild(flutterViewController)
    flutterViewController.view.frame = self.view.bounds
    presentAsModalWindow(flutterViewController)
  }
}

现在,你已在 macOS 应用中嵌入了 Flutter 屏幕。

另一种方式 — 使用隐式 FlutterEngine 创建 FlutterViewController

#

作为上一示例的替代,可让 FlutterViewController 隐式创建自己的 FlutterEngine,而无需事先预热。

通常不推荐这样做,因为按需创建 FlutterEngine 可能在呈现 FlutterViewController 与渲染首帧之间引入明显延迟。但若 Flutter 屏幕很少显示、难以判断何时应启动 Dart VM,且 Flutter 无需在 view controller 之间保持状态时,这种方式可能有用。

要让 FlutterViewController 在没有现有 FlutterEngine 的情况下呈现,省略 FlutterEngine 的构建,并创建不带引擎引用的 FlutterViewController

ContentView.swift
swift
// Existing code omitted.
func makeNSViewController(context: Context) -> FlutterViewController {
  return FlutterViewController()
}
ViewController.swift
swift
// Existing code omitted.
func showFlutter() {
  let flutterViewController = FlutterViewController()
  self.addChild(flutterViewController)
  flutterViewController.view.frame = self.view.bounds
  presentAsModalWindow(flutterViewController)
}

有关延迟与内存使用的更多探讨,请参阅 加载顺序与性能

使用 FlutterAppDelegate

#

建议(但非必须)让应用的 UIApplicationDelegate 子类化 FlutterAppDelegate

FlutterAppDelegate 执行的功能包括:

创建 FlutterAppDelegate 子类

#

在 UIKit 应用中创建 FlutterAppDelegate 子类的方法见 启动 FlutterEngine 与 FlutterViewController 一节。在 SwiftUI 应用中,可创建 FlutterAppDelegate 子类并用 Observable() 宏标注,如下所示:

MyApp.swift
swift
import SwiftUI
import FlutterMacOS

@Observable
class AppDelegate: FlutterAppDelegate {
  let flutterEngine = FlutterEngine(name: "my flutter engine", project: nil)

  override func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Runs the default Dart entrypoint with a default Flutter route.
    flutterEngine.run(withEntrypoint: nil)
    // Used to connect plugins (only if you have plugins
    // with macOS platform code).
    RegisterGeneratedPlugins(registry: self.flutterEngine)
  }
}

@main
struct MyApp: App {
  // Use this property wrapper to tell SwiftUI
  // it should use the AppDelegate class for the application delegate
  @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

随后,在视图中可通过视图环境访问 AppDelegate

ContentView.swift
swift
import SwiftUI
import FlutterMacOS

struct FlutterViewControllerRepresentable: NSViewControllerRepresentable {
  // Access the AppDelegate through the view environment.
  @Environment(AppDelegate.self) var appDelegate

  func makeNSViewController(context: Context) -> FlutterViewController {
    return FlutterViewController(
      engine: appDelegate.flutterEngine,
      nibName: nil,
      bundle: nil
    )
  }

  func updateNSViewController(_ nsViewController: FlutterViewController, context: Context) {}
}

struct ContentView: View {
  var body: some View {
    NavigationStack {
      NavigationLink("My Flutter Feature") {
        FlutterViewControllerRepresentable()
      }
    }
  }
}