Slide 1

Slide 1 text

@BlrKotlin KotlinConf’23 Bengaluru Exploring a KMM Developer’s Toolkit Subhrajyoti Sen Motive

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Shared Resources

Slide 5

Slide 5 text

Shared Resources

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Shared Resources

Slide 10

Slide 10 text

Shared Resources

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Dependency Injection - Earlier

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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 )

Slide 24

Slide 24 text

Dependency Injection - Now

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Shared ViewModel

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Current Flow Repository UseCase ViewModel Android iOS

Slide 34

Slide 34 text

Coroutines - Dispatchers.IO

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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 }

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Observable State

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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 }

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Build Optimizations

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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 }

Slide 53

Slide 53 text

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 }

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Thank You @iamsubhrajyoti

Slide 61

Slide 61 text

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