The future of apps:
Declarative UIs with Kotlin MultiPlatform (D-KMP) — Part 3/3

A 3-part article explaining the new D-KMP architecture, based on DeclarativeUIs, Kotlin MultiPlatform and MVI pattern.

- Part 1: The D-KMP architecture and the Declarative UIs
- Part 2: Kotlin MultiPlatform and the MVI pattern
- Part 3: D-KMP layers and Team organization

Image for post
Image for post

The App State

The AppState is the most important component of our app, as it’s where we define all data needed by our UI layer.
The AppState is platform-independent and takes the form of a simple Kotlin data class. Here is an example of the AppState:

data class AppState (
val homeScreen : HomeState,
val listScreen : ListState,
val detailScreen : DetailState,
val profileScreen : ProfileState,
val settingsScreen : SettingsState,
 ...
)

It’s very convenient to split the AppState into multiple (data class) state objects, one per screen. This provides a very neat structure for our our declarative UI components.

Any modification to the AppState is made by the State Reducers, which are specific functions inside the ViewModel, called by the Events, as you can see in the diagram below.

Everything follows a unidirectional data flow:

  • a user via the UI layer triggers an Event
  • the Event calls the State Reducers
  • the State Reducers modify the AppState
  • the new AppState is displayed on the UI layer
Image for post
Image for post

The D-KMP ViewModel

In our D-KMP architecture, the ViewModel is platform-independent and is defined as a Kotlin class holding the StateFlow, and the StateManager:

class KMPViewModel() {    val stateFlow: StateFlow<AppState>
get() = stateManager.mutableStateFlow
internal val stateManager by lazy { StateManager() }}

The StateFlow is the component capable of propagating the AppState to the UI layer. As you can see from the class definition above, the AppState is defined as the StateFlow’s type.
You can notice that StateFlow is a computed property, with only a getter and not a setter. This is because StateFlow is read-only, as the MVI pattern requires the AppState to be immutable.

The actual modifications to the AppState happen via the read/write version of the StateFlow, the MutableStateFlow, which is defined as part of the StateManager component, as you can see in the class definition below:

class StateManager() {    internal val mutableStateFlow = MutableStateFlow(AppState())    internal var state : AppState
get() = mutableStateFlow.value
set (value) { mutableStateFlow.value = value }
internal val dataRepository by lazy { Repository() }}

Specifically, the AppState modifications are performed by the State Reducers, which are defined as StateManager extension functions, called by the Events functions.
Their role is to create a new AppState, by processing 3 inputs:

  • event parameters (provided by the user, via the UI Layer)
  • repository data (provided by the StateManager)
  • current AppState (provided by the StateManager)
Image for post
Image for post

The Events are defined as KMPViewModel’s extension functions, directly callable by the UI Layer. They define the high-level operations of the app, by deciding which State Reducers to call.

We are defining both Events and StateReducers as extension functions (respectively, of the KMPViewModel and StateManager classes) instead of methods, because this allows us to organize them in separate files.

Image for post
Image for post

Inside the “viewmodel” folder, we can have an “events” and a “statereducers” folder, where we place a file for each of our screens.

In each file, we group the Events or StateReducers functions that have to do with that specific screen.

Real example: an event calling state reducers

Let’s say we have a screen with a list of cities. The user clicks on one of them. This triggers a call to an event function, defined on the shared KMP ViewModel.

Image for post
Image for post
fun KMPViewModel.selectCity(city: String) {
stateManager.setLoading()
myScope.launch(Dispatchers.Main) {
stateManager.setCityData(city)
}
}

The selectCity event function calls 2 state reducers: the first one to display a loading screen, the second one (asynchronously) to display the detail data for the selected city.

Image for post
Image for post
fun StateManager.setLoading() {
state = state.copy(isLoading = true)
}
suspend fun StateManager.setCityData(city: String) {
val detailData = dataRepository.fetchCityData(city)
state = state.copy(detail = detailData, isLoading = false)
}

The AppState is an immutable object, as required by the MVI pattern, but we can still easily modify its properties, by using Kotlin’s copy() function, which copies the existing object into a new object having new values for the specified properties, while keeping the rest unchanged.

The new AppState is then automatically propagated to the UI layer, via the KMPViewModel’s StateFlow.

Collecting the app state on the UI layer

Let’s now see how we can collect the AppState, on each platform, via StateFlow.

In all cases, we wrap our KMP ViewModel ( KMPViewModel) into a platform-specific ViewModel ( AppViewModel), which helps us to add required platform-specific code and to fully comply to the platform lifecycles.

Collecting the StateFlow on Android is really straightforward. It’s just one line! This is because the Android team has implemented a special method collectAsState() as part of the JetpackCompose library.

class MainActivity : ComponentActivity() {
private val appViewModel: AppViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyNavigation(appViewModel.coreModel)
}
}
}
class AppViewModel : ViewModel() {
val coreModel = KMPViewModel()
}
@Composable
fun MyNavigation(model: KMPViewModel) {
val appState by model.stateFlow.collectAsState()
....
}

Collecting the StateFlow on iOS currently requires a little boilerplate code, but we are hoping that JetBrains will soon make it easier. We have already opened an issue here. Please upvote it.
At the moment, we need to initialize a “listener” on the platform-specific ViewModel:

@main
struct iosApp: App {
@StateObject var vm = AppViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(vm)
}
}
}
class AppViewModel: ObservableObject {
let coreModel : KMPViewModel = KMPViewModel()
@Published var appState : AppState = AppState()
init() {
coreModel.onChange { newState in
self.appState = newState
}
}

}

where onChange is an extension function we need to add to our KMP shared ViewModel, only needed by iOS:

