Blog Infos
Author
Published
Topics
,
Published

We will review a framework for building KMM apps with the objective of not only share business logic but also presentation logic.
Kotlin shared code will contain the core logic while from the client side (iOS and Android) we will have very few code, just Compose UI and SwiftUI.

TL;DR
If you want to go directly to the code, here is the demo code of my proposal which has been added to the official list of KMM samples

  • Share business and presentation logic following a unique architecture on both platforms.
  • Minimize iOS and Android specific code
  • Observe state changes from Compose UI / Swift UI and react to them

If you are reading this article, I will assume you already have a notion of what KMM is, in which case you can move on to the next section. Otherwise let’s make a quick review.

From its official web:

Kotlin Multiplatform Mobile (KMM) is an SDK designed to simplify the development of cross-platform mobile applications. You can share common code between iOS and Android apps and write platform-specific code only where it’s necessary. For example, to implement a native UI or when working with platform-specific APIs.

How does it work?

 

Source: https://kotlinlang.org/lp/mobile/

 

Kotlin shared code is compiled to JVM bytecode on Android and to native binaries on iOS, so on Android we just add a Gradle dependency and on iOS we just link a framework.

Why use KMM?
  • Write your app’s logic just one time, less code means less potential bugs.
  • Unlike other cross platform solutions, we don’t lose performance and we are able to use all the native libraries made for Android and iOS
  • Using the actual/expect protocol we can write platform specific code if needed, for example we will use this feature to be able to inherit our custom ViewModel class from Jetpack’s ViewModel

 

  • iOS and Android: these modules contains the iOS and Android apps (Xcode and Android Studio projects). Most of the code should be only SwiftUI and ComposeUI code.
  • Kotlin shared code: this is the module containing the KMM project, here we will implement our business and presentation logic. So the core of our app will be here.
  • Arch: this is a KMM library made by me which we will add as a dependency to our shared code.

Arch is a small and lightweight Kotlin multiplatform library that helps us to architecture mobile applications, it is based on Spotify’s Mobius library but instead of relying on RxJava, it relies on coroutines, SharedFlow and StateFlow

We will review the core concepts of the library, I will omit some topics like Events and error handling just for simplicity.

Key features:
  • Unidirectional flow
  • Simple state management
  • Support for Jetpack’s ViewModel (by using the actual/expect protocol)
App´s flow using Arch

 

 

This diagram shows how the components in this architecture interact between them, let’s review each one of them and give a brief explanation:

  • Actions: an action is a request to modify the state of the application (or a part of it), actions are dispatched from iOS and Android code.
  • Updater: you can imagine the updater as a pure function which will receive an action and the current state, and will return a new state. Besides modifying the state, Updaters can dispatch SideEffects and Events
  • SideEffects: generically speaking, a side effect is an operation that modifies some state outside its local environment, in our architecture SideEffects are operations like a HTTP request, a database transaction or any I/O operation.
  • Processor: the processor is the element that process SideEffects , it is in charge of executing the database transaction, http request or whatever the received SideEffect should do. When the task is completed (successfully or not) the processor will dispatch a new Action in order to update the state.

Let’s dive into real code, we will write a very simple movies app with this proposed architecture using the movie db api

Steps
  • Define State
  • Define Actions
  • Define SideEffects
  • Create ViewModel
  • Implement Updater
  • Implement Processor
  • Observe state changes from SwiftUI and ComposeUI views.

Except for the last one, all these steps are implemented inside our shared kotlin module.

State

For this app we just need to save two things in our state:

  • The list of movies
  • The selected movie, since we want to show the details of a movie when the user taps it from the list.
data class MoviesState(
val movies: List<Movie> = emptyList(),
val selectedMovie: Movie? = null,
) {
init {
freeze()
}
}
view raw State.kt hosted with ❤ by GitHub
Actions

we just need three actions:

sealed class MoviesActions {
object FetchMovies : MoviesActions()
data class SelectMovie(val movie: Movie) : MoviesActions()
data class SaveMovies(val movies: List<Movie>) : MoviesActions()
}

The first two actions will be dispatched by user events (like tapping or opening the app), the last one will be dispatched by the Processor when we get the response from the API.

SideEffects

We have a single SideEffect: get the list of movies. We could set on which dispatcher or coroutine scope we want to run our SideEffect, by default it will use the viewModelScope on Android and a custom scope on iOS

sealed class MoviesEffects(
override val dispatcher: CoroutineDispatcher = ...,
override val coroutineScope: CoroutineScope? = null) : SideEffectInterface {
object LoadMovies : MoviesEffects()
}

Job Offers

Job Offers


    API Engineer

    American Express
    New York, USA
    • Full Time
    apply now

    Senior Compiler Engineer C++/LLVM – Munich

    Guardsquare
    Munich
    • Full Time
    apply now

    Engineering Manager – Apps Lifecycle

    Zalando SE
    Berlin
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

Jobs

Updater

Here we decide how we want to modify the state based on the received action, we have to create a class implementing the Updater interface, which has only one method that returns a new state and (maybe) new SideEffects wrapped inside a Next object.

