Which Multi-platform framework should I use to write my app? Let’s try Flutter.

David W. Gray
11 min readAug 14, 2023

--

The one where I do my best to quickly cobble together my sample app using Google’s Flutter and share my first impressions.

This article is part of a series comparing some of the top multi-platform frameworks. While I hope this works reasonably well as a stand-alone introduction to Flutter, you’ll get more out of this if you go back and read the introductory article that describes the overall effort and the app I’m writing.

Top-level capabilities

Flutter supports iOS, Android, Windows, MacOS, Linux, and the Web.

All except the Web applications are compiled to native machine code for deployment using the AOT (Ahead Of Time)¹ compiler. For Web, they obviously can’t compile to native code, but they do render reasonable versions of the application as a progressive web single-page app.

First impressions

Flutter doesn’t depend on a declarative language (like HTML or XAML) to define its layout. Instead, you must define your components (Widgets) directly in your Dart source code. This lack completely rules out a separate design team’s ability to control the layout and would make building a WYSIWYG tool harder to write. But, I’ve yet to see that promised division of labor work effectively or effective WSYWYS tools developed. And, at least in the Visual Studio Code implementation of the tooling, refactoring and formatting tools make the layout in code pattern reasonably easy to manage.

The installation on both Windows and Mac was smooth. They include a “flutter doctor” tool that helps diagnose missing dependencies, and I found it very useful to get the Android dependencies I needed all set up.

The documentation is thorough, and their introductory tutorial was quick and helpful. Making it through their Getting Started section, including the Write your first app code lab combined with a quick read of the Dart Language Overview and Flutter Architectural Overview was enough to comfortably move into writing my small app.

Screenshot of Windows version of the Flutter Beat Counter
Windows Version

Development environment

I used Visual Studio Code with the Flutter extension in my testing. These tools gave an effectively identical experience on both Windows and Mac OS. Presumably, that would be the same for Linux. They also support Android Studio, IntelliJ, and Emacs, but I saw no need to go past the VSCode support on a first pass.

Hot reload works smoothly on most targets and is essential for quick development (says someone who spent not a small number of cycles working on reducing the time from code change to test in other environments). Since the Web implementation only supports hot restart, I did much of my initial development on the Windows version of my app. I probably leaned a little too far in that direction. For my app, hot restart was still fast, and it was only slightly annoying that the aplication state was lost between restarts.

One thing that is particularly nice about Flutter for small sample applications like mine is that it’s easy to encapsulate all of my user code into a single file. I wouldn’t consider this layout for any real-world application, but it sure makes a demo app easier to read. Also, having spent most of the last decade in the web tooling world, I expect things from DartPad and feel lost without them. I also found that DartPad was helpful when experimenting with Dart syntax. I imagine when working with a larger project; it would be even more helpful when experimenting with Widgets.

Widgets for everything?

Flutter doesn’t use native controls to manage its user interface.

Instead, it uses a technique common in game development where it manages the entire drawing/interaction service itself. This technique has pros and cons. The pros are that they can optimize the heck out of their rendering engine (which is why games use it) and that they have complete control over how widgets (or controls) look and act. On the flip side, they have to re-implement all controls and behaviors, so there are inevitably bugs and differences in behavior between Flutter and the native controls. In addition, there is a lag between when controls are added/changed in the native OS and when they show up in Flutter. Somewhere in the middle is the fact that you, as an application developer, always know how your controls will render — and updates in the OS won’t change the look or behavior of your app or introduce inconsistencies. On the other hand, it won’t magically fix a bug for you. And you have to take steps to update your application to keep in sync with the OS.

Flutter is implemented in Dart, which is Google’s version of Java or C#. A quick scan of the Dart documentation was all I needed to dive in. I suspect that would be true of any C# or Java developer. It would probably require more ramp-up for a Javascript or Python developer though.

Install and Deploy to devices

Deploying for testing purposes was smooth on all platforms. For iOS, I did need to sign up for a development account and run Xcode to associate my developer credentials with the project, but Apple pretty much forces everyone to do those things to access iOS.

Debugging tools

