Creating a cross-platform app with Flutter

22 May 2023

22/5/23

Leon Ross

6 MIN READ

6 MIN READ

Flutter is a cross-platform mobile application development framework that competes directly with React Native. To get an overview of Flutter, check out our previous post Flutter: First Impressions. While the core technology hasn’t changed since Elliot gave his first impressions, the Flutter team has been busy with a continuous stream of improvements and feature drops. Below I’ll highlight how truly cross-platform Flutter has become, and provide a small demo project to help get you started.

Cross-platform with Flutter

At the recent Flutter Forward 2023 event, the Flutter team announced they have succeeded in their mission of taking Flutter beyond mobile by having full production support for all major platforms–Android, iOS, Web, Windows, MacOS, Linux and Embedded.

While Flutter might not yet be the best choice for replacing traditional “search engine optimised” websites, there are many use cases where a Flutter application targeting multiple platforms–including mobile, desktop, and the web–would be suitable. And anyone who has been frustrated trying to build a native Windows or Mobile Application can understand why cross-platform solutions are growing in popularity.

In the Real World?

There is ample evidence that cross-platform Flutter is gaining traction in production. Canonical, the team behind the Ubuntu operating system–one of the most popular Linux distributions–have declared that Flutter will be the default choice for any future Ubuntu apps they develop, starting with the Ubuntu installer itself.

Speaking of Flutter’s growing presence beyond mobile, a couple of developer products, written in Flutter, are worth a shout out: FlutterFlow is an intuitive editor that enables you to design, build and deploy fully functioning Flutter apps, using little to no code on the web.

Building a Flutter app with FlutterFlow

The Rive editor makes a useful companion app. Available for desktop and on the web, Rive lets you build interactive graphics that can change their state and animate based on user input and triggers. The demo project I’ll link below includes assets created with the aid of Rive.

Creating app-ready graphics assets with Rive

Cross-platform Out of the Box

Flutter is renowned for having a great developer experience, and this becomes evident straight away when creating your first app by running the command ‘flutter create my_app’. (If you aren’t set up for Flutter development, first take a detour here.) You should now have a new app called my_app with the default Flutter example Counter app.

Next, we can launch the app on our target devices. Open the new project in Visual Studio Code and select the target device in the bottom right.

Available target devices for debugging Flutter

Click the start debug session, or press F5, to launch the app. On my MacBook, I have four available devices, and have launched four debug sessions targeting four platforms: iOS mobile, Android mobile, macOS desktop, and Chrome web. If you want to start all four debug sessions at once, you can type ‘flutter run -d all’ into the terminal. You should see something like the screenshot below.

The default Flutter Counter app targeting four platforms

To close the dev loop, let’s make a small change to our app. Open the lib/main.dart file, and change the title property of the MyHomePage widget. Now save the file, and notice that on all our devices the app is “hot reloaded” and the title is instantly updated.

I have to say, it’s quite impressive. Within a few minutes we have created a new Flutter project and demonstrated it running on mobile, desktop and the web, with no additional configuration or third party packages required.

Smart Home Dashboard

To dig a little deeper, I have put together an example Smart Home app that displays a dashboard of a number of switches and sliders for controlling smart lights.

Flutter will not automatically style apps for each platform; it is still up to the developer to decide how to style the app for each platform. To show platform-specific UI elements, such as Android and iOS specific buttons, switches and progress indicators, we have to do a little work.

Flutter provides two comprehensive UI packages, the Material package and the Cupertino package, for styling Android and iOS apps respectively. We also have excellent community packages, including macos_ui and fluent_ui for styling macOS and Windows apps.

These UI packages implement the design language of their platforms to give a native look and feel, including things like fonts, icons, scroll behaviour and modal animations just to name a few. The challenge for our cross-platform app is to use these libraries to render our native looking UI for each platform, while sharing the same codebase.

A Cross-platform Switch

Here is an implementation of a cross-platform switch. This class provides a _builder method for each platform that we want to support. Each builder method returns a switch from the relevant UI package.

The PlatformSwitch build method returns a PlatformWidget which detects the current Platform and runs the appropriate _builder method for the platform.

