Reland [macOS] Make FlutterEngine support multiple views (#39576)

diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h
index 957cae0..7c7bea7 100644
--- a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h
+++ b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h
@@ -32,6 +32,9 @@
 
 /**
  * Coordinates a single instance of execution of a Flutter engine.
+ *
+ * A FlutterEngine can only be attached with one controller from the native
+ * code.
  */
 FLUTTER_DARWIN_EXPORT
 @interface FlutterEngine : NSObject <FlutterTextureRegistry, FlutterPluginRegistry>
@@ -76,10 +79,9 @@
 - (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint;
 
 /**
- * The default `FlutterViewController` associated with this engine, if any.
+ * The `FlutterViewController` of this engine, if any.
  *
- * The default view always has ID kFlutterDefaultViewId, and is the view
- * operated by the APIs that do not have a view ID specified.
+ * This view is used by legacy APIs that assume a single view.
  *
  * Setting this field from nil to a non-nil view controller also updates
  * the view controller's engine and ID.
diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h b/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h
index c341f23..39fbba1 100644
--- a/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h
+++ b/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h
@@ -36,14 +36,20 @@
 @property(nonnull, readonly) id<FlutterTextureRegistry> textures;
 
 /**
- * The view displaying Flutter content. May return |nil|, for instance in a headless environment.
+ * The default view displaying Flutter content.
  *
- * WARNING: If/when multiple Flutter views within the same application are supported (#30701), this
- * API will change.
+ * This method may return |nil|, for instance in a headless environment.
+ *
+ * The default view is a special view operated by single-view APIs.
  */
 @property(nullable, readonly) NSView* view;
 
 /**
+ * The `NSView` associated with the given view ID, if any.
+ */
+- (nullable NSView*)viewForId:(uint64_t)viewId;
+
+/**
  * Registers |delegate| to receive handleMethodCall:result: callbacks for the given |channel|.
  */
 - (void)addMethodCallDelegate:(nonnull id<FlutterPlugin>)delegate
diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h b/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h
index e4d8df7..5979131 100644
--- a/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h
+++ b/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h
@@ -89,7 +89,9 @@
     NS_DESIGNATED_INITIALIZER;
 - (nonnull instancetype)initWithCoder:(nonnull NSCoder*)nibNameOrNil NS_DESIGNATED_INITIALIZER;
 /**
- * Initializes this FlutterViewController with the specified `FlutterEngine`.
+ * Initializes this FlutterViewController with an existing `FlutterEngine`.
+ *
+ * The initialized view controller will add itself to the engine as part of this process.
  *
  * This initializer is suitable for both the first Flutter view controller and
  * the following ones of the app.
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm
index 7417315..51b874f 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm
@@ -19,7 +19,7 @@
   // TODO(dkwingsmt): This class only supports single-view for now. As more
   // classes are gradually converted to multi-view, it should get the view ID
   // from somewhere.
-  FlutterView* view = [view_provider_ getView:kFlutterDefaultViewId];
+  FlutterView* view = [view_provider_ viewForId:kFlutterDefaultViewId];
   if (!view) {
     return false;
   }
@@ -37,7 +37,7 @@
 bool FlutterCompositor::Present(uint64_t view_id,
                                 const FlutterLayer** layers,
                                 size_t layers_count) {
-  FlutterView* view = [view_provider_ getView:view_id];
+  FlutterView* view = [view_provider_ viewForId:view_id];
   if (!view) {
     return false;
   }
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm
index bd6bf4d..4005b17 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm
@@ -11,6 +11,8 @@
 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h"
 #import "flutter/testing/testing.h"
 
+extern const uint64_t kFlutterDefaultViewId;
+
 @interface FlutterViewMockProvider : NSObject <FlutterViewProvider> {
   FlutterView* _defaultView;
 }
@@ -30,7 +32,7 @@
   return self;
 }
 
-- (nullable FlutterView*)getView:(uint64_t)viewId {
+- (nullable FlutterView*)viewForId:(uint64_t)viewId {
   if (viewId == kFlutterDefaultViewId) {
     return _defaultView;
   }
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm
index 9fc17bd..2c5aef3 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm
@@ -85,6 +85,29 @@
 - (nullable FlutterViewController*)viewControllerForId:(uint64_t)viewId;
 
 /**
+ * An internal method that adds the view controller with the given ID.
+ *
+ * This method assigns the controller with the ID, puts the controller into the
+ * map, and does assertions related to the default view ID.
+ */
+- (void)registerViewController:(FlutterViewController*)controller forId:(uint64_t)viewId;
+
+/**
+ * An internal method that removes the view controller with the given ID.
+ *
+ * This method clears the ID of the controller, removes the controller from the
+ * map. This is an no-op if the view ID is not associated with any view
+ * controllers.
+ */
+- (void)deregisterViewControllerForId:(uint64_t)viewId;
+
+/**
+ * Shuts down the engine if view requirement is not met, and headless execution
+ * is not allowed.
+ */
+- (void)shutDownIfNeeded;
+
+/**
  * Sends the list of user-preferred locales to the Flutter engine.
  */
 - (void)sendUserLocales;
@@ -141,6 +164,8 @@
   FlutterEngine* _flutterEngine;
 }
 
+@dynamic view;
+
 - (instancetype)initWithPlugin:(NSString*)pluginKey flutterEngine:(FlutterEngine*)flutterEngine {
   self = [super init];
   if (self) {
@@ -161,10 +186,18 @@
 }
 
 - (NSView*)view {
-  if (!_flutterEngine.viewController.viewLoaded) {
-    [_flutterEngine.viewController loadView];
+  return [self viewForId:kFlutterDefaultViewId];
+}
+
+- (NSView*)viewForId:(uint64_t)viewId {
+  FlutterViewController* controller = [_flutterEngine viewControllerForId:viewId];
+  if (controller == nil) {
+    return nil;
   }
-  return _flutterEngine.viewController.flutterView;
+  if (!controller.viewLoaded) {
+    [controller loadView];
+  }
+  return controller.flutterView;
 }
 
 - (void)addMethodCallDelegate:(nonnull id<FlutterPlugin>)delegate
@@ -214,6 +247,11 @@
   // when the engine is destroyed.
   std::unique_ptr<flutter::FlutterCompositor> _macOSCompositor;
 
+  // The information of all views attached to this engine mapped from IDs.
+  //
+  // It can't use NSDictionary, because the values need to be weak references.
+  NSMapTable* _viewControllers;
+
   // FlutterCompositor is copied and used in embedder.cc.
   FlutterCompositor _compositor;
 
@@ -230,6 +268,8 @@
 
   // A method channel for miscellaneous platform functionality.
   FlutterMethodChannel* _platformChannel;
+
+  int _nextViewId;
 }
 
 - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project {
@@ -249,10 +289,14 @@
   _semanticsEnabled = NO;
   _isResponseValid = [[NSMutableArray alloc] initWithCapacity:1];
   [_isResponseValid addObject:@YES];
+  // kFlutterDefaultViewId is reserved for the default view.
+  // All IDs above it are for regular views.
+  _nextViewId = kFlutterDefaultViewId + 1;
 
   _embedderAPI.struct_size = sizeof(FlutterEngineProcTable);
   FlutterEngineGetProcAddresses(&_embedderAPI);
 
+  _viewControllers = [NSMapTable weakToWeakObjectsMapTable];
   _renderer = [[FlutterRenderer alloc] initWithFlutterEngine:self];
 
   NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
@@ -284,7 +328,7 @@
     return NO;
   }
 
-  if (!_allowHeadlessExecution && !_viewController) {
+  if (!_allowHeadlessExecution && [_viewControllers count] == 0) {
     NSLog(@"Attempted to run an engine with no view controller without headless mode enabled.");
     return NO;
   }
@@ -311,8 +355,11 @@
   flutterArguments.platform_message_callback = (FlutterPlatformMessageCallback)OnPlatformMessage;
   flutterArguments.update_semantics_callback = [](const FlutterSemanticsUpdate* update,
                                                   void* user_data) {
+    // TODO(dkwingsmt): This callback only supports single-view, therefore it
+    // only operates on the default view. To support multi-view, we need a
+    // way to pass in the ID (probably through FlutterSemanticsUpdate).
     FlutterEngine* engine = (__bridge FlutterEngine*)user_data;
-    [engine.viewController updateSemantics:update];
+    [[engine viewControllerForId:kFlutterDefaultViewId] updateSemantics:update];
   };
   flutterArguments.custom_dart_entrypoint = entrypoint.UTF8String;
   flutterArguments.shutdown_dart_vm_when_done = true;
@@ -374,7 +421,14 @@
   }
 
   [self sendUserLocales];
-  [self updateWindowMetrics];
+
+  // Update window metric for all view controllers.
+  NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator];
+  FlutterViewController* nextViewController;
+  while ((nextViewController = [viewControllerEnumerator nextObject])) {
+    [self updateWindowMetricsForViewController:nextViewController];
+  }
+
   [self updateDisplayConfig];
   // Send the initial user settings such as brightness and text scale factor
   // to the engine.
@@ -408,28 +462,59 @@
   }
 }
 
