Building KMM mobile projects only with the Android knowledge

Aurimas Žarskis
5 min readJun 23, 2023

--

Introduction

Kotlin multiplatform mobile (KMM) is a cross platform mobile development framework. It is developed by Jetbrains that allows developers to share code written using Kotlin between iOS and Android platforms. This allows developers to use all the powerful tools and features that comes with Kotlin to develop native cross platform applications.

In this article, I will discuss how we successfully utilised KMM to develop iOS and Android applications for one of our services, even without dedicated iOS developers on our team. While I won’t delve into the specifics of setting up and using KMM (which you can find here), I will focus on the reasons behind our choice of KMM and highlight key strategies that facilitated our application development process.

About the app

The application is designed with a simple structure consisting of three tabs: transactions, terminals, and profile. It incorporates a secure login system that utilises email and password, with session state management handled through bearer tokens. Within the profile tab, users have the capability to change their password or log out. In the transactions tab, users can access and view all transactions associated with their account, along with detailed information for each transaction. Similarly, in the terminals tab, users can explore all available terminals and access details about them.

App home screen

Why we chose KMM

There were several reasons why we chose KMM instead of other cross -platform frameworks:

  1. Android developers already know and love Kotlin. They know how to use it and make everything reactive using coroutines and flows.
  2. KMM and Android project structures are very similar, making it easier for Android developers to start using KMM.
  3. Some of the libraries we were using in Android projects are available as KMM libraries. Some examples include Ktor, Serialization, Realm and the list is growing every day.
  4. Both SwiftUI and Jetpack Compose are declarative UI frameworks. For this reason Android developers should be able to learn some basic SwiftUI in order to write UI for iOS.
  5. Learning Swift for some basic UI development should be easier than learning the whole framework together with a new language. Especially when we don’t need any advanced features like threading, since all of that can be done using Kotlin.

As evident, Android developers can seamlessly leverage their existing experience in Android development when transitioning to KMM. This advantage significantly influenced our decision to opt for KMM over Flutter. While adopting Flutter would have necessitated learning an entirely new framework and language, starting with KMM allowed us to capitalise on our team’s existing skill set, saving valuable time and expediting the development process.

Now lets see how we did it

Tech stack:

  1. MVI with shared ViewModels
  2. Koin for dependency injection
  3. Ktor for networking
  4. Realm and Multiplatform-Settings
  5. SwiftUI and Jetpack Compose
  6. UINavigationController and Compose Reimagined

Some of these libraries should definitely be familiar to some Android developers, so I won’t go into detail about them, but instead will show you how to create and share ViewModels that work on both platforms.

MVI and shared ViewModels

I think that being able to share ViewModels is very important in KMM, especially when Android developers will be writing SwiftUI code. It allows you to focus on native UI and leave all of the presentation logic handling to Kotlin where Android developers are much more comfortable.

Now sharing ViewModels wasn’t easy in KMM. Since iOS target doesn’t have native ViewModel class we will need to implement it ourselves. This wouldn’t be a big issue if we didn’t need it to be lifecycle aware and have support for viewModelScope. Implementing all of this isn’t trivial, but luckily the KMM community has already solved these problems and there are 2 very helpful libraries available.

  1. KMM-ViewModel. This library provides common ViewModel implementation that has access to coroutines scope and has integration with platform lifecycle.
  2. KMP-NativeCoroutines. This library is complementary to the previous one. It makes it easier to consume coroutines and flow from swift code. It also allows using State flow as if it was just a regular swiftUI state.

Lets see how BaseViewModel implementation looks like:

