Are there “gotcha’s” around using `Flow` with Comp...
# compose
b
Are there “gotcha’s” around using
Flow
with Composables and
produceState
? For example, consider:
Copy code
@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:
Copy code
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?
z
I think this code is fine.
produceState
will only restart its coroutine if the keys you pass in change between compositions (i.e.
viewModel
).
although you don’t need to use
produceState
for this, there’s already a function for doing what you’re doing:
Copy code
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:
Copy code
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.
b
Yep, but I seem to remember that there are (or were) some issues with
collectAsState
in this manner, particular when operators are applied to the flow. Let me see if I can find that thread …
z
Ah yes, if you call operators then the leaf
Flow
instance will be different for every composition, so it will restart the collection.
Why do you need to construct the flow chain in
getDataFlow
, instead of just creating it once in your ViewModel?
b
yep. Here we go …
Adam Powell [G]  [12:24 PM]
one thing to be careful about when combining flows with compose: creating operator chains inside of a 
@Composable
 function can get a bit tricky. If you’re assembling them outside of composition these considerations don’t come into play
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 new 
.map {}
 operator chain every time it recomposes, which means the input 
Flow
 to 
.collectAsState
 is different every time, so it has to unsubscribe from the old and resubscribe to the new on every recomposition
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()
I could create the flow chain once with a
val
in the viewmodel, but how does that change anything in regard to the above issues with creating a new operator chain on every recomposition?
And, there are times when it really does need to be a method instead of a property … for example:
Copy code
fun getProduct(productId: Int): Flow<Product> {
    myDb.getProduct(productId).distinctUntilChanged().map { doSomeProductMapping(it) }
}
z
If you put it in a val (as an actual field, not a getter), then you’re only creating the operator chain once and storing the returned Flow. Then in each composition, you’re using the same Flow instance, so the compose runtime won’t restart the collection.
If you actually do need a method, you can just use `remember`:
Copy code
val 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.
b
ha! I suppose if I had looked at the docs for produceState instead of the source code, I’d have found there’s an example that’s pretty much exactly what I’m trying to accomplish: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#producestate
Copy code
val uiState by produceState<UiState<List<Person>>>(UiState.Loading, viewModel) {
    viewModel.people
        .map { UiState.Data(it) }
        .collect { value = it }
}