+- (void)registerViewController:(FlutterViewController*)controller forId:(uint64_t)viewId {
+  NSAssert(controller != nil, @"The controller must not be nil.");
+  NSAssert(![controller attached],
+           @"The incoming view controller is already attached to an engine.");
+  NSAssert([_viewControllers objectForKey:@(viewId)] == nil, @"The requested view ID is occupied.");
+  [controller attachToEngine:self withId:viewId];
+  NSAssert(controller.id == viewId, @"Failed to assign view ID.");
+  [_viewControllers setObject:controller forKey:@(viewId)];
+}
+
+- (void)deregisterViewControllerForId:(uint64_t)viewId {
+  FlutterViewController* oldController = [self viewControllerForId:viewId];
+  if (oldController != nil) {
+    [oldController detachFromEngine];
+    [_viewControllers removeObjectForKey:@(viewId)];
+  }
+}
+
+- (void)shutDownIfNeeded {
+  if ([_viewControllers count] == 0 && !_allowHeadlessExecution) {
+    [self shutDownEngine];
+  }
+}
+
+- (FlutterViewController*)viewControllerForId:(uint64_t)viewId {
+  FlutterViewController* controller = [_viewControllers objectForKey:@(viewId)];
+  NSAssert(controller == nil || controller.id == viewId,
+           @"The stored controller has unexpected view ID.");
+  return controller;
+}
+
 - (void)setViewController:(FlutterViewController*)controller {
-  if (_viewController == controller) {
+  FlutterViewController* currentController =
+      [_viewControllers objectForKey:@(kFlutterDefaultViewId)];
+  if (currentController == controller) {
     // From nil to nil, or from non-nil to the same controller.
     return;
   }
-  if (_viewController == nil && controller != nil) {
+  if (currentController == nil && controller != nil) {
     // From nil to non-nil.
     NSAssert(controller.engine == nil,
              @"Failed to set view controller to the engine: "
              @"The given FlutterViewController is already attached to an engine %@. "
              @"If you wanted to create an FlutterViewController and set it to an existing engine, "
-             @"you should create it with init(engine:, nibName, bundle:) instead.",
+             @"you should use FlutterViewController#init(engine:, nibName, bundle:) instead.",
              controller.engine);
-    _viewController = controller;
-    [_viewController attachToEngine:self withId:kFlutterDefaultViewId];
-  } else if (_viewController != nil && controller == nil) {
+    [self registerViewController:controller forId:kFlutterDefaultViewId];
+  } else if (currentController != nil && controller == nil) {
+    NSAssert(currentController.id == kFlutterDefaultViewId,
+             @"The default controller has an unexpected ID %llu", currentController.id);
     // From non-nil to nil.
-    [_viewController detachFromEngine];
-    _viewController = nil;
-    if (!_allowHeadlessExecution) {
-      [self shutDownEngine];
-    }
+    [self deregisterViewControllerForId:kFlutterDefaultViewId];
+    [self shutDownIfNeeded];
   } else {
     // From non-nil to a different non-nil view controller.
     NSAssert(NO,
@@ -437,10 +522,14 @@
              @"The engine already has a default view controller %@. "
              @"If you wanted to make the default view render in a different window, "
              @"you should attach the current view controller to the window instead.",
-             _viewController);
+             [_viewControllers objectForKey:@(kFlutterDefaultViewId)]);
   }
 }
 
+- (FlutterViewController*)viewController {
+  return [self viewControllerForId:kFlutterDefaultViewId];
+}
+
 - (FlutterCompositor*)createFlutterCompositor {
   _macOSCompositor = std::make_unique<flutter::FlutterCompositor>(
       [[FlutterViewEngineProvider alloc] initWithEngine:self], _platformViewController);
@@ -485,6 +574,17 @@
 
 #pragma mark - Framework-internal methods
 
+- (void)addViewController:(FlutterViewController*)controller {
+  [self registerViewController:controller forId:kFlutterDefaultViewId];
+}
+
+- (void)removeViewController:(nonnull FlutterViewController*)viewController {
+  NSAssert([viewController attached] && viewController.engine == self,
+           @"The given view controller is not associated with this engine.");
+  [self deregisterViewControllerForId:viewController.id];
+  [self shutDownIfNeeded];
+}
+
 - (BOOL)running {
   return _engine != nullptr;
 }
@@ -544,11 +644,19 @@
   return [[[NSProcessInfo processInfo] arguments] firstObject] ?: @"Flutter";
 }
 
-- (void)updateWindowMetrics {
-  if (!_engine || !self.viewController.viewLoaded) {
+- (void)updateWindowMetricsForViewController:(FlutterViewController*)viewController {
+  if (viewController.id != kFlutterDefaultViewId) {
+    // TODO(dkwingsmt): The embedder API only supports single-view for now. As
+    // embedder APIs are converted to multi-view, this method should support any
+    // views.
     return;
   }
-  NSView* view = self.viewController.flutterView;
+  if (!_engine || !viewController || !viewController.viewLoaded) {
+    return;
+  }
+  NSAssert([self viewControllerForId:viewController.id] == viewController,
+           @"The provided view controller is not attached to this engine.");
+  NSView* view = viewController.flutterView;
   CGRect scaledBounds = [view convertRectToBacking:view.bounds];
   CGSize scaledSize = scaledBounds.size;
   double pixelRatio = view.bounds.size.width == 0 ? 1 : scaledSize.width / view.bounds.size.width;
@@ -579,7 +687,14 @@
     return;
   }
   _semanticsEnabled = enabled;
-  [_viewController notifySemanticsEnabledChanged];
+
+  // Update all view controllers' bridges.
+  NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator];
+  FlutterViewController* nextViewController;
+  while ((nextViewController = [viewControllerEnumerator nextObject])) {
+    [nextViewController notifySemanticsEnabledChanged];
+  }
+
   _embedderAPI.UpdateSemanticsEnabled(_engine, _semanticsEnabled);
 }
 
@@ -595,17 +710,6 @@
 
 #pragma mark - Private methods
 
-- (FlutterViewController*)viewControllerForId:(uint64_t)viewId {
-  // TODO(dkwingsmt): The engine only supports single-view, therefore it
-  // only processes the default ID. After the engine supports multiple views,
-  // this method should be able to return the view for any IDs.
-  NSAssert(viewId == kFlutterDefaultViewId, @"Unexpected view ID %llu", viewId);
-  if (viewId == kFlutterDefaultViewId) {
-    return _viewController;
-  }
-  return nil;
-}
-
 - (void)sendUserLocales {
   if (!self.running) {
     return;
@@ -669,8 +773,10 @@
 }
 
 - (void)engineCallbackOnPreEngineRestart {
-  if (_viewController) {
-    [_viewController onPreEngineRestart];
+  NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator];
+  FlutterViewController* nextViewController;
+  while ((nextViewController = [viewControllerEnumerator nextObject])) {
+    [nextViewController onPreEngineRestart];
   }
 }
 
@@ -682,7 +788,11 @@
     return;
   }
 
-  [self.viewController.flutterView shutdown];
+  NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator];
+  FlutterViewController* nextViewController;
+  while ((nextViewController = [viewControllerEnumerator nextObject])) {
+    [nextViewController.flutterView shutdown];
+  }
 
   FlutterEngineResult result = _embedderAPI.Deinitialize(_engine);
   if (result != kSuccess) {
@@ -747,7 +857,12 @@
 
 - (void)onAccessibilityStatusChanged:(NSNotification*)notification {
   BOOL enabled = [notification.userInfo[kEnhancedUserInterfaceKey] boolValue];
-  [self.viewController onAccessibilityStatusChanged:enabled];
+  NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator];
+  FlutterViewController* nextViewController;
+  while ((nextViewController = [viewControllerEnumerator nextObject])) {
+    [nextViewController onAccessibilityStatusChanged:enabled];
+  }
+
   self.semanticsEnabled = enabled;
 }
 
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm
index 911ff76..d3b2be0 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm
@@ -614,6 +614,69 @@
   rasterThread.join();
 }
 
+TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByController) {
+  NSString* fixtures = @(flutter::testing::GetFixturesPath());
+  FlutterDartProject* project = [[FlutterDartProject alloc]
+      initWithAssetsPath:fixtures
+             ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
+
+  FlutterEngine* engine;
+  FlutterViewController* viewController1;
+
+  @autoreleasepool {
+    // Create FVC1.
+    viewController1 = [[FlutterViewController alloc] initWithProject:project];
+    EXPECT_EQ(viewController1.id, 0ull);
+
+    engine = viewController1.engine;
+    engine.viewController = nil;
+
+    // Create FVC2 based on the same engine.
+    FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
+                                                                                   nibName:nil
+                                                                                    bundle:nil];
+    EXPECT_EQ(engine.viewController, viewController2);
+  }
+  // FVC2 is deallocated but FVC1 is retained.
+
+  EXPECT_EQ(engine.viewController, nil);
+
+  engine.viewController = viewController1;
+  EXPECT_EQ(engine.viewController, viewController1);
+  EXPECT_EQ(viewController1.id, 0ull);
+}
+
+TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByEngine) {
+  // Don't create the engine with `CreateMockFlutterEngine`, because it adds
+  // additional references to FlutterViewControllers, which is crucial to this
+  // test case.
+  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
+                                                      project:nil
+                                       allowHeadlessExecution:NO];
+  FlutterViewController* viewController1;
+
+  @autoreleasepool {
+    viewController1 = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
+    EXPECT_EQ(viewController1.id, 0ull);
+    EXPECT_EQ(engine.viewController, viewController1);
+
+    engine.viewController = nil;
+
+    FlutterViewController* viewController2 = [[FlutterViewController alloc] initWithEngine:engine
+                                                                                   nibName:nil
+                                                                                    bundle:nil];
+    EXPECT_EQ(viewController2.id, 0ull);
+    EXPECT_EQ(engine.viewController, viewController2);
+  }
+  // FVC2 is deallocated but FVC1 is retained.
+
+  EXPECT_EQ(engine.viewController, nil);
+
+  engine.viewController = viewController1;
+  EXPECT_EQ(engine.viewController, viewController1);
+  EXPECT_EQ(viewController1.id, 0ull);
+}
+
 }  // namespace flutter::testing
 
 // NOLINTEND(clang-analyzer-core.StackAddressEscape)
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h
index 6eed864..1879cb9 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h
+++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h
@@ -48,9 +48,39 @@
 @property(nonatomic, readonly, nonnull) NSPasteboard* pasteboard;
 
 /**
- * Informs the engine that the associated view controller's view size has changed.
+ * Attach a view controller to the engine as its default controller.
+ *
+ * Practically, since FlutterEngine can only be attached with one controller,
+ * the given controller, if successfully attached, will always have the default
+ * view ID kFlutterDefaultViewId.
+ *
+ * The engine holds a weak reference to the attached view controller.
+ *
+ * If the given view controller is already attached to an engine, this call
+ * throws an assertion.
  */
