My experience with Flutter as an Android developer

  • Harsh Shandilya
7 minute read
Flutter article image

Over the past month, I’ve been building the mobile app for Pause, our first in-house product at Obvious. We chose Flutter as the framework to build it in, specifically for the development velocity of being able to build iOS and Android apps with a single team. It’s been a great journey so far, but it might be worthwhile to note that there are some very pronounced differences between building native Android apps and using Flutter—ones that were equal parts interesting to observe and adapt to.

Hot reload is great!

On Android, UI development is typically done by using the IDE preview and then deploying on device repeatedly, making for a rather slow iteration cycle. Flutter entirely fixes this, because here you can deploy a debug build once, and then hot reload to iterate on your UI. This is significantly faster and more reliable, since you don't need to rely on a mocked preview where the UI could look potentially different once deployed to a real device.

Caveats

Hot reload only works for debug builds which tend to be slower, and they can render the UI to be of poor quality or unreliable (notably bad on Android, not so much on iOS). This is especially evident after a few subsequent hot reloads.

Widgets are straightforward to build and easy to theme

The core and material libraries included in Flutter are great for building design systems at a rapid pace, and theming is easy as well. On Android, theming requires the use of an XML driven system that often ends up being rather verbose and is generally hard to get right. As opposed to that, all Flutter requires to set an app theme is for you to build an instance of ThemeData and pass it to MaterialApp. This removes a lot of complexity compared to Android where styles and themes can interact in confusing ways, leading to theming issues that might be hard to debug.

Caveats

The explicit distinction between stateful and stateless widgets can feel welcome to some, but after having used Jetpack Compose for a few months, I feel that Flutter could be doing more to handle the distinction by itself rather than punt it onto developers. It's easy to get confused between StatefulWidget and StatelessWidget, but at least Flutter offers a great guide to explain the differences between the two.

Documentation covers all bases

Flutter documentation does a pretty good job of covering their bases, so you'll get the usual combination of API reference and broad overviews of topics like layout rendering. There are a lot of samples within the cookbook and the Flutter Gallery too. Flutter clearly knows its target audience well, and offers detailed onboarding guides for Android and iOS among others, making 1:1 comparisons between native and Flutter concepts. It's an awesome jump-off ramp to get people started, but the declarative UI explainer could probably use some more words.

Dart is not the best of programming languages

Dart as a language still feels like it hasn’t escaped its JavaScript roots. It has a lot of rather undesirable features enabled by default, such as implicit interfaces and most notably: dynamic as a fallback for type inference.

Undeterministic type inference

Traditionally, a type system that can infer types based on values simply fails compilation if it cannot infer a type. Dart however opted for a weird middle ground where it will sometimes raise a compilation error and on other occasions, assume the type to be dynamic, which basically means “anything”. This lack of consistency leaves the door open for a variety of subtle type related bugs that would be resolved by always throwing an error.

A lot of the Flutter APIs suffered significantly from this type un-safety. Anything that requires a Navigator result is implicitly typed to dynamic, and can fail at runtime for a variety of reasons. A simple real world example from our codebase:

final _logoutResult = await showDialog(context: context, child: LogoutDialog());
if (_logoutResult) {
 _startLogout();
}

What you’re looking at appears obvious at first glance. We expect showDialog to return a bool, and use that to determine whether or not to start the logout flow. However, you can easily encounter a situation where type inference fails at runtime and trigger a crash because _logoutResult can no longer be coerced as a boolean value. We faced one such problem, where dismissing the dialog by tapping outside would cause no result to be returned and make the value null. Here's how we ended up fixing it.

// the ?? operator is "if-null" and ensures _logoutResult is always a boolean.
final bool _logoutResult = await showDialog(context: context, child: LogoutDialog()) ?? false;
if (_logoutResult) {
 _startLogout();
}

The root cause here is how Flutter determines types, where it doesn't use the left-hand side of the expression but the right-hand side instead. In this example you can see that the editor shows no errors, infers the type of nullableVar as dynamic, and the app crashes at runtime. Slightly changing the code to this correctly fails at compile time citing nullability issues, and also automatically infers the type of nullableVar as bool. Coming from Kotlin, that's surprising behavior and bound to trip up developers.

Generics are too easy to get wrong

Dart has support for generics, but you are free to simply not supply those types when calling methods or instantiating classes and in most cases, Dart will pepper in dynamic. This again contributes to type unsafety and makes generics a real footgun in the language. The examples from above illustrate this problem as well.

Extension functions are hard to discover

Extension functions exist in Dart, but IDEs are unable to offer autocomplete for them. You’ll first need to import the file defining your extension (Dart has a designated keyword for extension functions) before your IDE can autocomplete them. This significantly worsens the experience as a Kotlin developer, where I’ve come to expect my IDE to surface extension functions via completion suggestions pretty much every time.

Lack of sum types

The language also does not support sum types, like Kotlin does with sealed classes, making the modeling of a variety of real-world situations slightly harder. There are community developed libraries to fill in some of these gaps (sum_types), but they involve codegen, which is another pain-point.

File-based imports often cause problems

Because Dart scopes imports based on files rather than classes and functions, namespace collisions are commonplace. These can be tedious to track down, and having to alias imports frequently or hide specific symbols from them doesn't scale particularly well.

Flutter tooling boosts developer productivity

I’ll say this upfront: having the flutter CLI is amazing. It encapsulates a lot of functionality that is usually all over the place, and puts them into an easily accessible interface. You can use it to install and launch your apps, manage emulators, interact with the pub package manager and a lot more. Android needs this!

Caveats

However, there are some gripes with one specific tooling aspect: codegen. Coming from the Android ecosystem, there are certains expectations from code generation—that it should be transparent, one should not have to explicitly run it, and the generated code must not be a part of the source tree.

To run codegen tasks in Flutter, you need to call into a package called build_runner that performs the actual codegen. There is no support for doing this from the IDE, which means a spot of furious googling ensued as I tried to find out how to do it using the flutter CLI. Once you've figured that out and generated these sources, you’ll find that your source tree is littered with *.g.dart files. Now you have a decision to make: whether to check them into version control or not. Here, build_runner does offer some guidance around it, but it's still more involved than what you'd typically do on Android.

Closing notes

There are pretty interesting developments in supporting all platforms for mobile app products and that seems to be the focus of where the ecosystem is headed as well.

Flutter makes for a pretty great cross-platform toolkit for apps that are UI-intensive or innovative with a unified platform-agnostic design system, and has certainly found success as can be seen in the Flutter Showcase.

For teams with existing Android and/or iOS codebases that wish to adopt Flutter, Airbnb's experiences with adopting React Native and Dropbox's learnings from their failed attempt at sharing code using C++ are great reads on the challenges of introducing radically new technologies into existing teams and how you can attempt to smooth over these transitions.

The Android Jetpack team's work on the declarative UI toolkit, Jetpack Compose is an extremely exciting development in this space. It is built entirely in Kotlin, and is fully supported by Google on Android. JetBrains maintains a desktop port of it with intentions to introduce Compose in IntelliJ in the future, and there are community efforts to make the core runtime and compiler work with Kotlin/JS as well, making it a competitive option in the cross-platform space in the coming days.

With the web target reaching stable in Flutter 2.0, it is now an even more lucrative option for creating apps that are simultaneously available on multiple platforms, with a single team.