Build Adaptive iOS Applications Using Flutter

Michael Collins
Neudesic Innovation
7 min readMar 12, 2022
Photo by Daniel Korpai on Unsplash

In the mobile space, there are many form factors and screen sizes that you have to take into consideration when designing your application. Flutter provides a great framework for building responsive applications that can adjust their layout to the size of the screen or host window. But when using Flutter to develop a user experience specific to iOS and iPadOS, there’s one thing missing from Flutter: size classes. In this article, I will show you how to extend your Flutter iOS application to be able to use iOS size classes to build adaptive UIs for iPhone and iPad.

About iOS Size Classes

In the early years of the iPhone, Apple released one phone model per year. Initially, all of the phone models maintained the same resolution from year-to-year. That made it easy to build an iPhone application because there was only one single screen size. After a few years, Apple started introducing multiple models of phones, and then the screen sizes started to differ. Then Apple introduced the iPad which brought with it a completely different screen resolution. And then there were retina displays. And then Apple introduced multitasking which allows the user to run two apps concurrently on the screen and allows the user to adjust the screen so that one application has more space or both applications have equal space on the screen.

To make it easier for application designers to design adaptive user interfaces, Apple introduced the concept of size classes. There are two size classes: regular and compact. Depending on the device and the orientation, the horizontal size class and vertical size class will either be regular or compact. An iPad Pro in either portrait or landscape mode is going to have both a horizontal and vertical size class of regular. An iPhone 13 in portrait mode will have a horizontal size class of compact and a vertical size class of regular. In landscape mode, the iPhone 13 will have both size classes set to compact.

For an iPad Pro that supports multitasking, when the applications are in portrait mode, the horizontal size class will be compact and the vertical size class will be regular. Then the iPad Pro is in landscape mode and have equal space on the screen, both applications have a horizontal size class of regular and a vertical size class of regular.

The concept of size classes allows iOS developers to adapt their user experiences to the available space on the device based on size classes instead of having to know the specific width or height of the screen. Controls can be shown or hidden, or even entire user experiences changed, based on the size class. For example, on an iPad with regular x regular size classes, the user experience may be based on a two or three column split view like the Mail app. On an iPhone or in a reduced multitasking scenario, the user experience may change and use a tabbed layout.

For more information on size classes, read Adaptivity and Layout in the iOS Human Interface Guidelines.

The Argument for Size Classes on iOS

Flutter has excellent tools for building responsive applications using MediaQuery and LayoutBuilder, but to use them requires knowing specific a specific width or height to use to adjust the layout. While this can certainly be done, it’s not as easy or friendly as laying out a compact user interface versus a regular user interface like we can do with size classes. When building a Flutter user experience based on the Cupertino widget set, you want to build a user experience that is as close to the native iOS look-and-feel as possible. To get it completely right, we need support for being able to build device adaptive user experiences using size classes, especially when we want to target the iPad as well as the iPhone.

Add Size Class Support to the iOS Runner App

I want to target iPad as well as iPhone for my Flutter application. On the iPad, I also want to support multiple windows and multitasking. To do that, I need a way for the host view controller to be able to notify the Flutter widget tree that the size classes change when the user rotates the device or enters multitasking mode. I determined that the best way to do this is to use a Flutter event channel. Using an event channel, my native code can raise events when the size classes change and the Flutter widget tree can receive those events and adapt to the updated size classes.

On iOS, the horizontal and vertical size classes are part of the traits of the application and are part of the UITraitCollection object that is available to all view controllers from the traitCollection property. When the traits change, the traitCollectionDidChange(_:) function on the view controller is called with the updated trait collection. I can use traitCollectionDidChange(_:) to fire events containing the new size classes to the Flutter widget tree.

The first thing that I need to implement is a FlutterStreamHandler object that Flutter can use to subscribe to receive events from the host iOS application. I defined the TraitCollectionStreamHandler class:

The next(_:) function will take a UITraitCollection object and will serialize it in a map to be sent to the Flutter code. While there are other traits that may be interesting, for this article I only care about the horizontal and vertical size classes. The onListen(withArguments:) function is called to link the TraitCollectionStreamHandler with the subscriber. onListen(withArguments:) sends the current trait collection for the parent view controller to the new subscriber to seed the stream.

Flutter provides the FlutterViewController class to host the Flutter view. I can subclass FlutterViewController and create the AdaptiveFlutterViewController class to create the event stream and forward trait collection changes to subscribers:

I can then use AdaptiveFlutterViewController to host my Flutter application by using it in my scene delegate:

And I can launch the scene and manage the Flutter engine group in my custom AppDelegate:

Add Size Class Support to Flutter

Now that the iOS side is complete and size class updates are being published to the event channel, I need to implement the Flutter side of the app to capture and update the user experience when the size classes change. The first thing that I need to do is to introduce the size classes into Flutter. I will create an enumeration named CupertinoSizeClass:

The index values match the integer values on iOS. The integer values are passed from the iOS Runner app to the Flutter app, so 0 will correlate to unspecified, 1 will correlate to compact, and 2 will correlate to regular.

To make it easier to pass around the size classes and future traits, I will create a data class that I will deserialize the event into:

I need a widget that my Flutter application can use to adapt the user experience to the current size class and will cause the widget tree to be rebuilt when the size classes change. I can expose the size classes to the widget tree by creating a new InheritedWidget named CupertinoTraitQuery:

CupertinoTraitQuery exposes the horizontalSizeClass and verticalSizeClass properties to descendent widgets in the widget tree. When the size classes change, the child widgets will be updated with the new values of the size classes.

To wrap the CupertinoTraitQuery widget and automatically inject it into the widget tree, I created the CupertinoAdaptiveWidget widget class. CupertinoAdaptiveWidget can be used as the root widget for a widget tree that needs to adapt based on the size classes (or other future traits). CupertinoAdaptiveWidget will subscribe to receive events from the event channel and will rebuild the widget tree when the traits change:

In CupertinoAdaptiveWidget, I created _createTraitCollection to convert an event from the event channel to a CupertinoTraitCollection object. I am then using a StreamBuilder widget to subscribe to the event channel and update the widget tree when new trait updates are received from the stream.

I can now create adaptive applications by using CupertinoAdaptiveWidget in the widget hierarchy. I’m using it in my test as the child of my CupertinoApp widget, but it may be better as the parent:

Testing Size Classes

For a quick test, I created a HomeScreen widget that simply outputs the current size classes for the device:

On the iPhone 13 simulator in portrait mode, I can see that my size classes are compact width and regular height:

The image shows an iPhone 13 in portrait mode. The screen shows that the horizontal size class is compact and the vertical size class is regular.
iPhone 13 in portrait mode

When I rotate the iPhone 13, my size classes change to compact width and compact height:

The image shows an iPhone 13 in landscape mode. The screen shows that the horizontal and vertical size classes are compact.
iPhone 13 in landscape mode

My size class implementation appears to be working.

Where Have We Gone

Flutter has good support for building responsive user interfaces, but if you’re really focusing on building iOS-like applications in Flutter to run on iPhone and iPad, programming with size classes makes building adaptive user experiences easier. In this article, I showed how to use an event stream to publish the initial size classes and publish changes to the size classes at runtime. I then showed how to build widgets that can receive and process the changes to the size classes and update the application’s user experience by adapting to the size classes for the application. Hopefully as you build iPhone and iPad applications using the Cupertino widget collection, you can build fully-adaptive iOS-like apps that support multiple scenes and multitasking based on size classes, just like you do in native development.

--

--

Michael Collins
Neudesic Innovation

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