Migrating a MediatorLiveData to SharedFlow - android

I have a MediatorLiveData that uses three LiveData sources. When any of them emits a new value and I have at least one of each, I use the three values to produce the output for the UI.
Two of the sources are user settings for how to sort and filter a list, and the third is the list data, pulled from a Room database Flow.
It looks something like this:
val thingsLiveData: LiveData<List<Thing>> = object: MediatorLiveData<List<Thing>>() {
var isSettingA: Boolean = true
var settingB: MySortingEnum = MySortingEnum.Alphabetical
var data: List<Thing>? = null
init {
addSource(myRepo.thingsFlow.asLiveData()) {
data = it
dataToValue()
}
addSource(settingALiveData) {
isSettingA= it
dataToValue()
}
addSource(settingBLiveData) {
settingB= it
dataToValue()
}
}
private fun dataToValue() {
data?.let { data ->
viewModelScope.launch {
val uiList = withContext(Dispatchers.Default) {
produceUiList(data, isSettingA, settingB)
}
value = listItems
}
}
}
}
I'm looking for a clean way to convert this to a SharedFlow, preferably without any #ExperimentalCoroutinesApi. The only SharedFlow builder function I've come across is callbackFlow, which isn't applicable. Are you intended to use flow { ... }.asSharedFlow(...) in most cases, and if so, what would that look like here?
The two settings LiveData I also plan to migrate to flows.

The source Flows can be combined using combine(), which creates a cold Flow that, when collected, will start collecting from its source Flows, which may be hot or cold.
I was originally thinking that I must be missing something and that there should be some way to directly combine hot Flows into a combined hot Flow. But I realized it makes sense that the operators should only return cold Flows and leave it up to you to convert it back to a hot Flow if that's what you need.
In many cases, such as mine, it's perfectly fine to leave it cold. I only collect this Flow from one place in my UI, so it doesn't matter that it only starts combining the sources when it's collected. The source hot Flows don't care whether something is currently collecting them or not...they just keep emitting regardless.
If I collected this Flow from multiple places or multiple times, then it might make sense to use shareIn on the combined Flow to make it hot, which would avoid redundant work of combining the sources. The potential downside would be that it would combine those sources even when nothing is collecting, which would be wasted work.
val thingsFlow: Flow<List<Thing>> = combine(
myRepo.thingsFlow,
settingALiveData.asFlow(),
settingBLiveData.asFlow()
) { data, isSettingA, settingB -> produceUiList(data, isSettingA, settingB) }
// where produceUiList is now a suspend function that wraps
// blocking code using withContext

Related

Flow emits different values when collecting it multiple times

I created a Flow from which I emit data. When I collect this flow twice, there are 2 different sets of data emitted from the same variable instead of emitting the same values to both collectors.
I have a simple Flow that I created myself. The text will be logged twice a second
val demoFlow: Flow<String> = flow {
while (true) {
val text = "Flow ${(0..100).random()}"
Log.d("TAG", text)
emit(text)
delay(1000)
}
}
In my viewModel I have a simple function that gets the previous Flow
fun getSimpleFlow() = FlowRepository.demoFlow
And in my Fragment I collect and display my Flow
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.getSimpleFlow().collect {
binding.tv1.text = it
}
}
launch {
viewModel.getSimpleFlow().collect {
binding.tv2.text = it
}
}
}
}
If I transform the Flow to a StateFlow or a SharedFlow, I no longer have this problem.
I don't understand how or why this happens since I'm using the same 'demoFlow' variable.
Is there a way to get the same values from 'demoFlow' without converting to a StateFlow or a SharedFlow?
Regular Flows are cold, this behaviour is by design.
The demoFlow is the same, so you have the same Flow instance. However, collecting the flow multiple times actually runs the body inside the flow { ... } definition every time from the start. Each independent collection has its own variable i etc.
Using a StateFlow or a SharedFlow allows to share the source of the flow between multiple collectors. If you use shareIn or stateIn on some source flow, that source flow is only collected once, and the items collected from this source flow are shared and sent to every collector of the resulting state/shared flow. This is why it behaves differently.
In short, reusing a Flow instance is not sufficient to share the collection. You need to use flow types that are specifically designed for this.

