Purpose of the experiment
Our Android team already proved the usefulness of KMM in creating the multiplatform libraries in the company. We know that proverbial meat can be shared between apps on iOS and Android. The part that interested us the most was: can we create something in our way – the Android way, meaning the Kotlin way, meaning a better way 😉 and run it on our Nemesis – a device with a not fully digested apple logo on it. 😉 We’ve planned nothing fancy: just a simple app with very common features just like storing data locally, getting it from a remote source, searching through the device’s gallery – you know – the usual stuff. We won’t get into the topic of building and publishing the application on both platforms. At this point we’re only interested in the implementation: what can be achieved and at what cost, what are the drawbacks and are there any blockers so far. We’ll be looking at this from an Android developer’s perspective. And what’s worth noting is that most of us working on that experiment had no previous experience in KMM, except maybe for some articles or a quick tutorial. We were in possession of MacBook and no physical device so all our tests for iOS were conducted on a simulator.
Thanks to the KMM plugin for Android Studio, starting the multiplatform project from scratch is quite easy. We’ve decided to keep our dependencies in one place in a libs.versions.toml file and then use them in a purely Kotlin file system for Gradle build. First problem we got was a strange error during rebuild saying “could not find atomicfu”. However, the app was installing correctly on both platforms. So, we’ve decided on implementing the workaround described here.
Jvm target was set to JAVA 17. And since we are on this topic: it turns out that java code doesn’t run nicely in Kotlin Gradle files. For example, we’ve decided to keep the variables for project build in one place in the gradle.properties file like that:
and retrieve them by running Integer.getInteger(System.getProperty(”compileSdk”)). That “obviously” threw an error. What we should have done is System.getProperty(”compileSdk”).toInt() since that is a Kotlin extension function.
Actually problems with rebuilding continue up until the time of writing this article. We’re getting the NPE on
which is a part of the shared module that is responsible for displaying the compose content on iOS.
Also since we’re talking about builds. The clean build of our project can last even 20 minutes. We have even longer periods on record which depend on a device on which they were run on. Usually a normal build lasts about 4-5 minutes and installation is performed within a few seconds.
Here in this section we can also briefly write about the architecture of our app. We decided to follow the multi module pattern which we used so far in Android applications development. So code was splitted to core, core-model, domain and network which all were connected to our shared module containing multiplatform logic.
As for dependency injection we simply choose Koin, since it is quite straightforward to use and also recommended for KMM.
Having the same color palette on both platforms is a must. Again you could face the issues while implementing dark themes between UI/UX, iOS and Android teams. A simple miscommunication can result in subtle differences between screens. You can lose some precious time with bugfixing those issues. Wouldn’t it be nice if – once defined – a color theme would look the same on both platforms almost effortlessly? Compose Multiplatform to the rescue!
The function isSystemInDarkTheme() from androidx.compose.foundation handles the dark mode on both platforms. Simply define your color palettes and you’re free to go.
For remote data retrieval we decided to use Ktor. And we simply followed this documentation. So, in our network module we’ve implemented the repository which uses an interface from the core module. Also you need to remember about adding the Internet permission in manifest.xml file. The Ktor client is prepared in Koin’s commonModule in the core module, and then injected where needed. The whole thing could look like this:
Whether you need a caching strategy or you just want to save a note or a shopping list, you’ll need a device’s database. For years we’ve been using Room right? It’s almost second nature to us. Well, at least it’s a common choice for a database on Android. There is something similar on multiplatform. It requires a bit more configuration, but it works just as well. And you know what? It supports Kotlin’s Flow. I’m talking about SQL Delight. The configuration is quite straightforward. You need to add dependencies for Android, iOS and also for shared modules. Also, you need to create a .sq file for storing the SQL queries and database information. This is a potential drawback – if you’ll need to store complex data with multiple queries this file could grow to some substantial size. Here’s a small example of a simple .sq file.
Second configuration quirk, unusual to everyday Room users, is a definition in Gradle where to find the database .sq file.
For more information visit this site, where you can be guided from start to finish how to implement a local database using SQL Delight.
Allright, let’s focus now on a quite essential thing – navigation between screens. Immediately some basic problems come to mind. What about navigation backstack, how to handle configuration changes and where to pass and save data shared between screens? We know that Android apps have their own lifecycles. It’s similar for iOS. However, if we start to think about multiplatform implementation we can sum up the behavior in few essential steps:
We can start the application from scratch – meaning it wasn’t running before and all of its processes are initialized for the first time
We can restart the app – when it was running before an it was paused for some reason and brought back to the user interest
We can pause the application – which is generally speaking: putting it to a background
We can stop the app – which basically means we are ready to kill it if the system decides to do so
So, having established that common ground, how did we resolve that issue? First library that we used was Decompose. There were however two drawbacks. First – a extensive implementation of ChildStack. Second – there was a bug on Android: when a configuration change occurred the navigation was going back to the initial screen in navigation. It looked like whole backstack wasn’t recreated properly. Then, we decided to try a second popular library – Voyager. And to be honest, this was the game changer for us. Similarly to ViewModel on Android, this library has ScreenModel to offer. It even has its onDispose() method. We can also use the StateScreenModel abstract class to implement screen state as StateFlow. So, we basically write an Android ViewModel which works on both platforms. Awesome right?
Alright, but how do we navigate? Everything depends on the Screen interface from Voyager which implements the Serializable interface. It means that automatically every parameter passed between voyager’s Screens must also implement Serializable. More details are described here in documentation. So, the solution we’ve implemented is as follows: first we create a composable screen function, next we create the Voyager’s screen in which we use override function Content() to call that composable screen. Also, in this function we are creating a screen model using Voyager’s inline function rememberScreenModel(). Apart from that there is no problem to implement KoinComponent here and inject dependencies that could be needed. The complete example could look something like this:
And the main screen of the app can simply be:
You just need to pass your starting screen as a parameter in Voyager’s Navigator and that’s it!
These days it’s very common that mobile apps must access restricted data or perform restricted actions – to fulfill the use cases we need to declare appropriate permissions. Unfortunately, since we’re getting into consideration not only two platforms but also their systems versions we need to split the implementation into two parts.
When it comes to requesting runtime permissions on Android, we need to use rememberLauncherForActivityResult() API, specify contract ActivityResultContracts.RequestPermission() and finally define permission (in our case ACCESS_FINE_LOCATION)
On the other hand, on iOS we need to declare CLLocationManager() and then use the method requestWhenInUseAuthorization(), and it’s just as simple as that!
One of the strongest cases for Compose Multiplatform may be sharing the custom components. There are quite a few cases in our experience when the customer along with the graphic designer came up with an ingenious UI solution for presenting data or for interacting with a user. Almost everyone in our team can recall a time when a very beautiful and functional feature needed to be created and it took days, even weeks to implement. Not to mention that it also took our will to live. 😉 Can you imagine that you can cut that time and effort in half? That’s what Compose Multiplatform is proposing. Below you can see the example of a wheel knob, something that works like a volume adjustment. It has a gradient and it animates when a Reset Wheel button is pressed. Both views look and behave exactly the same, but the code was written just once – in Compose. And since the screens are also a composable function, there was no problem integrating the custom view.
We understand that Compose Mutliplatform for iOS is still in alpha, so we can encounter some unexpected problems, however there weren’t any in this case. Also, what’s worth noting here: you cannot use Android Studio’s layout inspector while working on Compose Multiplatform. This may prove debugging issues with views quite troublesome.
Now, it’s time for a little disclaimer: what you have read and our conclusions below are our experience alone. If you’ll think of something fancier, you could find some problems that we haven’t. There are many more possible rabbit holes that we didn’t explore, such as sensors and camera usage, Google Maps integration, and so on.
Having said that, at first I’ll risk repeating other articles concerning the Compose Multiplatform and I’ll say: it’s promising. It has quite a potential. Let’s look now at it from two perspectives: programming and business.
From the programming perspective there are few conclusions. First, if you haven’t already, learn Compose, use it in your apps. Try convincing your team to use it – it’s compact, robust and quite easy to use. Also, it looks like it’s the future for KMM development. Secondly, from an Android developer standpoint: remember to look around for already created solutions in libraries for KMM. However, test them before use. Some of them can have limitations that may affect your app. Also, if you think you’ll write the code for Android and it will run on iOS – no, it won’t. You’ll need to implement external libraries that are not part of Google’s Jetpack. 😉 You’ll probably spend some time on initial configuration of the project. Compose Multiplatform Wizard can be helpful here. In our opinion it’s definitely time to try KMM, so learn it, test it, and try to have fun with it!
Ok, now from a business standpoint: can I offer this solution to my client? Well… If you are brave enough. 😉 I would see Compose Multiplatform working in a simple app: you get the data, you store it, you send it. I know, I know… I’ve just described 90% of the apps out there. 😉 But let’s face it, you can easily implement a simple client for getting and sending data with Ktor. You can cache that data for offline usage with SQL Delight. You can easily navigate between screens with Voyager. But at the end of the day it will all just depend on the specifics of your use case.
Compose Multiplatform is still in alpha for iOS at the time of writing this article. So, it’s not production ready. There is also a case of using libraries provided by a third party that are solving key issues for KMM. What if that crucial library stops being supported? It would be safer if you’ll create those solutions on your own, right? In that case it would take time. Time and money that you wanted to save by going multiplatform.