Clean Architecture example with Kotlin Multiplatform

Personally I’m a big fan of Clean Architecture on Android and use it in most of my projects. I use it with the Model-View-Presenter pattern in the presentation
layer. As most of you probably know the view
is usually just an interface
and we don’t really care about what component implements that interface
. I was asking myself if it’s already feasible to write a common data
and domain
layer to share code on Android and iOS. Then also try to share the presenter
, let on Android an Activity
and on iOS a ViewController
implement the view
, and then only implement a native UI on each platform to display data.
Long story short, here is what this post will cover:
- Shared API, shared network client, shared JSON parsing in the
data
layer - Shared models, shared
use cases
with coroutines for our business logic in thedomain
layer - Shared
presenter
in thepresentation
layer with anActivity
and aViewController
visualising the data received from thepresenter
- TESTS! We have tests for our shared code, effectively reducing possible errors by half - write once, test once for both platforms.
- GitHub repository
The aim will be to fetch a list of popular movies from TMDb and display them in a grid.


Data layer
For networking I’m using Ktor and for JSON deserialisation Kotlinx serialization.
For a call to get current popular movies from TMDb we have to write a function like this:
As this method is doing a network call we’re marking it as suspend
, so it has to be called from a coroutine. Inside we’re doing a simple HTTP request with Ktor. Due to unsupported reflection on Kotlin/Native we have to manually call serializer()
from the entity we want to parse the response to (in this case PopularMoviesEntity
), otherwise we could simply return the call here and the parsing would happen automatically.
The clientEngine
we pass to Ktor’s HttpClient
is passed via the constructor of the class and in the end is different per platform. This isn’t necessary, but I didn’t manage to enable network logging with Ktor, so I made use of Ktor’s Engines feature and told it to use a specific client for networking, which is for Android:
So in the common module I expect
a httpClientEngine
and in the Android module I simply use OkHttp like we’re all used to and enable HTTP logging.
For iOS I only added a default engine:
actual val httpClientEngine: HttpClientEngine by lazy { Ios.create() }
Entities
Defining an entity, what we want to parse our network response into, is pretty straight forward with Kotlinx serialization:
@Serializable
data class MovieEntity(
@SerialName("server_name") val name: String
)
Simply add @Serializable
and with @SerialName
you can define what the JSON property is named, so you can use a different variable name.
Tests
Tests have been added to test the JSON parsing from a string to some entity. I added a sample JSON representation and then parsed it via:
val movie = Json.parse(MovieEntity.serializer(), json)
using serializer()
like we learned before. Afterwards I’m doing some basic assertions on the result.
Currently there’re no tests for the network call itself.
Domain layer
Here I’m defining on the one hand all my models that I want to use throughout the project, e.g. PopularMovies
, which is mostly a copy of PopularMoviesEntity
in terms of properties, but decouples my JSON deserialisation logic from the rest of the project. Each file in the model
package contains the model itself and an extension function to map an entity to the actual domain model.
Use cases
One main aspect of Clean Architecture is to define various use cases for specific tasks. In my case it will be GetPopularMovies
to get the currently most popular movies from TMDb.
So far, I always created my use cases with RxJava
, but since Coroutines are on the rise and I don’t want to use Rx in the common module, I created a base Coroutine use case. I’ve overwritten the operator function invoke()
so I got rid of function calls like execute()
on a use case, which you may have seen in other projects. Instead I can just write getPopularMovies()
in a CoroutineScope
and the use case will work:
Notice that the return type is Either<Exception, Type>
. Either
is a custom class that contains either an exception or a specified type, in case of GetPopularMovies
that’s PopularMovies
, but never both. Using Kotlin’s fold()
the Either
will be mapped to different lambdas onSuccess
or onFailure
, similar to RxJava’s onSuccess
and onError
, so there’s no need to use try catch
in the presenter later.
Here’s a quick and final example how we’re going to use the use case later:
GlobalScope.launch {
getPopularMovies(
UseCase.None,
onSuccess = { },
onFailure = { }
)
}
GetPopularMovies
use case
So, we discussed the base class for use cases. Let’s have a look at a specific use case to get the currently most popular movies.
The implementation is very simple: we’re using our MoviesApi
and just call getPopularMovies()
on it, returning an instance ofPopularMoviesEntity.
As we want to return our domain model PopularMovies
we call toModel()
on the entity to map it. Then we wrap it in a Success
, which is a subclass of Either
.
The whole call is wrapped in a try catch
and in an error case we return a Failure
, which is again a subclass of Either
:
The
try catch
could be moved to the base use case class, avoiding the repetition in every subclass.
Tests
In this module I’ve added a test for the GetPopularMovies
use case, verifying our business logic.
However, as of writing the unit test, testing suspending functions did not work in common modules. There are workarounds to move the execution of the test to e.g. the Android module, but for now I decided to just leave it there and wait for a fix from JetBrains.
Presentation layer
In this layer we’re going to share a presenter
and a view interface
from the MVP architecture. The UI will then be written natively and each platform has to implement a PopularMoviesView
interface.
I’ve created a simpleBasePresenter
class, providing methods to get informed when a view
was attached or detached, and also providing a CoroutineScope
coupled to this presenter instance:
On Android, attachView()
and detachView()
are called in Activity.onStart()
resp. Activity.onStop()
and on iOS in ViewController.viewWillAppear()
resp. ViewController.viewWillDisappear()
. When the view detaches, our launched coroutines will be canceled, avoiding any leaks.
PopularMoviesPresenter
After discussing the base presenter, let’s have a look at our PopularMoviesPresenter
to load popular movies and let a view display them:
First, our view interface contains three simple methods to show/hide a loading indicator, show an error or set the loaded movies.
As soon as a view attaches we tell it to show the loading indicator. Afterwards we execute our GetPopularMovies
use case, effectively doing a network request to the TMDb API. We don’t have any input parameters, so we’re passing UseCase.None
as the parameters.
If the call was successful we set to movies to the view, in case of an error we tell the view to show some kind of error. In any case we hide the loading indicator.
Tests
The presenter is completely tested using mockk
. No issues here.
Android
On Android an Activity
implements PopularMoviesView
and displays the movies in a simple RecyclerView
with a GridLayoutManager
. When the movies failed to load we just show a Toast
. You’re completely free to use any native features you’d like to use.
iOS
On iOS we’re using a UICollectionViewController
with a custom UICollectionViewCell
to display the movies. Our interface PopularMoviesView
was converted to a protocol
so it can be used in Swift.
We don’t show any loading indicator, because there’s on big issue: Kotlin/Native does not support multithreading at the moment.
This means, all of our code has to run on the main thread, therefore a loading indicator doesn’t make much sense when we’re doing network on the main thread.
Summary
If JetBrains keeps actively working on Kotlin Multiplatform I personally see a bright future for the framework and it would become a viable choice to share code between platforms. Android and iOS will rely on the same business rules and data structures, the code will only need to be tested once and we can still have a native UI will the latest features on each platform. It’s super easy to jump between shared and native code and with Kotlin we have a modern language that Android devs already know and is easy to learn for iOS devs due to the similarity of the languages.
However, getting the project to run on iOS is quite difficult currently. Let alone integrating the framework in Xcode. I hope to see more news here at KotlinConf later this year.
In the meantime I’ll try to improve and add features to the project. So you’re welcome to star the project on GitHub and keep up to date. 🙂
If you want to get started with Kotlin Multiplatform yourself check out my other article about current issues and how to solve them