Kotlin Flow Offline Caching

I am new with kotlin flow and I am working about this document. Kotlin Flows. In this code every five seconds datasource fetch data from api and emits it.
This is my example datasource class.
I am getting data and emitting it.
class RemoteDataSourceImpl #Inject constructor(
private val api:CryptoApi
): RemoteDataSource {
override suspend fun cryptoList(): Flow<List<CryptoCoinDto>> {
return flow {
while (true){
val data = api.getCoinList()
emit(data)
delay(5000L)
}
}
}
}
This is my example repository.
I am mapping data and saving it room database. I want to get data from room database and emit it because of single source of truth principle but I still have to return dataSource because if I open new flow{} I can't reach datasource's data. Of course I can fix the problem by using List instead of Flow<List> inside of RemoteDataSource class. But I want to understand this example. How can I apply here single source of truth.
class CoinRepositoryImpl #Inject constructor(
private val dataSource:RemoteDataSource,
private val dao: CryptoDao
):CoinRepository {
override fun getDataList(): Flow<List<CryptoCoin>> {
dataSource.cryptoList().map { dtoList ->
val entityList = dtoList.map { dto ->
dto.toCryptoEntity()
}
dao.insertAll(entityList)
}
return dataSource.cryptoList().map {
it.map { it.toCryptoCoin() }
}
}
This is actually more complicated than it seems. Flows were designed to support back-pressure which means that they usually only produce items on demand, when being consumed. They are passive, instead of pushing items, items are pulled from the flow.
(Disclaimer: this is all true for cold flows, not for hot flows. But cryptoList() is a cold flow.)
It was designed this way to greatly simplify cases when the consumer is slower than producer or nobody is consuming items at all. Then producer just stops producing and everything is fine.
In your case there are two consumers, so this is again more complicated. You need to decide what should happen if one consumer is slower than the other. For example, what should happen if nobody collects data from getDataList()? There are multiple options, each requires a little different approach:
Stop consuming the source flow and therefore stop updating the database.
Update the database all the time and queue items if nobody is collecting from getDataList(). What if there are more and more items in the queue?
Update the database all the time and discard items if nobody is collecting from getDataList().
Ad.1.
It can be done by using onEach():
return dataSource.cryptoList().onEach {
// update db
}.map {
it.map { it.toCryptoCoin() }
}
In this solution updating the database is a "side effect" of consuming the getDataList() flow.
Ad.2. and Ad.3.
In this case we can't passively wait until someone asks us for an item. We need to actively consume items from the source flow and push them to the downstream flow. So we need a hot flow: SharedFlow. Also, because we remain the active side in this case, we have to launch a coroutine that will do this in the background. So we need a CoroutineScope.
Solution depends on your specific needs: do you need a queue or not, what should happen if queue exceeded the size limit, etc., but it will be similar to:
return dataSource.cryptoList().onEach {
// update db
}.map {
it.map { it.toCryptoCoin() }
}.shareIn(scope, SharingStarted.Eagerly)
You can also read about buffer() and MutableSharedFlow - they could be useful to you.

Collecting from Flow in UI with repeatOnLifeCycle

I started to replace LiveData with Flow since it is more flexible. But then I find out you need to write enormous amount of boilerplate code to observe from Flow in UI.
In the StateFlow documentation, it says that
LiveData.observe() automatically unregisters the consumer when the view goes to the STOPPED state, whereas collecting from a StateFlow or any other flow does not stop collecting automatically. To achieve the same behavior,you need to collect the flow from a Lifecycle.repeatOnLifecycle block.
It's also mentioned in the article by Manuel Vivo that using collecting from lifecycleScope.launchWhenX is dangerous and should not be used in UI because the producer flow will not stop emitting.
He recommended us to use
// Listen to multiple flows
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// As collect is a suspend function, if you want to collect
// multiple flows in parallel, you need to do so in
// different coroutines
launch {
flow1.collect { /* Do something */ }
}
launch {
flow2.collect { /* Do something */ }
}
}
}
The amount of boilerplate code is too much. Is it not possible to do it in a two liner like what LiveData does?
viewModel.movieData.observe(viewLifecycleOwner) {
...
}
Why is it so complex to collect from Flow in UI? Is it advisable to convert the Flow to LiveData with asLiveData()?
You could build extensions to reduce the boilerplate
inline fun <T> Flow<T>.collectIn(
owner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline action: suspend CoroutineScope.(T) -> Unit
) = owner.addRepeatingJob(minActiveState, coroutineContext) {
collect {
action(it)
}
}
This makes collecting flows similar to LiveData as
flow.collectIn(viewLifecycleOwner){ /* do stuff */ }
First answer your first question: Flow is a cold flow. And Flow is stateless. If you provide Flow, then it means that you need to construct and collect Flow frequently.
In another case, if Hot Flow is provided, such as (StateFlow), although the hot flow provides state (.value), it does not know anything about the life cycle of Android. As you said, you can use launchWhenXXX() to collect Flow.
When using launchWhenXXX(), you must pay attention to the life cycle of the hot flow. When to start collect and when to end collect, these need to be paid attention to. So it seems very troublesome. Of course, Flow is a way to get rid of using LiveData.
For details, please refer to: https://proandroiddev.com/should-we-choose-kotlins-stateflow-or-sharedflow-to-substitute-for-android-s-livedata-2d69f2bd6fa5
The second question: LiveData manages the life cycle of Android. Flow.asLiveData() is completely desirable. At this time, only a simple Observe is needed.