import 'package:cross_platform/custom_ui/custom_switch.dart';
import 'package:cross_platform/platform_widgets/platform_widget.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:macos_ui/macos_ui.dart';

// we have imported four ui packages above; material, cupertino, macos_ui and fluent_ui.

class PlatformSwitch extends StatelessWidget {
 final bool value;
 final ValueChanged<bool> onChanged;

 // the constructor takes the switch value and onChange handler
 const PlatformSwitch({
   super.key,
   required this.value,
   required this.onChanged,
 });

 // for each supported UI povide a builder that returns the respective UI Widget
 Switch _androidBuilder(BuildContext context) {
   // return material switch
   return Switch(
     value: value,
     onChanged: onChanged,
   );
 }

 CupertinoSwitch _iosBuilder(BuildContext context) {
   return CupertinoSwitch(
     value: value,
     onChanged: onChanged,
     activeColor: Theme.of(context).primaryColor,
   );
 }

 MacosSwitch _macOSBuilder(BuildContext context) {
   return MacosSwitch(
     value: value,
     onChanged: onChanged,
   );
 }

 Widget _windowsBuilder(BuildContext context) {
   return Center(
     child: ToggleSwitch(
       checked: value,
       onChanged: onChanged,
     ),
   );
 }

 Widget _customUIBuilder(BuildContext context) {
   return MyCustomSwitch(
     checked: value,
     onChanged: onChanged,
   );
 }
 
 // the build method returns PlatformWidget detects the platform and runs the coresponding builder
 @override
 Widget build(BuildContext context) {
   return PlatformWidget(
     androidBuilder: _androidBuilder,
     iosBuilder: _iosBuilder,
     macOSBuilder: _macOSBuilder,
     windowsBuilder: _windowsBuilder,
     customUIBuilder: _customUIBuilder,
   );
 }
}

UI Contrarian?

But what if you aren’t satisfied with any of the standard UI libraries? This can be the case when your app’s styling takes priority over native look and feel–e.g., if your brand is paramount, or your app is a game. Flutter makes it easy to tread your own path. Stay tuned for the next blog in this series, Creating custom UI Components, animated with Rive.

But if you’re eager to explore, take a look in the custom_ui directory. There you’ll find logic for using the UI elements I’ve created with the help of Rive.

Responsive Layout

Responsive layout refers to an app’s ability to adapt to different screen or window sizes. For mobile, this often simply means allowing for portrait or landscape orientation. But when also targeting desktop, the range of screen sizes to support balloons.

For this demo we’ll add a sidebar menu, which will be hidden by default on small screens and mobile devices, and shown by default on larger screens. Again the UI is conditioned on platform using different builders.

...
  
// Responsive androidBuilder returns a scaffold with an expanded drawer when screenWidth >= 600
Widget _androidBuilder(BuildContext context) {
    var breakpoint = 600.0;
    final screenWidth = MediaQuery.of(context).size.width;
    if (screenWidth >= breakpoint) {
      // large screen layout with expanded drawer
      return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Row(
          mainAxisSize: MainAxisSize.max,
          children: [
            SizedBox(
              width: 240,
              child: Drawer(
                child: SettingsList(title: widget.title),
              ),
            ),
            Expanded(
              child: ListView(
                children: widget.children,
              ),
            ),
          ],
        ),
      );
    } else {
      // small screen layout with collapsed drawer
      return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        drawer: Drawer(
          child: SettingsList(title: widget.title),
        ),
        body: ListView(
          children: widget.children,
        ),
      );
    }
  }

...

Conclusion

The Flutter ecosystem seems to be on a tear right now, with a solid development push from Google, coupled with prime time coverage at major events including Flutter Forward and Google I/O. We are starting to get a feel for the direction Flutter is headed, with continued focus on developer experience, performance, cross-platform and the web, it will be interesting to see if we start to see more apps written in Flutter show up on desktop, the web as well as mobile.

In this post we explored how easy it is to spin up a cross-platform project and make your app look native. In the next post I’ll do a deep dive into how to create stateful, animated, custom UI elements with Rive, including how to unlock the power of AI to truly make your apps stand out. Until then, happy coding 🙂