-- (void)updateWindowMetrics;
+- (void)addViewController:(nonnull FlutterViewController*)viewController;
+
+/**
+ * Dissociate the given view controller from this engine.
+ *
+ * Practically, since FlutterEngine can only be attached with one controller,
+ * the given controller must be the default view controller.
+ *
+ * If the view controller is not associated with this engine, this call throws an
+ * assertion.
+ */
+- (void)removeViewController:(nonnull FlutterViewController*)viewController;
+
+/**
+ * The `FlutterViewController` associated with the given view ID, if any.
+ */
+- (nullable FlutterViewController*)viewControllerForId:(uint64_t)viewId;
+
+/**
+ * Informs the engine that the specified view controller's window metrics have changed.
+ */
+- (void)updateWindowMetricsForViewController:(nonnull FlutterViewController*)viewController;
 
 /**
  * Dispatches the given pointer event data to engine.
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm
index 78c18dd..ad50476 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm
@@ -57,7 +57,7 @@
 /**
  * The text input plugin set by initialization.
  */
-@property(nonatomic) id<FlutterKeyboardViewDelegate> viewDelegate;
+@property(nonatomic, weak) id<FlutterKeyboardViewDelegate> viewDelegate;
 
 /**
  * The primary responders added by addPrimaryResponder.
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm b/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm
index 007a3cc..d7d2c2f 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm
@@ -89,7 +89,7 @@
 #pragma mark - Embedder callback implementations.
 
 - (FlutterMetalTexture)createTextureForView:(uint64_t)viewId size:(CGSize)size {
-  FlutterView* view = [_viewProvider getView:viewId];
+  FlutterView* view = [_viewProvider viewForId:viewId];
   NSAssert(view != nil, @"Can't create texture on a non-existent view 0x%llx.", viewId);
   if (view == nil) {
     // FlutterMetalTexture has texture `null`, therefore is discarded.
@@ -99,7 +99,7 @@
 }
 
 - (BOOL)present:(uint64_t)viewId texture:(const FlutterMetalTexture*)texture {
-  FlutterView* view = [_viewProvider getView:viewId];
+  FlutterView* view = [_viewProvider viewForId:viewId];
   if (view == nil) {
     return NO;
   }
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm
index 01146e1..46da658 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm
@@ -9,37 +9,54 @@
 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h"
 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h"
+#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
 #include "flutter/shell/platform/embedder/embedder.h"
 #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
 #include "flutter/testing/testing.h"
 
+@interface RendererTestViewController : FlutterViewController
+- (void)loadMockFlutterView:(FlutterView*)mockView;
+@end
+
+@implementation RendererTestViewController {
+  FlutterView* _mockFlutterView;
+}
+
+- (void)loadMockFlutterView:(FlutterView*)mockView {
+  _mockFlutterView = mockView;
+  [self loadView];
+}
+
+- (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id<MTLDevice>)device
+                                          commandQueue:(id<MTLCommandQueue>)commandQueue {
+  return _mockFlutterView;
+}
+@end
+
 namespace flutter::testing {
 
 namespace {
 // Returns an engine configured for the test fixture resource configuration.
-FlutterEngine* CreateTestEngine() {
+RendererTestViewController* CreateTestViewController() {
   NSString* fixtures = @(testing::GetFixturesPath());
   FlutterDartProject* project = [[FlutterDartProject alloc]
       initWithAssetsPath:fixtures
              ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
-  return [[FlutterEngine alloc] initWithName:@"test" project:project allowHeadlessExecution:true];
-}
-
-void SetEngineDefaultView(FlutterEngine* engine, id flutterView) {
-  id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
-  OCMStub([mockFlutterViewController flutterView]).andReturn(flutterView);
-  [engine setViewController:mockFlutterViewController];
+  RendererTestViewController* viewController =
+      [[RendererTestViewController alloc] initWithProject:project];
+  return viewController;
 }
 
 }  // namespace
 
 TEST(FlutterRenderer, PresentDelegatesToFlutterView) {
-  FlutterEngine* engine = CreateTestEngine();
-  FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine];
+  RendererTestViewController* viewController = CreateTestViewController();
+  FlutterEngine* engine = viewController.engine;
 
   id viewMock = OCMClassMock([FlutterView class]);
-  SetEngineDefaultView(engine, viewMock);
+  [viewController loadMockFlutterView:viewMock];
+  FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine];
 
   id surfaceManagerMock = OCMClassMock([FlutterSurfaceManager class]);
   OCMStub([viewMock surfaceManager]).andReturn(surfaceManagerMock);
@@ -61,11 +78,12 @@
 }
 
 TEST(FlutterRenderer, TextureReturnedByFlutterView) {
-  FlutterEngine* engine = CreateTestEngine();
-  FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine];
+  RendererTestViewController* viewController = CreateTestViewController();
+  FlutterEngine* engine = viewController.engine;
 
   id viewMock = OCMClassMock([FlutterView class]);
-  SetEngineDefaultView(engine, viewMock);
+  [viewController loadMockFlutterView:viewMock];
+  FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine];
 
   id surfaceManagerMock = OCMClassMock([FlutterSurfaceManager class]);
   OCMStub([viewMock surfaceManager]).andReturn(surfaceManagerMock);
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
index 72d67f1..994cc8f 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
@@ -309,11 +309,12 @@
             @"The FlutterViewController is unexpectedly attached to "
             @"engine %@ before initialization.",
             controller.engine);
-  engine.viewController = controller;
+  [engine addViewController:controller];
   NSCAssert(controller.engine != nil,
             @"The FlutterViewController unexpectedly stays unattached after initialization. "
             @"In unit tests, this is likely because either the FlutterViewController or "
-            @"the FlutterEngine is mocked. Please subclass these classes instead.");
+            @"the FlutterEngine is mocked. Please subclass these classes instead.",
+            controller.engine, controller.id);
   controller->_mouseTrackingMode = FlutterMouseTrackingModeInKeyWindow;
   controller->_textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:controller];
   [controller initializeKeyboard];
@@ -355,11 +356,6 @@
                        nibName:(nullable NSString*)nibName
                         bundle:(nullable NSBundle*)nibBundle {
   NSAssert(engine != nil, @"Engine is required");
-  NSAssert(engine.viewController == nil,
-           @"The supplied FlutterEngine is already used with FlutterViewController "
-            "instance. One instance of the FlutterEngine can only be attached to one "
-            "FlutterViewController at a time. Set FlutterEngine.viewController "
-            "to nil before attaching it to another FlutterViewController.");
 
   self = [super initWithNibName:nibName bundle:nibBundle];
   if (self) {
@@ -412,7 +408,9 @@
 }
 
 - (void)dealloc {
-  _engine.viewController = nil;
+  if ([self attached]) {
+    [_engine removeViewController:self];
+  }
   CFNotificationCenterRef cfCenter = CFNotificationCenterGetDistributedCenter();
   CFNotificationCenterRemoveEveryObserver(cfCenter, (__bridge void*)self);
 }
@@ -583,8 +581,7 @@
 - (void)initializeKeyboard {
   // TODO(goderbauer): Seperate keyboard/textinput stuff into ViewController specific and Engine
   // global parts. Move the global parts to FlutterEngine.
-  __weak FlutterViewController* weakSelf = self;
-  _keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:weakSelf];
+  _keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:self];
 }
 
 - (void)dispatchMouseEvent:(nonnull NSEvent*)event {
@@ -795,7 +792,7 @@
  * Responds to view reshape by notifying the engine of the change in dimensions.
  */
 - (void)viewDidReshape:(NSView*)view {
-  [_engine updateWindowMetrics];
+  [_engine updateWindowMetricsForViewController:self];
 }
 
 #pragma mark - FlutterPluginRegistry
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.mm
index a063784..95c3aa5 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.mm
@@ -22,14 +22,8 @@
   return self;
 }
 
