I've the following code that seems to blocking the main thread even though the flow is called on IO coroutine. I'm a kotlin and flow noob. What am I doing wrong here that's blocking the main thread?
Repository:
fun observeData(): Flow<Data> {
return flow {
//third party api is getting data from a ContentProvider
ThirdPartyApi.getData().map { convertFromExternalModelToDataModel(it) }
.collect {
emit(it)
}
}
}
ViewModel:
fun updateUI() {
scope.launch(Dispatchers.IO) {
repository.observerData().collect {
withContext(Dispatchers.Main) {
textView.text = data.name
}
}
}
}
Upon running the following code it I see logs from Android Choreographer "Skipped 200 frames. App is going too much work on main thread"
CodePudding user response:
To collect the data stream with Kotlin Flows as they're emitted, use collect
. And as collect
is a suspending function, it needs to be executed within a coroutine. It takes a lambda as a parameter that is called on every new value. Since it's a suspend function, the coroutine that calls collect may suspend until the flow is closed.
And you shouldn't be updating your UI inside a ViewModel.
In this case we collect flow inside an activity's lifecycle scope that is main safe and has activity's lifecycle awareness.
And to make our service or repository to execute in a different CouroutineContext
, use the intermediate operator flowOn
.
flowOn
changes the CoroutineContext
of the upstream flow, meaning the producer and any intermediate operators applied before (or above) flowOn
.
The downstream flow (the intermediate operators after flowOn
along with the consumer) is not affected and executes on the CoroutineContext
used to collect from the flow.
ViewModel:
fun getData():Flow<Data> = repository.observeData() // Execute on the io dispatcher
// flowOn affects the upstream flow ↑
.flowOn(Dispatchers.IO)
// the downstream flow ↓ is not affected
.catch { exception -> // Executes in the consumer's context
emit(Data())
}
Activity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch { // Consumer's context
viewModel.getData().collect { // Suspended
textView.text = data.name // Collect on consumer's context
}
}
}