fun KMPViewModel.onChange(provideNewState: ((AppState) -> Unit)) : Closeable {
val job = Job()
stateFlow.onEach {
provideNewState(it)
}.launchIn(
CoroutineScope(Dispatchers.Main + job)
)
return object : Closeable {
override fun close() {
job.cancel()
}
}
}

Also on Kotlin/React, we need some boilerplate code for collecting StateFlow. We have opened an issue here, where also the code is shown.

However, on the upcoming Compose for Web we mentioned earlier, we can be confident that collecting StateFlow will be a one-liner, same as in Android.

The D-KMP DataLayer

In our D-KMP architecture, we are aiming for a very clear separation between the ViewModel and the DataLayer.

Image for post
Image for post

The DataLayer consists of a Repository collecting data from multiple sources, which typically are: webservices, runtime objects, platform settings, platform services, sqlite databases, realtime databases, local files, etc.

The Repository also manages any suitable caching mechanism, which can be extremely smart, given the opportunity to be written once and applied to all platforms. Any complexity doesn’t need to be replicated for every platform.

The Repository’s role is to process any data and provide it unformatted to the ViewModel, which is where all data formatting happens.

The ViewModel doesn’t need to be aware of the data source. It’s entirely up to the Repository to manage the sourcing/caching mechanism.

Image for post
Image for post

KMP libraries

On KMP, we clearly cannot use platform-specific libraries. But we don’t need to worry, as the new KMP libraries are even better! In some cases they are entirely written in Kotlin, in others they wrap the native platform-specific libraries under the hood. In any case, you don’t need to care about their implementation.

Here is a table, summarizing the libraries for each key area:

Image for post
Image for post

We already know everything about Declarative UIs, StateFlow and Coroutines. Let’s quickly mention the other ones:
- Ktor Http Client, developed by JetBrains. It’s the best KMP networking library, wrapping the native HTTP clients on each platform.
- Serialization, developed by JetBrains. It provides a very simple way to serialize data. It’s typically used in conjunction with Ktor Http Client, to parse Json data.
- SqlDelight, developed by Square. It provides multi-platform support to local SQLite databases.
- MultiPlatform Settings, developed by Russell Wolf (TouchLab). It’s wrapping SharedPreferences on Android, NSUserDefaults on iOS, Storage on Javascript.

Beside the existing KMP libraries, anyone can build its own custom KMP libraries, using the expect/actual mechanism, which allows you to wrap platform-specific code.

Some KMP libraries we expect coming very soon would be:
- Location, wrapping Android and iOS location services/APIs
- Bluetooth, wrapping Android and iOS bluetooth services/APIs (UPDATE: since the release of this article, a KMP library for BLE is now available!)
- In-App-Purchases, wrapping GooglePlay and AppStore in-app-purchases APIs
- Firebase, an official KMP implementation of services like Analytics, Firestore, Authentication (but no worries, there is already a third party KMP library wrapping the full Firebase suite)

Team organization

Image for post
Image for post

Let’s now talk about the new way of organizing your development structure around D-KMP.

We see 4 main roles:

  • Declarative UI developer
    (JetpackCompose and SwiftUI)
  • ViewModel developer
    (KMP)
  • DataLayer developer
    (KMP)
  • Backend developer (Kotlin/JVM? Golang?)

Declarative UI developer
We believe that in a D-KMP team, the UI developer should be multi-platform, managing both JetpackCompose and SwiftUI implementations.
Considering the simplicity of the DeclarativeUIs frameworks, it’s totally feasible (and fun!) for the same developer to manage both. Keeping the focus on both frameworks, helps the developer to have a better understanding of the UI trends and to build the best user experience.

ViewModel developer
This role is the most crucial, and managerial. It’s the one at the center of the development, which requires a full understanding of the project.
The ViewModel developer needs to engage with the UI developer for defining all properties of the AppState, and with the DataLayer developer for defining the required data.
The ViewModel developer also needs to organize text localizations.

DataLayer developer
This is the most demanding role in terms of technical knowledge.
The DataLayer developer has to deal with anything that is data, including the implementation of the caching mechanisms.
He/she needs to organize all data sources, including platform-specific ones if needed: for example in case of location or bluetooth services.
This role requires a full understanding of Kotlin MultiPlatform, including the capability of writing custom KMP libraries.

BackEnd developer
In a D-KMP team, this role is still important, but less than in platform-specific development, where webservices had to be coordinated with all app teams (Android, iOS, Web).
In D-KMP, the BackEnd developer is coordinated directly by the DataLayer developer, who is the one responsible to define which is the data required by the app. The webservices don’t need to have an understanding of what’s happening in the app.
Webservices could be written in any language of choice (for example Golang), but if you are interested in a Kotlin full-stack approach you can also consider writing them in Kotlin/JVM, via the Ktor framework. Using Kotlin on the server-side allows you to share data class definitions between the client’s DataLayer and the webservices.

This is the end of the article. Thanks for reading!
If you have any question, feel free to contact me or leave your comments!

You can follow me on Twitter @dbaroncellimob

Upcoming talks on D-KMP:

  • 14th December 2020: DroidCon APAC
  • 7th January 2021: Google Developers Group Washington (USA)
  • 16th January 2021: Google Developers Group San Francisco (USA)
  • 27th January 2021: Google Developers Group Berlin (Germany)
  • 3rd February 2021: Kotlin London (UK)
  • 25th February 2021: Google Developers Group Montreal (Canada)
  • 3rd March 2021: Google Developers Group Johannesburg (South Africa)
  • 4th March 2021: DroidCon Online series

Special thanks! (I owe very much to the knowledge shared by these guys)

Mobile architect (client/server), with over 10 years experience. Focusing on Android, iOS, Kotlin, Golang. Now researching rich client architectures with D-KMP.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store