Kotlin Multiplatform Mobile (KMM) at Granular

Andrea Prearo
Granular Engineering
9 min readMar 28, 2022

All mobile developers, sooner or later, are confronted with the following question: “Can we write code that works across iOS and Android?”. Over the years, many tools have been created to be able to achieve the Holy Grail of “Write once, run anywhere”.

Over the last 2–3 years, the mobile teams at Granular have adopted Kotlin Multiplatform Mobile (KMM) to write cross-platform code that works across iOS and Android.

What we share across mobile platforms

KMM allows mobile teams to keep their UI fully native and share the common business logic (written in Kotlin). But how much of your business code should be shared depends on many factors. Many of the available KMM samples and real world examples tend to draw the upper boundary of the shared code at the data layer or model domain level (through some version of the Repository pattern).

API wrapping/integration is one of the most common kind of logic that can be easily shared across mobile platforms using Ktor. Another one is on-device data persistence which can be easily be taken care of using SQLDelight.

Generally speaking, this allows to adopt different architecture patterns as you get close to the specific platform UI. For instance, your iOS developers may want to use VIPER or MVVM for the iOS app while your Android developers may prefer MVP or MVI.

At Granular, we decided to be more prescriptive in terms of the desired architectural approach. Our mobile teams agreed to adopt an MVI approach for both platforms (iOS and Android), to maximize our business logic code sharing. Broadly speaking, this means that we’re creating MVI based cross-platform view models on top of what is the common type of shared business logic (API integration, data layer, persisted settings, …). This allows us to define our specific best practices and patterns to ensure clean separation between platform-specific UI presentation and business logic. This also brings up the interesting topic of observing for data changes from a view model, which likely deserves its own post (stay tuned!).

Reusing code across multiple apps

At Granular, we have two production apps: Insights and Business. When we started adopting KMM to share our business logic, we wanted to make sure we can easily integrate new functionality into both apps. This was critical for functionality such as SSO which would be extremely wasteful to implement multiple times (once per app and/or once per platform). In addition to that, we also want to make sure we can create platform-specific UI, on top of the KMM shared business logic, that could be reused in multiple apps. The aforementioned SSO functionality is a prime example of this requirement: Wouldn’t be great to build a platform-specific UI login screen and be able to reuse it in every single app that needs to leverage the same SSO functionality?

From reusable components to an SDK

That’s what we set up to build and, after the initial success with the SSO functionality, we became very fond of building reusable components that bundle the necessary KMM shared business logic (up to the view model) with the related platform-specific UI. Nowadays, we approach every new feature with a componentized mindset to make sure from the inception phase that we can easily reuse both the shared logic and the platform-specific UI.

In Granular lingo a component is usually a software module that is defined by some KMM shared logic and any related platform-specific UI (See Fig.2). From the point of view of Build and Release engineering, this usually translates in publishing a platform-specific binary artifact (iOS .framework, Android .aar) for each component.

KMM based Mobile SDK at Granular

Our flagship app, Granular Insights, is currently bundling about a dozen feature-oriented reusable components. We also have a few “foundational” libraries containing helper classes and methods commonly used across the codebase, to maximize code reuse. This collection of components and libraries can be bundled together, as needed, to serve as an SDK to allow new internal, or external, teams to start building new mobile apps for their particular agricultural-based use case without having to start from scratch or “reinvent the wheel” (See Fig.1).

Learning, challenges, and the way forward

Until a couple of years ago, my main focus as a mobile developer has been iOS development. So, naturally, my opinions on KMM are heavily influenced by how easy, or difficult, is to integrate KMM into the usual iOS development workflow.

Let’s start with some of the more general challenges that apply to both iOS and Android. KMM is still in its early phases, so there aren’t many consolidated standardized best practices in regards to the Kotlin shared code and it’s usually up to the mobile team(s) to decide what works best for them.

For instance, as you approach KMM, your company mobile team(s) should define how to:

  • Inject platform-specific functionality into the shared business logic (for instance, network monitoring).
  • Expose your data layer / repository / view model to each platform.
  • Structure your teams to be able to work across KMM, iOS and Android in a shared codebase.

There’s no “one size fits all” here and every company/team needs to figure out what’s the most reasonable approach toward sharing business logic and working together in a shared codebase.

In regards to KMM-related coding best practices, one iOS specific item we spent quite some time to fully flesh out is how to expose Kotlin Flow to iOS and consume it in Swift. Since we decided to share view models across platform, we opted to go all in with a functional/reactive approach and consume Kotlin Flow through RxSwift. The approach we landed on has been successfully used for some time and we’ll likely implement something similar as we start leveraging SwiftUI and Combine. Similarly to the topic of observing for data changes from a view model, briefly discussed earlier, consuming Kotlin Flow through RxSwift (or Combine) likely deserves its own post so I won’t go into details here.

I’ll now illustrate some of the specific challenges related to the KMM adoption use case for Granular (i.e.: Building a mobile SDK). Most of the open source KMM examples are covering the use case of embedding the shared Kotlin code into a single app per platform (the same app for Android and iOS). In our use case, since we set out to create an SDK based on reusable components, we need to:

  1. Support multiple apps on both iOS and Android.
  2. Leverage the shared Kotlin code to implement platform-specific reusable UI components (iOS and Android) on top of it (as illustrated in Fig.2).

