From Kotlin to Flutter: A New Perspective on Mobile App Development

Jasmin Vučelj
10 min readMar 12, 2021

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 make the 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 differ from native Android overall, and how problematic it is to adjust to? Here are the main aspects I believe should be considered:

  1. UI design
  2. App architecture
  3. Programming language — Kotlin vs Dart
  4. Libraries & documentation

UI design

We’re all familiar with the dual nature of Android native layouts: you lay your views out and stylise 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 different approach with Widgets. They serve as primary UI elements and as such roughly correlate to Views, but aren’t fully equivalent. While there are some that correspond more directly — e.g. Rows and Columns are essentially LinearLayouts — some widgets are really wrappers for certain view object properties — e.g. the Padding widget replaces the padding/margin properties. In Flutter, each widget focuses solely on its own 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 make it perform an action on 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 kind of 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 major difference. 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 StatefulWidgets are governed by their associated State object. A state is defined by variables that keep track of relevant data; as soon as any of that data changes (inside a setState function), the corresponding widget tree will be redrawn.

This difference, of course, requires a different 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. You should instead define your widgets and their attributes conditionally, to have them automatically update when their state changes. 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 isFormValid 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 it comes to designing UI, one last thing I feel deserves a 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, just use 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
  • etc, etc, etc.

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, and it is essential in both native Android and Flutter, though achieved in noticeably different ways. Due to the way the UI is laid out (immutable widget trees), state management is the name of the game in Flutter architecture.

The intuitive initial take on this concept is to simply use 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 simply 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 concept of streams (asynchronous data sequences) and sinks (the inverse — receivers of streamed data). A Bloc class will function as both; a sink to receive inputs and stream to output respective data, with business logic to determine 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, which will compute and yield 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 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. Should be used as 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 different alternative to Bloc that can be used — 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 than (yet full interoperability with) Java, as well as countless QoL concepts it introduces (extension functions, data classes, scoping functions etc.) 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 though, and 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 clearly considered 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 differences between the languages are very minor, as is my criticism of either of them. I like the ternary operator and variable declaration syntax in Dart, but there are definitely some things I prefer 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 differences 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, but 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 differently 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 in an uncompleted state before that. To synchronize a future expression, the await keyword is used before it to get 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 even to Kotlin coroutines, not to mention some other ways of handling asynchronicity inherited from Java.

Last thing that deserves a 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 really positively surprised with Dart. It didn’t blow me away at first like 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 official support, Google left nothing to be desired for Kotlin and native Android, and they seem to be taking the same route with Flutter. Their official documentation includes detailed explanations, examples, tutorials and codelabs for common 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 difficult.

Flutter’s fast growth in popularity is naturally followed by a growth of its dev community. It is easy to get your questions answered on StackOverflow, there are tutorials and examples all over the place, and on top of that, the SDK (as well as the Dart language) itself is being updated constantly according to feedback from developers who use it. Even in comparison to only a year ago Flutter has been improved tremendously, and it doesn’t appear that growth will be slowing down any time soon. 3rd party library support deserves a special mention; on 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, being a matter of 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 a common use-case, usually encountered only when integrating certain devices (sensors, cameras…) with your app that support only platform-specific APIs. Flutter does provide a solution through PlatformChannels, which allow you to call native functions and pass basic 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 really see a way it could be done any better. Fortunately, with the growth of Flutter’s popularity, these situations are growing ever more rare.

Conclusion

Everyone knew the advantages of Flutter over native development ever since first hearing the term “cross-platform”: less code, fewer developers required, easier 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 as about as low as it can be. There are some things that will require a somewhat different approach, such as the UI design and state management-oriented architecture, but the overall transition is very intuitive and simple, made even simpler by a very accessible and efficient Dart language and a great development ecosystem. In this kind of an environment, any Kotlin Android developer will be developing cross-platform apps with Flutter in no time.

--

--