// Marker interfaces for UI state, UI intents and UI events
interface State
interface Intent
interface Event
abstract class BaseViewModel<S : State, I : Intent, E : Event> : KMMViewModel() {

protected abstract val initialState: S
@NativeCoroutinesState
abstract val state: StateFlow<S>

private val eventsChannel = Channel<E>(Channel.BUFFERED)

@NativeCoroutines
val events: Flow<E> = eventsChannel.receiveAsFlow()
private val intentFlow = MutableSharedFlow<I>(extraBufferCapacity = 32)

init {
intentFlow.onEach {
handleIntent(it)
}.flowOn(Dispatchers.Main.immediate).launchIn(viewModelScope.coroutineScope)
}

fun sendIntent(intent: I) {
viewModelScope.coroutineScope.launch(Dispatchers.Main.immediate) {
intentFlow.emit(intent)
}
}

suspend fun collectEvents(collector: FlowCollector<E>) {
withContext(Dispatchers.Main.immediate) {
eventsChannel.receiveAsFlow().collect(collector)
}
}

protected suspend fun sendEvent(event: E) {
withContext(Dispatchers.Main.immediate) {
eventsChannel.send(event)
}
}

protected abstract fun handleIntent(intent: I)
}

Most of the things in this base class are related to our implementation of the MVI pattern. Things specific to KMM are base class that we extend and two annotations. KMMViewModel base class provides implementations of lifecycle and viewModelScope for the iOS target. Under the hood it uses a regular ViewModel class for the Android target. NativeCoroutinesState annotation allows you to use StateFlow as a regular SwiftUI state, making iOS implementation very simple. NativeCoroutines annotation is meant to make consumption of other flows easier on iOS.

Here is an example of ViewModel implementation:

class PaymentListViewModel : BaseViewModel<PaymentListState, PaymentListIntent, PaymentListEvent>() {
override val initialState = PaymentListState()

@NativeCoroutinesState
override val state = combine(
// combine different flows here
) {
// convert them to your state object
}.stateIn(this)

override fun handleIntent(intent: PaymentListIntent) {
when (intent) {
// Handle different intents here
}
}
}

An here is an example how you would use this ViewModel in UI code:

@Composable
fun PaymentListScreen(
viewModel: PaymentListViewModel
) {
val state by viewModel.state.collectAsStateWithLifecycle()

LaunchedEffect(viewModel) {
viewModel.collectEvents { event ->
when (event) {
// Handle different events here
}
}
}

PaymentListScreen(state)
}

On Android there is no difference on how you would use this ViewModel between native and KMM. On the other hand, iOS usage is a bit different compared to native. You have ObservedViewModel annotation which adds all of the lifecycle and coroutineScope functionality. After that, you can use viewModel.state_ to get access to your state object and you can use this state as a regular SwiftUI state. To collect events, you have to create an asyncSequence that is provided by KMP-NativeCoroutines library. Using it you get access to events that are type safe

struct PaymentsView: View {
@ObservedViewModel var viewModel = PaymentListViewModel()

init() {
let viewModel = viewModel

Task {
do {
let sequence = asyncSequence(for: events)
for try await event in sequence {
// There is full type safety.
// Just match event to one of defined event types
}
} catch {
print("Failed with error: \(error)")
}
}
}

var body: some View {
MainContent(
transactions: viewModel.state_.transactions
)
}
}

Conclusion

  1. For simple projects it’s possible to use KMM to develop iOS and Android apps without having dedicated iOS developers in the team.
  2. It’s easy for Android devs to start writing KMM code, allowing sharing business code between platforms.
  3. When setup correctly, it’s possible to share not only business code, but also ViewModels.
  4. If Android developers are willing to spend some time to learn Swift and SwiftUI, it then becomes possible to write whole application without having iOS developers in the team.

Considering all these factors, I firmly believe that KMM is emerging as an excellent alternative to other cross-platform frameworks, such as Flutter. Furthermore, as Jetpack Compose continues to evolve and mature for the iOS target, the prospects for KMM are bound to improve even further.

--

--

Aurimas Žarskis
Aurimas Žarskis

Written by Aurimas Žarskis

Staff Android Software engineer @ kevin.

No responses yet