Map multiple suspend functions to single LiveData

The company I just started working at uses a so called Navigator, which I for now interpreted as a stateless ViewModel. My Navigator receives some usecases, with each contains 1 suspend function. The result of any of those usecases could end up in a single LiveData. The Navigator has no coroutine scope, so I pass the responsibility of scoping suspending to the Fragment using fetchValue().
Most current code in project has LiveData in the data layer, which I tried not to. Because of that, their livedata is linked from view to dao.
My simplified classes:
class MyFeatureNavigator(
getUrl1: getUrl1UseCase,
getUrl1: getUrl1UseCase
) {
val url = MediatorLiveData<String>()
fun goToUrl1() {
url.fetchValue { getUrl1() }
}
fun goToUrl2() {
url.fetchValue { getUrl2() }
}
fun <T> MediatorLiveData<T>.fetchValue(provideValue: suspend () -> T) {
val liveData = liveData { emit(provideValue()) }
addSource(liveData) {
removeSource(liveData)
value = it
}
}
}
class MyFeatureFragment : Fragment {
val viewModel: MyFeatureViewModel by viewModel()
val navigator: MyFeatureNavigator by inject()
fun onViewCreated() {
button.setOnClickListener { navigator.goToUrl1() }
navigator.url.observe(viewLifecycleOwner, Observer { url ->
openUrl(url)
})
}
}
My two questions:
Is fetchValue() a good way to link a suspend function to LiveData? Could it leak? Any other concerns?
My main reason to only use coroutines (and flow) in the data layer, is 'because Google said so'. What's a better reason for this? And: what's the best trade off in being consistent with the project and current good coding practices?
Is fetchValue() a good way to link a suspend function to LiveData?
Could it leak? Any other concerns?
Generally it should work. You probably should remove the previous source of the MediatorLiveData before adding new one, otherwise if you get two calls to fetchValue in a row, the first url can be slower to fetch, so it will come later and win.
I don't see any other correctness concerns, but this code is pretty complicated, creates a couple of intermediate objects and generally difficult to read.
My main reason to only use coroutines (and flow) in the data layer,
is 'because Google said so'. What's a better reason for this?
Google has provided a lot of useful extensions to use coroutines in the UI layer, e.g. take a look at this page. So obviously they encourage people to use it.
Probably you mean the recommendation to use LiveData instead of the Flow in the UI layer. That's not a strict rule and it has one reason: LiveData is a value holder, it keeps its value and provides it immediately to new subscribers without doing any work. That's particularly useful in the UI/ViewModel layer - when a configuration change happens and activity/fragment is recreated, the newly created activity/fragment uses the same view model, subscribes to the same LiveData and receives the value at no cost.
At the same time Flow is 'cold' and if you expose a flow from your view model, each reconfiguration will trigger a new flow collection and the flow will be to execute from scratch.
So e.g. if you fetch data from db or network, LiveData will just provide the last value to new subscriber and Flow will execute the costly db/network operation again.
So as I said there is no strict rule, it depends on the particular use-case. Also I find it very useful to use Flow in view models - it provides a lot of operators and makes the code clean and concise. But than I convert it to a LiveData with help of extensions like asLiveData() and expose this LiveData to the UI. This way I get best from both words - LiveData catches value between reconfigurations and Flow makes the code of view models nice and clean.
Also you can use latest StateFlow and SharedFlow often they also can help to overcome the mentioned Flow issue in the UI layer.
Back to your code, I would implement it like this:
class MyFeatureNavigator(
getUrl1: getUrl1UseCase,
getUrl1: getUrl1UseCase
) {
private val currentUseCase = MutableStateFlow<UseCase?>(null)
val url = currentUseCase.filterNotNull().mapLatest { source -> source.getData()}.asLiveData()
fun goToUrl1() {
currentUseCase.value = getUrl1
}
fun goToUrl2() {
currentUseCase.value = getUrl2
}
}
This way there are no race conditions to care about and code is clean.
And: what's the best trade off in being consistent with the project
and current good coding practices?
That's an arguable question and it should be primarily team decision. In most projects I participated we adopted this rule: when fixing bugs, doing maintenance of existing code, one should follow the same style. When doing big refactoring/implementing new features one should use latest practices adopted by the team.

