Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Updated Lessons from a KMP Developer's Toolkit

Updated Lessons from a KMP Developer's Toolkit

Avatar for Subhrajyoti Sen

Subhrajyoti Sen

July 07, 2025
Tweet

More Decks by Subhrajyoti Sen

Other Decks in Programming

Transcript

  1. Shared Resources task("renameAndroidSharedResource") { outputs.upToDateWhen { false } doLast {

    copy { from("build/generated/moko/androidMain/res/values-fr") into("build/generated/moko/androidMain/res/values-fr-rCA") } delete("build/generated/moko/androidMain/res/values-fr") } dependsOn("generateMRandroidMain") }
  2. Shared Resources task("renameIosSharedResource") { outputs.upToDateWhen { false } doLast {

    copy { from("build/generated/moko/iosMain/res/fr.lproj") into("build/generated/moko/iosMain/res/fr-CA.lproj") } delete("build/generated/moko/iosMain/res/fr.lproj") } dependsOn("generateMRiosMain") }
  3. Shared Resources tasks.preBuild { if (tasks.findByName("generateMRiosMain") != null) { dependsOn("renameIosSharedResource")

    } if (tasks.findByName("generateMRandroidMain") != null) { dependsOn("renameAndroidSharedResource") } }
  4. Dependency Injection - Earlier // commonMain open class ChatMessageChannelViewModelImpl( private

    val messageChannelManager: MessageChannelManagerInterface ) : SharedViewModel(), ChatMessageChannelViewModel
  5. Dependency Injection - Earlier // commonMain open class ChatMessageChannelViewModelImpl( private

    val messageChannelManager: MessageChannelManagerInterface ) : SharedViewModel(), ChatMessageChannelViewModel // android @HiltViewModel class ChatMessageChannelViewModelWrapper @Inject constructor( private val messageChannelManager: MessageChannelManagerInterface, ) : ChatMessageChannelViewModelImpl( messageChannelManager )
  6. Dependency Injection - Later // commonMain expect annotation class KMMViewModel

    // androidMain actual typealias KMMViewModel = HiltViewModel
  7. Dependency Injection - Later @KMMViewModel class ChatMessageChannelViewModelImpl( private val messageChannelManager:

    MessageChannelManagerInterface ) : SharedViewModel(), ChatMessageChannelViewModel
  8. Dependency Injection - Now // commonMain expect class KoinStringsModule //

    androidMain @Module @ComponentScan actual class KoinStringsModule // iosMain @Module @ComponentScan actual class KoinStringsModule
  9. Dependency Injection - Now @Module( includes = [ KoinSharedModule::class, ]

    ) @ComponentScan class KoinAppAndroidModule { //.. }
  10. Dependency Injection - Now class MainApplication : Application() { override

    fun onCreate() { super.onCreate() startKoin { androidLogger() androidContext(this@MainApplication) modules(KoinAppAndroidModule().module) } } }
  11. Dependency Injection - Now private fun getKClass(type: ObjCObject): KClass<*> {

    return when (type) { is ObjCProtocol -> getOriginalKotlinClass(type) is ObjCClass -> getOriginalKotlinClass(type) else -> null } ?: throw IllegalArgumentException("Unknown type: $type") }
  12. Dependency Injection - Now fun <T : ObjCObject> get(type: ObjCObject):

    T { val koin = KoinPlatformTools.defaultContext().get() val kotlinClass = getKClass(type) return try { koin.get(kotlinClass) } catch (e: Throwable) { throw e.getRootCause() } }
  13. Dependency Injection - Now import Foundation import shared public func

    kmmGet<T: AnyObject>() -> T { guard let object = DiKt.get(type: T.self) as? T else { fatalError("error in casting") } return object }
  14. Dependency Injection - Now import Foundation import shared public func

    kmmGet<T: AnyObject>() -> T { guard let object = DiKt.get(type: T.self) as? T else { fatalError("error in casting") } return object }
  15. Dependency Injection - Now @Suppress("unused") // Used on iOS fun

    start( netConnectivity: NetConnectivity, ) { val iosNativeModule = module { factory { netConnectivity } } startKoin { modules(iosNativeModule, KoinSharedModule().module) } }
  16. Shared ViewModel // commonMain expect abstract class SharedViewModel() { protected

    val sharedScope: CoroutineScope protected open fun onCleared() }
  17. Shared ViewModel // androidMain actual abstract class SharedViewModel : ViewModel()

    { actual val sharedScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) actual override fun onCleared() { super.onCleared() sharedScope.cancel() } }
  18. Shared ViewModel // iosMain actual abstract class SharedViewModel { actual

    val sharedScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) protected actual open fun onCleared() { sharedScope.cancel() } }
  19. Observable State // CamerasViewModelNative.kt @ObjCName(name = "effect") public val CamerasViewModel.effectNative:

    NativeFlow<CamerasEffect> get() = effect.asNativeFlow(null) public val CamerasViewModel.effectReplayCache: List<CamerasEffect> get() = effect.replayCache
  20. Observable State // CamerasViewModelNative.kt public val CamerasViewModel.uiStateFlow: NativeFlow<CamerasUiState> get() =

    uiState.asNativeFlow(null) @ObjCName(name = "uiState") public val CamerasViewModel.uiStateValue: CamerasUiState get() = uiState.value
  21. Observable State extension CamerasViewModel { func uiStateObservable() -> Observable<CamerasUiState> {

    return createObservable(for: self.stateFlow) } func effectObservable() -> Observable<CamerasEffect> { return createObservable(for: self.effect) } }
  22. Build Optimizations • We won’t want to run iOS tasks

    when running CI for Android • We won’t want to run Android tasks when running CI for iOS
  23. Build Optimizations // Podfile def pod_KMM if %r{^true$}i.match ENV['USE_KMM_DEV'] pod

    'KotlinMultiplatformModule', :path => '../path' else pod 'KotlinMultiplatformModule' end end
  24. General Tips • Keep a close look on Xcode versions

    • Understand the generated code • Not everything needs to be KMP • Remember to use `internal` implementation definitions: ◦ constructors ◦ usecases ◦ repositories