What I Like And Don’t Like About Flutter

Bouwe Ceunen
Axons
Published in
8 min readJul 19, 2020

Let’s start with some background information on how I got here. I already made apps for native Android in Kotlin, apps for Android and iOS with React Native, and shortly decided to use Flutter for a small app for a small company. It was very clear what the upsides and downsides were with regards to working with Flutter as opposed to React Native. I wanted to share my findings of what I like and don’t like about Flutter in this post.

✔ Running Flutter

Running on multiple devices is very straightforward, no more running specific commands for each device you want to use, whether it is Android or iOS. Just execute flutter run. If you have multiple emulators or simulators running Flutter will notify you on which device you would like to run your application. You can run it on all of them with flutter run -d all.

✔ Less Boilerplate

Flutter is really clean to start with, it only has a few needed files. No Redux-Saga boilerplate, no initializing of barrel files, etc. There’s only a main.dart in the lib folder. It’s very easy to start.

standard main.dart

✔ Everything Is A Widget

As you’d already probably noticed from the .dart files if you are familiar with Flutter, (most) components are considered a parent for one or multiple child components. It’s really easy to build your application. The most basic one is the Container component, it can have a fixed height and width. Commonly used components are the Row and Column, they specify how you want to group the children, horizontally or vertically. Another useful component is the Card, it has an ‘elevation’ parameter so you can easily create an elevated effect. I found it very convenient to build an app with the motto of ‘everything is a widget’. The documentation about Flutter layouts is very comprehensive.

✔ Launcher Icons

Launcher icons can be cumbersome to implement. Android and iOS both have different ways to integrate them. No more fiddling with xxhdpi images or opening XCode, there’s a package called flutter_launcher_icons that updates your icons based on only one png. You place your icon in the assest/icon folder and run the following command.

flutter pub run flutter_launcher_icons:main

Before you can run this, you’ll also need to add some information on where to find the main icon in your pubspec.yaml file.

flutter_icons:
android: "launcher_icon"
ios: true
image_path_android: "assets/icon/icon_round.png"
image_path_ios: "assets/icon/icon.png"

✔ Sentry Integration

Sentry is used for monitoring exceptions thrown from your app. It was very easy to set up and required almost no code editing. The only thing you need to do is change your main.dart to incorporate exception handling.

Sentry integration main.dart

✔ Future- And StreamBuilder

Futures are a very powerful mechanism to execute asynchronous tasks, such as fetching data from an API. It’s not always ideal to integrate this into your application because you will have to do error handling, insert a loading spinner and also show your content whenever it is ready. Flutter designed a very useful component for this, the FutureBuilder.

FutureBuilder

It will trigger the calculation, show the loading spinner, and depending on whether an error has been thrown or not it will show either the error message or the content the Future returned!

There’s a downside to this, you can’t manually retrigger the FutureBuilder, this is why we will have to turn our face to the StreamBuilder. With the StreamBuilder we can manually clear the stream and trigger the Future again by adding null to the StreamController _accountController.add(null).

StreamBuilder

The FutureBuilder and StreamBuilder are very useful widgets to implement when you are working with asynchronous API calls. It’s very clean and easy to use and will automatically handle loading and error handling for you!

✔ Testing

Mobile testing can take quite some effort to get it right. For React Native I used Jest and Enzyme, although it works quite well it was not easy to mock quite some libraries. Integration tests were done with Detox and again, this was not very straight forward to get all the boilerplate in order.

With Flutter, testing has been made easy. Integration tests require setting up 2 files, app.dart and app_test.dart in a folder called test_driver. You can execute the integration test by running the Flutter drive test command.

flutter drive --target=test_driver/app.dart
app.dart integration test
app_test.dart integration test

As you can see, this code snippet also contains a part of code you need to take automated screenshots. This is explained in the next section on how to do so. Unit tests only require 1 file, test.dart that is placed in the test folder.

test.dart unit test

In order to execute unit tests, you’ll need the following Flutter test command.

flutter run test

✔ Screenshots

Creating automated screenshots with Flutter is a breeze. The screenshots package also provides Fastlane support so you don’t have to manually move screenshots after they are taken. The only exception to this is if you use older Android tablets as I did. See the issue I created.

Screenshot taking executes automatically when running integration tests with the screenhots command. You only need to create a screenshots.yaml file in which you specify certain things like the test file you would like to run. In this case, it will run the app.dart file that was created when running integration tests in the previous section.

