A client of mine needed a cross-platform mobile app. I had been wanting to try Flutter for a while, the client was stack-agnostic, so we decided to build it using Flutter. The initial version of the app was limited in scope, but we had tight deadlines so it was very much so trial by fire. Such is the life of a consultant (and I wouldn’t trade it for anything!).

Installation was relatively painless. The only manual step was: you have to manually add the CLI binaries to your PATH. But somebody on my team is pretty new and non-technical and they had no problems with installation. The installation also aggressively pushes installing Android Studio but you can get up and running without it initially (by targeting your current platform or Web).

The first thing I noticed is: if you’re using VSCode, the integration is great. Everything (F5 to debug, switching targets from Web to Windows to Android, etc.) just works out of the box. It felt like a more mature experience than I expected it to be. Documentation and examples are pretty complete, and the CLI / package management just works without having to Google endless errors to get your project building (I’m looking at you, NPM).

Dart

Dart feels ver C#-ish. If you have a .NET background, you’ll probably feel right at home. It’s designed for building UIs, so async / await is very much so a first-class citizen. It can be built JIT with the Dart VM or AOT. If you’re building a Flutter app, there is no support for reflection.

From the faq under “Why isn’t Dart syntax more exciting?”:

One team member’s personal testimonial: “I wish it had a little more razzle dazzle but I can’t deny that literally on my first day of writing Dart code, I was productive in it.”

That feels pretty fair to me. It’s almost go-ish in spirit with the reticence to add “exciting” features at the cost of simplicity. I was able to pick it up quickly, and that’s the point.

One interesting feature is that strings can be combined by placing them adjacent to each other:

// Prints "Well this is nice"
print('Well ' 'this ' "is" ' nice');

Honestly not sure how useful that is since seeing strOne + strTwo is pretty obvious. Dart also supports the usual suspects for quality of life such as string interpolation:

String interpolation = 'interpolation';
print('Now with more $interpolation');

The way you interact with the Flutter framework is generally by extending classes then overriding important functions:

// Extend StatelessWidget...
class MyApp extends StatelessWidget {
  // Override the build function so we can build our widget with
  // whatever UI components we want
  @override
  Widget build(BuildContext context) {
  //...
  }
}

Due to the declarative nature of building UIs and how nested their components are, you can get stuck in indentation hell pretty quickly. VSCode automatically adds comments to help with this and you will immediately see exactly why:

VSCode auto gen comments

It would be easier to manage if the closing characters were the same. But since you are using arrays [] mixed with single-item constructors (), things get tricky (as you can see in the image above). My advice here is: aggressively constrain the scope of your widgets before they become unwieldy. Widgets are cheap. Make many of them, and use stateless where possible.

Ecosystem

My strategy for learning a new framework/language is: I am a hands-on learner initially then I go back and more carefully read the docs after getting my hands dirty a bit. If this sounds like you, after you get your development environment working, head straight to samples. You can find an example for most of what you will want to do in a mobile app.

Dartpad is great and exactly what you’d expect from a modern framework/toolkit. It’s nice (though expected these days) that you can run all of the examples in your browser. Go developers will feel right at home.

As you might imagine from a Google project, the material design widgets are nice. The Cupertino (iOS-style) widgets are also quite capable. Barring the occasional headache, the UI catalog is full-featured and ready for production.

I like the concept of Flutter Favorites: to identify packages that the Flutter Ecosystem Committee has deemed you should first consider when building your app. How much they actually vet the packages, I don’t know. I tried using package_info_plus (a Flutter Favorite) and for the life of me couldn’t get it working using the package manager. I checked the folder where it was downloaded and there were no .dart files so when I tried importing them it wouldn’t work. I had to clone the source directly.

Random observations

Certain things that feel like they should be easy to do, like set the background color of a Material icon button, are more complicated than you might think:

return Material(
  color: Colors.white,
  child: Center(
    child: Ink(
      decoration: const ShapeDecoration(
        // This is the background color for our button
        color: Colors.lightBlue,
        shape: CircleBorder(),
      ),
      child: IconButton(
        icon: const Icon(Icons.android),
        color: Colors.white,
		onPressed: () {},
      ),
    ),
  ),
);

I also observed that floating that Ink component over a container with a different background renders in an odd way (when you tap the button there’s a square outline for some reason).

Padding / Container

Certain things that I am constantly doing like setting padding on elements felt inconsistent at first. For example, Container accepts a padding property directly:

