Supporting Multiple Scenes with Flutter on iPad

Michael Collins
Neudesic Innovation
8 min readFeb 13, 2022
Photo by Nicolas Thomas on Unsplash

I love to learn things, but I have a nasty habit. Whenever I jump into a new technology that works on an existing platform that I know and love, I always try to jump into the most undocumented feature to try to see if I can do it. This has been the case with my journey into Flutter. I have seen and used Flutter apps on my iPhone and on my Google Pixel 6 Pro, but I am not sure if I have used a Flutter app on my iPad Pro. I love developing apps for my iPad Pro and I figure that if I am going to learn to build apps using Flutter, then I want to build apps that look good on an iPad.

One of the coolest and most underused and misunderstood features of iPad applications is the ability to have multiple application windows and run different scenes in each window. In my first Flutter app, I dove in to see how well Flutter works on an iPad and whether it is possible to build a multi-scene app using Flutter. This post documents my journey and the (rather positive) end result.

The Challenge Begins

I began this challenge by installing the Flutter SDK (not actually true; I’ve had it installed, but haven’t really gotten around to doing anything with it) on my MacBook Pro (M1 MAX with 64 GB memory). I next created a starter app skeleton using the Flutter CLI:

flutter create pleasebrewcoffee

📝 I registered pleasebrewcoffee.app and pleasebrew.coffee, so if this app goes anywhere, then I’m ready for it!

I have my local Flutter SDK configured for all platforms, so my starter application includes the runtime code for Android, iOS, web, macOS, Linux, and Microsoft Windows. I’m going to focus on the iOS target only in this post. I’ll leave my exploration of the other platforms for future posts.

Looking at the starter runtime that was generated for iOS, I see:

  • An Info.plist file with the application’s settings.
  • Some .xcconfig files for configuring the debug and release builds.
  • GeneratedPluginRegistrant.h and GeneratedPluginRegistrant.m. I have no idea what these do yet.
  • AppDelegate.swift that contains the implementation of the standard AppDelegate class.
  • LaunchScreen.storyboard that shows the standard iOS launch screen when the application is loading and launching.
  • Main.storyboard which implements the main view controller for the application. Looking at Main.storyboard, I can see that the starting view controller is a FlutterViewController and I am deducing that this view controller starts the execution of the Flutter application.

Given that Flutter is open source, I do some keen Google heroics and find the source code for the Flutter runtime. I see that my new AppDelegate class inherits from FlutterAppDelegate and I do a quick read through of FlutterAppDelegate to see what it does. It looks like FlutterAppDelegate captures lifecycle events and provides a way for Flutter plugins to tap into the application lifecycle events. Plugins are beyond my comprehension at this point, so I moved on.

Looking at the Flutter source code, I see that only one file has been created: main.dart. main.dart contains the main function that runs the Flutter application. There’s a MyApp stateless widget that hosts the UI for the application and a MyHomePage stateful widget that really presents the UI.

Introducing the Main Scene

I began my exploration by opening the iOS Runner workspace in Xcode. I went to the project settings and enabled multiple windows. This had the effect of adding the scene manifest in the Info.plist file. I then added MainScene to the first entry in the scene list and setting the delegate class to $(PRODUCT_MODULE_NAME).MainSceneDelegate.

Back to my heroic Google searching capabilities, I came across an article on StackOverflow that gave me hints on how to create a scene delegate for Flutter. But further searching led me to the Flutter Samples GitHub repository that included a sample showing a scene delegate and how to use multiple Flutter engines in an application. I also learned from this sample that it is possible to define multiple entry points (main functions) that can be used to run different application scenes. With this little knowledge in hand, it was time to explore to see what I could do.

I created the MainSceneDelegate class and implementing the scene(_:willConnectTo:options:) function to launch the scene. I realized from my initial research that the Flutter application started when a FlutterViewController started running, and this was when the Main storyboard was loaded. Since I introduced a scene that would now be called to run the application and the Main storyboard wasn’t being used anymore, the scene would override the default behavior.

Using the Flutter sample, I read up on Flutter engine groups and the performance benefits of them, so I started to use that. I updated my AppDelegate class to maintain a Flutter engine group:

My MainDelegate class can then use that to create a new Flutter engine for the scene:

When the scene is connected, I can use the AppDelegate’s Flutter engine group to create a new Flutter engine for the scene. By passing nil as the value of the withEntrypoint parameter, Flutter will call the default main method to start the Flutter application.