-- (nullable FlutterView*)getView:(uint64_t)viewId {
-  // TODO(dkwingsmt): This class only supports the first view for now. After
-  // FlutterEngine supports multi-view, it should get the view associated to the
-  // ID.
-  if (viewId == kFlutterDefaultViewId) {
-    return _engine.viewController.flutterView;
-  }
-  return nil;
+- (nullable FlutterView*)viewForId:(uint64_t)viewId {
+  return [_engine viewControllerForId:viewId].flutterView;
 }
 
 @end
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm
index 6a1aa66..86c94a7 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProviderTest.mm
@@ -6,6 +6,7 @@
 #import <Foundation/Foundation.h>
 #import <OCMock/OCMock.h>
 
+#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h"
 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.h"
@@ -19,23 +20,29 @@
 
 TEST(FlutterViewEngineProviderUnittests, GetViewReturnsTheCorrectView) {
   FlutterViewEngineProvider* viewProvider;
-  id mockEngine = OCMClassMock([FlutterEngine class]);
+  id mockEngine = CreateMockFlutterEngine(@"");
   __block id mockFlutterViewController;
-  OCMStub([mockEngine viewController]).andDo(^(NSInvocation* invocation) {
-    if (mockFlutterViewController != nil) {
-      [invocation setReturnValue:&mockFlutterViewController];
-    }
-  });
+  OCMStub([mockEngine viewControllerForId:0])
+      .ignoringNonObjectArgs()
+      .andDo(^(NSInvocation* invocation) {
+        uint64_t viewId;
+        [invocation getArgument:&viewId atIndex:2];
+        if (viewId == 0 /* kFlutterDefaultViewId */) {
+          if (mockFlutterViewController != nil) {
+            [invocation setReturnValue:&mockFlutterViewController];
+          }
+        }
+      });
   viewProvider = [[FlutterViewEngineProvider alloc] initWithEngine:mockEngine];
 
   // When the view controller is not set, the returned view is nil.
-  EXPECT_EQ([viewProvider getView:0], nil);
+  EXPECT_EQ([viewProvider viewForId:0], nil);
 
   // When the view controller is set, the returned view is the controller's view.
   mockFlutterViewController = OCMStrictClassMock([FlutterViewController class]);
   id mockView = OCMStrictClassMock([FlutterView class]);
   OCMStub([mockFlutterViewController flutterView]).andReturn(mockView);
-  EXPECT_EQ([viewProvider getView:0], mockView);
+  EXPECT_EQ([viewProvider viewForId:0], mockView);
 }
 
 }  // namespace flutter::testing
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h b/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h
index f873c72..9548b7a 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h
+++ b/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h
@@ -20,6 +20,6 @@
  *
  * Returns nil if the ID is invalid.
  */
-- (nullable FlutterView*)getView:(uint64_t)id;
+- (nullable FlutterView*)viewForId:(uint64_t)id;
 
 @end