The Dos and Dont’s of Mobile Development with Kotlin Multiplatform: Part II
This is the second post from our mini-series about the best practices of mobile app development with Kotlin Multiplatform. Check out the first piece with our hands-on advice on Multiplatform here to take away the experience we have gained with the technology. In this post, we continue to share the practical aspects of working with Kotlin / Native and Multiplatform that will help you to get out robust and high-performing apps faster and with an optimal budget.
RUNNING APP
These were ready apps, built natively for iOS and Android. The major pain point with the project was a complicated server API: poorly documented, it significantly complicated the development of the apps because of its unexpected behavior, which showed only during tests.
Goals
Combine the existing client-server logic, REST-based DTOs, and repositories into a shared library on Kotlin to simplify further work with the API.
Outcomes
We managed to combine the client-server code, entities, and repositories into a shared module on Android, but the iOS piece turned out too laborious for realization and was put on hold.
Challenges
- Ktor’s beta versions made a complete mess of Kotlin / Native compiler, that kept spitting out unintelligible errors. The solution came with the release of another beta that automatically fixed the problem.
- Existing iOS and Android implementations were too different. And so the shared module that we created on Android would take too many changes to integrate into iOS.
Takeaways
When creating a shared module to encompass existing functionality of native apps, first consider if the apps were built with the same architectural principles in mind, and get ready to lots of changes.
APATRIS, a crypto wallet
A recent project for development from scratch, with plans for Android and iOS, but starting only with iOS. We decided to use Kotlin Multiplatform Project (MPP) to address the need to cover both mobile platforms so that only UI is done on each platform natively. The app is a crypto wallet with a lot of client-server interactions but without an offline database.
Goals
The main objective was to get a library that’s ready (or almost ready) to be used on Android, while developing an iOS app — with the ViewModels, repositories, client-server code, and all other non-UI components.
Outcomes
We got the library that’s almost ready to be implemented on Android: it missed only four classes.
Challenges
- Since we were working with crypto currencies, where values like 0.00000000001 are normal, we had to use BigDecimal, not available in Kotlin’s stdlib. So we realized our own expect/actual using NSDecimalNumber on iOS.
- Compared to Android, Kotlin / Native compilation is very slow. It does not support incremental compilation yet, and the only thing that helps us to speed up the process is the use of modules. The modules are compiled into a KotlinLibrary, and after that, all libraries are bundled into a big module compiled to the iOS framework. But this approach still slowed things down, compared to native Android development.
Takeaways
Using MPP on projects that start with an iOS app, probably doesn’t add too much overhead (given a team has some experience with the technology) and significantly simplifies the development of an Android version in future because by that time the business logic has been already verified. The app was successfully released to the App Store, with no review issues.
BEGREAT, a habit tracking app
Another project for development from the ground up, also with plans for Android and iOS, but starting with Android. We use MPP for all new Android projects because it’s risk-free (Kotlin and jvm are pretty common on Android) and may speed up iOS development later on. The project includes the server integration, sync with a local database, around ten screens, and in-app purchases.
Goals
To get a shared library with all logic, including ViewModels, which can then be used on iOS.
Outcomes
The shared library has everything except database realization. The database was singled out into an expect/actual class to be realized separately on each platform (using Room for Android). Compilation of an iOS build will require database realization.
Challenges
The main challenge was the database. Room and Realm do not support MPP, and so we had to make an expect/actual class to work with the database. NB, there has already appeared an MPP-compliant realization of SQLDelight. Realm is still discussing whether Kotlin / Native and MPP is something their dev community needs.
Takeaways
It worked out in the end, but next time we’ll try SQLDelight to bring database-related code into a shared library. We are still unaware of solid ORMs for MPP, except for SQLDelight and SQLiter, an even more low-level tool, which only grants SQLite access to MPP. The app was successfully released to Google Play.
COFFEE app
A large project from scratch for two platforms at once, with real-time objects (cars) on a map, a catalog, a shopping cart, and a profile for managing addresses — to start with.
Goals
- Get a shared library with all logic including ViewModels
- Get a complete module for working with sockets on MPP that we’ll be able to reuse
Outcomes
All objectives were achieved. The socket module was built with SOCKET.IO libraries for Swift and Java.
Challenges
- Sockets implementation
SOCKET.IO interfaces a little differently with Swift and Java, and so we had to resort to a couple of assumptions (mostly concerning typical events and errors).
2. Sockets in iOS
When implementing the socket library on iOS, we wanted to plug the Swift variant of SOCKET.IO directly into Kotlin and realize everything right in Kotlin. However, the version of Kotlin we worked with did not support enum forward declarations from the framework’s header. To solve the issue, we had to create an interface for the iOS platform and realize Swift integration. The issue was resolved in Kotlin 1.3.20.
3. Abstract classes in iOS
The abstract classes became regular classes in iOS after compilation (there’s no such thing as abstract classes there), and so the compiler could not tell if a particular method should be realized or not. So we had to add a new rule: “The public interface of a shared library should not contain abstract classes — interfaces should be used instead.”
4. Parcelize in Android
For the convenience of Android developers, Parcelize had to be added into the shared library. This was easily solved with expect/actual.
5. Iterable ’s odd behavior on iOS
If we use Iterable in the public interface of the shared library, it turns into Any. So instead of Iterable, we use List, Set, and other more precise collection types.
6. Time zones, date formatting, etc.
The app had to flawlessly handle time, time zones, and time & date formats in the UI. The Klock library sealed the deal with just a couple of small tweaks to make it work correctly with a particular time zone.
Takeaways
Debugging in a large project built with MPP went noticeably faster: We did not get discrepancies between platform implementations, and there were no cases when we had to fix problems twice (for each platform). We haven’t kept count, but it feels like debugging took at least twice less effort compared to native development on iOS and Android without MPP.
EDUCATION app
A midsize project with lots of reference materials, a chat, and a video-streaming feature. The app had to be developed for two platforms and meet a tight deadline. Eighty percent of the app’s UI were lists of items.
Goals
- Use a modern kotlin-multiplatform plugin (instead of kotlin-platform-common/android/native)
- Move UI layout description (besides logic and ViewModels) to the shared library to be able to quickly build screens right from the shared library by defining the structure of a screen.
- Create Ktor-client code generation using Swagger.
Outcomes
- We successfully updated the library with the kotlin-multiplatform plugin, so IDE started indexing the iOS-specific code.
- The shared library incorporated all non-UI components plus widgets — the descriptions of UI elements that each platform uses to create final views, meaning the screens generated from the widgets were controlled entirely by the shared code and didn’t require any modification on either of the platforms.
- Code generation was successfully implemented and has been actively reused since then.
- While working on the project, we also singled out shared MPP modules to handle permissions and media pickers.
Challenges
- Kotlinx-io issue
On iOS, we stumbled on a bug in kotlinx-io (used by Ktor-client) when implementing the uploading of large (5 Mb) photos via the API: it took too long to transform into bytes. We reported the issue, and though it currently does not have a solution, according to Nikolai Igotty, the Kotlin team found a bug in the compiler — so it’s quite possible that version 1.3.40 will solve the issue. Our workaround was to transform a string to bytes with our own expect/actual (using NSData for iOS) instead of kotlinx-io.
2. Logic duplication
To avoid the duplication of the logic for working with permissions and getting images from a camera or gallery, we created MPP modules (via coroutines right from a ViewModel).
Takeaways
- Code generation of the REST client from Swagger works well. It speeds up the development process if the server API keeps changing.
- The idea about the widgets that let partially transfer UI controls to the shared library proved as quite viable. We’ll keep on working with this concept.
- Work with permissions and pickers directly from ViewModels simplified development even further, removing unnecessary duplication.
- Debugging went smoothly as fixes were applied on both platforms simultaneously.
APP à la UBER (ongoing)
The project entails the development of the taxi-ordering iOS and Android apps with the same features as in Uber. The apps are to be developed from scratch and released simultaneously.
Goals
- Improve the concept with widgets by moving more of the common UI into a shared library without compromising the native UX. Users should get the same native experience they are used to, with the interface built using platform views and common to each platform approaches.
- Move map-related functionality into a separate MPP module
- Bundle all app features into separate MPP modules within the shared multiplatform library.
Challenges
There are no blockers or critical issues on our way at this moment in time.
FREELANCE app
Two platforms, from scratch, starting with Android only. Separate apps for client and service provider users. The feature list includes orders (with a specific workflow for orders: accepting, handling, closing), payments, and user profiles for both types of apps.
Goals
- Continue improving the widgets concept
- Enrich the widgets library by adding forms building with validation
- Bundle all app features into separate MPP modules within the shared multiplatform library.
Challenges
There are no blockers or critical issues on our way at this moment in time.