Kotlin Multiplatform for Clean Architecture

Gepostet am: 06. August 2019

As mobile developers, we often rewrite the same logic in another language, maintaining two similar codebases for Android and iOS. Wouldn’t it be nice to write Kotlin once and compile it everywhere? Enter Kotlin Multiplatform, which enables us to write the same code for the JVM as well as LLVM! 

 

The project is getting more and more traction in the community right now. It is also heavily supported by JetBrains and Google, which probably means that this is more than yet another hyped cross-platform framework.

Following the write once, compile everywhere pattern, Kotlin multiplatform code is regular Kotlin code compiled to JVM bytecode for Android and to LLVM bytecode for iOS. The concept of compiling a shared library to native code for both mobile platforms is not new. For instance, it is possible to write a shared library in C++ and integrate it in Android via JNI and iOS via Swift/C interoperability. However, Kotlin multiplatform takes this idea a step further and handles (most of) the difficult parts of the integration. And after all, Kotlin is a great language and we think its use should not be limited to the JVM!

For us mobile developers at inovex, several questions arise:

  1. Is it ready for production yet?
  2. Can we use Kotlin multiplatform to build apps following clean architecture guidelines?
  3. How much code can be shared between platforms? In other words, how much development time (=money) can be scrapped by using Kotlin MP?

It is hard to fully answer these questions without releasing multiple apps into production and evaluating the results, i.e., feeling the pain of Kotlin multiplatform being still beta and in active development. Before we take that path, we could build a minimal example to evaluate whether Kotlin multiplatform is feasible for the next Android and iOS project at inovex.

Goal

Our primary goal is to build a minimal sample app which uses the MVVM pattern with the components persistence layer (i.e., database), repositories, view models and a HTTP-client to sync data. All components should be kept in the shared codebase, only the code for the UI should be platform-specific.

In terms of functionality, the app loads a list of todos (read: strings) from a public lorem-ipsum JSON-API and displays them in a list. The todos are persisted in the database.

The following part of this post covers the example in detail, but you can also skip to the end if you just want to know our opinion on Kotlin MP.

You can find the final code on GitHub: https://github.com/inovex/kotlin-multiplatform-sample

Scaffolding

For our first Hello world app we followed the official tutorial from JetBrains. The tutorial is pretty concise, the only hiccup was that Gradle source set names are case-sensitive and we first used iosMain instead of iOSMain.

As our primary IDE we use Android Studio 3.5 beta. iOS builds are tested with Xcode, of course. IDE support for Kotlin multiplatform in Android Studio is not complete at the moment and the builds take very long. However, Google is working on better IDE support, as we heard on the Conference for Kotliners this year. Let’s hope that build times can be improved, too.

Persistence

For a shared database, we use the SQLite wrapper SQLDelight. The framework uses a database scheme followed by queries for data access written in plain SQLite  to generate all necessary classes like data access objects and such. The documentation on GitHub is a bit outdated. We recommend using the latest version of SQLDelight and refer to our sample code for the details.

In the Gradle file, the database is configured as follows:

The SQLDelight Gradle plugin will generate a Kotlin class called Database (if no other name is specified). To add an entry to the Todo table, all we need is a simple method call:

Note that booleans are not supported, thus we have to convert the flag to an SQLite INTEGER, which is handled as Long in Kotlin.

SELECT queries can also be monitored for changes, which helps us to emulate a simplified version of Android LiveData in our view model later. We keep track of registered observers using unique numerical IDs.

To make SQLDelight actually work on both platforms, it needs a platform-specific database driver. We use the actual/expect pattern for this task. For iOS, this works out of the box, for Android it does not because we need the famous Android Context to initialize the driver.

HTTP client

As Android devs, we are happy to use OkHTTP and Retrofit to build API clients. With Kotlin multiplatform, we cannot use these Java libraries. Fortunately, the new Ktor framework provides an HTTP client ready to be used in Kotlin multiplatform projects. Integration using Gradle dependencies is described in the official documentation.

As a replacement for Gson, we use Kotlinx-Serialization to convert JSON strings to Kotlin classes and vice-versa.

With these libraries and the wonderful SQLDelight database interface our API client is pretty small:

Did you notice the applicationDispatcher co-routine scope? Correct, we can use Kotlin co-routines on iOS and Android! Initialization is a bit of a bummer, though. On Android, it is as simple as

On iOS, we copied the code from this issue’s comment to use iOS’s dispatch_async on the main queue for our co-routine dispatcher. Kotlin Native—the LLVM compiler used in Kotlin multiplatform—does not yet support co-routines on background threads, using a background queue will cause crashes instead. There is an open issue in the Kotlinx repo and we hope there will be a fix soon, because we do not want to run network calls on the main thread in production!

