Flutter in late 2021

It had been some time since I'd used Flutter and I wanted to take another look. I've mostly been working in React and React Native for the last few years, along with native iOS via SwiftUI. It had been just over two and half years since I made Modern Colour Picker with Flutter. At the time Flutter offered a remarkable developer experience with a combination of performance and tooling that was head and shoulders above the rest. But how does it look now?

In this article I will report on my recent use of Flutter where I was using it for creative coding and how it compares to both React Native and SwiftUI.

Checking in (again) with Flutter

  • Extension methods - such a nice feature, really allows you to hide messy/ugly/repetitive code by extending functionality of built in classes.
  • Improved tooling - everything seems to work nicely, and there is now great support for VSCode (I think it was there before but lacked various things compared to IntelliJ).
  • Fixes for iOS animation/warm up performance issues - this was a bit of a pain for the last project: it actually appeared to run better on a very old Android phone than my iPhone X.
  • Ecosystem maturity - lots of established third party things.
  • More platforms: now desktop and web have a good level of support. This seems particularly interesting for desktop as it is lacking in React-like UI frameworks which do declarative UI and hot reloading.

Once again I could see a big contrast with React Native and with native iOS/Xcode around the robustness of the developer experience; Dart/Flutter come with great, comprehensive tooling that feels years ahead of the competition.

What is still not 'fixed'?

Semicolons feel even more dated now. The tooling seemed to do a better job of picking up on those, but I'm so used to Swift, Kotlin and TypeScript (with prettier either omitting unnecessary semicolons, or adding them all) that I find this really tedious.

No way to do Sum types (i.e. what Swift or Rust call enums). Doesn't even do the Kotlin sealed class thing, which is frustratingly verbose but gets the job done. This is actually quite bad for code quality as really hard to push mental load around consistency into the code (and out of your head).

I'm also not a fan of how seemingly joyously Flutter embraces verbosity. The defence would be this is for consistency and explicitness, but I don't buy it, you often end up with so many brackets that fixing stuff up become reminiscent of writing Lisp.

But saying all that the overall experience is great, certainly way better than doing OOP UI dev (with standard Android or UIKit iOS work).

Versus the competition

On paper Flutter basically ticks all the boxes.

Flutter React Native SwiftUI
Declarative UI
Hot (Stateful) Reload
Compiles to Native
Reliable Performance
Interpreted Mode
Not obviously buggy/incomplete
Good tooling
Cross Platform
Web Support
Ecosystem maturity
Modern language features

Uniquely it offers both a compiled (for release) and interpreted (for development) mode (allowing for hot reloading). The only area it falls down on a bit is the lack of modern language features like algebraic data types. I was actually surprised at just how badly SwiftUI does when you set up a table like this; it does have some very nice features and certainly because it is just native iOS/MacOS code it can be easier to use system APIs, although even here it falls down a bit as Apple have some dreadful legacy APIs (e.g. open a file) that mean using some kind of wrapper would be preferable anyway. While SwiftUI 3.0 still has a surprising number of limitations and bugs it has now just about reached viability for a large range of Apps (e.g. it recently added a way to control input focus).

There are a few areas Flutter is a bit weaker at than React Native and SwiftUI.

Styling and Layout

Here React Native offers a CCS-the-good-parts system; it is very easy to give things a custom look and avoids nesting.

<View
  style={{
    background: "red",
    borderRadius: 8,
    shadowColor: "black",
    shadowOpacity: 0.2,
    shadowRadius: 4,
  }}
>
  //...
</View>

Shadow is a bit annoying as the way it works on Android and iOS is different, but for the most part stuff is pretty simple and flexible. With TypeScript autocompletion is generally fine.

SwiftUI does a kind of utility approach, with View extensions:

Text("Hello")
  .padding()
  .background(Color.red)
  .cornerRadius(8)
  .shadow(radius: 4)

This generally works pretty well (and you can extract easily into View extension methods for reuse). The main problem is Xcode's weak code completion, which is really frustrating for things like this (e.g. remembering that shadow's argument here needs the radius name).

