My Experience After Using Kotlin Multiplatform in Production Apps for a Year

With a kickstarter project goodie to help you get started

Harshith Shetty
Better Programming

--

Photo by Gift Habeshaw on Unsplash

After using Kotlin Multiplatform (KMP) in production apps, Android and iOS, for approx a year now here are some of our statistics:

  • 2.5 Million+ MAU(s)
  • Perfectly stable. Crash-free sessions: >99.75 %
  • >70% code reuse.
  • 24 mobile devs (14 Android + 10 iOS)
  • Kotlin version currently: 1.6.21

Here’s a look at the processes we use to keep development velocity high!

Keep KMP repository separate from your app repositories.

  • Results into better segregation of common modules from specific platform app repository(android, ios, web, backend app, etc).
  • This will keep common KMP tooling, plugins, libraries, etc away from existing/new apps.
  • This makes it easy for existing/new apps to plug and play common code as a third-party library instead of setting up common KMP tooling, plugins, libraries, integration, etc in a platform-specific app repository.

Publish the common KMP code as libraries.

  • You can share common KMP code as libraries for the android and ios apps via maven.
  • Publishing will output the common KMP code into all the targets(android, ios, js, etc) and maven will host the artifacts.
  • The host apps will just load their own platform artifacts from the maven just like we load a third-party library via maven in Gradle.
  • For local development, maven has a local repository that hosts it on the same machine for easy sharing artifacts between projects on the same machine with just a single Gradle command: publishToMavenLocal

Tip:

  • You can use GitLab to make it super easy as it has a maven repository out of the box. So the git repository and the maven packages will be accessible from a single GitLab repository.
  • By adding remote maven configuration in “KMP Repository”, you can directly use the gradle publish task to build and push the artifacts on the maven.
  • On the App Repository side, the same maven configuration will be needed to pull the libraries from maven.
  • Example: Maven configuration to be added here.

Study Kotlin Native (K/N) rules and create a PoC.

  • The Worst part of KMP is that anyone can build common KMP code like they write Kotlin in android and it will run flawlessly in Android.
  • It is worst because it will crash in iOS due to Kotlin Native rules like freezing of objects in multithreading etc so a PoC should be made to understand what is possible and what is not to make guidelines and rules for development in common code. For e.g: InvalidMutabilityException
  • Learn about the rules from here. Create a list of guidelines so that other developers can follow them when working on any new/existing KMP features.

Use expect-actual rarely. Depend on interfaces.

  • Expect-actual gives a very easy way of creating platform-specific access but it has limitations like the same constructor signature.
  • Has scalability issues to support new platform dependencies. For e.g: A new platform may need some different constructor dependencies.
  • Also, you will need to make actual class from the Kotlin file only.
// commonMain
expect class NetworkGateway(debug: Boolean) {
val client: HttpClient
}
// androidMain
actual class NetworkGateway actual constructor(debug: Boolean) {
actual val client: HttpClient = TODO("Not yet implemented")
}
// iosMain
actual class NetworkGateway actual constructor(debug: Boolean) {
actual val client: HttpClient = TODO("Not yet implemented")
}
  • Using abstractions instead will let you freely create the implementation with any platform-specific constructor.
  • You can implement the interface in any platform-specific language like Swift for iOS. So you can send dependency from platform sides too.
  • So you can pass existing platform dependencies implementing the interfaces and passing via DI to common KMP code with more freedom.
// commonMain
interface INetworkGateway{
val client: HttpClient
}
// androidMain or from Android App Repository.
class AndroidNetworkGateway(
debug: Boolean,
interceptors: List<Interceptor>,
networkInterceptor: List<Interceptor>
) : INetworkGateway {
override val client: HttpClient = TODO("Not yet implemented")
}

// iosMain
class IOSNetworkGateway(
debug: Boolean
) : INetworkGateway {
override val client: HttpClient = TODO("Not yet implemented")
}

Write lots of unit tests

  • Even after adhering to K/N rules, there is always room to miss some rules or some conditions, so there should be a plan to accommodate them.
  • Suppose a common feature is already rolled out on android and while integrating it into iOS, there is an issue and the code needs to be fixed.
  • Now the fix will impact both android and iOS on the next KMP library release. So to minimize the impact, if unit tests are already present after whatever changes we do for K/N, we can be sure that it is not breaking any existing flow while updating code to adhere to rules for iOS.
  • Writing Integration tests for iOS will be much better as it will create a regression suite for K/N rule crashes.

Use the android and ios modules in your separate KMP repository as testing hosts.

  • Publishing a common code locally and including it in the actual app repository for integration testing and rebuilding will slow you down.
  • So if you need to test it E2E then you can connect the shared code in the testing host modules that are already made in a new KMP project and use that to test any integration before local/remote publishing.
  • This will be much faster to work and even debug issues.

Keep Common KMP and platform-specific dependency in different files.

  • You can create a common dependency file and different platform dependency files.
  • For e.g: CommonDependencies, AndroidDependencies, etc.
  • This lets you keep common platform third-party libraries in sync on both KMP Repository and platform app Repository.

Improve Crash logging for iOS

  • By default, crashes on common KMP code in the iOS app reported on Crashlytics will not have any significant cause/trace of the crash which will make it useless for debugging the cause.

For e.g. Crash log of InvalidMutabilityException on var loading1: Boolean

  • The above doesn’t have any useful info about the crash.
  • We can solve this using Kermit on the iOS side. It will capture a lot of significant information about crashes from the Kotlin side which can be used for debugging.

Same above crash example after using Kermit,

The exception is clearly visible in the heading and index 7 makes it clear where the crash happened, i.e line 94. The overall information is useful for understanding and fixing the crash. (Note: The log says Non-fatal which is incorrect.)

Some guidelines we use to avoid K/N crashes:

  • Don’t use var for larger scopes in Common code. Use MutableStateFlow instead. For example, for class scoped variables. So goes without saying, use val mostly. To avoid: InvalidMutabilityException (as the variable may be mutated by different threads in a class)
// Don't
class PersonViewModel{
private var loading:Boolean = false
}
// Do
class PersonViewModel{
private val loading = MutableStateFlow(false)
}
  • Even in the case of using val , make sure it's final.
// Don't
abstract class
BaseView() {
abstract val screenName: String
val title = "$screenName Screen" //screenName is not final
}
// Do
abstract class BaseView(val screenName: String) {
val title = "$screenName Screen" //screenName is final
}
Even IDE will give a warning.
  • Use init {} block after declaring all class variables. This is to avoid anyone creating a coroutine in init, which will make the whole class instance freeze and result in throwing an exception on the loading field declared after it.
// Don't
class
PersonViewModel{

init {

}

private val loading = MutableStateFlow(false)

}
// Do
class PersonViewModel{

private val loading = MutableStateFlow(false)

init {

}
}

Create DI File in the platform apps to let the host app have the freedom and flexibility to pass different dependency implementations as it needs. Use cases can be:

  • Passing an existing UserRepository in platform app to common KMP Code instead of creating it from scratch in common code.
  • There can be multiple implementations of a dependency in the KMP repository and platform repository. So switching or A/B can be done on the platform level as per needs.

The below repository is used for blog reference. Also if you need a KMP project template for reference, quickstart, or play with then also you can check it out:

Thank you!

--

--