Repositories

The TodoRepository serves as a proxy class for the database. As a wrapper around the generated Database class we use a singleton CommonDatabase.

Because of Kotlin Native’s object freezing behavior, singletons do not work very well with multi-threaded access on iOS. To prevent crashes on iOS, we use the @ThreadLocal annotation, which creates one copy of the object on each thread. It works for this example, but handle with care!

When you get started with Kotlin multiplatform, prepare to run into such issues from time to time. Most things work without problems on Android because of the underlying JVM, but there might be some quirks when the same code is compiled to native LLVM bytecode.

View models

To complete our architecture stack, we introduce a view model for the list of todos.

The view model is our interface towards the platform-specific implementation. From Swift, it can be used as follows:

Since our callback is expected to return the Kotlin type Unit, we have to return an instance of KotlinUnit in Swift.

On Android, observing the data works similar:

Please note that advanced architecture features like saved-state View Models and LiveData are not available. For smaller apps, we think it is feasible to implement by hand, for larger apps, we will have to wait for advanced libraries aimed at Kotlin multiplatform.

Tests

We thought about keeping the Unit tests in the shared codebase. The problem is that no JVM and JUnit framework can be used here. Thus we decided to put the Unit test for the TodoViewModel class into the Android JUnit test folder.

The advantage for us Android developers is that we have all the tools available we know from regular Android apps, like Mockk for example. Mocking is also a lot easier on the JVM than on native platforms. A small disadvantage is that the common code is not unit-tested on iOS. Of course, we might naively assume that the behavior is the same as on the JVM, but experience in this first project taught us that this is not true. For instance, consider the object freezing issues mentioned above.

Verdict

Our minimal sample app enables us to answer the three questions from the beginning:

  1. Production readiness: For small projects, Kotlin multiplatform feels manageable. For larger projects, we would suggest to wait until IDE support in Android Studio and/or Xcode has improved.
    Stack traces for crashes are human readable on both the JVM and LLVM, so crash monitoring on production is possible.
    A major showstopper at the moment  is the lack of support for co-routines on a background thread—JetBrains is working on it, though.
    We did not test Integration with CI/CD yet but we believe it can be achieved in the usual ways known from single-platform Android and iOS app projects.
  2. Building apps with clean architecture? Yes, it is possible to some extent. Check out the example code and judge for yourself.
  3. Code sharing between platforms? This was better than expected—55% of total LOC was in the shared code base.
PlatformLOC (including blanks)Percentage
Shared (including tests)21255%
Android (without XML-resources)8322%
iOS9123%

Kotlin multiplatform code base pie chart

Based on the aforementioned results we strongly believe that Kotlin multiplatform is a technology worth pursuing. Of course, we are very interested in your opinion on this topic, so please drop a comment below.

Lastly, a friendly note to any sales person reading this: We know you will do the math, scrapping 33% off the budget on the next project because 50% of the code can be shared between Android and iOS. Please don’t be too optimistic—cross-platform code is harder to write than single-platform code, even in Kotlin 😊 But we still think that sharing code will save some time in the end and also make maintenance of the code base for both platforms less of an effort.

2019-08-12T16:24:29+00:00

4 Kommentare

  1. Avatar
    ken 19. August 2019 um 19:31 Uhr - Antworten

    „scrapping 33% off the budget on the next project because 50% of the code can be shared“

    You also have to factor in tools/debuggability/libraries in your overall speed.
    E.g., I was on a Xamarin project w/ 70% code share…still took the same amount of time it would have taken to write two separate apps because we couldn’t use all the 3rd party libs we were used to using for the native apps and also we were fighting tools and quirks.

    • Avatar
      Jan Freymann 20. August 2019 um 8:32 Uhr - Antworten

      Agreed! That’s why the expectations on the budget should not be too high. For apps with a lot of business logic, going cross-platform might save a lot of time, though, because you have to fix every bug in the logic only once, not twice.
      About the libraries: it depends on the use case. For basic tasks like persistence and networking, you can use SQLDelight and Ktor on both platforms. If Kotlin Multiplatform continues to get more popular, more libraries might come. I am optimistic that this will happen, similar to Google’s switch to „Kotlin first“ on Android.

  2. Avatar
    RhonyAbdullah 19. August 2019 um 11:18 Uhr - Antworten

    The sqldeligth Database class is unresolved, i was open issue on your example through github, please check it.

Hat dir der Beitrag gefallen?