Flutter embraces verbosity/explicitness. Coming from SwiftUI or React things like:

Container(child:
  Center(child:
    Padding(
        padding: const EdgeInsets.all(16),
        child: Text("Hello")
    )
  )
);

are mildly horrifying (this is a small example, but imagine how this kind of thing builds up as stuff gets more nested). React wouldn't require this kind of nesting as you can apply styles as a prop (not by nesting many components). However, with extension methods you can easily do SwiftUI style utility methods:

extension FlutterUI on Widget {
  Widget padded(double padding) =>
      Padding(padding: EdgeInsets.all(padding), child: this);

  Widget flex({int weight = 1}) => Expanded(flex: weight, child: this);

  Widget centered() => Center(child: this);
}

// Example use:
Text("Hello").padded(12).centered()

You could even use property getters to leave off some brackets there. This has the additional benefit of reducing the amount of brackets which need to be matched correctly.

There is a comprehensive attempt to do this and more in VelocityX though I'm uncertain as to whether it is optimal to use that or add the perhaps 20 or so methods like those above that might be needed in any particular project, which could also ensure consistency in things like padding. The general view from the Flutter community would be to reject the above few paragraphs. If Widgets have lots of nesting then that will prompt you to split things up.

In terms of layout I've found React Native and Flutter to be easy to achieve desired results, though Flutter will likely be more verbose/harder to change (with all the nesting). SwiftUI seems like the weakest here, especially when you want something quite custom. It is often very unintuitive; after some time I feel like I can achieve most things, but like using CSS, it may be ugly and not much fun.

Animation

Doing custom animations in Flutter involves quite a bit of setup/boilerplate. Compared to Reanimated 2 (in React Native) it is quite tedious/confusing. However it will probably be more performant. Flutter does have a load of built in Widgets for simple animations. SwiftUI initially embraced a implicit animation system, but this has been deprecated as in practice you end up with things you don't want to be animated being animated. For simple animations it is probably the easiest (you wrap stuff in withAnimation but for more advanced things it might even be the hardest).

State

Flutter has quite a few options for state management. I consider this good, but can be a little bit challenging to start with. Though to be fair React can also be very hard to get started with in terms of figuring out what libraries to use to fill in the gaps (React itself is wonderfully simple and easy to learn, but for 'real' projects you typically end up adding lots of libraries). SwiftUI comes with a lot of built in facilities for state, dependency injection and so on. In the latest release there are one line ways to have simple persistence and state, among other improvements.

Global State

Most Apps these days use some kind of global state. Flutter has a lot of options here, though many seem pretty verbose and awkward. Often each solution has about a dozen different classes that must be composed in some way to read/update state and it is often hard to figure out which things to use to get the best results. Compared to something like Valtio for React this is a huge pain (admittedly Valtio is pretty cutting edge and a lot of people use more tedious options like Redux).

On Flutter personally favour MobX (with codegen), as this allows for concise, clearly defined state, fine grained reactivity (performance?) and ergonomic consumption. You use Observer widgets to observe the state, but for any particular project you can easily set up a (or a set of) specific helper widgets that do any dependency injection and this observation plumbing). The codegen part is a bit unfortunate (hopefully in the future something like Macros could allow for this to go away). A more standard suggestion is Provider.

Local State

Flutter has a strange way of doing state in a Widget, you switch to a 'stateful widget' which really means defining two classes. But it gets even worse: for many stateful things you now need to both set up the state and dispose of it. Yuck. Instead just use flutter_hooks: it works with hot reload, there is one class HookWidget and you can easily add new pieces of state without building up complexity. I think Flutter only gets away with its defaults here as most developers are coming from Android or other traditional UI dev contexts, where things are even worse. Coming from React the standard approach feels like a huge step backwards and it is puzzling that Flutter Hooks appears to be regarded as somewhat niche.

Conclusions

Again I was really impressed with Flutter. Technically it is remarkable. There are a few things coming from React (Native) which seem like backward steps, but there are also lots of major advantages. I was surprised how badly SwiftUI came out of a head to head comparison with Flutter, though for an app that leans very heavily on Apple APIs it may still make sense.