Bradleycorn
12/02/2020, 6:34 PMFlow
with Composables and produceState
?
For example, consider:
@Composable
fun MyUi(viewModel: ViewModel) {
val data by produceState(initialValue = "data", viewModel) {
viewModel.getDataFlow().collect {
value = it
}
}
}
What if the viewModel.getDataFlow()
method uses some flow operator(s)? For example:
fun getDataFlow(): Flow<String> {
return myRoomDb.getData().distinctUntilChanged().map { convertEntityToString(it) }
}
I think I remember reading awhile back that this can be a problem, because every time there is a recomposition, the operators (in this case map) have to get re-wired up. Which (I think) I understand. So if so, what’s the solution?Zach Klippenstein (he/him) [MOD]
12/02/2020, 6:46 PMproduceState
will only restart its coroutine if the keys you pass in change between compositions (i.e. viewModel
).Zach Klippenstein (he/him) [MOD]
12/02/2020, 6:49 PMproduceState
for this, there’s already a function for doing what you’re doing:
val data by yourFlow.collectAsState("data")
In that case, if you’re calling getDataFlow()
every time then it will be called on every recomposition. However, this is a weird way to write the view model imo. I’d do something like this instead:
class ViewModel {
// Or use `by lazy {}` or something if `myRoomDb` isn't initialized yet.
val dataFlow: Flow<String> = myRoomDb.getData().distinctUntilChanged().map { … }
}
Then calling viewModel.dataFlow
directly inside the composition would be fine.Bradleycorn
12/02/2020, 6:52 PMcollectAsState
in this manner, particular when operators are applied to the flow. Let me see if I can find that thread …Zach Klippenstein (he/him) [MOD]
12/02/2020, 6:54 PMFlow
instance will be different for every composition, so it will restart the collection.Zach Klippenstein (he/him) [MOD]
12/02/2020, 6:54 PMgetDataFlow
, instead of just creating it once in your ViewModel?Bradleycorn
12/02/2020, 6:56 PMAdam Powell [G] [12:24 PM]
one thing to be careful about when combining flows with compose: creating operator chains inside of afunction can get a bit tricky. If you’re assembling them outside of composition these considerations don’t come into play@Composable
Adam Powell [G] [12:25 PM]
but, for example, this code is wrong:
@Composble
fun MyComposable(flow: Flow<Foo>) {
val current by flow.map { mapper(it) }.collectAsState()
Adam Powell [G] [12:26 PM]
what makes it wrong is that it will create a newoperator chain every time it recomposes, which means the input.map {}
toFlow
is different every time, so it has to unsubscribe from the old and resubscribe to the new on every recomposition.collectAsState
Adam Powell [G] [12:26 PM]
the correct (if verbose) way to do the above is:
val current by remember(flow) { flow.map { mapper(it) } }.collectAsState()
Bradleycorn
12/02/2020, 7:00 PMval
in the viewmodel, but how does that change anything in regard to the above issues with creating a new operator chain on every recomposition?Bradleycorn
12/02/2020, 7:02 PMfun getProduct(productId: Int): Flow<Product> {
myDb.getProduct(productId).distinctUntilChanged().map { doSomeProductMapping(it) }
}
Zach Klippenstein (he/him) [MOD]
12/02/2020, 7:05 PMZach Klippenstein (he/him) [MOD]
12/02/2020, 7:06 PMval productFlow = remember(viewModel, productId) {
viewModel.getProduct(productId)
}
productFlow.collectAsState(…)
If that code gets passed a different ViewModel or a different product ID, then remember
will return a different Flow
instance, and collectAsState
will cancel the previous one and start collecting the new one.Bradleycorn
12/02/2020, 7:22 PMval uiState by produceState<UiState<List<Person>>>(UiState.Loading, viewModel) {
viewModel.people
.map { UiState.Data(it) }
.collect { value = it }
}