Kotlin Multiplatform Mobile Docs Help

Architect your KMM application

This document provides architecture guidelines on how to decide which layers of your application make cross-platform that work both on iOS and Android and which keep native.

On the backend, the business logic is similar for both Android and iOS, so it's a great candidate for making it cross-platform. For platform-specific features such as network requests, local database access, hardware manipulation, and cryptographic storage, use a multiplatform library that does this for you or connect to platform-specific APIs with the expect declaration, by providing the actual implementation for each platform.

KMM presumes that the best user experience is when it is native to the platform itself, and therefore we don’t recommend sharing it in KMM. However, you can share the code for the UI behavior that defines what happens with any user interaction and how the frontend communicates with the backend with the help of compatible architectural patterns.

Best practices for the KMM app architecture

To summarize, we recommend making cross-platform the following layers in your application:

LayerRecommendation on sharing
Business logicYes
Platform accessYes/no. You’ll still need to use platform-specific APIs, but you can share the behavior.
Frontend behavior (reaction to inputs & communication with the backend)Yes/no. Consider these architectural patterns.
User interface (including animations & transitions)No. It needs to be platform-specific.

Architectural patterns for sharing UI behavior

You can choose to share the UI behavior using the Model-View-Presenter (MVP) or the Model-View-Intent (MVI) pattern. These patterns:

  • Make a clear distinction between the UI and presentation layers.

  • Are completely decoupled from the UI platform.

  • Are easily testable without a UI environment.

MVP for legacy UI frameworks

Model-View-Presenter (MVP) forces you to create an API for both the Presenter that receives inputs and the View that displays outputs, allowing you to test each independently.

Here is an example of a simple MVP presenter:

class LoginPresenter { interface View { fun displayError(code: Int) fun displayLoading(loading: Boolean) fun goToNextScreen() } private var view: View? = null private var lastCommand: View.() -> Unit = { displayLoading(false) } private fun commandView(command: View.() -> Unit) { lastCommand = command view?.command() } fun attach(view: View) { this.view = view.apply(lastCommand) } fun detach() { this.view = null } fun login(username: String, password: String) { MainScope().launch { try { commandView { displayLoading(true) } getNetwork().login(username, password) // suspending commandView { goToNextScreen() } } catch (ex: LoginException) { commandView { displayLoading(false) displayError(ex.code) } } } } }

Note the commandView and lastCommand mechanism, which allows a view to detach and re-attach, for example for configuration changes on Android.

MVI for declarative UI frameworks

Model-View-Intent (MVI) is the natural evolution of MVP when working with declarative UI frameworks (although it also works with legacy UI frameworks). It is therefore recommended to be used with Swift UI or Jetpack Compose.

In MVI, the entire UI structure is described in one tree. Here is an example of a simple MVI presenter:

class LoginPresenter { sealed class Model { object Form : Model() object Loading : Model() data class Error(val code: Int): Model() object GoToNextScreen : Model() } interface View { fun displayModel(model: Model) } private var view: View? = null private var lastModel: Model = Model.Form private fun displayModel(model: Model) { lastModel = model view?.displayModel(model) } fun attach(view: View) { this.view = view.apply { displayModel(lastModel) } } fun detach() { this.view = null } sealed class Intent { data class Login(val username: String, val password: String) : Intent() } fun process(intent: Intent) { when (intent) { is Intent.Login -> { MainScope().launch { try { displayModel(Model.Loading) getNetwork().login( intent.username, intent.password) // suspending displayModel(Model.GoToNextScreen) } catch (ex: LoginException) { displayModel(Model.Error(ex.code)) } } } } } }

Note the displayModel and lastModel mechanism, which allows a view to detach and re-attach, for example for configuration changes on Android.

Thanks to contributors

We'd like to thank the Kodein Koders team for helping us write this article.

Last modified: 20 February 2021