There are dozens of cross-platform technologies, which promise you "write-once run-anywhere" experience out of the box without any considerable drawbacks. But in the end, we all know it's not 100% true.
The latter is the youngest player in the field, but the most promising one. It's super-flexible, concentrates on sharing business-logic, ensures native UI/UX, and enables native development experience.
In this article, I want to share our migration journey of native iOS and Android apps to Kotlin Multiplatform. I'll emphasize both, pros and cons, of chosen technology + some workarounds for bugs we've faced in the process.
Codeforces WatchR is an open-source mobile client for Codeforces platform, where thousands of programmers compete in weekly algorithmic challenges. There are both, iOS and Android apps, which are available in stores.
Codeforces API is far from being mobile-first, so a lot of business-logic happens directly on devices, which includes, but not limited to — caching server data, merging data sets, extra-features not supported by the server.
This is the perfect use case for Kotlin Multiplatform, which allows sharing business-logic in the form of a library (Android) or framework (iOS) compiled to binaries, flawlessly executed by native platforms.
Migration has been done in multiple steps:
- Gradle setup + copy/paste of modified ReKotlin library
- Migration to SQLDelight (more details in this article)
- Migration from Retrofit to Ktor
- Moving business-logic and binding iOS project
- Reusing KMP framework in iOS app
We are huge fans of Redux pattern, which perfectly works with Kotlin Multiplatform. But unfortunately, the library we are used to (ReKotlin) hasn't been yet ported to KMP (you can't just use Android libs in KMP project).
So along with the basic setup of modules and Gradle scripts, we were forced to copy/paste official ReKotlin code, which is thankfully 100% Kotlin without any dependency on Android-specific stuff. Publishing your own KMP library is a tedious task, so it was decided to go with the simplest solution.
I won't describe project setup in details, because there are already many great starter projects and articles on the topic. For example, KaMPKit from Touchlab or kotlin-multiplatform-template from Guillermo Orellana.
As mentioned previously, it's not possible to use Android libraries in KMP module out of the box, but there are already many alternatives, which have covered about 100% of our needs.
Some of the core libraries are developed and supported by JetBrains, other ones — by the community. The full list of what's available up to date can be found here: https://github.com/AAkira/Kotlin-Multiplatform-Libraries.
KMP database library SQLDelight is probably one of the best solutions, which I worked with on Android. It allows you to write almost pure SQL code, which then used to generate 100% Kotlin class wrappers.
In our case, 100% of database code is written in a common module and shared between iOS / Android. Using Kotlin libraries in Swift has many caveats, so I would recommend to hide them behind your custom interfaces.
Ktor is a nice alternative to Retrofit even though it's quite different and you need to get used to it. But again, if you hide 3rd parties behind your custom interfaces (repository in this case), changing the networking library is an easy task.
Once the database and network levels were migrated to KMP, we had a chance to move the business-logic. With Redux all your business-logic should be located in Action or Request (for async code) classes.
Not that easy! We actually needed to move all Redux to common module (store, state, middleware, etc.), not just actions. There we had some code relying on Android-specific classes, which took us a few days to decouple.
That's it! We've moved all we wanted to KMP module. It's time to bind shared code to iOS project: copy/paste it as sub-folder of Android app, configure Kotlin XCode Sync and CocoaPods Gradle plugins, import it in Swift. Voilà.
"Nothing works" was the first phrase I've heard from our iOS dev, which tried to reuse the common KMP module. I've checked and nothing really worked!
But why? What's a problem? Can we fix it?
There were a few problems actually, which we were able to fix in 2 days:
- Kotlin/Native initialises global variables in alphabetical order, which is quite painful with Redux where you have store, middleware and reducers declared as global variables. As a workaround we needed to add
zto package name of
storeto make sure it's initialised in the last turn.
- Changing context in coroutines didn't work, at least with a
CustomScopefrom kotlin-multiplatform-template, which we used. Removed all
withContextcalls from the common code.
- For some reason
===didn't work as expected within Kotlin/Native, even though printed addressed were the same. Changed them to
- Kotlin libraries aren't accessible in iOS code, if you add them as
implementationin KMP module, which is expected. But if you add them as
apiall classes are prefixed with library name to prevent name clashing.
- But your own classes aren't prefixed and sometimes you are trying to use the wrong class from iOS framework.
Platformis one of such examples, which we needed to use as
common.Platformat the end.
- All classes should extend other classes or
Any. Otherwise you will have messed up generics, which are limited even without this problem.
But at the end, it was rather the victory with a great benefit of consistency between our previously inconsistent apps.
Kotlin Multiplatform is a young, but very promising technology, which can bring many advantages for your development team. But you need to make sure that you have enough time and knowledge to tackle challenges.
Android dev experience won't be changed a lot. iOS devs will need some time to adapt, learn some Kotlin programming and dig deeper into how Kotlin -> ObjC -> Swift conversion works :)