Container(
  padding: const EdgeInsets.all(8),

… which might make you think other widgets do as well (that’s a very CSS-y mindset, right?). They don’t. So you have to either wrap anything you want padding on in a Container widget with padding or a Padding widget:

Padding(
  padding: const EdgeInsets.all(8),
  child: Text("Whew! Now my text has padding!"),
),

From the docs:

There isn’t really any difference between the two. If you supply a Container.padding argument, Container simply builds a Padding widget for you.

Container doesn’t implement its properties directly. Instead, Container combines a number of simpler widgets together into a convenient package. For example, the Container.padding property causes the container to build a Padding widget and the Container.decoration property causes the container to build a DecoratedBox widget. If you find Container convenient, feel free to use it. If not, feel free to build these simpler widgets in whatever combination meets your needs.

DateTime

When I began my software career, I didn’t realize how much of it would involve me dealing with quotidian DateTime implementation details like timezones and leap years. With every language, date handling has its unique pitfalls. The only thing I found odd with Dart/Flutter is that you have to install a dependency to be able to format dates. Other than that, the DateTime class will feel pretty familiar for most developers.

Numbers

Want to round a number to a fixed precision? The only way I found to do it is to round it as a string then num.Parse said string:

n = num.parse(n.toStringAsFixed(2));

To quote ThinkDigital on StackOverflow:

That’s the quickest way to do it. But it’s dumb that dart doesn’t have a direct way to do this

Thinking in terms of widgets

One thing that I needed to wrap my head around is: everything is a Widget. What does this mean? Let’s take an example where I want a button with both icons and text. It’s easy to think of the child property of button as only text as the examples show:

ElevatedButton(
  style: style,
  onPressed: () {},
  child: const Text('Enabled'),
),

… so initially I did something like search for “how to add an image to ElevatedButton”. But that child property is actually of type Widget, which means you can add in a table with rows and columns and images if you want. So, you want an icon next to your text? Create a layout inside the ElevatedButton:

ElevatedButton(
  onPressed: () {},
  child: Row(children: [
    Icon(Icons.add),
    Text('Add Item'),
  ]),
)

I had a similar experience with setting the leading property in my AppBar to an image instead of an icon (the default behavior). Here’s the example from the docs:

AppBar(
  leading: Builder(
    builder: (BuildContext context) {
      return IconButton(
        icon: const Icon(Icons.menu),
        onPressed: () { Scaffold.of(context).openDrawer(); },
        tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
      );
    },
  ),
)

Do you instead want an image? Padding? Change BoxFit? No problem. Since leading is a widget you can do whatever you want with it:

AppBar(
  leading: Padding(
      padding: const EdgeInsets.fromLTRB(6, 0, 0, 0),
      child: Image.asset('mylogo.png', fit: BoxFit.contain)),
)

Once I got in this mindset, I found myself referring to the docs/google far less even after only a few days with Flutter. Once you are familiar with a few controls you will need (buttons, AppBar, inputs, etc.) and have some knowledge of general layout stuff you can be pretty productive in short order. The next step is to compose multiple widgets into your own widgets to avoid repeating yourself:

/// Styled button with icon and text. Usage:
/// iconButton("Add Item", Icons.add, () {})
Widget iconButton(String text, IconData icon, void Function()? onPressed) {
  return ElevatedButton(
    onPressed: onPressed,
    child: Row(children: [
      Icon(icon),
      Text(text),
    ]),
  );
}

I think this will come more naturally to people with a React background.

The navigator is one of the most capable routing implementations I’ve used. There are certain things I find myself doing frequently and it was built with all of them in mind. For example, you have a list screen that navigates to a detail screen. After you navigate from the detail screen back to the list screen you want to refresh. Or conditionally refresh if you know the detail screen changed something that affects the list screen. No problem:

// Push returns a Future<T?> so you can just wait for your 
// other controls to Navigator.pop until we are back on this
// page. Potentially with a value that you can do something
// with.
Navigator.push(context, 
MaterialPageRoute(
    builder: (context) => DetailView(
          someID
))).then((value) {
	// Do something with value potentially.
})

And on your detail screen, here’s how you return data to the list screen:

Navigator.pop(context, "Here's the message. You better update!");

Being able to call Navigator.pop and always go back to your previous route with your state preserved and a return value is very useful.

State Management

For state management I went with the simplest, recommended option: Provider. It doesn’t involve a lot of boilerplate, you’re writing straightforward Dart code, and I had a good experience with it. Another popular state management solution is BLoC but it involves far more boilerplate / layers. For relatively simple projects it seems like overkill.

Most of what I did involved using ChangeNotifierProvider in conjunction with Consumer:

class SomeListState with ChangeNotifier {
  bool loading = true;
  List<Model> items = [];

  void refreshItems() async {
    // Set loading to true and let listeners know
    loading = true;
    notifyListeners();
    // Get our items from an async call
    items = await someAsyncServerCall();
    // Set loading false and let listeners know
    loading = false;
    notifyListeners();
  }
}

To consume this state with a Consumer:

return Consumer<SomeListState>(
  builder: (context, state, child) {
    // Loading data. Show loading indicator of some kind
    if (state.loading) {
      return Text('Loading...');
    }
    // Build the list with our items.
    return ItemList(state.items);
  },
);

Conclusion

Overall, I had a great experience with Flutter. The tooling feels mature and relatively stable. We were able to ship a cross-platform mobile app with relative ease on a tight timeline. My company is planning on using it instead of React Native on multi-platform projects going forward.