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. @BlrKotlin
    KotlinConf’23
    Bengaluru
    Exploring a KMM
    Developer’s
    Toolkit
    Subhrajyoti Sen
    Motive

    View Slide

  2. Agenda
    ● Shared Resources
    ● Parcelable
    ● Dependency Injection
    ● Shared ViewModel
    ● Coroutines
    ● Flow
    ● Build optimizations

    View Slide

  3. Shared Resources
    icerockdev/moko-resources
    Can be used to share:
    ● Strings
    ● Color
    ● SVG

    View Slide

  4. Shared Resources

    View Slide

  5. Shared Resources

    View Slide

  6. Shared Resources
    public actual object MR {
    public actual object strings : ResourceContainer {
    public actual val now: StringResource = StringResource(R.string.now)
    public actual val ago: StringResource = StringResource(R.string.ago)
    }
    }

    View Slide

  7. Shared Resources
    Initially no region support for localization: fr-CA, es-US, etc.
    ‼🚨

    View Slide

  8. Shared Resources
    Initially no region support for localization: fr-CA, es-US, etc.
    ‼🚨
    Support added in v0.21.0

    View Slide

  9. Shared Resources

    View Slide

  10. Shared Resources

    View Slide

  11. 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")
    }

    View Slide

  12. 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")
    }

    View Slide

  13. Shared Resources
    tasks.preBuild {
    if (tasks.findByName("generateMRiosMain") != null) {
    dependsOn("renameIosSharedResource")
    }
    if (tasks.findByName("generateMRandroidMain") != null) {
    dependsOn("renameAndroidSharedResource")
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  16. Shared Resources - Helper
    // commonMain
    expect class SharedResource {
    fun getLocalisedString(string: StringResource, vararg value: Any): String
    fun getLocalisedString(string: PluralsResource, quantity: Int): String
    }

    View Slide

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

    View Slide

  18. 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()
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  21. Dependency Injection - Earlier

    View Slide

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

    View Slide

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

    View Slide

  24. Dependency Injection - Now

    View Slide

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

    View Slide

  26. Dependency Injection - Now
    // commonMain
    actual annotation class KMMViewModel
    // androidMain
    actual typealias KMMViewModel = HiltViewModel
    // commonMain
    @KMMViewModel
    class ChatMessageChannelViewModelImpl(
    private val messageChannelManager: MessageChannelManagerInterface
    ) : SharedViewModel(), ChatMessageChannelViewModel

    View Slide

  27. Shared ViewModel

    View Slide

  28. Shared ViewModel
    class CloseableCoroutineScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    fun close() {
    coroutineContext.cancel()
    }
    }

    View Slide

  29. Shared ViewModel
    // commonMain
    expect abstract class SharedViewModel {
    internal val sharedScope: CloseableCoroutineScope
    protected open fun onCleared()
    }

    View Slide

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

    View Slide

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

    View Slide

  32. Coroutines
    // Kotlin
    suspend fun asyncFunc() = coroutineScope {
    launch {
    delay(1000L)
    println("World!")
    }
    println("Hello")
    }
    // Swift
    func test() {
    PlatformKt.asyncFunc(completionHandler: { _ in
    print("Completed")
    })
    }

    View Slide

  33. Current Flow
    Repository
    UseCase
    ViewModel
    Android iOS

    View Slide

  34. Coroutines - Dispatchers.IO

    View Slide

  35. Coroutines - Dispatchers.IO
    // commonMain
    expect fun ktIoDispatcher(): CoroutineDispatcher

    View Slide

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

    View Slide

  37. 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
    }

    View Slide

  38. Be careful
    // creates a new thread pool everytime
    actual fun ktIoDispatcher(): CoroutineDispatcher {
    return newFixedThreadPoolContext(64, "Dispatchers.IO")
    }

    View Slide

  39. Observable State

    View Slide

  40. Observable State
    interface ChatViewModel {
    val uiState: KLiveData
    fun method1()
    fun asyncMethod2()
    }

    View Slide

  41. Observable State
    interface ChatViewModel {
    val uiState: KLiveData
    fun method1()
    fun asyncMethod2()
    }
    KLiveData: https://github.com/florent37/Multiplatform-LiveData

    View Slide

  42. Coroutines Flow
    interface ChaViewModel {
    val uiState: KSharedFlow
    fun method1()
    fun asyncMethod2()
    }

    View Slide

  43. KFlow
    open class KStateFlow(
    private val initialValue: T? = null
    ) {
    protected val _flow = MutableStateFlow(initialValue)
    val flow: StateFlow = _flow
    open val value: T?
    get() = _flow.value
    }

    View Slide

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

    View Slide

  45. KFlow
    open class KStateFlow(
    private val initialValue: T? = null
    ) {
    fun collect(scope: KCoroutineScopeInterface, block: (T?) -> Unit) {
    scope.launch {
    flow.filterNotNull()
    .collect {
    block.invoke(it)
    }
    }
    }
    }

    View Slide

  46. KFlow - iOS
    class BaseViewController: UIViewController {
    let scope = KCoroutineScope()
    deinit {
    scope.cancel()
    }
    }

    View Slide

  47. Flow on iOS
    kmmViewModel.uiState.collect(scope: scope) {[weak self] uiState in
    guard let uiState = chatUiState else {
    return
    }
    self?.handleKMMUIState(uiState)
    }

    View Slide

  48. KFlow - iOS
    https://github.com/rickclephas/KMP-NativeCoroutines

    View Slide

  49. KFlow - iOS
    https://github.com/rickclephas/KMP-NativeCoroutines

    View Slide

  50. Build Optimizations

    View Slide

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

    View Slide

  52. 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
    }

    View Slide

  53. 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
    }

    View Slide

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

    View Slide

  55. Build Optimizations
    fun KotlinTarget.disableCompilations() {
    compilations.configureEach {
    compileKotlinTask.enabled = false
    }
    }

    View Slide

  56. Build Optimizations
    fun disableKapt(project: Project) {
    project.tasks.withType().configureEach {
    enabled = false
    }
    project.tasks.withType().configureEach {
    enabled = false
    }
    }

    View Slide

  57. Build Optimizations
    fun KotlinTarget.disableCompilationsIfNeeded(project: Project) {
    if (!isCompilationAllowed()) {
    disableCompilations()
    }
    if (EnvParams.disable_android) {
    disableKapt(project)
    }
    }

    View Slide

  58. Build Optimizations
    // build.gradle.kts
    kotlin {
    targets.configureEach {
    disableCompilationsIfNeeded(project)
    }
    }

    View Slide

  59. Build Optimizations
    // build.gradle.kts
    kotlin {
    targets.configureEach {
    disableCompilationsIfNeeded(project)
    }
    }
    https://github.com/arkivanov/gradle-setup-plugin

    View Slide

  60. Thank You
    @iamsubhrajyoti

    View Slide

  61. Share your feedback
    to help us better
    understand your
    KotlinConf’23
    Global experience!

    View Slide