SECRET OF CSS

My Experience After Using Kotlin Multiplatform in Production Apps for a Year | by Harshith Shetty | Jul, 2022


With a kickstarter project goodie to help you get started

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!

  • 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 platform-specific app repository.
  • 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 which hosts it on the same machine for easy sharing artifacts between projects on the same machine with just a single Gradle command: publishToMavenLocal
1*5qrTtpc5gREWsS p6EuGhg

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 “publish” Gradle 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.
  • 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.
  • 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
get() = TODO("Not yet implemented")
}
// iosMain
actual class NetworkGateway actual constructor(debug: Boolean) {
actual val client: HttpClient
get() = TODO("Not yet implemented")
}
  • Using interfaces instead will let you freely create the implementation with any platform-specific constructor.
  • You can implement the interface in a 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.
// Using interface
interface INetworkGateway {
val client: HttpClient
}

// androidMain or from Android App Repository.
class AndroidNetworkGateway(
private val debug: Boolean,
private val interceptors: List<Interceptor>,
private val networkInterceptor: List<Interceptor>
) : INetworkGateway {
override val client: HttpClient
get() = TODO("Not yet implemented")
}

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

  • 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.
  • 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.
  • 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.
  • 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

1*X9t2ZGaWlLqjXepsXyO15A
  • 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,

1*uzFYSweGLlRT1nD33DpEkA

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.)

  • Don’t use vars for larger scopes in Common code. Use MutableStateFlow instead. For example, for class scoped variables. So goes without saying, use vals mostly. To avoid: InvalidMutabilityException (as the variable may be mutated by different threads in a class)
class PersonViewModel{
private var loading:Boolean = false //Don't
private val loading = MutableStateFlow(false) //Do
}
  • 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.
class PersonViewModel{

init {

} //Don't

private val loading = MutableStateFlow(false)init {

} //Do
}

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.



News Credit