Adopting Kotlin Multiplatform Mobile (KMM) at Meetup

Sam Broad
Making Meetup
Published in
6 min readDec 16, 2021
Photo by Alexandr Podvalny from Pexels

Introduction

When we were in the early stages of scoping out our new Meetup Organizer App Beta we decided to adopt an architecture that leveraged Kotlin Multiplatform Mobile (KMM). While this is a relatively new programming framework, KMM shines because it allows you to write the business logic for your iOS and Android apps just once, in pure Kotlin.

KMM was ideal for our use case for several reasons:

  • It doesn’t impose any restrictions on how you develop your app’s UI. We wanted to work with the latest declarative UI frameworks on iOS and Android, SwiftUI and Jetpack Compose respectively. KMM didn’t stand in the way of us doing that.
  • It doesn’t prevent you from working with the platform directly. Whenever a task can’t be solved in the shared code, you can use the expect/actual pattern to seamlessly write platform-specific code.
  • Kotlin. We didn’t want to introduce a new programming language into our stack. Our Android Engineers were already very strong Kotlin programmers. We knew that due to its similarity to Swift it would be easy for our iOS Engineers to pick up.

And so we began our initial explorations into developing with this new technology.

Initial explorations

We knew about the proposed benefits of working with KMM, but we needed to prove it to ourselves. To do this, we allocated two months to uncover any potential blockers with our plan. Specifically, we wanted to implement one complete flow in our new application with KMM.

By implementing one flow we would be able to learn about the following:

  • Use of the expect/actual pattern
  • Properly handling asynchronous shared code
  • Working with Ktor for authenticated HTTP request
  • Integrating with Apollo-Android in shared code
  • Best practice for handling translations
  • Using Firebase for behavioral tracking

We started to tackle these items one at a time. The Kotlin community was instrumental in helping us get through this phase. Whenever we encountered blockers we were able to reach and get support. Often, by the next release of a given library, we had our fix. This is something that you should keep in mind when you’re developing against a beta library, however. Changes come frequently and you should bake in time for making the upgrades.

Besides these specific concerns, we also needed to settle on the architecture of the application.

Architecture

When we started asking ourselves how much code and logic we could share between platforms, we knew we’d want to, at a minimum, use the same network stack. Since we are building the app against a GraphQL backend, we started by integrating the Apollo client into the shared module. As we would be making some REST requests too, we also pulled in Ktor and configured that.

Using multiplatform-settings, we were able to persist OAuth tokens and write our authentication interceptor once for all network calls, be they REST or GraphQL, iOS or Android. This uses EncryptedSharedPreferences on Android and Keychain on iOS to make sure tokens get stored encrypted on the device.

Beyond that, since we also need persistence, we opted to use SqlDelight in the shared component, abstracting all database operations from the client code. This means our entire data layer is in shared code, and we only need to expose repositories/interactors to the platforms.

The “beta” architecture

The repository layer retrieves data needed to render the UI for the apps and abstracts all of the inner workings. When one of the ViewModels asks for data, the repository can check the database for cached versions, combine multiple network requests, persist some of the data to the local database, map the data to domain classes, etc. The ViewModels do not need to know about any of this happening behind the scenes. They just care that the suspend function they called returns the data they expect.

@Throws(MeetupError::class)
suspend fun getEventDetails(eventId: String): Event { … }

The iOS and Android apps each have their own ViewModels for each screen, which call suspend functions on the shared repositories (also one per screen in general). When calling the repositories’ Kotlin suspend functions from iOS, we simply use Swift completion handlers with closures.

At some point in the future, we may consider writing shared ViewModels too (StateFlows would be wrapped to Combine Publishers for iOS), though we haven’t really spent much time investigating this yet.

All of these components are assembled using the Koin framework, allowing each platform to provide their native implementation for the shared components (SharedPreferences, Keychain, OkHttp engine…) and each ViewModel to obtain the repositories they need via constructor injection.

Finally, the UI layers for Android and iOS use Jetpack Compose and SwiftUI respectively, both relatively new to us (though the iOS member app does use SwiftUI for some of the newer screens).

Challenges

Since this is a greenfield project, all the engineers involved were excited about getting to try out some new tech, but Android Engineers had a distinct advantage, as we were all familiar with Kotlin already. Initially, the iOS Engineers weren’t thrilled about having to work with Kotlin. They only focused on the UI portions of the application.

Over time, however, their perceptions began to change. Now everyone contributes to the shared KMM code on a regular basis. This has helped us to increase the speed with which we’ve been able to develop features. In fact, we’ve pulled extra Android engineers into the team so that we can keep both platforms in feature-sync, and not let the Android app UI work fall behind.

Besides this initial language barrier, another inconvenience we quickly ran into was the fact that KMM generates Objective-C code for all the shared interfaces. Since we prefer to use Swift, we ended up having to add an extra layer of mapping, to hide away the generated code, and use the Swift constructs we’ve grown used to. This complicates the codebase unnecessarily and is a possible source of bugs. It is on JetBrains’ roadmap to add direct Swift interoperability, but until then, we’ll have to keep writing the wrapper layer ourselves…

Next steps

At this stage in the project, we’re quite happy with the way working with KMM has turned out for us. Now that we’ve shipped the first version of the app to the first group of Meetup organizers who have volunteered to be early testers, we’re starting to turn our attention to a few architectural improvements we’d like to make.

As mentioned earlier, we will be looking into pulling more code out of the separate platform modules and down into the KMM shared module, possibly starting with a shared ViewModel that would expose the UI state for each screen as a stream that could be observed by the views, using Kotlin Flows on Android and Combine Publishers in Swift.

Another optimization we’ll be looking into is using the new Kotlin/Native memory manager to improve threading and concurrency issues on iOS.

Takeaways

While adopting KMM has had its own set of challenges, we are ultimately satisfied with this choice of framework. If you or your organization are considering using KMM for your next app, we would recommend the approach we’ve laid out here.

Here are a few helpful resources to get started:

Do you organize events on Meetup? Sign up to get early access to our new Organizer Application.

--

--