Android developers probably know that reusing code is a common practice for any programmer. This approach speeds up the development process and reduces the likelihood of errors. When you see concise code, you better focus on the business logic of the product. This was exactly our goal when my team and I first tried out Kotlin Multiplatform Mobile. In this article, I'll explain in more detail how to use code for multiple target platforms and why using Kotlin is a valuable skill.
It’s often the case with outsourcing, the customer wants a ready-made solution quickly, efficiently, and made by one developer. Kotlin Multiplatform Mobile (KMM) saves time and effort and helps to achieve the desired result. The NIX team has been successfully using Kotlin in commercial projects for a long time. This time we decided to go further and find out what opportunities KMM gives.
The essence of the approach lies in the slogan on the official website of the KMM: Save time and effort by writing the business logic for your iOS and Android apps just once. We can create modules with common code and connect them to different native applications on Android and IOS.
However, clean Kotlin itself is not as simple as it may sound. It’s not difficult to abandon Android imports in the business logic layer, but it is much more difficult to get used to the idea that Java imports should not exist either. We looked at KMM in terms of Clean Architecture. There’s a clear separation of the business logic layer from the rest of the application (the Domain layer). We made the division into layers using modules. This makes it easier to ensure that unnecessary imports and platform-specific code do not end up where they should not. We got three modules - Presentation, Domain, Data:
- Presentation is a module with presentation logic and views;
- Domain is responsible for a business logic;
- Data is focused on repositories and data sources.
Domain is our KMM module, written in pure Kotlin. You can assemble it without any problems for IOS and Android. Presentation and Data are platform-specific modules and cannot be reused. But this is not always so. We decided to find out if Data and Presentation can become KMM modules based on Pure Kotlin.
First, we isolated another UI module from Presentation and added Views, Activities, Fragments to it. In the Presentation module, we left MVP contracts and presenter implementations. Often, you can do without Android imports in them. Then Infrastructure was separated from Data. Here we have DataSources and Repositories implementations that require Java or Android imports. In the Data module, there are models that the application operates with and the Repositories contracts. As a result, our assumptions came true: Data and Presentation are also ready for reuse.
KMM allows you to make Infrastructure and View multiplatform, but things are more complicated with them. Let's create another simple KMM module for logging. After that, instead of the usual java -> main, you will have commonMain. androidMain and iosMain. How do you use them?
CommonMain contains "pure" Kotlin code. In the case of the Domain module, all the code will be here, and we won't need androidMain and iosMain at all. Since we want Android logging to happen through the standard Log, and not println(), we need androidMain and iosMain. For the implementation in commonMain, we created the MultiLogger class and , it with the keyword ‘expect.’ In androidMain we declared the MultiLogger class and implemented the log using the Log we are used to. We use println in iosMain and hope it suits us :).
By analogy with the logger, the same can be done with the UI module and Infrastructure. The purpose of such a division is to separate what can be assembled for Android and IOS and in the future can be easily implemented on each of these platforms. If you don't want to manually write a View module separately for two platforms, use ready-made solutions. For example moko-widgets. Likewise, with the Data layer, there is a moko-permissions library that you can use for your repositories and not resort to ‘expect’ and ‘actual’.
When we first encountered Kotlin, we faced a few surprises in store for us. I'll tell you about them so that you could avoid them.
On the flipside, multi-platform projects require the latest version of the Gradle build system. The transition to old projects is difficult. If you have already switched to a multi-platform, but after a while, you need to return to a previous commercial project, it will be difficult to do this. Gradle always caches something, you have to constantly clear the cache, but more often it does not help. Ok, reinstalling. But all this takes too long.
In addition, we had to manually create packages and folder structures for three Source-sets: Android main, iOS main, and Common main. It was also inconvenient to create build.gradle.kts ourselves and write the paths to the sources in them.
There were also problems with communication between modules. The studio highlighted all imports and entities from neighboring modules in red and did not see them, although everything was successfully assembled and worked.
If up to this point you have successfully postponed acquaintance with Coroutines, it’s high time to do so. With the help of Coroutines in Kotlin, you can write asynchronous, non-blocking code. We had no problems with them. But colleagues shared with us that sometimes Coroutines can freeze in iOS for unknown reasons, and this is difficult to prevent and control. As far as I know from the latest sources, this point has been fixed. But there is another problem - iOS code can throw errors that turn into basic iOS ones.
Now almost all the problems are gone. You can use the latest android studio and everything will work fine. Also, the Kotlin Multiplatform Mobile Plugin greatly simplifies the creation of new multiplatform projects and support of the current ones and even allows you to debug the code compiled for IOS.
Summing up, I would like to say that all Android developers have long been accustomed to using Kotlin. The cool thing is that by changing our module structure quite a bit, we can get code that can be reused. So let's try, create tickets about the problems we face, promote the community, and learn from each other's mistakes.