Question regarding `Flow` collection, `RecyclerVie...
# android
m
Question regarding
Flow
collection, `RecyclerView`s and MVVM. For a simple list to be displayed in a
RecyclerView
, the
ViewModel
exposes a
Flow
of
List<Item>
which is collected in the
Fragment
which in turn sets the items on a `RecyclerView`’s adapter (and calls
notifyDataSetChanged()
). I believe this is standard procedure. However, suppose I want to ‘decorate’ certain items in
List<Item>
(think of making a specific DB call for an item ID to obtain a
Flow
specific to that item). Where to collect such Flows? 1.
ViewModel
- hide this complexity from the fragment/adapter, so that
List<DecoratedItem>
contains an immutable snapshot of the data with the ‘decoration’ already applied 2.
Fragment
-
Item
could expose a ‘decoration’
Flow
which is collected in the
Fragment
. With this level of granularity, the fragment would be better able to call the relevant
notifyItemChanged()
function 3.
RecyclerView.Adapter
- when binding the item we can start collecting the ‘decoration’ flow for that item. This fits well, because there is no point collecting that when the item is not bound. My notes: (1) Seems like the more correct, since it keeps non-UI logic out of the fragment/adapter, but then we have a potentially long list with some update caused by an item that’s long been scrolled past, and applying
DiffUtil
etc (2) I’m not particularly convinced by this approach, but I’ve included it because it’s where collection already occurs (3) Seems neat because we are only collecting items that are currently bound Maybe it depends on how active these ‘decoration’ flows are? Low activity would mean the concerns of (1) are not such a big deal. Any thoughts, please?
g
I would say 1 + diff utils So you can just do
Copy code
flow.map { it: List<T> ->
   it.map(::MyItemViewModel) 
}
.collect { it: List<MyItemViewModel> ->
DiffUtil.calculateDiff(MyDiffCallback(it))
}
m
Yes, that would certainly work (and is nice and simple), but what if the decoration flows are expensive to calculate?
g
But what is problem with it? You can use flowOn, or just extract it to a suspend function so, MyItemViewModel will be something like:
Copy code
suspend fun MyItemViewModel(item: T) = withContext(Default) { SomeHeavyComputation }
or you mean how to avoid recalculating it?
for this, I think the only simple solution is keep own cache of those MyItemViewModel instances, it can be as simple as:
Copy code
val vmCache = mutableMapOf<T, MyItemViewModel>()

.map { it: List<T> ->
   it.map(::MyItemViewModel) 
}
vmCache.getOrPut { MyItemViewModel(item }
m
I mean each decoration is minimum one DB call (and each one might be expensive). So if you are collecting 100 decoration flows, then you have minimum 100 DB calls even though you might only be showing first 10 items.
g
Why is decoration is 100 db calls? If you want optimize db calls do this in batch, request data for all items, if you want to optimize it
You of course can be completely lazy, but managing lazy loading for every item possible, but much-much more complex thing with a lot of pitfalls and not always obvious performance gain
m
one item decoration is minimum one db call
g
I still don’t understand why, you have all items, if you have all items why request data for 1 on of them, you can request data for all of them
itemId IN (item1, item2, item3, …)
anyway, you still can do 100% lazy loading of every item, in this case you need a wrapper which manages different loading state (because item may appear without data), and you need a cache of VM with state which request data, you don’t want to cancel item loading request if it detached from recycler view
m
In that case, yes, but in my real case the List of items is not constructed from a DB call, but rather from a stream of items. Think
Flow<Item>
->
Flow<List<Item>>
(A, B, C) -> ([], [A], [A, B], [A, B, C]) so items are only gradually discovered, which means you would have to do that potentially expensive
IN
query each time the
Flow<List<Item>>
emits.
g
lazy loading of individual items is quite tricky indeed
there are a different ways to do that, or as I wrote in my previous comment with stateful view model for every item + own cache for them, or maybe just combine a bunch of flows to a single flow + diff utils, though UI update may look strange in this case
m
Yep, I’ll have more of a play. Thanks Andrey, much appreciated 🙏
Regarding option (3), each view holder could have its own flow (of flows) and so use
flatMapLatest
for the decoration flow. When binding an item to the viewholder, we just set the view holder’s flow to use that item’s decoration flow. We could use MutableStateFlow (set up in ViewModel) to cache the latest decoration of each decoration flow.
We therefore need to launch a new coroutine for each view holder. But that could be in view lifecycle scope (which I think is more appropriate than viewmodel scope). Decoration DB calls are only done when needed (when item is first bound) but not repeated (because of caching in MutableStateFlow) when an item is later rebound
g
We therefore need to launch a new coroutine for each view holder
if it a part of viewmodel which represents this item, I think it’s fine
m
BTW, is there a standard way to cache a flow’s latest value such that a new collector will first get that cached value (if it exists) thus not causing a new DB call. I know I can hack something together with MutableStateFlow and flatMapLatest but is there a nicer way?
g
.stateIn()? But it requires default value (which can be null)
m
Thanks, though it seems to be doing more than I need. I can’t see why I should provide a scope, since if there is no existing value, it should just behave as the original flow.
g
I can’t see why I should provide a scope
Because flows are cold, you can have only one subscriber, this operators creates StateFlow, which starts collecting original flow on provided state and use StateFlow to store last emmited valud, without scope it would violate strucutred concurrency
it’s general rule, any hot flow requires scope, because to be hot it need some parent context to run on
m
But why can there not be a cold cached flow?
g
but who will cache it? Cached where? Cold Flow is not started until someone is start collecting it, when it will be collected second time it will be new flow, they do not share cache
StateFlow itself is hot, but doesn’t require scope, but when you collect other flow you need some scope to collect it
m
I’m thinking of an outer flow and an inner flow. The inner flow (for example, returned from a Room DB) is only collected once (by the outer flow). All other collections are done on the outer flow. The first collection on the outer flow triggers the collection of the inner flow and the outer flow will cache the latest values.
g
outer flow will cache the latest values
Exactly, it will be cached, but to get this cache, it should be saved somewhere, so next subscriber could collect it too, remember, that inner flow may never completem, so you will have situation when outer flow keeps inner flow open forever, and there is no way to cancel it, but if you pass scope, you can cancel scope
m
That makes sense, thanks!
201 Views