After creating the file you only need to insert following statements into it. These are also already included in the section about integration tests, it could not be easier. Automated screenshots in React Native was a pain, I eventually decided to work with Detox, which in turn worked well but was a lot (with strong emphasis) of work to set up correctly. With screenshots it’s nothing more than adding a few lines to your already excisting test files.

  • import 'package:screenshots/screenshots.dart';

An import has to be added of course to find the screenshots method.

  • final config = Config();

A final config has to be added at the beginning of each test file.

  • await screenshot(driver, config, 'myscreenshot1');

You can insert an await screenshot in every test you want in order to take a snapshot of that current test. After filling in this information in your test files, you can trigger automated screenshots with the screenshots command.

✔ Biometric Authentication

I found it very easy to set up biometric authentication within Flutter. The local_auth package is very easy to use and will catch all needed interactions with the underlying OS regarding biometrics. It’s possible to create an authentication.dart file, the only thing left is calling it where you want it.

if (await Authentication.authenticate(context)) {
...
} else {
...
}
authentication.dart

I also did this with a library in React Native, it was a lot more configuration and tweaking to get things working and in the end, it didn’t quite do everything as expected. For example, if you enter your fingerprint wrongly 3 times, it will lock your biometrics. In Flutter it displays a message “Biometric authentication is disabled. Please lock and unlock your sreen to enable it.” without any extra configuration. With React Native I would have to catch each exception and manually show messages based on which exception had been thrown. With the local_auth package I didn’t have to do anything special to get this all working as I was expecting to.

✘(✔) Shared State Management

EDIT: Support for shared state management out of the box is not that great, but there are very good packages to handle shared state. After some useful remarks, I tried out Provider with MobX and I must say this will resolve the shared state management issues. If you are already familiar with MobX this shouldn’t be hard to get you going with shared state management in Flutter. Definitely try it out!

I stumbled across a very peculiar problem, shared state handling. In Flutter you have StatefulWidgets, that hold state. Now how do you access state from another component? The answer was not so easy. I found some answers on StackOverflow and Medium. It looks like to me that there isn’t one, clear and preferred way to be able to access state from other StatefulWidgets. Not sure how this will hold with larger apps that have a lot of StatefulWidgets.

I implemented the answer where you don’t make your State private by renaming it and letting it start with a capital letter and not an underscore (if it starts with an underscore, it’s considered private within Flutter). Let’s say we have a parent component Parent and a child component Balance that is created by the parent component.

Instead of creating the following StatefulWidget with a private State,

balance.dart private state

we create one with a public State that can be obtained in the parent.

balance.dart public state

This way when we create the StatefulWidget, we can create it with a key for which we hold a reference in the parent component. Balance(key: _balance) where _balance is obtained by final GlobalKey<BalanceState> _balance = GlobalKey();. This enables us to call a public method that lives in the child component by calling the currentState on that GlobalKey reference _balance.currentState.somePublicMethod().

There are probably better ways to resolve this, but for my application, this was a clean and least invasive way to get things done without refactoring code.

✘ Android Release

You obviously want to deploy your app after development right, this should be easy to do. And it is for iOS, but you will have to do some changes to get it right for Android. This was already done right with React Native, so I peeked at the build.gradle of the React Native project and stole some lines.

I was having difficulties with the setup of signed Android apps and multiple building flavors. After some searching through GitHub issues and some debugging, I ended up adding the following things to the build.gradle in the app folder. Note that the following file only contains what I have added.

added method to build.gradle

You will also need to create a key.properties in the android folder that contains the following filled fields. This will enable Flutter to build signed Android apps that are ready to be uploaded to the Play Store.

storePassword=
keyPassword=
keyAlias=
storeFile=release.keystore

TL;DR

That’s all! Flutter really makes it easy to start working on, and test your Android and iOS app. It is very clean with the everything is a widget philosophy and automated screenshots are very practical with the screenshots package. Biometric authentication is also very easy to implement. Testing also has never been easier, as well as getting your launcher icons right with the flutter_launcher_icons package. Shared state management is not so convenient as it should be and some Android build.gradle knowledge is needed to create production builds.

I would really recommend Flutter if the app you are building is small or medium in size. Flutter makes a lot of things much easier and makes your apps more structured and readable. In my opinion, it’s not quite ready to tackle the behemoths of apps that require massive amounts of state, but Flutter is heavily worked on so it will surely get there someday.

--

--