The Visual Studio Code debugger integration is nearly seamless. It gives me all the basics that I have come to expect in a source-level debugger, including breakpoints, single stepping, watching variables, and examining the call stack. One small caveat is that if you want to step into the Flutter framework or external package code, there is an extra step to enable that. This step is similar to disabling “Just My Code” in .NET. I have always discouraged engineers from using Just My Code by default since understanding the code running around the code you are writing is essential to a deep understanding of your program.

In addition, the Widget Inspector gives much the same experience I’m used to with the Elements tab in the browser developer tools. It lets you see the widget tree and select a widget to see the association between the widget tree, the code, and the widget as it is rendered. It also has options to decorate the view on the running app with guidelines and baselines and several other options. While I didn’t dig into this in detail on my first pass, a tool like this is invaluable for any UI design work.

The App

I’m not going to go through a step-by-step description of how I created this app; you can look at the code on GitHub, which I highly recommend as a way of orienting yourself for this discussion. For a simple application, I was able to keep all of my custom code in the maint.dart file without things getting out of control.

Below are screenshots of the Android and iPhone versions of the application. Note that they are identical except for the slight differences in the frame.

An screenshot of the android version of the flutter beat counter
Android Version
Screenshot of the iphone version of the flutter beat counter
iPhone Version

I started from the simple template using the flutter cli and simply running the following:

flutter create flutter_beat_counter

I then sketched out the UI by adding the widgets to MyHomePage.

This first pass included an ElevatedButton for the beat counter itself and Cards for measures per minute and beats per minute, but I didn’t get in a way to show the minimal settings in the very first pass.

From there, I iterated.

The very next thing I did was fill the hole in the initial sketch with a way of selecting the meter and counting method. I decided to use Segmented Buttons since that was a nearly direct equivalent to what I used in the original web (Vue.js) app. In this step, I just got the segmented buttons working with encapsulated state, which Flutter does in that widget's documentation. The buttons managed their own state and worked correctly when the user clicked on various parts of the buttons, but the state wasn’t visible in the rest of the app.

At this point, I decided to share the state between widgets. For instance, the “Measures Per Minute” card must have access to the “Meter Selector” segmented button group. Up to this point, I had been using the very basic method that Flutter includes to mark parts of its widget tree as dirty and manage updates to the UI. I created a StatefulWidget, which creates a State<T> object and makes all changes to the state within a lambda function passed to setState. This method is what the template app uses to manage the simple counter state. While setStateworks for isolated widgets, I couldn’t see a clean way to use this method to manage the state between widgets, even in my very small app.

Even the “Your first Flutter app” app code lab uses Provider Package to manage state, so that’s what I did as well. To do this, I needed to include the Provider Package by adding it to my pubspec.yaml file. Once I included the Provider Package, setting up my simple state/business logic object derived from ChangeNotifier was easy. One of the side effects of the built-in state management mechanism being so rudimentary is that there is a rich ecosystem of alternate state management modules. Flutter has documented them here.

As an aside, the Flutter and Dart package manager is pub.dev. I used thisto add the Provider Package dependency. Puv.dev works basically the same way most other package managers, such as NuGet and npm do.

To plug into the provider framework, I created a combined state and business logic class derived from ChangeNotifier that I simply called Counter. This class records the time of the last n clicks and translates that information into beats per minute (bpm) and measures per minute (mpm) based on the current settings of the meter and count method properties. I made heavy use of smart properties with backing fields to make this work. An example of this is the “meter” property, which has a simple getter returning the backing field, but does some work to manage the internal state and then calls notifyListeners when doing a set. The last part of this is how the Provider Package manages reactivity.

enum Meter { none, beat, double, waltz, common }

class Counter with ChangeNotifier {
// The current meter - changing the meter will udpate the internal state to keep
// BPM constant
Meter _meter = Meter.common;
Meter get meter => _meter;
set meter(Meter value) {
if (_method == CountMethod.measure) {
_convertIntervals(_meter, value);
}

_meter = value;
notifyListeners();
}
}