The real challenge is to achieve the second goal. Once that’s taken care of, building an SDK and supporting multiple apps is rather straightforward using the consolidated platform-specific approaches for dependency management.

Reusable UI components challenges and possible approaches

KMM and Android work seamlessly

Creating reusable UI components, dependent on the shared Kotlin code, is relatively seamless on Android. In this scenario all dependencies (shared Kotlin code and Android UI library) can be handled through Gradle without too much effort. Once you become familiar with it, Gradle can fulfill all your needs in terms of build process and dependency management.

KMM and iOS use different build tools and dependency managers

For iOS, instead, the situation is more challenging. KMM provides the necessary Gradle tasks for creating iOS binary artifacts (universal frameworks and xcframeworks) from the shared Kotlin code. But we need to be able to invoke the appropriate Gradle task and assemble the shared Kotlin code into the required iOS binary artifact before we can use it as a dependency for a reusable iOS library. This means that, in order to have a seamless iOS development experience, we need to find a way to inject a pre-build phase into the iOS build process, to make sure we assemble the required iOS artifact from the Kotlin code before building the reusable iOS library that depends on it. This requirement translates into being able to inject a pre-build phase (for the shared Kotlin code iOS artifact) through the iOS dependency manager of choice.

At Granular, we want to be able to support our iOS SDK dependencies through both CocoaPods and SwiftPM. This will allow iOS app implementers to choose the dependency manager they feel most comfortable with. At the time being, we have a solution to inject a pre-build phase (to invoke Gradle) in place for CocoaPods. We’re still exploring this topic in regards to SwiftPM.

If injecting a pre-build phase in the build process is not feasible, there are some possible workarounds. For instance, every time we make changes to the shared Kotlin code, we could manually invoke the appropriate Gradle task to assemble the iOS binary artifact, before building the dependent iOS UI library. This ensures we get the updated shared code functionality, but also means that additional manual steps are required when updating the iOS UI library. This is mainly a developer workflow challenge. It’s usually easily solvable when running tests through a CICD pipeline, as all the required steps can be easily handled through scripting. But, as you develop using Xcode, you’d likely want to just being able to hit the Run button and have all the required build steps automatically taken care for you. Having to manually invoke the Gradle task, to assemble the iOS binary artifact from the shared Kotlin code as we make changes, before building any dependent iOS library is both extremely tedious and error prone (because it's really easy to forget to manually invoke the Gradle task).

Shared business logic debugging challenges and possible approaches

KMM and Android Studio work seamlessly

As it happens in most scenarios, when working across Android and KMM you find out that the development experience is pretty seamless. Debugging the shared business logic from an Android app running through Android Studio is not really different from what Android developers are used to.

Xcode doesn’t know about Kotlin

Of course, Xcode has never been built to support Kotlin. But it’d be really great to be able to debug the shared Kotlin code while running an iOS app through Xcode.

Kudos to Touchlab for providing an xcode-kotlin plugin, which we’ve been experimenting with for some time. The plugin allows to set breakpoints in the shared Kotlin code from Xcode. Variable inspection through LLDB is also supported by the plugin. But the debugger tends to be a bit unstable, especially if you are trying to inspect Kotlin code that is executed inside a coroutine scope. For our teams, this is the most common case as our cross-platform view models are exposing their data through one or more Kotlin flows. In the context of our iOS SDK, the iOS binary artifact for the shared Kotlin code is imported and consumed through a few different Swift frameworks, which can definitely create a more complicated and unstable scenario for the plugin. For simpler scenarios, for instance when leveraging a shared Kotlin repository, the debugger is definitely more reliable.

At least some of the issues we ran into, when trying to debug Kotlin code from Xcode, are definitely of our own making. Because of the highly functional/reactive nature of our common shared business logic, where much of the high level functionality hinges on a cross-platform view model, debugging would likely be tricky even if all our code was in Swift.

The combined effect of the above, led us to rely on using print statements as our main approach to debugging Kotlin code from Xcode. How much of an issue this is I think depends mostly on personal preferences/habits as a developer. I’m sure some of the reactive programming purists among us would say we shouldn’t set breakpoints inside reactive code anyway, as it interferes with the threading behavior and may end up altering it altogether, possibly creating concurrency issues that could very well be a by-product of the debugger and not due to the code itself.

KMM plugin and iOS app

We also looked into the KMM Android Studio plugin. Unfortunately, the plugin is not able to build our iOS app, which depends on many Swift frameworks and is also not bundling the shared Kotlin code directly into the app itself (but using it as a dependency for more than a few internal Swift frameworks).

Conclusion

As mentioned earlier, KMM is still in its early phases. Nevertheless, we think it’s a very promising cross-platform technology: It allows to write fully platform specific UI code (ideally using SwiftUI and Jetpack Compose) and share all the common business logic across mobile platforms.

We’ve been successfully using KMM in production for about 2–3 years now. As with any technology, after our teams figured out how to deal with some of the rough edges (especially on iOS), we definitely enjoyed the benefits of sharing business logic across mobile platforms. Our plan for the future is to keep writing cross-platform reusable components and keep improving our shared codebase as KMM provides new and improved functionality.

--

--

Andrea Prearo
Granular Engineering

Experienced iOS Engineer and Software Craftsman with extensive expertise in building reusable libraries and components to scale development teams and products.