Reputation: 296
I'm trying to figure it out about flow / stateflow and databinding.
In my project, I have a list of "printer" from Room, and the id of the "default printer" in my SharedPreferences.
Am I using the good kind of flow and doing well or something is wrong ?
Room give me a Flow<List<Printer>>
. I display it in a recycler view thanks to viewModel.allPrinters.collect
to my adapter.
But when i want to bind a resource from room, the binding "need" a LiveData or a StateFlow and this is my difficulty because the stateIn need a coroutine.
How can i get the default printer from my Flow<List<Printer>>
(find it in the list by his attribute "id") and make it sure if the list/item is updated, my reference will get the update too ?
If i change the name of the defaultPrinter
and update it to room (thanks to my repository / room DAO), I want the name changed in my object defaultPrinter
and in my list.
Also can you confirm the variable type of my defaultPrinter
should be StateFlow<Printer?>
Note : When i update item from printerRoomDAO.update(item:Printer)
I have to pass a copy of my data object Printer
(new ref), else my UI will not be updated.
Here the exemple of my isPrinterListEmpty
in my ViewModel
val hasNoPrinter = allPrinters.mapLatest {
it.isEmpty()
}.asLiveData()
It's working well, but shouldn't I use a StateFlow ? How to do it ? Because stateIn()
require a coroutine and my try below doesnt work.
val hasNoPrinter = runBlocking {
allPrinters.mapLatest {
it.isEmpty()
}.stateIn(viewModelScope)
}
I tried to not confuse you, thanks for your help. Best regards.
Upvotes: 1
Views: 3476
Reputation: 19524
It might help to go over a bit of terminology first
A Flow
is a thing that produces a stream of values - kinda like a Sequence
, but async. You get the values over time. Each time you call collect()
(or some other terminal operator) on a Flow
, it starts over. There's no shared state between those consumers, the flows run independently.
So what if you do want some shared state? Instead of having separate instances of a Flow
, what if you want a single stream that multiple consumers can observe? You have two main options - a SharedFlow
and a StateFlow
.
A SharedFlow
is what it sounds like - a single stream of data that multiple consumers (subscribers) can observe. When an update happens, every subscriber sees that new value.
When you configure one (e.g. with shareIn
) you can set a replay
value - this is basically the number of old values that are stored, which new subscribers will receive when they start collecting from the SharedFlow
. If it's set to 0, they won't see a value until the next new one arrives. If it's set to 1, they'll see the most recent item (this is similar to how a LiveData
behaves). If it's set to a higher number, the subscriber will get that number of past events (if available).
A StateFlow
is similar - it's basically a SharedFlow
with a replay
value of 1, so it always provides the most recent value to subscribers. It also requires an initial value, because it's meant to represent a state - one of many possible values, rather than remembering the last item (if any) that was produced upstream. (So this behaves like a LiveData
with a default value set.)
Which one you use depends on what you want - are you exposing a state, where there has to be some value representing it? Or just a stream of values, which may or may not have started yet? Do you have a default value? If you're thinking null, maybe you don't need one!
Normal Flow
s are cold - they don't do anything until a consumer calls a terminal operator (like collect
) on them, or on a Flow
downstream from them (so the "start producing" call propagates up). So they only run while something is observing them, and as soon as that observation is cancelled, the Flow
stops producing.
SharedFlow
and StateFlow
are both hot though - they exist whether a subscriber is observing them, and they're always observing their own upstream flows, so they can collect those recent values to store for any subscribers that want them. What this means is, if you create a SharedFlow
or StateFlow
, they keep the producers alive for as long as they're around.
So you need a way to control how long they run for! And that's what the CoroutineScope
is for, in the shareIn
/stateIn
builders. It's not a coroutine, it's a CoroutineScope
- it's an organisational thing that you start coroutines inside, which allows you to control things like their lifetime. When you're done with the scope, you can cancel it, and it's like saying "ok, everything that's doing work in here - pack it up, we're done".
You've probably already used a few of these - viewLifecycleOwner.lifecycleScope
in a Fragment
is a scope that lives for as long as the current view hierarchy exists. Once that's torn down, the scope is automatically cancelled for you by the Lifecycle
system. This lets you run coroutines that deal with those views specifically without outliving them - when a new view hierarchy is created, you start a new set of coroutines to work with those.
viewModelScope
lasts for as long as the ViewModel
exists, which is longer than the views in a Fragment
. This allows you to create longer-running coroutines in the VM, that persist across things like Fragment
recreations when you rotate the device, or navigating around the app. So that's good for long-running work and things like maintaining state (e.g. in a StateFlow
) that was expensive to produce, like via a network call.
So you need to provide one of these CoroutineScope
s to these builders, so there's some sense of a lifetime for those hot flows. There's an article here from one of the Android devs about how you can create your own scopes - it's literally just a case of creating one, and keeping a reference to it so you can cancel it when all the work in there needs to stop.
The recommended approach is to create an application-wide scope which will be torn down when the app's process is destroyed - that way, anything you run in that scope will survive for the lifetime of the app (unless its work ends sooner). So if you have things you want to maintain, and maybe run constantly, that's how you can do it! If it's something that should be scoped to say the lifetime of a ViewModel
(i.e. only runs when one has been created to be used) then you'd use that scope instead.
So as far as your printers go, if you have your allPrinters
Flow
that was the result of a Room query:
// cold Flow
val defaultPrinter = allPrinters.map { printers ->
printers.firstOrNull { it.id == user.id }
}
// hot flow
val defaultPrinter = allPrinters.map { printers ->
printers.firstOrNull { it.id == user.id }
}.stateIn(someScope, SharingStarted.Lazily, initialValue = null)
So what this does is maps every List<Printer>
that comes through to a single Printer?
, and stores that most recent value in a StateFlow
. I'm using a state because this is a thing that either exists or doesn't, so it's either a state of "this printer" or "no printer", and I'm using null for the default - "no printer".
Have a look at the documentation for the SharingStarted
stuff, but Lazily
basically means it doesn't do any printer checking until the first subscriber appears. You could use Eagerly
if you want it to start fetching values immediately, so it's "warmed up" for when the first subscriber appears. The scope
is what we talked about before - you can inject one into the repo, or if this defaultPrinter
StateFlow
exists in a ViewModel
, you could just use viewModelScope
since its existence (and lifetime) is tied to the VM.
That was long, hope it helps though. And if anyone has any pointers or corrections to anything in here, let me know - I'm still wrangling some of this myself (and explaining it helps me understand things better too)
Upvotes: 11