I tested the application and the scene delegate was called successfully and the Flutter application ran. This was good news. Now to test the scene delegate and Flutter engine group by trying to create multiple windows.

To test out multiple windows, I ran the application on a simulated iPad Pro. I put the application in the background and went back to the Home screen on my iPad. I then found the application icon and long pressed the icon. The context menu appeared and I could choose Show All Windows. I clicked the + button to create a new window and a new scene was created, a new Flutter engine was created, and I had two of the same scene running.

Launching a Different Scene

At this point, I have proven that I can run multiple scenes with the same Flutter UI in each scene. Each scene has its own copy of the program and the UIs are not related or connected to each other in any way. The next task is to try to launch a different scene.

In order to launch a different scene, I need to implement something in the UI that will trigger a platform-specific call to the UIApplication.requestSceneSessionActivation(_:userActivity:options:errorHandler:) function to launch a new scene. After reading about executing native code from Flutter, I created a FlutterMethodChannel in the scene’s Flutter engine that can receive requests from the Flutter code:

In this update, I created a message channel named pleasebrewcoffee.app/scenes that can receive messages from the Flutter application to create a new scene. A new scene is requested using an NSUserActivity with the activity type set to app.pleasebrewcoffee.createscene.

To implement this scene, I will create the SecondScene scene in the Info.plist scene manifest and I will set its delegate class to $(PRODUCT_MODULE_NAME).SecondSceneDelegate. I will begin by updating the AppDelegate class and implementing the application(_:configurationForConnecting:options:) function to intercept the user activity and to create scene configuration for the new scene:

Normally, I would add logic to evaluate the NSUserActivity object to determine what is in the activity and which scene I need to create to handle the user activity. For the purpose of this exploration though, it wasn’t important to me so I’m just checking if an activity is present to trigger the second scene.

I next created the SecondSceneDelegate class to drive the new scene:

Notice in the call to appDelegate.engines.makeEngine, I set the withEntryPoint argument to secondScemeMain. In MainSceneDelegate I passed nil as the value of the withEntrypoint argument which caused Flutter to look for the default main function. In this case, I am specifying that the Flutter engine should instead find and use the secondSceneMain function as the entry point for the new Flutter engine instead of the main function. This allows me to customize the Flutter user interface for the new scene.

Now the second scene is declared and implemented, and I have a way to request that the second scene be launched from the Flutter code. It is time to switch over to Flutter and implement the second scene UI and the trigger to create the second scene.

Implementing the Second Scene

So far, my Flutter application is the default application where you tap a button and a counter is incremented. I am going to replace this UI with a new UI that presents a button that, when tapped, will display the second scene. I will implement all of this by changing the implementation of the _MyHomePageState class in main.dart:

This code displays a button in the UI that when tapped will invoke the createScene method using the pleasebrewcoffee.app/scenes method channel. Invoking the method on the method channel will send the message to the handler in MainSceneDelegate that will create the new scene.

I then implemented the second scene in Flutter:

Notice the @pragma('vm:entry-point') attribute for the secondSceneMain function. The @pragma tells Flutter that secondSceneMain function is an entry point that can be called by a Flutter engine to run the scene. The rest of the scene just presents a line of text indicating that the view you are looking at is the second scene.

Testing The Solution

With all of the code in place now, it is time to test in an iPad Pro simulator. I started by running the application and seeing my main scene appear with the button to trigger a new scene:

The first scene for my test app

I started with the multiple window test. I returned to the home screen and long-tapped the app icon. When the context menu appeared, I chose the Show All Windows menu item:

The context menu showing the Show All Windows option

In the windows screen, I tapped the circular + button in the upper right corner to create a new window:

The windows view where I can create an additional window

A new window appeared showing the main scene again:

The windows view showing both windows running

I closed the second window and returned to the first window. I next tapped the button to create a new scene. The second scene opened up in a side-by-side view:

The second scene appeared in side-by-side view after tapping the button

Where Did We Go?

In this post, I set out to determine whether Flutter can be used to build multiple window and multiple scene applications for an iPad. Surprisingly, it was very simple to implement once I understood the nature of how Flutter gets launched from iOS and how to manage multiple Flutter engines. I feel like I have a good starting point toward considering using Flutter to build applications for an iPad or iPad Pro.

--

--

Michael Collins
Neudesic Innovation

Senior Director of Application Innovation at Neudesic; Software developer; confused father