The initial version of Google’s cross-platform SDK Flutter came out in mid-2017, and it didn’t take long for it to grow into one of the top cross-platform frameworks on the mobile app market. As a result, developers and companies are increasingly opting to transition from native Android (and iOS, of course) development to the Flutter-powered cross-platform alternative.
But what are the advantages of it, and disadvantages (if any)? What challenges will a native Android developer likely face when making the transition?
Why (and how) Flutter?
Generally, the main reasons for choosing Flutter are the same you’ll hear with all “write once, run everywhere .” SDK’s: the quicker and cheaper development and maintenance processes (since a single codebase means less overall code needs to be written, fewer developers are required to write and maintain it).
But for now, let’s leave those considerations to the management side and look at things from a developer’s point of view. How does Flutter development dier from native Android overall, and how problematic it is to adjust to? Here are the main aspects I believe should be considered:
- UI design
- App architecture
- Programming language — Kotlin vs. Dart
- Libraries & documentation
UI design
We’re all familiar with the dual nature of Android native layouts: you lay your views out and stylize them in an XML file and later bind them with some Kotlin code to give them purpose and functionality. Since Views are mutable, it is possible to define them in XML and modify them at runtime as necessary in code.
Flutter takes a dierent approach with Widgets. They serve as primary UI elements and, as such, roughly correlate to Views but aren’t fully equivalent. While some correspond more directly — e.g., Rows and Columns are essentially LinearLayouts — some widgets are wrappers for specific view object properties — e.g., the Padding widget replaces the padding/margin properties.
In Flutter, each Widget focuses solely on its primary purpose, rather than subclassing a ton of higher-level classes and inheriting all their attributes; the often-praised-yet-rarely-utilized “composition over inheritance” principle in action. This approach results in a tree of UI elements with a greater depth but at no significant loss to performance or even readability once you get used to how it works.
For example, let’s say we want to create a simple button with some text and a margin around it and click. In Flutter, it might look something like this:
Padding(
padding: EdgeInsets.all(16.0),
child: ElevatedButton(
onPressed: () => {
// Do something here
},
child: Text("Click me!"),
),
)
Things to note:
- widget tree depth: Padding -> ElevatedButton -> Text
- no need for XML or any binding — everything is handled in your Dart code
- no view ID’s — no need for them since the views aren’t bound or addressed elsewhere
That last point brings us to another significant dierence. Unlike views, widgets are immutable: any change to a widget in the tree will require a redraw of the whole tree. Here is where Stateless and Stateful Widgets come into play.
Essentially, StatelessWidgets have only one State, which does not dynamically change, while their associated State object governs StatefulWidgets. A state is defined by variables that keep track of relevant data; as soon as any data changes, a setState function will redraw the corresponding widget tree.
This dierence, of course, requires a dierent approach when laying out your widgets. Since setState can only be called from within the actual State, you cannot modify your widgets from outside, as you would with Android views. It would help if you instead defined your widgets and their attributes conditionally to update them when their state changes automatically.
For example, if you want a button to enable itself when some form it’s associated with is valid, and additionally change its text to indicate it when it’s disabled, you can do something like this in your widget tree:
ElevatedButton(
onPressed: isFormValid ? () {
// Some function goes here
} : null, // Passing null here disables the widget
child: Text(
isFormValid ? "Confirm" : "Invalid"
),
);
Things to note:
- attribute values depend on is FormValid state variable — changing it under setState redraws the widget tree
- no calling attribute setters from outside like in Kotlin — everything is handled in the widget definition
When designing UI, one last thing I feel deserves mention (and praise) in Flutter is creating custom views/widgets. It makes the whole process intuitive to the point of being almost “descriptive”; you don’t have to look for the view closest to what you need to subclass/customize. Instead, use the composition of widgets to build whatever you need:
- Image and Text widgets for… well, images and text
- GestureDetector to make the Widget clickable
- Container for size, padding, background, etc.
- Row/Column/Stack to position widgets as needed
No XML + Kotlin, no view binding, no attrs.xml to define custom attributes, nothing. Just take the building blocks and compose your custom UI elements.
App architecture
Separating the view and business logic is one of the fundamental concepts in software development. It is essential in both native Android and Flutter, though achieved in noticeably dierent ways. Due to the way the UI is laid out (immutable widget trees), state management is the game’s name in Flutter architecture.
The initial intuitive take on this concept is simply using a StatefulWidget and the associated State, which we could mutate as necessary. But in such an implementation, the UI and the underlying logic are too closely coupled; the State object handles practically everything, and the Widget asks the State, “please give me the widget tree I should display.”
Why not separate the state data from the view layer instead? If only we could have a way to define the application’s intended state, with the view layer only in charge of how to display its data… Well, yes, we do have exactly such a thing — BLoC.
BLoC (Business Logic Components) is an architectural concept that revolves around the idea of streams (asynchronous data sequences) and sinks (the inverse — receivers of streamed data). A Bloc class will function as both; a sink receives inputs and streams to respective output data, with business logic to determine the input-output mapping.
The Flutter-specific Bloc implementation handles it through Events and States. In a nutshell, events are inputs, and states are outputs. Any UI interaction that the business logic should respond to has to send an event to the bloc, computing and yielding a corresponding state.
Since blocs function as streams output-side, you can yield multiple states for a single event (e.g., for a more resource-intensive operation, first emit a “loading,” then a “data” state). In the end, all the view layer needs to do is create an appropriate UI for the received states, optionally using the data additionally provided.
The essential widgets used in the view layer in a Flutter Bloc architecture are:
- BlocProvider — provides a bloc dependency to its child widgets. It should be used as a parent to all widgets that interact with a bloc.
- BlocBuilder — builds widgets to display in response to states; this is used to build new screens.
- BlocListener — performs actions in response to states (used for states that do not require a screen redraw, e.g., to show dialogs/notifications)
- BlocConsumer — combines BlocBuilder and BlocListener (reduces boilerplate code).
There is also a slightly dierent alternative to Bloc that can use — Cubit, which replaces events with direct calls to exposed bloc methods. This approach shortens and simplifies the code even more, but at the expense of traceability (no way of knowing what exactly caused a state change). It is often used for more simple cases, where a full bloc would be overkill.
Overall, I found the BLoC architecture fairly easy to move on to from Kotlin’s MVP/MVVM as soon as I got used to the concept of full state management. As with designing widgets, it might feel unusual to have to think of the entire big picture at once, but once you get used to thinking that way, it will help you keep a high level of control over what’s going on in your app at any given time. In addition, the well-executed separation of business and UI logic will help make the code clean and easily testable.
Programming language — Kotlin vs. Dart
In almost any metric, matching or surpassing Kotlin is a tall order. Its simplicity reduced verbosity to (yet full interoperability with) Java and countless QoL concepts it introduces.
Extension functions, data classes, scoping functions simplify a developer’s life to the point they’ll be unlikely to go back to Java or any similar language ever again. Google devs were fully aware of this when developing Flutter.
They chose Dart for their SDK, a language that, in my opinion, does a great job emulating the “modern” feel of Kotlin while also enabling what they considered a high priority: speed, productivity, and optimization for UI. We already covered designing UI using Dart, so let us now focus on what else it brings to the table.
In terms of syntax, the dierences between the languages are minor, as is my criticism for them. I like the ternary operator and variable declaration syntax in Dart. Still, I prefer some things in Kotlin, such as “when” blocks instead of switch-case, unifying class extension and inheritance, and a much less clunky and verbose declaration of extension functions.
Overall, I slightly prefer Kotlin here, but not by much, and the dierences shouldn’t pose any noticeable issues when adapting to Dart.
In terms of null safety, Dart has recently caught up with Kotlin; both languages now make types non-nullable by default, requiring nullable types to be explicitly declared as such (by adding “?” after their type).
Dart even takes a step further by supporting sound null safety, meaning “if its type system determines that something isn’t null, then that thing can never be null.” This requires migrating the whole project (including its dependencies) to use null safety. Still, if you can manage to do so, it will enable various compiler optimizations resulting in fewer bugs, smaller binaries, and faster execution.
Asynchronous operations are handled quite dierently in Dart. While Kotlin introduces coroutines — lightweight threads — Dart deals in Futures. A Future is a wrapper around a type that declares that a value of that type will be available after some delayed computation and is uncompleted before that.
To synchronize a future expression, the await keyword is used before it gets its completed result. You can easily handle exceptions by surrounding the await call with a try-catch block. Overall I found this approach to be quite intuitive and easy to use compared to Kotlin coroutines, not to mention some other ways of handling asynchronicity inherited from Java.
The last thing that deserves mention, but most certainly not the least, is the “hot reload” feature. This is technically a feature of the tooling, but it is also the language design that enables it. Hot reload/restart handles compilation incrementally, considering only the changes to the code rather than rebuilding the whole app, to redeploy the updated app to the device/emulator in mere seconds.
This eliminates the need for a (buggy and rarely accurate) layout preview and speeds up development tremendously, to the point I’d consider it my one of favorite aspects of Flutter development.
Overall, I was positively surprised with Dart. It didn’t blow me away at first as Kotlin did, and in many aspects, it doesn’t do anything better, but not doing worse already exceeded my expectations. The most important thing for me was for the transition to be as painless as possible, and for the most part, it was; after getting used to the new way of laying out the UI, everything else went smoothly.
And, of course, the hot reload feature is an absolute treasure that makes it hard to ever return to developing without it.
Libraries & documentation
When it comes to documentation and oicial support, Google left nothing to be desired for Kotlin and native Android, and they seem to be taking the same route with Flutter. Their oicial documentation includes detailed explanations, examples, tutorials, and code labs for standard features and use cases and various other resources. As long as you have time to spare to go through the docs and examples properly, learning any new Flutter concept shouldn’t prove diicult.
The growth of its dev community naturally follows Flutter’s fast growth in popularity. It is easy to get your questions answered on StackOverflow. There are tutorials and examples all over the place. On top of that, the SDK (as well as the Dart language) itself is constantly updated to feedback from developers who use it. Even in comparison to only a year ago, Flutter has been tremendously improved, and it doesn’t appear that growth will be slowing down any time soon.
3rd party library support deserves a special mention; in the pub. Dev, you can find a wide range of packages contributed by other developers for just about anything you can think of. Users can filter packages and rate their quality, making it easy to find the best solution for whatever you need. Adding a package to your project is trivial, only adding a line to your pubspec.YAML file and running a CLI command to fetch dependencies.
The only thing that might pose issues is using 3rd party native libraries in a Flutter project. This is not an everyday use case, usually encountered only when integrating specific devices (sensors, cameras…) with your app that supports only platform-specific APIs.
Flutter does provide a solution through PlatformChannels, which allows you to call native functions and pass primary data between Dart code and the native platform; however, that involves writing a native implementation of those APIs and calling those functions in Dart.
This is unfortunate as it will require some native (Android and iOS) coding, which a Flutter dev might not be familiar with, but I honestly don’t see how it could be done any better. Fortunately, with the growth of Flutter’s popularity, these situations are growing ever rarer.
Conclusion
Everyone knew the advantages of Flutter over native development ever since first hearing the term “cross-platform“: less code, fewer developers required, more accessibility, and cheaper development and maintenance. But the transition is what every developer is anxious about — how much of our previous knowledge will we need to “unlearn” to learn this new thing?
Fortunately, for a Kotlin dev, the barrier of entry is about as low as it can be. Some things will require a somewhat dierent approach, such as the UI design and State management-oriented architecture, but the overall transition is intuitive and simple, made even simpler by a very accessible and eicient Dart language and a significant development ecosystem. In this kind of environment, any Kotlin Android developer will be developing cross-platform apps with Flutter in no time.