At this point, I have a fully functional, if somewhat ugly, version of my beat counter app, depending on where you draw the line between form and function. So I did a few things to get the app looking the way I wanted it.

  • I used Spacer and SizedBox widgets to make my overall layout look decent.
  • I changed the theme to use a ColorScheme that I prefer.
  • I used Dart’s collection control flow to make the MPM card and the count method buttons visible only if the “meter” beats.

I thought I had this completely wrapped up when I noticed that some of my widgets disappeared when running my app on extra small devices or flipping to landscape mode on a normal phone. This behavior didn’t make me happy, even for a tiny demo app. So I dug around and discovered that there is a Widget for that. So I added a LayoutBuilder and SingleChildScrollView to make my app scroll if it doesn’t fit. Pretty nifty.

You can see the final app and a snapshot of the code on DartPad (you have to hit run in the upper right corner). If you want to look at the code’s evolution in more detail, it is available at dwgray/flutter_beat_counter at v0.1.0-first-impressions (github.com).

Dart

Since Dart was entirely new to me, I decided to end with a quick run-through of what I found interesting about the language. Definitely go through the Dart language overview yourself, but I thought it worth pulling out some highlights.

Dart doesn’t use the new keyword when invoking a constructor. This syntax totally broke my internal parser for a while, but once I got used to it, I have to agree that it’s nice. It significantly cleans up the semi-declarative and sometimes highly nested build functions.

Another thing that threw me for a loop was that non-empty case statements implicitly break. This is a new feature with Dart 3.0, and when I ran into it, the main Dart documentation and tutorials still needed to be updated, so the only place I found it documented was in the change log (that main documentation has since been corrected). This syntax is another one of those things that is a real improvement in C-style languages. Many modern C-style languages (including Dart before version 3.0) don’t allow implicit fall-through in a switch statement. This is because it’s exceptionally easy to forget a break statement and get into all kinds of trouble. Leaving out a break in a non-empty case statement in those languages will generate a compiler error. But if the compiler can detect this condition, why not just take care of it?

Dart has a concept called Control-flow operators for collections that allow conditionally adding individual items to a collection. This was useful in making widgets appear or disappear conditionally in my build methods.

Enums are pretty simple in Dart. They map directly to integers, and you have no control over what integers they map to; it just starts with zero. This behavior was easy to work with in my case since the only enum that I wanted to map to integers naturally mapped to 1–4 for the different meters. I merely added a placeholder none value for zero and got things to line up the way I wanted. Then I could just use the index property on the enum to get the integer representation out to manipulate.

Flutter heavily uses Dart’s named argument syntax. This gives something that they refer to as nearly declarative syntax for creating widgets. It looks something like this:

  @override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.headlineLarge!.copyWith(
color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold);

return SizedBox(
width: double.infinity,
child: Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Text(text, textAlign: TextAlign.center, style: style),
),
),
);
}

Note the property: value syntax in the SizedBox Padding and Card constructors.

Conclusion

I enjoyed building this app in Flutter. Since my most recent UI work was in Vue.js, most of Flutter’s paradigms seemed natural. The best part is that it just worked on all the platforms I tried. This smooth experience might not hold for a more complex app that uses device capabilities, but they’re starting from a good place if a simple app works without changes on all supported platforms.

The biggest downside of Flutter, which is also something of an upside, is that it’s very new and built from scratch. If you or your team has yet to work with Dart, there will be ramp-up time for that, and you won’t be able to leverage previous experience like you might from either MAUI or React. But since it’s so new, the path to building things is more straightforward.

Sometimes fewer options can be a good thing.

¹ I find the apparently unironic use of the term AOT (Ahead of Time) compiler amusing. I’m assuming this is to distinguish this from JIT (Just In Time) compilation, but it seems a bit ironic since “AOT” compiling predates JIT compiling by decades.

This reminds me of how the folks on the C++ team at Microsoft started getting annoyed when the .NET folks started referring to the code they generated as “unmanaged code” rather than “native code” or just code.

--

--

David W. Gray

I am a software engineer, mentor, and dancer. I'm also passionate about speculative fiction, music, climate justice, and disability rights.