DEV Community

Maximizing Code Sharing between Android and iOS with Kotlin Multiplatform

Kurt Renzo Acosta on January 02, 2020

This article aims to discuss code sharing between Android and iOS using Kotlin Multiplatform. You may or may not have the knowledge of Android and ...
Collapse
 
baudouxbenjamin profile image
Benjamin

Hi Kurt! Great article but I still have a question:

"Suspending functions aren't compiled to ObjC so we can't use those on iOS" and yet you defined the following function

suspend fun getItems(): List = client.get("url.only.fortest/items")

which is a suspending function. Are they compiled to Objective-C or not?

Collapse
 
kuuurt profile image
Kurt Renzo Acosta

You can still use Coroutines but only in Kotlin. You won't be able to call them in Objective-C.

Collapse
 
baudouxbenjamin profile image
Benjamin • Edited

So what's the point of the getItems() function if it cannot be called on iOS? Or is it meant to be used only inside the multi-platform code itself?

Thread Thread
 
kuuurt profile image
Kurt Renzo Acosta • Edited

It's being called on Kotlin's side through the ViewModel. The ViewModel calls it and exposes it through a CFlow which is just a wrapper of a Flow with an extra watch function that we can use in Objective-C.

Edit: More explanation (I was on a phone earlier and it was hard)

To consume a Flow, you can use collect which has this signature suspend fun Flow<*>.collect(): Unit. Since this is a suspending function, you won't be able to call this in Objective-C. suspend functions' interoperability are still unsupported so we expose a normal watch function that takes a lambda and handles launching the coroutine.

I take no credit on the watch function. The guys from Jetbrains did a good job on it. github.com/JetBrains/kotlinconf-ap...

Collapse
 
qrezet profile image
qrezet • Edited

Hi Kurt!

First of all, in my opinion this is a great article. :D

I just don't agree with this:

"For Android developers:
ConflatedBroadcastChannel = MutableLiveData
Flow = LiveData"

More like:
ConflatedBroadcastChannel = (MutableLiveData - LifeCycleAware)

And:
(Flow + Stores Latest Value) = LiveData

Also, isn't it possible to keep using LiveData (with Flow.asLiveData()) for Android and only use CFlow when working with iOS. I don't really get the goal of wrapping it to CFlow for both platform or am I missing something?

By converting Flow to LiveData over wrapping it with CFlow, you no longer have to worry about the "Closables" of CFlow (atleast in android). Whatcha think?

Its actually awesome to see a fellow filipino write articles about Kotlin Multiplatform. Awesome dude awesome! :D

Collapse
 
kuuurt profile image
Kurt Renzo Acosta • Edited

Hello! Glad to see another Filipino interested in this!

You're right about that!

ConflatedBroadcastChannel is a MutableLiveData minus the lifecycle awareness.
Flow however, is just the ConflatedBroadcastChannel which we just change into Flow to prevent the consumers from overwriting the value.

Yes, you can use Flow.asLiveData() since CFlow is still a Flow. You can use whatever you like but for me, I use watch() so that iOS devs aren't that far from it just in case they want to peek. I had to write some lifecycle management code, though, for the Closeable. It's much like the Disposable in RxJava2 so I created a base Fragment which would handle the lifecycle management.

I think that would make sense since CFlow is just for iOS.

Maybe an expect/actual implementation would do?

// commonMain
expect class CommonFlow<T>(flow: Flow<T>)

// androidMain
actual class CommonFlow<T> actual constructor(flow: Flow<T>) : Flow<T> by flow

// iosMain
actual class CommonFlow<T> actual constructor(flow: Flow<T>) : Flow<T> by flow {
    fun watch(...) { ... }
}

Thanks for your response!

Collapse
 
kuuurt profile image
Kurt Renzo Acosta • Edited

The disadvantage in usage the snippet above though would be for testing.

Since CommonFlow isn't a Flow in commonMain, unit tests in commonTest turn into red. For that reason, I still used the CFlow but just used asLiveData() in Android

Thread Thread
 
qrezet profile image
qrezet • Edited

Aha! I see, it is because you want to make the Android ViewModel and iOS "ViewModel" seem to be just the same.

Hmmm... In my opinion, and this is purely my opinion, I would keep Android ViewModel and whatever iOS "ViewModel" seperate from each other. They are platform specific concepts and I don't think its good idea to mix them as one. I would rather therefore move all logic outside these "ViewModels" which would make these "ViewModels" just "wrapper" or "wiring class".

This would make me safe from any changes these platform specific constructs. :) But then again just my pure opinion. I guess it is at this point that developers tend to argue. Until what layer should be shared?

Thread Thread
 
kuuurt profile image
Kurt Renzo Acosta • Edited

Not really. The ViewModel is already different based on the platform. You can check BaseViewModel wherein on Android, it uses Architecture Components, and on iOS, it's just a custom implementation.

I beg to differ, the ViewModel can be shared. Ideally, it shouldn't have a dependency on the platform's framework. You have the view (aka Fragment / Activity / ViewController) for that. For example:

class LoginViewModel(private val loginUseCase: Login) : BaseViewModel() {
    private val _loginState: ConflatedBroadcastChannel<UiState>()
    val loginState = _loginState.wrap() 

