We Fast-Tracked Our App Development With Kotlin Multiplatform Mobile

Sunil
motive-eng
Published in
10 min readJul 18, 2022

Motive Fleet is a mobile app available on both Android and iOS, which our customers use to access critical real time information about their fleets and drivers on the go. We are continually adding new features to Motive Fleet to enhance our customers’ experience. To execute faster and ensure consistency in business logic across our Android and iOS mobile platforms, we have been exploring mobile cross-platform tools. Our goals included easier code management, fewer bugs, better build quality, and improved development timelines—and we achieved them with Kotlin Multiplatform Mobile (KMM). Read this blog to learn:

  • Advantages of a code-sharing framework
  • Kotlin Multiplatform Mobile (KMM) evaluation
  • Learnings & challenges from integrating KMM in our Motive Fleet app
  • Impact of KMM and future work

Advantages of a Code-Sharing Framework

  • API clients: Android and iOS platforms use different API clients, which can lead to configuration mismatch, such as differences in connection and read timeouts, missed API headers, etc.
  • Querying persistent data: Both platforms have different local database solutions for persisting data, which result in differences in DB queries. Android uses Room to perform SQL queries, whereas iOS uses Core Data. This causes differences in how we actually write DB query code.
  • Business logic: Often, differences creep up in the business logic of the apps; for example, date and timezone calculations. We had a production issue where the timezone date calculation differed slightly between the two apps and affected a number of our users.QA ends up testing the same logic on both platforms to verify that they match. Developers are also obligated to sync up frequently to make sure that the business logic is the same across both platforms.

These were some of the challenges we wanted to overcome by utilizing code-sharing frameworks.

Choosing Kotlin Multiplatform Mobile (KMM)

We compared Kotlin Multiplatform Mobile (KMM) to two other code-sharing frameworks: Flutter and React Native. Here’s why we selected Kotlin Multiplatform Mobile:

  • Easy to learn: Android developers already know Kotlin. Kotlin is also very similar to Swift. When we develop a product, we typically have Android and iOS counterparts. These platform teams often review each other’s code, so our iOS team is already exposed to Kotlin. This helps reduce the barrier to entry for anyone wanting to pick it up.
  • Performance: Shared code written in Kotlin is compiled to different output formats for different targets: to Java bytecode for Android, and to native binaries for iOS. This eliminates additional runtime overhead when executing this code on platforms, and the performance is comparable to native apps.
  • Integration: Both the Flutter and React Native frameworks require deep integration while KMM is more of a library, which allows for incremental integration and less risk. For most developers on our iOS team, using KMM is simply a CocoaPod integration to a pre-built XCFramework. For Android, it’s just a module.
  • UI: With KMM, we can develop the UI in the native platforms, so the apps can continue to make full use of their platform UI/UX experience.

Our comparisons pointed clearly to KMM. Once we came to a consecutive decision, we had to figure out how to integrate it in our existing app. Choosing a new architecture can require rewriting a lot of code and reorganizing modules to support development over the next few years. We carefully thought through the organization and how each shared component will be used and implemented across Android and iOS.

Next we’ll go over the integration of KMM in the Fleet app and provide a few examples of how KMM components interact between iOS and Android. We hope you will find the following insights useful as you consider KMM or other unified mobile architecture.

Fleet App KMM Architecture

Both our Android and iOS Fleet apps use a Clean Architecture, and the UI layer is separate from the domain and data layers. Having a Clean Architecture made it easier for us to integrate KMM for the existing features.

At Motive we use a monorepo, so both the Android and iOS Fleet apps reside under the src/mobile folder. This removes any complexities in sharing the common code during local development.

We added a new KMM module called shared to the Android Fleet app. The equivalent is added as a CocoaPods dependency for the iOS Fleet app, with shared as the framework name.

The shared module contains the view models, use cases, repository, data models, resources (localized strings, colors, images), etc. Practically every component except the UI is shared by both apps. At the time of writing, we have already shipped three large features using KMM.

App Structure

Code Structure

The shared library currently only supports Android and iOS.

  • commonMain: This module is a pure Kotlin library. All of the shared classes, interfaces, and expect classes reside in this module. It also contains the resources (strings, images) that are shared between both apps. For now we only share strings that support localization.
  • androidMain: This is an Android module that has Android-specific implementations for the expect classes.
  • iosMain: The equivalent of androidMain, it contains iOS-specific implementations for commonMain classes.

Integrating the KMM Module in Our Existing Fleet Apps

On the Android Side

On Android, the shared module works like a normal Kotlin module; we don’t need any extra configurations to use the shared code. Integrating KMM on existing apps is a little trickier. We need to use the CocoaPods Gradle plugin to generate a podspec file, for the shared module. Using the generated Podspec, we can add the shared module to the iOS project as a CocoaPods dependency.

On the iOS Side

On the Fleet iOS app, we use custom names in the Xcode build configuration, like “Staging” or “Production.” When we try to build the iOS app from Xcode, the CocoaPods plugin fails because it can’t find the ‘release’ or ‘debug’ build configuration.

To solve this problem, we had to map our custom build configuration to Kotlin NativeBuildType by using an xcodeConfigurationToNativeBuildType parameter. We also export another KMM module called sharedbase as part of a single Framework package using the export keyword.

We added our shared module as a local dependency to the iOS Podfile because both Android and iOS apps are part of a monorepo.

Networking

