I've been having a shot at kotlin multiplatform and it's brilliant, but threading stumps me. The freezing of state between threads makes sense conceptually, and works fine in simple examples where small objects or primitives are passed back and forth, but in real world applications I can't get around InvalidMutabilityException.
Take the following common code snippet from an android app
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
)
private var coreroutineSupervisor = SupervisorJob()
private var coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main + coreroutineSupervisor)
private fun loadResults() {
// Here: Show loading
coroutineScope.launch {
try {
val result = withContext(Dispatchers.Default) { objectWhichContainsNetworking.fetchData() }
// Here: Hide loading and show results
} catch (e: Exception) {
// Here: Hide loading and show error
}
}
}
Nothing very complex, but if used in common code and run from Kotlin/Native then pow InvalidMutabilityException on MainViewModel.
It seems the reason for this is that anything passed in withContext is frozen recursively so because objectWhichContainsNetworking is a property of MainViewModel and is used in withContext then MainViewModel gets caught in the freeze.
So my question is, is this just a limitation of the current Kotlin/Native memory model? Or perhaps the current version of coroutines? And are there any ways round this?
Note: coroutines version: 1.3.9-native-mt. kotlin version 1.4.0.
Edit 1: So it appears that the above slimmed down code actually works fine. It turns out the incriminating code was an updateable var in the view model (used to keep a reference to the last view state) which becomes frozen and then throws the exception when it tries to be mutated. I'm going to make an attempt of using Flow/Channels to ensure there's no var reference needed and see if this fixes the overall problem.
Note: if there is a way to avoid MainViewModel being frozen in the first place it would still be fantastic!
Edit 2: Replaced the var with Flow. I couldn't get standard flow collecting in iOS until using the helpers here: https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/mobileMain/kotlin/org/jetbrains/kotlinconf/FlowUtils.kt.
MainViewModel still gets frozen, but as all it's state is immutable it's no longer a problem. Hope it helps someone!
In your original code, you are referencing a field of the parent object, which causes you to capture the whole parent and freeze it. It is not an issue with coroutines. Coroutines follows the same rules as all the other concurrency libraries in Kotlin/Native. It freezes the lambda when you cross threads.
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
)
//yada yada
private fun loadResults() {
coroutineScope.launch {
try {
val result = withContext(Dispatchers.Default) {
//The reference to objectWhichContainsNetworking is a field ref and captures the whole view model
objectWhichContainsNetworking.fetchData()
}
} catch (e: Exception) {}
}
}
To prevent this from happening:
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
){
init{
ensureNeverFrozen()
}
//Etc
The most complicated thing with the memory model is this. Getting used to what's being captured and avoiding it. It's not that hard when you get used to it, but you need to learn the basics.
I've talked about this at length:
Practical Kotlin/Native Concurrency
Kotlin Native Concurrency Hands On
KotlinConf KN Concurrency
The memory model is changing, but it'll be quite a while before that lands. Once you get used to the memory model, the immutable issues are generally straightforward to diagnose.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With