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 of PopularMoviesEntity. 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 simple BasePresenter 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. 🙂