Damiano Giusti
Hi everybody, Damiano's here! Android Engineer at Empatica, Kotlin lover, and passionate about Android apps architecture. Welcome to my blog, a place where I share some pills from my every day experience about mobile apps development and software engineering.
A trip into Kotlin Multiplatform Projects, Part 3
In the previous post of this series, we enjoyed a full immersion trip about using BLE in a Kotlin Multiplatform project. We integrated a BLE device by reading an exposed characteristic which sends notifications. Then we delivered the updates to the user every time the characteristic changed.
Such a scenario is challenging but not so common in the every day app development. Therefore, I wanted to explore a better use case, like a simple rest call for fetching some users and the UI code for displaying them.
Structuring the Kotlin MPP
A key point of Kotlin MPP is the source sets organization, in order to share as much code
as possible. The new kotlin-multiplaform
Gradle plugin helps such intent by allowing you to
define various source directories, each targeting a different platform.
You will have a commonMain
source set containing all the pure Kotlin code, shared across the
platforms. Then, if you’re creating a mobile app, you can have a androidMain
source set together
with an iosMain
set, defining the platform-specific code.
This structure introduces a clear separation of scopes, forcing the pure Kotlin module to be framework agnostic. For that reason, if you want to make out the most of this organization, you can take advantage of various design patterns to share as much logic as you can.
In addition, the powerful Kotlin Coroutines library allows you to make an abstraction over threading. Using it, you gain the advantage of writing asynchronous code in a synchronous and more readable form.
Such capabilities play very well in this multiplatform context.
The Kotlin project
As introduced before, our project will be a multiplatform app which fetches a list of users by consuming a REST API. Once done, it shows the results to the application user as a list. Pretty common use case.
We’ll use randomuser.me, an open API which returns a list of mock users, even with a profile picture. By consuming such service, we’ll get a Json response from which we’ll keep only the data we really need. Then we’ll compose a simple UI-representable model for letting the UI code to be as simple as we can.
We’ll try to maximize the code sharing by taking advantage of the mentioned multiplatform source sets.
Also – since we are good developers and we must keep the app responsive – we will dispatch in the background all the work that is not related to the UI. In order to do this, we will delegate the task to Kotlin Coroutines, applying the work deferral only in one point. Thus, all our code will be synchronous, but sent in the background only in one place.
IMHO: this idea should be applied to most of the cases in which you need to defer work in the background. Most importantly, it helps you to avoid spreading of async code, callbacks or whatever. Frameworks like RxJava help you to overcome this, but for simple projects you may don’t want to add such dependency. By the way, this is another topic!
Kotlin MPP – Fetching the users
As said before, we’re going to consume the randomuser.me API for getting some users data.
Structuring sources. How?
In order to maximize the code sharing, will be great if we could reuse also the code which performs the HTTP call. We can follow two paths for achieving the result:
- Create a common interface defining our requirements, and implement an Android version (OkHttp and the Moshi converter) and an iOS version (Moya and models implementing Swift Codable protocol);
- Create a common class using a multiplatform HTTP client as Ktor client and a multiplatform JSON serialization library, like the cool kotlinx.serialization provided by the Kotlin team itself.
The second point looks attractive to us, but we want to maintain the loose coupling that the
first point introduces. Hence, we can still create the common interface, called UserApi
.
We’ll implement it in the common module using Ktor and kotlinx.serialization.
We’ll write all the code to depend on the interface. Therefore, if needed, we can throw away such
implementation in favor of the first solution, without changing anything. Remember the
Liskov Substitution Principle (LSP) and the Dependency Inversion Principle (DIP)?
Uncle Bob will be proud of us!
So let’s create a class called AllUsersResponseDto
, defined here, and the UserApi interface, defined as:
interface UsersApi {
suspend fun getUsers(): AllUsersResponseDto
}
It’s very simple. Ignore for now the suspend
keyword. More on it will be explained later in this post.
Kotlin MPP – Dependencies
The AllUsersResponseDto
class will be annotated with the kotlinx.serialization.Serializable
annotation, allowing the compiler to generate serializers for us.
We add the serialization Gradle plugin to the classpath dependencies of project build.gradle
file.
Remember that it requires a Kotlin version greater than 1.3.20
.
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
And then we update our module’s build.gradle
with the platform-specific dependencies.
We need to distinguish the artifact based on the source set we target.
commonMain {
dependencies {
...
api "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serializationVersion"
}
}
androidMain {
dependencies {
...
api "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializationVersion"
}
}
iOSMain {
dependencies {
...
api "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serializationVersion"
}
}
Here we’re exposing the serialization lib using the api
directive to other modules.
If we decide to implement the UserApi
interface with platform-dependent code, we’ll be free
to use the kotlinx.serialization
lib.
Then, as said before, we have to create the shared implementation of the UsersApi
.
It will use the Ktor Client library for making HTTP calls.
Import the Ktor Client library as we did previously. Moreover, we add the
Ktor JsonFeature,
which integrates perfectly with kotlinx.serialization
.
It allows to define serializers for custom types, delegating to Ktor and Kotlin serialization lib
the dirty work.
commonMain {
dependencies {
...
implementation "io.ktor:ktor-client-core:$ktorVersion"
implementation "io.ktor:ktor-client-json:$ktorVersion"
}
}
androidMain {
dependencies {
...
implementation "io.ktor:ktor-client-android:$ktorVersion"
implementation "io.ktor:ktor-client-json-jvm:$ktorVersion"
}
}
iOSMain {
dependencies {
...
implementation "io.ktor:ktor-client-ios:$ktorVersion"
implementation "io.ktor:ktor-client-json-native:$ktorVersion"
}
}
Kotlin MPP – Shared implementation
Once we finish the setup, we can implement the UserApi
interface.
class SharedUsersApi : UsersApi {
private val httpClient = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer().apply {
setMapper(
type = AllUsersResponseDto::class,
serializer = AllUsersResponseDto.serializer()
)
}
}
}
override suspend fun getUsers(): AllUsersResponseDto =
httpClient.get("https://randomuser.me/api/?results=50")
}
In less than 20 lines of code we have a fully working implementation, usable on both the platforms.
We marked the getUsers
method as suspend. It’s an interface method defined in a context where
implementations could be long-running tasks. In our case, the Ktor Client uses Kotlin Coroutines
to perform the HTTP call in background. Due to this, the HttpClient.get(String)
method must be
called in a suspend function or inside another coroutine. Knowing that, we chose to implement it
as suspend
, delegating the coroutine management to the caller.
The class responsibility is only to fetch the users, not to explicitly manage background stuff.
Kotlin MPP – Shrinking the model
All we have done is about the data-access layer of our application. We haven’t implemented any UI logic nor any UI widget.
By looking at the DTO, a question lights up immediately.
Do we really need all such fields?
Obviously, we don’t.
The domain of our application wants to know only some basic info about the user. Hence something like:
data class User(
val id: String,
val name: String,
val surname: String,
val username: String,
val email: String,
val gender: Gender,
val profilePictureUrl: String
) {
enum class Gender {
MALE, FEMALE
}
}
Then we need to reduce the data obtained from the DTO to a simpler form.
This mapping work will be implemented in a Repository
class, in the common module.
Such class allows to fetch all the users, hiding their origin. A basic repository pattern!
A pretty dummy implementation can be created as:
class UsersRepository(
private val usersApi: UsersApi = SharedUsersApi()
) {
suspend fun getAllUsers(): List<User> {
val users = usersApi.getUsers()
return users.results.map(::mapToUser)
}
}
private fun mapToUser(result: AllUsersResponseDto.Result): User = User(
id = result.login.uuid,
name = result.name.first,
surname = result.name.last,
username = result.login.username,
email = result.email,
gender = if (result.gender == "female") User.Gender.FEMALE
else User.Gender.MALE,
profilePictureUrl = result.picture.large
)
In the constructor we get an instance of UserApi
, accessed by interface.
For simplicity we set as default value a new SharedUserApi
instance, that is the class
we created before. In this way we can easily use it or replace it, as said at the beginning of the post.
Displaying the users in our Kotlin app
Well done guys! We have a fully working structure usable on both Android and iOS (…oh well, we can’t say it works until we write down some tests…).
Reached this point, all we have to do is to present that user list into the UI. At first, we can be tempted to start writing the code directly into an Android Activity or inside an iOS UIViewController. However, doing this is not exactly what we want. In fact, we want to maximize the code sharing between the platforms, mainly to avoid duplication of code and bugs.
In order to accomplish our purpose, we apply the Model View Presenter pattern to our UI code. Our Activity / UIViewController will implement a View interface. The Presenter will hold and instance of the View, removing the need to be coupled to the framework classes. In the Presenter then, we’ll write down the data manipulation code, for preparing the User models to be shown.
How to present users
First, let’s define how users should be presented in the UI. For the sake of example, our app will show a user in a simple row of a list. Each row will display the user name, the email, and the profile picture. Such minimal, much wow.
Having wrote down some “presentation ideas”, we create the model that will represent a displayed user:
data class UiUser(
val id: String,
val displayName: String,
val email: String,
val pictureUrl: String
)
We could have called it DisplayableUser
, UserToShow
… but UiUser
was meaningful enough
for the scope of this example.
The id
in the model is used only for maintaining a simpler bidirectional flow.
In fact, in such way we can reference the domain model starting from the displayable model.
Once defined how data will be presented, let’s define the View
contract.
This will explain how data are delivered to the UI. Also, it sums up the actions that the view will perform.
Since it will display a list of users, we’ll call it UsersListView
:
interface UsersListView {
fun showLoading()
fun hideLoading()
fun showUsers(displayableUsers: List<UiUser>)
fun hideUsers()
}
Pretty immediate. Our Activity and UIViewController will respect this contract.
Kotlin MPP – Present them!
Finally, we need to implement our Presenter class. We’ll call it UserListPresenter
.
Even this class will reside in the shared module, and thus will be written in pure Kotlin code.
In fact, the UserListView
is a key point to allow the Presenter to be framework-agnostic.
Our Presenter will expose two “lifecycle” methods:
-
attachView(v: UsersListView)
: the entry point. This will be called when the View is created. The Presenter will then store the reference of the given View as an instance property; -
detachView()
: the exit point. Will be called when the view is going to be destroyed. Here the Presenter will set tonull
the reference to the View. This helps us also to avoid retain cycles in Swift code.
Also, the Presenter will manage a CoroutineScope. In such way we’ll be able to launch
Coroutines and perform all the repository work in the background.
We can take advantage of class delegation for this purpose. The Presenter will conform
to the CoroutineScope interface, and we delegate the implementation to the MainScope()
function. This usage is experimental, but we like to live on the edge.
At the end, our Presenter looks like this:
class UsersListPresenter(
private val usersRepository: UsersRepository,
private val backgroundDispatcher: BackgroundDispatcher
) : CoroutineScope by MainScope() {
private var view: UsersListView? = null
fun attachView(v: UsersListView) {
view = v
launch {
view?.hideUsers()
view?.showLoading()
val users = withContext(backgroundDispatcher) {
usersRepository.getAllUsers()
}
val displayableUsers = users.map(::mapToUiUser)
view?.hideLoading()
view?.showUsers(displayableUsers)
}
}
fun detachView() {
view = null
cancel()
}
}
How cool is this.
Once implemented the Activity and the UIViewController, the job is done.
But let me explain you a drawback.
Background dispatching
Notice that our Presenter takes as a constructor parameter also a BackgroundDispatcher
instance.
This is a class used to abstract the CoroutineDispatcher we are using. But why?
The Repository by definition is an object which works with data. Thus, is a good practice
to perform its work in the background. Kotlin Coroutines allow background dispatching by using,
for example, the Dispatchers.IO
object. While used on Android, all goes well.
The problem resides in the Kotlin implementation used in the iOS target. Background dispatching is not yet supported, due to the complexity of managing object references between threads. The feature seems to be planned for a future release, but right now only the Main thread dispatcher is available. You can find the GitHub issue here.
That being said, then we cannot use the Dispatchers.IO
object. For overcoming this issue,
we created a BackgroundDispatcher
which is an expect object
. In the Android codebase is
“actualized” using Dispatchers.IO
, while in the iOS codebase is “actualized” using the Main dispatcher.
This doesn’t mean that the REST call will be made on the Main thread. Ktor Client sends computation in the background by itself. However, all the subsequent processing will take place on the UI thread.
In my opinion, this is an acceptable tradeoff for mobile applications that don’t require hard work in a background thread. In fact, threading might be safely handled by some other data-access library, as Ktor, so the application may not need to handle work deferral by itself.
Conclusions: our Kotlin every day app
We successfully build a Kotlin Multiplatform application. And look how cool it is:
With a pretty architecture, we maximized the code sharing, starting from the presentation layer and even reaching the data layer.
JetBrains team made a huge work making this possibile, and we must admit that such technology opens a plenty of possibilities. More to say, I believe Kotlin Multiplatform started walking the road to become a standard in multiplatform development.
Multiplatform projects are suitable also for other environments. You can build your backend using Ktor Server targeting JVM, and then share models with a frontend common module. Such module will contain all the logic we discussed about in this blog post. Further, it will be used by other platforms modules, like Android, iOS, JS and JVM desktop. And, obviously, it will contains tests!
Have a nice Kotlin and thanks for reading!
Originally posted in MOLO17 Blog