    fun login(username: String, password: String) {
        clientScope.launch {
            _loginState.offer(UiState.Loading)
            loginUseCase(username, password)
            _loginState.offer(UiState.Complete)
        }
    }
}

There's no platform-specific implementation. It's just pure logic.

The watch function on the CFlow class is just sort of an extension for iOS which you can choose to use or not in Android. iOS cannot get suspend functions which is why we have to resort to some workaround for now.

You can choose to use the expect/actual implementation for CommonFlow and use the normal Flow in Android and use CFlow for iOS however this makes you unable to test it in commonTest since this sourceSet will use commonMain and in the declaration, it's not a Flow and you can't let and expect declaration inherit from something so you have to create both of the tests in androidTest and iosTest

I think exposing a function doesn't hurt. It's not like we changed the whole implementation. We just added something for it to be used on the other side.

But that's my opinion and what's worked for me so you can still go ahead and try your implementation out and see if it works for you. :)

TL;DR Add a watch function so we can use Flow on iOS vs. Write them separately on each platform. I go for using watch

Collapse
 
qrezet profile image
qrezet

Nicee! :D Keep writing and Happy Coding! :D

Collapse
 
haiithust profile image
Lương Công Hải

Hi Kurt!
Thank for this article, I have a question. In the android studio, how can we turn on autocomplete code when writing code inside package iosMain?

Collapse
 
kuuurt profile image
Kurt Renzo Acosta

Hi! Autocomplete works for me out of the box but if you're using libraries from cocoapods then you have to build the project first on Xcode.

Collapse
 
haiithust profile image
Lương Công Hải

Currently, I just using the sample project without cocoapods but android studio no turn on code suggestion and autocomplete. Example "dispatch_get_main_queue" and "dispatch_async". is it need another plugin?

Thread Thread
 
kuuurt profile image
Kurt Renzo Acosta

How are you configuring your native targets?

Thread Thread
 
haiithust profile image
Lương Công Hải

this is my config

targets {
    fromPreset(presets.jvm, "jvm")

    fromPreset(presets.iosX64, "ios_x86_64")
    fromPreset(presets.iosArm64, "ios_arm64")
    configure([ios_x86_64, ios_arm64]) {
        compilations.main.outputKinds("FRAMEWORK")
    }
}

I following from github link github.com/adrianbukros/github-mul...

Thread Thread
 
kuuurt profile image
Kurt Renzo Acosta • Edited

Sorry for the delayed response. With your configuration, you're having two native source sets, ios_x86_64Main and ios_arm64Main. Autocomplete should work for both of those. However, I looked at the repo you linked and it's creating a common source set for both of those so you can code it iniosMain`. I'm afraid there's still an issue with this.

  1. If you are using cocoapods for your native targets, you can do a switching mechanism like they do here

`kotlin

val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
    if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
        ::iosArm64
    else
        ::iosX64

iOSTarget("ios") {
    binaries {
        framework {
            baseName = "SharedCode"
        }
    }
}

`

  1. If you're not using cocoapods, you can enable this flag in your gradle.properties.


kotlin.mpp.enableGranularSourceSetsMetadata=true

Thread Thread
 
haiithust profile image
Lương Công Hải

did it, Thank you so much!

Collapse
 
topherpedersen profile image
topherPedersen

Glad to hear about Kotlin Multiplatform. I was getting a little worried about "going all in" career-wise by focusing on cross platform development with React Native. It looks like cross platform development is here to stay!

Collapse
 
piannaf profile image
Justin Mancinelli

Glad to hear you are glad to hear about KMP 😄

I love your enthusiasm and want to be sure you understand the cross platform development with React Native is very different than cross platform development with Kotlin Multiplatform.

Yes, cross platform is here to stay. Code sharing has been with us since the first cross-compilers and transpilers that allowed the same code to run on different architectures. Since then, there have been many solutions, some that people still use, some that never gained traction. It's a super interesting space.

Collapse
 
petarmarijanovicfive profile image
Petar Marijanovic

Hi Kurt, it looks to me that everything is on the main thread or am I missing something?

Collapse
 
kuuurt profile image
Kurt Renzo Acosta

You got that right. This is because of K/N. Once Multithreaded Coroutines come out on Native, we can implement switching threads. :)

Collapse
 
pablodesiderioflores profile image
Pablo

Hey Kurt, thanks for the really well written article. I'm wondering,
how or why the ui is not causing visible "jank" or not getting android.os.NetworkOnMainThreadException for network calls if everything is on the main thread like Petar said?

Thread Thread
 
kuuurt profile image
Kurt Renzo Acosta

Thanks! You can take a look a this.

Thread Thread
 
pablodesiderioflores profile image
Pablo

But we cannot use with context dispatcher.io with kn as I read kn doesn't have multi threaded coroutines. My question is how the main thread doesn't get block? For example, I need to retrieve and save data from sqlDelight, Android and iOS. Database actions can be long running operations, if kotlin native just accepts kotlin coroutines single thread, so I cannot use dispatcher.io how can I do that?

Collapse
 
rhonyabdullah profile image
Siagian

So they now work in progress to support multi threading. Pull request