Hot flow as final observable?

I recently discovered Flows in Kotlin and ever since been trying to optimize the code and making it more readable by avoiding MediatorLiveData and friends.
I try to use Flow in my back ends and finally present it as LiveData to the UI. However, I am having an issue with LiveData unsubscribing after a timeout, which causes the cold flows to recalculate (and make a new network request) each time the timeout is exceeded. Is there any way around it?
Here's the problem:
private val storeProducts = combine(_currentBasket, _currentProduct) { basket, product ->
// network operation
}
val storeProductsLive = storeProducts.asLiveData()
From the above example, my UI observes it as a normal livedata observable. When I rotate my screen, everything works fine and the last value is returned. However, if I navigate to a subfragment and go back after 5s or if I go out of the app and return later, the network operation is done again. the .asLiveData() specifies a default timeout of 5s, after that the Flow is cancelled.
My current workaround:
private val productsState = MutableStateFlow<List<Product>>(emptyList())
init {
viewModelScope.launch {
storeProducts.collect {
productsState.value = it
}
}
}
However, this is quite hacky and it means the observer is always registered, regardless whether the UI actually cares about it. Any way around it?
UPDATE
Within an hour, I found another workaround (described here: https://github.com/Kotlin/kotlinx.coroutines/issues/2140 ):
val storeProductsLive: LiveData<List<Product>> by lazy {
MutableStateFlow<List<Product>>(emptyList()).apply {
viewModelScope.launch {
storeProducts.collect { value = it }
}
}.asLiveData()
}
Is this the recommended way?

Categories

Resources