Supporting Multiple Scenes with Flutter on iPad
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
andGeneratedPluginRegistrant.m
. I have no idea what these do yet.AppDelegate.swift
that contains the implementation of the standardAppDelegate
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 atMain.storyboard
, I can see that the starting view controller is aFlutterViewController
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:
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:
In the windows screen, I tapped the circular + button in the upper right corner to create a new window:
A new window appeared showing the main scene again:
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:
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.