We are using Ktor as our HTTP client to connect to the backend APIs. Ktor supports API logger, kotlinx serialization, gzip, custom interceptors, etc.

In Ktor, HTTP clients are represented by the HttpClient class. Our SharedApiService class uses the HTTP client to actually make API network calls. On Android, we use OkHttp as the HTTP client, while on iOS we use the default iOS client provided by Ktor.

On Android, we were accustomed to using OKHttp’s interceptors to modify the API request/response according to our needs. But OkHttp is a JVM only library, so we had to write new interceptors using Ktor features, such as UserAuthInterceptor. Having this feature adds the auth token value to API request headers.

Database

We use SQLDelight as our multi-platform database library. To use SQLDelight, we must configure the Gradle options:

The packageName parameter specifies the package name for the generated Kotlin sources.

We put our SQL statements in .sq files under src/commonMain/sqldelight. The first statement in the SQL file creates a table.

There is an official plugin for working with .sq files.

UseCase and ViewModels

Our team has created a base use case class in the shared module, which contains a coroutine scope. All use cases extend this base class to make it easier to cancel coroutines from both platforms.

Similarly, we have a SharedViewModel class, which is the base class for the viewmodels defined in the shared library.

On Android, SharedViewModel extends Android Jetpack’s ViewModel so we get Activity destroyed callbacks in onCleared:

ViewModel Example

Let’s go over how we share a ViewModel across the Android and iOS apps. TripHistoryViewModel in the shared library is used to get trip history for vehicles, drivers, and assets:

  • uiState publishes any change in UI State for trip history.
  • ​​getTripHistoryUseCase makes an API call for trip history.

On Android, we have added a TripHistoryViewModelWrapper class, so that we can inject the ViewModel through Hilt:

After that, TripHistoryViewModel usage is like a normal Android ViewModel.

Our Fleet app KMM module on iOS is named SharedFramework.

  • TripHistoryViewModel is accessed through the shared framework.
  • KLifecycle sends events when the screen is visible to the user or when it goes to the background.
  • DisposeSharedViewModel is used to dispose of the SharedViewModels like TripHistoryViewModel.

Other Resources

Shared Resources

Using the Moko Resources library, we are able to share strings, colors, images, and other resources between the two apps. The library also supports localization.

Date/Time

Kotlin has a multi-platform date/time library, kotlinx-date. This enables us to share the same date/time logic and calculation between the two apps.

Logger

Napier is a multi-platform logger. We use it to log exceptions in the shared library. It can also be configured to send exceptions to Crashlytics.

Debugging KMM on the iOS App

Jetbrains provides a KMM plugin for Android Studio. It enables us to run and debug the iOS app straight from Android Studio.

The Challenges We Faced

  • Initial setup for KMM in the Fleet App was complex and time-consuming. We wanted to make API calls from the shared module, so we had to first refactor our session management to be multi-platform. We also had custom interceptors which were JVM only, so we had to recreate the same on KMM.
  • KMM Memory management on iOS was quite restrictive and complicated. However, from Kotlin 1.6.10, we can use the New Kotlin/Native Memory Manager, which simplifies writing async code. Currently the Fleet app is using Kotlin 1.6.21.
  • Even though Kotlin is similar to Swift, there’s still a learning curve for iOS developers. They also had to understand the various KMM libraries like Ktor, SQLDelight which are different from iOS libraries.
  • Previously, Android test jobs were run on changes made to Android code, and iOS test jobs were run on iOS code changes. Because KMM changes affect both platforms, we had to make changes to our CI to run the test jobs of both platforms. This ensures that when developers are making changes to KMM code, they are testing both apps.

We use Prow as our CI, so our Android test job ran on Linux instances, and iOS test jobs ran on Mac instances. After the KMM integration, we had to update the jobs to use Mac instances so that both Android and iOS test jobs could be run.

Impact to Date

KMM on our Fleet app has been in production for almost six months now. In that time we have released three large product features that use KMM. We have been able to share all the business logic, resources, repositories, database, and ViewModels. Only the UI was built natively on each platform.

We recently released a product feature where users can select/deselect multiple parent and child groups. The feature required maintaining a tree and a child/ancestor nodes list, and was a very complex project to implement. Having a shared code ensured that we didn’t have any differences in business logic between the apps.

The number of critical or major bugs raised by QA has also greatly reduced. This is because we no longer spend extra resource hours making sure the same logic has been coded on both platforms.

Because of the above reasons we were able to save four weeks of developer effort for each feature. An additional two weeks were saved from the QA cycle thanks to the business logic being the same across the apps. This makes for an overall savings of six weeks of effort, and has been a remarkable advantage for us.

Conclusion and Future Work

Integrating KMM on our existing Fleet app was a complex task requiring thorough research and the creation of custom classes for those libraries which were only on JVM. In the end, our effort has dramatically improved our productivity, and reduced bugs and business logic differences between our Android and iOS apps.

We continue to make improvements to the shared library and to the developer experience, especially for iOS developers.

One of our goals is to create a base KMM library that our Motive Driver app can use (fleet drivers use this app to keep track of everything related to their vehicles; it replaces their physical log books and inspection reports). We also plan to refactor our analytics and feature flag modules to be multi-platform.

On the developer experience side, we are creating a Codelab exercise to onboard mobile developers to KMM, and have created documentation to onboard iOS developers to Kotlin and KMM.

Come Join Us!

Check out the latest Motive opportunities on our Careers page to join our rad engineering team.

--

--