Skip to main content

Add a Flutter screen to an macOS app

Learn how to add a single Flutter screen to your existing macOS app.

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

Start a FlutterEngine and FlutterViewController

#

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

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

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

Create a FlutterEngine

#

Where you create a FlutterEngine depends on your host app.

In this example, we create a FlutterEngine object inside a SwiftUI Observable object called FlutterDependencies. Pre-warm the engine by calling run(), and then inject this object into a ContentView using the environment() view modifier.

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

As an example, we demonstrate creating a FlutterEngine, exposed as a property, on app startup in the app delegate.

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

Show a FlutterViewController with your FlutterEngine

#

The following example shows a generic ContentView with a NavigationLink hooked to a flutter screen. First, create a FlutterViewControllerRepresentable to represent the FlutterViewController. The FlutterViewController constructor takes the pre-warmed FlutterEngine as an argument, which is injected through the view environment.

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

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

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

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

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

Alternatively - Create a FlutterViewController with an implicit FlutterEngine

#

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

This is not usually 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.

To let the FlutterViewController present without an existing FlutterEngine, omit the FlutterEngine construction, and create the FlutterViewController without an engine reference.

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

See Loading sequence and performance for 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:

Creating a FlutterAppDelegate subclass

#

Creating a subclass of the FlutterAppDelegate in UIKit apps was shown in the Start a FlutterEngine and FlutterViewController section. In a SwiftUI app, you can create a subclass of the FlutterAppDelegate and annotate it with the Observable() macro as follows:

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

Then, in your view, the AppDelegate is accessible through the view environment.

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