The journey of Lunabee Studio with KMM
At Lunabee Studio, we love building native apps, but what we love most of all is building great apps.
But the main issue with doing native apps is that some logic that don’t feel platform specific needs to be done twice, increasing the odds of having differences and also the development time.
To keep doing our lovely jobs and improve our working methods, we watch the Android and iOS sphere to be up-to-date on the latest technologies, best practices, etc… Thus, we couldn’t miss the emergence of Kotlin Multiplatform Mobile.
We already said that hybrid apps are crap, but KMM philosophy could suit us more, as it leaves the UI part out of the scope and promises performances as good as native apps.
We did a first dip of the toe more than a year ago, but the technology wasn’t mature at all at the time. But in the last few months, more and more companies speak about jumping to KMM, and the latest release note looked quite promising.
So we thought it was time to investigate the technology again. We started defining some phased objectives and goals that we would try to achieve through a simple but complete app.
🎯 Objectives
The objectives of Lunabee Studio that KMM could help us reach are :
- Improve the quality of our applications
- Since we would share the code base, the code would become more homogeneous between iOS and Android. Allowing us to have the same business logic between the two platforms, for sure.
- As we only need to write code once, we could improve the development speed drastically (ideally, only UI has to be done twice). - Working on cool projects
- With the development speed improvement, we could have more time to focus on the UI and animation. ✨
- As we’re geek at heart after all. It’s always a plus to work on cutting edge technology. - Building the team
- Shared code could be written and reviewed by anyone, leading to more interaction between teammates.
- No longer have 2 separate iOS and Android teams, but gather everyone in one team, even if you’d still have more affinity with one platform.
🥅 Goal
The main goal of this project is to answer one question : can it help us delivery apps faster without degrading the development time and premium quality of our application?
To answer this big question, we asked ourselves smaller questions that could help us have a better understanding on the state of KMM. Questions such as :
- What are the advantages and disadvantages of KMM?
- Is it ready for production?
- If not, what is missing? What do we need to keep a watch on before we can start using it?
- How much of our code base can we share between platforms?
🧭 Roadmap
To reach our goal and evaluate independently which module could be replaced using KMM, we thought of small projects to test them step by step before doing the main project.
- The most simple project to use KMM, a lib with a function to check a string in it
- A project to replace the server calls
- The third project aim to see if we could replace the local database
- Finally, a real app to combine all 3 first projects
Moreover, each project will allow us to answer some questions we have. They will be our checkpoints on each project. And to compare between native and KMM we built 3 projects.
- Android app
- IOS app
- KMM app
🔨 Tools
💻 IDE
- Android studio Chipmunk | 2021.2.1 Patch 1 accompanied by Kotlin Multiplatform Mobile, we used it to develop Android app and KMM module
- Android Studio Dolphin | 2021.3.1 Beta 3 we used it to develop KMM module
- Xcode | 13.4.1, accompanied by xcode-kotlin, we used it to develop IOS app
- AppCode 2022.1.2, accompanied by Kotlin Multiplatform Mobile for AppCode, we used it to develop IOS app
👨🎨 UI
- Android — Jetpack compose
- IOS — SwiftUI
📚 Library KMM
🗒 First project
The goal of this project is pure logic. We wanted to know how can we use KMM to do pure logic like test a string to know if it’s a valid email address
Checkpoints
❓ How to debug KMM lib on Android and iOS ?
❓ How long did it take to set up the whole thing ?
❓ Can we create a template to be faster next time we have to set it up ?
❓ Will it work on multiple repository ? (sub-module ?)
❓ Impact on application size and build time
The first project is not rocket science, we just have to do like always, write a function !
Android
Android manages to use it correctly with the String extension
IOS
IOS cannot use string extension because Kotlin native compiler is not advanced enough to handle class extension. Each function/extension that is not inside an object is placed at compile time in an object with the name of the files where the function is located. That’s why checkEmail has been put in an object.
📊 Checkpoints
❓ How to debug KMM lib on Android and iOS ?
On Android, there is no change, However, to debug Kotlin Multiplatform mobile on Xcode we have to download xcode-kotlin by Touchlab or use Kotlin Multiplatform Mobile for AppCode by JetBrains on appCode.
❓ How long did it take to set up the whole thing ?
We had no difficulty in preparing the project, it was fast.
❓ Can we create a template to be faster next time we have to set it up ?
It’s quite possible to create a template. In addition, there is already some template like moko-template or KaMKit.
❓ Will it work on multiple repository ? (Submodule ?)
The advantage of KMM is to be free. You can have a repository with 3 part inside (androidApp, iosApp and KMM module) or you can have 3 repository (Android, IOS, and KMM repository) and import it.
On Android we can import it via Submodule or via dependencies (mavenCentral, Google, …)
On IOS, we can import it via Submodule, CocoaPods or SwiftPackage
❓ Impact on application size and build time
Android
- Size There was no change in terms on size
- Build time It was the same duration
IOS
- Size It was much heavier with KMM. We have moved from 140 KB compressed, 290 KB uncompressed in natif to 489 KB compressed, 1.7 MB uncompressed in KMM
- Build time It was longer, we went from 2 to 5 seconds
🗒 Second project
The goal of this project is Network logic. We wanted to know how can we use KMM to do HTTP call like fetch some user from our API
Checkpoints
❓ performances/energy consumption? 📲
❓ timeouts? 📲
❓ Session manager (Connection pool)? 📝
❓ certificates pinning? 📲
❓ files download/upload? 📲
❓ background requests (iOS)? 📝
❓ Impact on application size and build time
To complete our goal on this project, we used Ktor by JetBrains because it is the most popular and is developed by the creator of KMM. In this project, we discovered the expect/actual keyword. The expect/actual mechanism is a way to have access to plateform-specific APIs. With this mechanism, the common source set defines an expected declaration, and platform source sets (Android, IOS, …) must provide the actual declaration that corresponds to the expected declaration. We have created an application that retrieves and displays a list of users from our API.
Creating our engine
To do HTTP call we have to create an engine. This engine is specific to the platform so we’re using the expect/actual keyword.
- KMM
- Android
We chose OkHttp as engine because we already know and use it.
- IOS
We chose Darwin because of its target (macOS, iOS, tvOS)
Creating our client
Once our engine was created, we could create our client with some options like transform Json to model, logs and expect success, there are many other options.
Do the HTTP network call
Our last step is to make an HTTP network call on our API.
We used the @Throws(Exception::class)
annotation to Say “Hey this function gonna maybe throw something”. We must implement it because of IOS, without this annotation, if the function throw something the IOS app gonna crash (even with a try/catch 😱). IOS is not able to catch a throw from KMM if it is not specified. To use the HTTP GET method, we use its get method with our API path and get its body. The body method gonna transform the Json result into our model.
How to use it ?
- IOS
We usedwithCheckedContinuation()
to convert completion handlers into asynchronous functions.getUsersList()
is a suspend function that can be seen as an asynchronous function in Swift, so we need a way to listen the suspend function. We did it withwithCheckedContinuation()
. A suspend function is used with a completion handler to get the result. The Native Kotlin compiler has compiled this function so that if the function returns a value, it’ll be in thedata
variable and if it throws something, it’ll be in theerror
variable.
- Android
On Android side it’s much easier, we just have to try/catch thegetUsersList()
function to get the result.
📊 Checkpoints
- ❓ performances/energy consumption? 📲
Android
- Natif
- KMM
IOS
CPU
- Natif
- KMM
Energy
- Natif
- KMM
Memory
- Natif
- KMM
- ❓ timeouts? 📲
It is possible to define a general timeout for both OS. We must define it when we create our engine
Android
IOS
- ❓ Session manager (Connection pool)? 📝
We can configure our engine with all the options offered by the object from the OS. So yes, there is a way to configure Connection Pool.
IOS-NSURLSession | Android — OkHttp - ❓ certificates pinning? 📲
We can configure our engine with all the options offered by the object from the OS. So Yes, there is a way to configure Certificates Pinning.
IOS — NSURLSession
❓ files download/upload? 📲
It is possible with Ktor. Upload / Download
❓ background requests (iOS)? 📝
Same limitation as the OS. Possible to use Extend background Idle mode
❓ Impact on application size and build time
🗒 Third project
The goal of this project is to handle Local database logic in KMM.
Checkpoints
❓ performances/energy consumption ?
❓ Database encryption ?
❓ Impact on application size and build time
To complete our goal, we used SQLDelight because it is the most popular and used by JetBrains on its samples. We have created a note application where we can write a note, save it, view it and delete it.
Creating our DB
The first thing is to create our table and functions to exchange with the database
Connect our DB to the app
- KMM
- Android
- IOS
We implement Koin to do dependency injection to help us meet the different requirements. Expect a module is much easier and understandable than expect the database or the driver.
Use the database
- KMM
To use the database we have to use noteQueries which is created by SQLDelight at compile time. Each of our function that we have created above are inside the noteQueries.
- IOS
On the second project we got to usewithCheckedContinuation()
to manage suspend or flow function. This solution is a bit boilerplate, so we tried to use KMP-NativeCoroutines.
This new library provides a new way, less boilerplate and a cancellation job. In this example, it create the functiongetNotesNative()
fromgetNotes()
.
- Android
Nothing change.
📊 Checkpoints
❓ performances/energy consumption ?
Android
- Natif
- KMM
IOS
CPU
- Natif
- KMM
Disk
- Natif
- KMM
Energy
- Natif
- KMM
Memory
- Natif
- KMM
❓ Database encryption ?
There are libraries to do encryption: Cipher Librairies There is also SQLDelight with SQLCipher
❓ Impact on application size and build time
📱 Complete app
The goal of this project is to create a complete app. We wanted to know how much KMM can help us in a real project
Checkpoints
❓ What version of IOS and Android can we use ?
Our goal on this application is to try to mix everything we have done so far and more. We have made a very simplified version of Tinder. During our research, we discovered moko-resources. It is a cross-platform Kotlin library that provides access to resources on iOS and Android with the support of the default system localization.
During this project we used:
- Ktor
- HTTP client
- SQLDelight
- Manage local database
- Koin
- Dependency injection
- Kermit
- Log
- moko-resources
🏗 Setup
HTTP Client
Like in the second project, we created an engine with the expect/actual mechanism. However, this time we set the timeout to 5 seconds.
Then with our engine, we created our client
Then we put it in a koin Module to get it easily and to created it once.
Local database
Create
Like in our third project, we have created our database.
Link database to the app
With our database created, we were able to connect our database to our application.
Helper with SQLDelight
To help us with SQLDelight we created a helper to use the SQL function. We did this so that we don’t have to manage the queries and dispatch them every time.
We put this helper in the user module.
Koin
The last thing to do is to manage all our data modules (HTTP and local database). This function is called when the application is launched. The app is able to enable network log and to add some module.
📭 Use case
To fetch data, like in the second project we use client.get()
with some query. We use the body
method to transform the Json response to our model. Moreover, we use a try/catch to get error like timeout, response error, … To use the database we use the helper.
All there is to do is to call those function to do an action like retrieve the last user without a like/dislike.
💬 Share resources
Our last step share the resources via KMM module. We use it to share English and French string. There is no difficulty to implement it with Android. On the other hand, with IOS… there were many problems. At first, we thought that we didn’t have to use the Info.plist, we thought that putting the data correctly in Xcode would be enough but no way ! After a while we created our Info.plist and everything worked ! We could change the language between French and English.
🧪 Unit test
One of the advantages of KMM is that it saves us time that we can put into unit testing.
KMM
Once again, the expect/actual mechanism help us. Thanks to it, we can test everything as our DB. To test a suspend function, we use runTest {}
Moreover, we can test them on the target that we want like Android or IOS.
IOS
Android
📊 Checkpoints
❓ What version of IOS and Android can we use ?
The libraries tell us the minimum version we can use, they are the ones that set the limit.
👀 Observation
At first glance, KMM appears to be very powerful. What we noticed in these projects is that it does not limit, it is versatile, thanks to the expect/actual mechanism. Moreover the performance seems to be equal.
About Android, There is no difficulty to implement it. Except for the libraries, nothing changes.
Concerning IOS, the Kotlin/Native compiler does not use the full power of Swift (Like enum or extension), and, we think, that the way to use suspend or flow function is a bit boilerplate even if the KMP-NativesCoroutines provides less boilerplate code and cancellation support.
Here is some trouble that we got:
Developing on iOS: It can be quite challenging to develop the KMM part and iOS compared to the Android one. Debugging is also quite tedious even with the plugin for Xcode. AppCode is a good replacement but it has it’s trade-off too.
Throw something: At the beginning we wanted to throw an error and get it back on the native app side. We tried, on Android there was no problem, but on IOS there was a crash, we could not catch the error. So after some research we find the @Throws(Exception::class)
that corrected our mistakes.
Memory management: we had to use the new Kotlin/Native Memory Manager. We had some problems with variable freezing, like changing a variable we got from KMM.
The comes from an experimental feature that has yet to reach Beta
# https://github.com/JetBrains/kotlin/blob/master/kotlin-native/NEW_MM.md
kotlin.native.binary.memoryModel=experimental kotlin.native.binary.freezing=disabled
Android studio commonMain- expect/actual fix
# https://kotlinlang.org/docs/multiplatform-hierarchy.html kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.native.enableDependencyPropagation=false
🔬 Assessment
KMM
On the level of performance, it is irreproachable.
On the portability level, it is very versatile, you can use it to implement what you want (Data layer, Data + Domain Layer, Network module, local module, pure logic, ….). In short it is really modular.
On the Community level, it is small but invested.
In terms of libraries, there are a few (just the most useful ones), most of them are in alpha or beta phase.
Unit test has some trouble, Android Studio Chipmunks doesn’t find actual
from Android unit test KMM folder (androidTest)
- 💡 fix Use the Android Studio Dolphin to fix this bug.
Android
No issues detected
IOS
A function that is not in an object in the KMM module, is put in an object created made from the file where it is.
the Kotlin/Native compiler does not allow to benefit from the full power of Swift.
The native function to use suspend or function is a bit boilerplate
We must to use xcode-kotlin if we want to debug the KMM part on xcode
Conclusion
As impressive as the evolution of KMM has been this past year, we still think KMM is not ready to be used to build a whole Android and iOS app for production just yet. JetBrains knows it too as the SDK is still in Alpha version even though they advertise a lot on it (Probably to get a maximum of feedback from the community).
With that being said, we’ll keep an eye on its evolution as it looks really promising and we most certainly will be using it for developing small libraries that has a restricted perimeter and can be easily tested.
So this is a case to follow very closely!