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

Exploring a KMM Developer’s Toolkit

Exploring a KMM Developer’s Toolkit

BlrKotlin May 2023

Subhrajyoti Sen

May 07, 2023
Tweet

More Decks by Subhrajyoti Sen

Other Decks in Programming

Transcript

  1. Agenda • Shared Resources • Parcelable • Dependency Injection •

    Shared ViewModel • Coroutines • Flow • Build optimizations
  2. Shared Resources public actual object MR { public actual object

    strings : ResourceContainer<StringResource> { public actual val now: StringResource = StringResource(R.string.now) public actual val ago: StringResource = StringResource(R.string.ago) } }
  3. 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") }
  4. 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") }
  5. Shared Resources tasks.preBuild { if (tasks.findByName("generateMRiosMain") != null) { dependsOn("renameIosSharedResource")

    } if (tasks.findByName("generateMRandroidMain") != null) { dependsOn("renameAndroidSharedResource") } }
  6. Shared Resources - Localization fun getMyString(): StringDesc { return StringDesc.Resource(MR.strings.my_string)

    } // Android val string = getMyString().toString(context = this) // iOS let string = getMyString().localized()
  7. Shared Resources - Formatting fun getMyFormatDesc(input: String): StringDesc { return

    MR.strings.my_string_formatted.format(input) } // Android val string = getMyFormatDesc("hello").toString(context = this) // iOS let string = getMyFormatDesc(input: "hello").localized()
  8. Shared Resources - Helper // commonMain expect class SharedResource {

    fun getLocalisedString(string: StringResource, vararg value: Any): String fun getLocalisedString(string: PluralsResource, quantity: Int): String }
  9. Shared Resources - Helper // androidMain actual class SharedResource(val context:

    Application) { actual fun getLocalisedString(string: StringResource, vararg value: Any): String { return string.format(value.asList()).toString(context) } actual fun getLocalisedString(string: PluralsResource, quantity: Int): String { return string.format(quantity, quantity).toString(context) } }
  10. Shared Resources - Helper // iosMain actual class SharedResource {

    actual fun getLocalisedString(string: StringResource, vararg value: Any): String { return string.format(value.asList()).localized() } actual fun getLocalisedString(string: PluralsResource, quantity: Int): String { return string.format(quantity, quantity).localized() } }
  11. Shared Resources - Helper // Android val resources = SharedResources(context)

    val text = resources.getLocalisedString(MR.string.my_string) // iOS let resources = SharedResources() let string = resources.getLocalisedString(MR.strings().my_string())
  12. Parcelable // commonMain expect annotation class Parcelize() expect interface Parcelable

    // androidMain actual typealias Parcelize = Parcelize actual typealias Parcelable = Parcelable // iosMain actual interface Parcelable actual annotation class Parcelize
  13. Dependency Injection - Earlier // commonMain open class ChatMessageChannelViewModelImpl( private

    val messageChannelManager: MessageChannelManagerInterface ) : SharedViewModel(), ChatMessageChannelViewModel
  14. 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 )
  15. Dependency Injection - Now // commonMain actual annotation class KMMViewModel

    // androidMain actual typealias KMMViewModel = HiltViewModel
  16. Dependency Injection - Now // commonMain actual annotation class KMMViewModel

    // androidMain actual typealias KMMViewModel = HiltViewModel // commonMain @KMMViewModel class ChatMessageChannelViewModelImpl( private val messageChannelManager: MessageChannelManagerInterface ) : SharedViewModel(), ChatMessageChannelViewModel
  17. Shared ViewModel class CloseableCoroutineScope(context: CoroutineContext) : CoroutineScope { override val

    coroutineContext: CoroutineContext = context fun close() { coroutineContext.cancel() } }
  18. Shared ViewModel // commonMain expect abstract class SharedViewModel { internal

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

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

    val sharedScope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main) protected actual open fun onCleared() { sharedScope.close() } }
  21. Coroutines // Kotlin suspend fun asyncFunc() = coroutineScope { launch

    { delay(1000L) println("World!") } println("Hello") } // Swift func test() { PlatformKt.asyncFunc(completionHandler: { _ in print("Completed") }) }
  22. Coroutines - Dispatchers.IO // commonMain expect fun ktIoDispatcher(): CoroutineDispatcher //

    androidMain actual fun ktIoDispatcher(): CoroutineDispatcher { return Dispatchers.IO }
  23. Coroutines - Dispatchers.IO // commonMain expect fun ktIoDispatcher(): CoroutineDispatcher //

    androidMain actual fun ktIoDispatcher(): CoroutineDispatcher { return Dispatchers.IO } // iosMain // coroutines 1.7.0-beta private val iosIoDispatcher = newFixedThreadPoolContext(64, "Dispatchers.IO") actual fun ktIoDispatcher(): CoroutineDispatcher { return iosIoDispatcher }
  24. Be careful // creates a new thread pool everytime actual

    fun ktIoDispatcher(): CoroutineDispatcher { return newFixedThreadPoolContext(64, "Dispatchers.IO") }
  25. Observable State interface ChatViewModel { val uiState: KLiveData<ChatUIState> fun method1()

    fun asyncMethod2() } KLiveData: https://github.com/florent37/Multiplatform-LiveData
  26. KFlow open class KStateFlow<T>( private val initialValue: T? = null

    ) { protected val _flow = MutableStateFlow(initialValue) val flow: StateFlow<T?> = _flow open val value: T? get() = _flow.value }
  27. KFlow class KMutableStateFlow<T>( private val initialValue: T? = null )

    : KStateFlow<T>(initialValue) { override var value: T? = super._flow.value get() = super._flow.value set(value) { field = value _flow.value = value } }
  28. KFlow open class KStateFlow<T>( private val initialValue: T? = null

    ) { fun collect(scope: KCoroutineScopeInterface, block: (T?) -> Unit) { scope.launch { flow.filterNotNull() .collect { block.invoke(it) } } } }
  29. KFlow - iOS class BaseViewController: UIViewController { let scope =

    KCoroutineScope() deinit { scope.cancel() } }
  30. Flow on iOS kmmViewModel.uiState.collect(scope: scope) {[weak self] uiState in guard

    let uiState = chatUiState else { return } self?.handleKMMUIState(uiState) }
  31. 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
  32. Build Optimizations object EnvParams { val disable_iOS: Boolean get() =

    System.getProperty("disable_iOS") != null val disable_android: Boolean get() = System.getProperty("disable_android") != null } enum class HostType { MAC_OS, ANDROID }
  33. Build Optimizations fun KotlinTarget.getHostType(): HostType? = when (platformType) { KotlinPlatformType.androidJvm

    -> HostType.ANDROID KotlinPlatformType.native -> when { name.startsWith("ios") -> HostType.MAC_OS name.startsWith("watchos") -> HostType.MAC_OS else -> error("Unsupported native target: $this") } else -> null }
  34. Build Optimizations fun KotlinTarget.isCompilationAllowed(): Boolean { if (!EnvParams.disable_iOS && !EnvParams.disable_android)

    { return true } else if (getHostType() == HostType.ANDROID && EnvParams.disable_android) { return false } return !(getHostType() == HostType.MAC_OS && EnvParams.disable_iOS) }