The future of apps:
Declarative UIs with Kotlin MultiPlatform (D-KMP) — Part 2/3
A 3-part article explaining the new D-KMP architecture, based on DeclarativeUIs, Kotlin MultiPlatform and MVI pattern.
Kotlin 1.4 has made MultiPlatform real!
With the release of Kotlin version 1.4 (August 2020), Kotlin MultiPlatform has left its Experimental stage and entered the Alpha.
We are now at a point where we can start working on production projects, as the stability of the technology is already outstanding. Any change or improvement will mostly happen under the hood.
If we had any blocker so far while implementing D-KMP projects, they all came from the UI part (JetpackCompose is still in active development, but getting a lot better after the Navigation arrived this month) and not from the KMP part.
Kotlin was originally conceived by JetBrains in 2011 as a language just targeting the JVM. Its aim was to become a better alternative to Java, by avoiding verbosity and making JVM development more fun.
Thanks to its success in the JVM community, in 2017 Google announced first-class support for Kotlin on Android. Two years later (in 2019) Google nominated Kotlin as the preferred language for Android development (effectively replacing Java).
Now Kotlin is evolving as a “MultiPlatform” language, with the capability of compiling code to 3 different targets:
- JVM (for Android and server-side)
- Native (for iOS/macOS, Windows and Linux)
Thanks to this, we can now use Kotlin to develop shared code, running natively on each platform.
There are currently 2 acronyms used for multi-platform:
- KMM = Kotlin Multiplatform Mobile (just Android and iOS)
- KMP = Kotlin MultiPlatform (also includes Desktop and Web)
Since Kotlin 1.4, a dedicated KMM portal is available, explaining how to get started with multi-platform mobile development. It’s a very useful material to anyone new to Kotlin MultiPlatform.
JetBrains (which beside Kotlin is also the creator of Android Studio) released a KMM plugin for Android Studio, which allows developers to run an iOS app directly from Android Studio. This is also incredibly useful.
Kotlin isn’t just the programming language that is making multi-platform possible. It’s a programming language that it’s much fun to develop with, avoiding lots of boilerplate code. It has the most advanced features you can dream of: coroutines, computed properties, delegated properties, extension functions, higher-order functions, lambdas, etc.
Kotlin is quickly becoming one of the top mainstream programming languages. The code you write in Kotlin will last for decades. It’s a very safe bet for long-term projects.
KMP vs Flutter vs ReactNative
When talking about multi-platform, the 2 main frameworks you hear most of are Flutter and React Native, both allowing you to have a shared codebase.
At the same time you hear a lot of people disliking these frameworks as they don’t give you the freedom to tailor the UI natively on each platform.
Kotlin MultiPlatform comes to provide you with both benefits:
- Sharing code among all platforms
- Having the freedom to tailor the native UI on each platform
In KMP, the shared code is written in Kotlin, but it’s compiled as a native library: a Jar file for Android, an ObjectiveC framework for iOS, a JS library for the web. For this reason, the native UI layer can talk to the shared code in the most natural way, on each platforms.
In Flutter, the code is written in Dart and compiled to a native library via NDK on Android, LLVM on iOS, JS on Web. However, unlike KMP, Flutter needs to ship its own engine, which considerably increases the bundle size of the app.
Flutter doesn’t use native UIs, it has instead its own set of declarative UI widgets which are drawn by pixel, via the Skia Graphics Engine. Flutter has gained adoption in the last 2 years, by proving that mobile development could be greatly simplified with a Declarative UI toolkit, instead of the traditional Android and iOS view systems. However, now that a Declarative UI toolkit is available natively on both Android and iOS, using Flutter has lost its main benefit, which is speeding up development. JetpackCompose and SwiftUI are here to stay, helping to build top-quality apps at full speed.
The overall architecture of React Native has proven to be not particularly performant, and even its creator Facebook is moving away from React Native. In 2018 also AirBnB announced to be sunsetting React Native.
Platform-specific code in D-KMP is just 15%
At this point there will be people who might think: “Ok. KMP is great, I get that. DeclarativeUIs have finally arrived to Android and iOS, I get that. But in this way I still need to write a separate UI for each platform. It’s a lot of replication!”
The answer is: “NO! :-) It’s not a lot of replication at all!”
In the new world of native Declarative UIs, the UI layer is very thin. In the apps we have been building so far using the D-KMP architecture, the UI layer is about 15% of the overall app code. And that’s the only platform-specific code. All the rest is KMP shared code.
This extra 15% is totally worth, because it allows us to tailor the UI on each platform, with no restrictions, unlike Flutter or ReactNative.
Android and iOS are two different frameworks which have meaningful differences. A first class app needs to keep UI/UX patterns authentic on each platform.
From our experience, once we have written the JetpackCompose UI layer for Android, it’s straightforward to create the equivalent SwiftUI for iOS. The code structure is mostly the same. For a simple app, it easily takes less than a day.
There are equivalent components in the two frameworks, with just different names. For example, if you want to organize a set of texts horizontally, you include them inside a component called
Row in JetpackCompose and
HStack in SwiftUI, while a text component is called
Text in both frameworks, with just a different syntax. Once you familiarize with the small differences, you can quickly recreate one declarative UI from the other one.
All data is coming from the app state, which is fully managed by the KMP shared code, delivered via StateFlow (we’ll talk about this later in detail). On the Declarative UI of each platform, it’s just a matter of organizing the views, but it’s a light task.
What it matters is that the Declarative UI doesn’t need to process any data. It just has to display it dumbly. This also dramatically decreases platform-specific bugs!
Once you have bug-free shared code, everything can be applied very smoothly to each platform. This is why having a specific UI for each platform is a no-brainer!
MVI pattern: the third pillar of D-KMP
As we mentioned at the beginning, the MVI pattern (which stands for Model-View-Intent) is the third pillar of our architecture. The main concept behind it is the unidirectional data flow, which differentiates it from older patterns such as MVC, MVP, MVVM. The MVI is a reactive pattern that can be seen as the evolution of MVVM, with a more solid and predictable app behavior.
In MVI, we have a single source of truth for the View, which is the app state. At any one time, the app state consists of immutable data and can only be modified by the Model. Everything works in just one direction. A user triggers an event/intent. The Model responds to it by performing the relevant actions and changing the app state. The new app state is then reflected on the View.
In our D-KMP architecture, we are implementing the MVI Model as KMP shared code (as you can see from the diagram below). This allows us to keep state management in the shared code! This is crucial!
For this reason, our platform-specific code is just the Declarative UI layer, which is very thin and stateless, as it fully delegates state management to the KMP ViewModel.
StateFlow: the MVI pattern enabler on KMP
If you search for articles talking about the “MVI pattern”, you will find that most of them regard RxJava as the best (or even the only) way to implement the MVI pattern. Luckily it’s not true anymore, by far!
In May 2020, JetBrains released StateFlow, as part of the Kotlin Coroutines 1.3.6 version, which is the Kotlin Flow implementation to manage and represent a state in an application. For a more detailed explanation, you can read the GitHub issue published by JetBrains Roman Elizarov.
In simple words, StateFlow is exactly what enables our D-KMP architecture to implement the MVI pattern, gracefully and elegantly, relying on the Kotlin language rather than on external libraries. And above all, in a multi-platform way! Because StateFlow works on any platform Kotlin supports!
As you can see from the diagram above, StateFlow allows data to circulate in a unidirectional flow. The UI layer calls events on the ViewModel, which makes modifications to the app state and propagates it back to the UI layer via StateFlow.
Let’s now have a look at some code:
go to the Part 3 of this article!