typealias NextResult = Next<MoviesState, MoviesEffects, MovieEvents>
class MoviesUpdater : Updater<MoviesActions, MoviesState, MoviesEffects, MovieEvents> {
override fun onNewAction(
action: MoviesActions,
currentState: MoviesState
): Next<MoviesState, MoviesEffects, MovieEvents> {
return when (action) {
is MoviesActions.FetchMovies -> fetchMovies(action, currentState)
is MoviesActions.SaveMovies -> saveMovies(action, currentState)
is MoviesActions.SelectMovie -> selectMovie(action, currentState)
}
}
private fun selectMovie(action: MoviesActions.SelectMovie, state: MoviesState): NextResult {
return Next.StateWithEvents(
state.copy(selectedMovie = action.movie),
setOf(MovieEvents.OpenSelectedMovie(action.movie))
)
}
private fun saveMovies(action: MoviesActions.SaveMovies, state: MoviesState): NextResult {
return Next.State(state.copy(movies = action.movies))
}
private fun fetchMovies(action: MoviesActions.FetchMovies, state: MoviesState): NextResult {
return Next.StateWithSideEffects(state, setOf(MoviesEffects.LoadMovies(action.type)))
}
}

Notice that fetchMovies() method is returning a NextResult object which contains a SideEffect, particularly the LoadMovies one. So the Processor will get notified of this new dispatched SideEffect and will execute the HTTP request operation

Processor

Now we have to implement the Processor interface which has a unique suspend function that returns an action.

class MoviesProcessor() : Processor<MoviesEffects, MoviesActions> {
private val api : TMDApi = TMDApi()
override suspend fun dispatchSideEffect(effect: MoviesEffects): MoviesActions {
return when(effect){
is MoviesEffects.LoadMovies -> getMovies(effect)
}
}
private suspend fun getMovies(effect: MoviesEffects.LoadMovies) : MoviesActions {
val movies = api.getMovies(effect.type)
return MoviesActions.SaveMovies(movies)
}
}

TMDApi class is in charge of making the HTTP request for getting the movies and converting the json response into an array of movies, we don’t care how it is implemented but if you are curious you can always check the full demo code repository, under the hood it is using Ktor.

After we get the list of movies, we just dispatch the action to save them to the state.

ViewModel

Almost there, now we have to wire up all these classes that we have implemented via the ViewModel which must inherit from ArchViewModel

class MoviesViewModel :
ArchViewModel<MoviesActions, MoviesState, MoviesEffects, MovieEvents, Nothing>(
updater = MoviesUpdater(),
initialState = MoviesState(),
initialEffects = setOf(MoviesEffects.LoadMovies()),
processor = MoviesProcessor(threadInfo),
) {
}

Yes! that is all the code our ViewModel needs.

Note that we have set an initial state and an initial effect.

Observe state changes

Final step, just observe state changes! but how?

ArchViewModel exposes a Flow which emits all state changes, so we just have to collect it.

On Android collecting a Flow is straightforward but on iOS it is not that simple, that is why the Arch library implements the following FlowWrapper

class FlowWrapper<T>(private val source: Flow<T>) : Flow<T> by source {
init {
freeze()
}
fun collect(onEach: (T) -> Unit, onCompletion: (cause: Throwable?) -> Unit): Cancellable {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch {
try {
collect {
onEach(it)
}
onCompletion(null)
} catch (e: Throwable) {
onCompletion(e)
}
}
return object : Cancellable {
override fun cancel() {
scope.cancel()
}
}
}
}
view raw FlowWrapper.kt hosted with ❤ by GitHub

If you have a better solution to this problem of collecting Kotlin Flows from Swift code, I’d love to read it.

Let’s see the final code:

class MoviesFragment : Fragment() {
private val viewModel: MoviesViewModel by activityViewModels {...}
override fun onCreateView(inflater: LayoutInflater): View = ComposeView(inflater.context).apply {
layoutParams = ...
setContent {
val state by viewModel.observeState().collectAsState()
MoviesListView(state = state, onMovieSelected = { movie ->
viewModel.action(MoviesActions.SelectMovie(movie))
})
}
}
}
struct MoviesListScreenView: View {
@State var state: MoviesState = MoviesState(movies: [], selectedMovie: nil)
let viewModel = MoviesViewModel()
var body: some View {
List() {
ForEach(state.movies, id: \.self) { movie in
MovieView(movie, ...)
}
}
}
}.onAppear(perform: {
viewModel.observeState().collect { self.state = $0 }
})
}

And that’s all! remember that the full code is here

From an Android developer perspective, the development experience using KMM (and this approach) is pretty much the same as just developing pure Android. But for iOS developers the experience is not as good as for Android developers, on iOS we have to take care of stuff like concurrency, InmutabilityException and other incompatibility issues that may arise.

Jetbrains is aware of these problems and is working on improving the experience for iOS developers, on August 31, 2021 they released the preview version of the new memory manager that should free us from the need of freezing objects.

This architecture is a great fit for teams who maintains big native apps for iOS and Android, specially if the developers on your team have knowledge on both platforms.

Usually iOS and Android versions of the same app have different architectures like MVC, MVP, MVVM, VIPER or Redux, keeping a same architecture on both platforms has a lot of advantages, requires less work, and helps to better distribute the tasks among your team, developers can even work on both platforms very easily.

Overall I think there is no perfect solution, each team should consider which is best suited to their needs.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Kotlin Multiplatform despite all of its benefits sometimes has its own challenges. One of…
READ MORE
blog
It’s been almost a year since I wrote an article on how I handle…
READ MORE
blog
I develop a small multi-platform(android, desktop) project for finding cars at an auction using…
READ MORE
blog
One of my 2021 new year’s resolutions was to dive in into Kotlin Multiplatform Mobile (KMM).…
READ MORE

Leave a Reply

Your email address will not be published.

Fill out this field
Fill out this field
Please enter a valid email address.

Menu