From cross-platform to multi-platform (6+): my Flutter journey
EDIT: This article and code repository has been updated (in November 2021) since the original was published back in January 2020 due to the support for all platforms described below, being available from the Flutter stable channel.
I have been intrigued by all the Flutter announcements related to increasing support for other platforms such as the web, macOS etc. I wondered whether this new multi-platform technology could be applied to a Flutter app I’m currently working on, and the amount of effort required to make it work.
Spoiler alert: the reality matches the promise! I was able to add support for many more platforms surprisingly easily, with the current count being a total of six. All platforms support the vast majority of my original app’s functionality, with the exception of Google Maps (more on that in the sections below).
This blog summarises the experiences of enhancing a Flutter app originally built for iOS and Android, to make it multi platform capable and targeting the following platforms:
- Android
- iOS
- Web
- macOS
- Windows
- Linux
It’s also worth reading this blog if you’re about to start implementing a new UI experience and are yet to select an implementation technology eg. React for Web and separate iOS and Android native implementations for native app experiences, possibly some desktop experiences in future; versus a single Flutter codebase for all three (or more) UI experiences.
Features of the original app
Here are some salient features of the app I used as a basis to enhance for multi-platform capability:
- Screens designed for a typical fixed screen sized mobile phone, not a tablet form factor
- Retrieves and saves JSON data from / to a REST / GraphQL endpoint
- Provides entry forms to allow a user to enter and save data
- Displays summary information including weather forecast using SVG icons
- Displays graphs / charts to show historical data to visualise trends
- Remembers user selections using shared preferences
- Displays selected locations using Google Maps
These features are quite typical of many UI experiences, regardless of whether they are for the web, native mobile or even desktop apps.
Demo github project: multiplat
I have created a github project called multiplat that demonstrates the basics of Flutter’s multi-platform capabilities. The repository is located at:
https://github.com/flexi-creator/multiplat
Follow the detailed instructions provided in the project’s README for how to install and run the app on each of the six platforms supported.
The app is set up to fetch sample data from my-json-servier.typicode.com
to demonstrate https calls across all platforms. If for any reason this endpoint fails, the demo can run instead using locally generated random data by editing data_item_service.dart
and changing USE_RANDOM_DATA = true
.
Multiplat distills the learnings from enhancing the original app and genericizing the demo code. For instance each row in the list we display is a DataItem
model object, whilst the data for the chart / graph consists of ItemHistory
objects.
The sample data created by DataItemService
in this demo individualises the app for the fictional business domain of Top contributors (of something unspecified!), each DataItem
represents a single person who is a top contributor. The app will display their fictional biography and profile photo, and the chart represents their fictional contribution history.
The app is architected using the Provider and service locator pattern described in detail here.
The three screens of the app (list, detail, chart / graph) are responsive and will switch between single panes on smaller screened devices, or a combined screen that has the list pane on the left, the detail pane on the top right and the chart pane on the bottom right on larger screens. The single pane to multi pane transition will react dynamically to window resizing on platforms that support it ie. web and the three desktops. CombinedView
determines whether to show single or multi-pane screens based on the window size returned from LayoutBuilder
and comparing those values against minimum width and height limits.
The PaneInteractionService
provides an example of a way for the different panes to coordinate data changes that will work across a single paned or multi paned layout.
Just a quick note on library terminology: a Flutter package is a library external to your app code that you can import to provide a particular feature you need in the app. A plugin is a special type of package that has platform specific code that is needed to perform the actual work. For the rest of this article, I’ll refer to Flutter libraries as:
- Packages for libraries containing only Dart code
- Plugins for libraries containing Dart + platform specific native code (eg. java, kotlin, objective C, swift, C++ etc)
The chart / graph pane ChartView
is implemented using the charts_flutter package. This demonstrates that libraries built using only Dart code work seamlessly across all platforms.
Contrast this with the shared_preferences plugin which relies on platform specific code to perform the actual work. Windows and Linux are currently not supported by this plugin, so it’s been wrapped in the demo app class MultiplatSharedPrefs
. This saves the selected item in shared preferences if the platform supports it (iOS, Android, Web, macOS); or in a file otherwise (Windows and Linux).
In the demo app, saving data to shared preferences allows the same selected item (in this case a person)’s details to be shown on the next app launch. Note that this is only evident when running multiplat on larger tablets in landscape orientation, web, and desktop devices.
Support for each platform resides in a platform specific subfolder immediately under the project folder, in this case the android, ios, web, macos, windows
and linux
subfolders under the main multiplat
folder.
General Flutter cross-platform considerations
Pure Dart code just works!
The experience I’ve found is that the app and package code that does not invoke cross platform plugin code, just works with no changes required. This category of code makes up the vast majority of the app codebase. This finding was a very satisfying result and a real tribute to the Flutter development team.
Release Mode is available on all supported platforms
Standalone binaries of apps can now be built by specifying a release build (rather than say debug build) on Android, iOS, Web, macOS, Windows and Linux. This means you can ship the standalone binaries rather than the folder structures with debug and profile builds as required previously on Windows and Linux.
Multi platform support is available from the stable channel
In this blog we only discuss the following “other” platforms: macOS, Web, Windows and Linux — in addition to iOS and Android — and they are all available from the main stable channel. There is now no need to switch to a “bleeding edge” channel like beta or master channel to write apps for the six main platforms.
Other platforms not covered here are Fuchsia, and there have been experiments with Raspberry Pi etc. Details for ChromeOS can be found here.
Responsive screen design
To have the app looking more natural on platforms with a larger window and less like a mobile app (ie. on web, macOS, Windows, Linux), consider designing all screens to be responsive, starting with wrapping each screen in a LayoutBuilder
widget. This widget provides the surrounding window’s dimensions. Refer to the CombinedView
class in multiplat above for an example of a responsive screen, and note how simple it is. You can also use MediaQuery
to find out more about the device your app is running on, for instance the user’s adjusted font size for accessibility, device orientation (portrait, landscape) etc.
Conditional code for web vs non-web platforms
Code for the flutter web target can be different to non-web targets. For instance flutter web code may need to refer to Html classes such as DivElement
or Node
, requiring import ‘dart:html’
. This would normally result in a lint warning, but this can be suppressed by adding the line// ignore: avoid_web_libraries_in_flutter
immediately above the html import line. To avoid confusion and possible runtime crashes between web and non-web code, I split widgets into web and non-web versions, with the same class name, but two differently named dart source files.
A concrete example of this is the need in my original app to support Google Maps on iOS, Android and web (currently I have no flutter map solution for the other platforms). On the mobile platforms google_maps_flutter plugin is used, whilst on the web google_maps is used. Please refer to the example code within the plugin links for details on who to use them. In this case, the web version requires initialisation of the map using manipulation of html elements, so it needs the dart:html import. Two dart files map_view.dart
and web_map_view.dart
were created, both containing the MapView
widget class (but with different non-web and web implementations). The calling code should then conditionally import them:
import '../map_view.dart'
if (dart.library.js) '../web_map_view.dart';
The flutter run
or flutter build
commands will now load the appropriate Dart file when the target platform is specified eg. flutter run -d web
vs flutter run -d ipad
. Ensure that the public constructors and accessors are identical between the two dart files. The calling code for MapView
now can be completely ignorant of which platform the map code is targeted to.
Different widgets for different platforms
Whilst it is relatively straightforward to get your app running on a new platform, visually the app may not follow the norms for that platform. There are several approaches to this. If the app already presents a highly branded look and feel, there may be little need to customise the widget set for the new platform. If the app has a very native look and feel for a particular platform, it may be worthwhile adopting the other widget set eg. if the app is using the Material library, consider adopting the Cupertino library when the iOS or macOS platform is detected. This video provides a way to implement this.
Flutter Plugins and platform support
When you’re hunting around for the appropriate packages and plugins to meet your required feature set, use https://pub.dev/ to view more details about the package (which could be a plugin). Each library details which platform(s) it supports, but this information may not be exhaustive so experimentation is required to confirm platform compatibility.
For example, charts_flutter only lists support for Android, iOS and Web, however I’ve discovered it works well on all six platforms discussed in this article. Packages written in Dart only (ie. without platform specific code) such as charts_flutter will most likely work on every platform.
You may need to import several unrelated plugins to give you adequate platform coverage, similar to the Google maps example described above.
Android
The android
platform specific folder is created by default when you issue the flutter create <project>
command.
Flutter provides the best support for Android with a complete Material design library. Most documentation including tutorials assume the use of this library, including the demo project multiplat. There are few if any cross-platform concerns with android, but do note the navigation differences including screen transitions between Material and Cupertino that is a natural consequence of the norms of those native platforms. See here for more details.
Be mindful of widgets that have totally different implementations between the platforms, such as the calendar selection widget: using the Material widget you can simply call the showDatePicker
function, whereas using Cupertino you have to embed a CupertinoDatePicker inside showModalBottomSheet to more closely match native app behaviour as detailed here or just use this package that greatly simplifies selecting a calendar date on iOS devices.
iOS
The ios
platform specific folder is created by default when you issue the flutter create <project>
command.
For a user experience closer to the native platform, adopt the use of Cupertino rather than Material widgets, which also provide navigation and page transition norms more native to iOS. See here and here for more details.
In multiplat, search for occurrences of Cupertino*
and compare to their Material*
widgets, used side-by-side depending on Platform.is*
.
For data entry, one inconvenience is that the iOS pop-up soft keyboard does not have any buttons for next and previous form input fields, nor a Done button when the keyboard is numeric. This necessitates the use of the keyboard_actions package that provides a widget sitting above the software keyboard to perform field navigation and form submission.
Web
There is additional information for Flutter web.
The web
platform specific folder is created by default when you issue the flutter create <project>
command and you’ve already enabled web development: flutter config --enable-web
All platform specific code which is NOT for the web needs to be written using this pattern:
if (!kIsWeb && Platform.isIOS) { ... add iOS widgets to tree ... }
ie. ensurekIsWeb
is always checked first because executingPlatform.is*
by itself causes a null crash on the web.
Also see the section above on Conditional code for web vs non-web platforms.
The SVG library does not support Web currently (it works well on the other five platforms). A simple workaround is to use Image.network (this can be used for both remote as well as local asset based images eg. ‘https://myserver/myIcon.svg’ and ‘assets/assets/myIcon.svg’ — the latter awkwardness is required due to the way Flutter packages the assets source folder for web) when the Web platform is detected. The other five platforms support SVG images. The flutter_svg library also brings in a transitive dependency called petitparser which causes a javascript crash on Safari web browsers (Chrome and Firefox tolerate this library), so the workaround is to use a conditional import in the app class using SVG code:
import 'package:flutter_svg/flutter_svg.dart'
if (dart.library.js) 'package:vinot_flutter/ui/shared/dummy_svg.dart';
Where dummy_svg.dart contains a do nothing implementation of SvgPicture to pass compilation.
Use the html renderer to avoid cross site issues (CORS) for network images sourced from a different host:
flutter run -d chrome --web-renderer html // run the app
flutter build web --web-renderer html --release // production build
The sections below discuss the desktop platforms. A great resource is the github repository:
This repo contains very useful examples of desktop specific plugins that have not migrated into the official pub.dev Dart package repo, such as file chooser and menu bar. There are also implementation details provided in the various READMEs that will give insight for working with each platform.
macOS
There is additional information for Flutter for desktop, of which macOS is one.
The macos
platform specific folder is created by default when you issue the flutter create <project>
command and you’ve already enabled macos development: flutter config --enable-macos-desktop
Google maps are not supported for macOS as of June 2020.
There is very limited support of macOS for plugins but Dart only packages will likely work well.
Windows
There is additional information for Flutter for desktop, of which Windows is one.
Enable Windows Desktop development: flutter config --enable-windows-desktop
You’ll need Visual Studio tools in order to compile and build for Windows. Follow the instructions provided from the output of flutter doctor
to download the right version of VS.
The windows
platform specific folder is created by default when you issue the flutter create <project>
command and you’ve enabled Windows development (see the flutter config
line above).
Shared preferences are now supported on Windows so file based solutions for this functionality are no longer needed.
Google maps are not supported for Windows.
Since the announcement of support for Windows on the main stable channel, the number of plugins is slowly but surely increasing for the Windows platform, however Dart only packages will likely work well.
Linux
There is additional information for Flutter for desktop, of which Linux is one.
Follow these instructions to install Flutter on linux.
The linux
platform specific folder is created by default when you issue the flutter create <project>
command and you’ve enabled linux development (see the flutter config
line above).
There is now a default font specified for linux so there is no longer any need to include a font in pubspec.yaml
unless you want to use non-default fonts.
Shared preferences are now supported on Linux so file based solutions for this functionality are no longer needed.
Google maps are not supported for Linux.
Since the announcement of support for Linux on the stable channel, the number of plugins is slowly but surely increasing for the Linux platform, however Dart only packages will likely work well.
The app icon is located separately to the linux app executable, unlike all the other platforms. See multiplat’s README for details on how to create a desktop icon for your app.
Conclusion
Despite the points mentioned above mostly relating to installation and particular plugins, providing support for each new platform was surprisingly easy.
In particular, since the vast majority of the app’s codebase is written in pure Dart code (using Dart only packages), it just worked with no modifications necessary across all the platforms.
After this positive experience I can recommend giving Flutter serious consideration for any future UI implementations, regardless of whether they’re for a single platform or if there are cross platform considerations.
The fact that Flutter supports mobile devices and the web from a single codebase is pretty compelling.
There are also tempting hints about future support for Universal Windows Platform so imagine deploying your Flutter app to Xbox in future, on top of all the platforms supported now. Wow!
For those who have already implemented apps on Flutter, why not try making it support your favourite desktop environment, or even make it a web app. Personally I’ve always wanted to develop Mac and Windows desktop GUI apps for fun and now I have an easy way to get started without having to learn many new frameworks and languages!
If you haven’t already, check out my other Flutter blog:
Is Flutter ready for Enterprise Mobile Apps?