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?
Related
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.
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.
I am trying to figure out how jobs with coroutines work. Basically, I want to launch this coroutine from FirstFragment and after that navigate to SecondFragment and get notified when this job is done. I call getData() in FirstFragment onViewCreated() and navigate to SecondFragment. Whether I write getData().isCompleted or getData().invokeOnCompletion { } in SecondFragment nothing happens. I don't know if I am missing something or not starting job correctly or something else.
private val _data = MutableStateFlow<GetResource<String>?>(null)
val data: StateFlow<GetResource<String>?> = _data
fun getData() = viewModelScope.launch {
repository.getData().collect {
_data.value = it
}
}
A Flow from a database never completes because it is supposed to monitor the database for changes indefinitely. It only stops when the coroutine is cancelled. Therefore the Job that collects such a Flow will never complete. Also, if you call getData() on the repo again, you are getting a new Flow instance each time.
Regardless of what you're doing, you need to be sure you are using the same ViewModel instance between both fragments by scoping it to the Activity. (Use by activityViewModels() for example.) This is so the viewModelScope won't be cancelled during the transition between Fragments.
If all you need is a single item from the repo one time, probably the simplest thing to do would be to expose a suspend function from the repo instead of a Flow. Then turn it into a Deferred. Maybe by making it a Lazy, you can selectively decide when to start retrieving the value. Omit the lazy if you just want to start retrieving the value immediately when the first Fragment starts.
// In the shared view model:
val data: Deferred<GetResource<String>> by lazy {
viewModelScope.async {
repository.getData() // suspend function returning GetResource<String>
}
}
fun startDataRetrieval() { data } // access the lazy property to start its coroutine
// In second fragment:
lifecycleScope.launch {
val value = mySharedViewModel.data.await()
// do something with value
}
But if you have to have the Flow because you’re using it for other purposes:
If you just want the first available value from the Flow, have the second Fragment monitor your data StateFlow for its first valid value.
lifecycleScope.launch {
val value = mySharedViewModel.data.filterNotNull().first()
// do something with first arrived value
}
And you can use SharedFlow so you don’t have to make the data type nullable. If you do this you can omit filterNotNull() above. In your ViewModel, it’s easier to do this with shareIn than your code that has to use a backing property and manually collect the source.
val data: SharedFlow<GetResource<String>> = repository.getData()
.shareIn(viewModelScope, replay = 1, SharingStarted.Eagerly)
If you need to wait before starting the collection to the SharedFlow, then you could make the property lazy.
Agreed with #Tenfour04 's answer, I would like to contribute a little more.
If you really want to control over the jobs or Structured Concurrency, i would suggest use custom way of handling the coroutine rather than coupled your code with the viewModelScope.
There are couple of things you need to make sure:
1- What happen when cancellation or exception occurrs
2- you have to manage the lifecycle of the coroutine(CoroutineScope)
3- Cancelling scope, depends on usecase like problem facing you are right now
4- Scope of ViewModel e.g: Either it is tied to activity(Shared ViewModel) or for specific fragment.
If you are not handling either of these carefully specifically first 3, your are more likely to leaking the coroutine your are gurenteed gonna get misbehavior of you app.
Whenever you start any coroutine in Custom way you have to make sure, what is going to be the lifecycle, when it gonna end, This is so important, it can cause real problems
Luckily, i have this sample of CustomViewModel using Jobs: Structured Concurrency sample code
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
I have several ViewModels that create observers on LiveData objects created via Room DAOs. These ViewModels merge various LiveData emissions into final ViewState LiveDatas, which are observed by their respective Fragments. These generally work without an issue, but there's a case in which almost all LiveData connected to Room suddenly break.
If I open my app, rapidly tap "Back" out of the application, and then rapidly resume the application, there's a 5-10% chance that most LiveData chains that interact with Room in any way become permanently stalled, as if the LiveData chain is not being observed. This happens everywhere on the app, even on fragments I haven't visited in the current session before the pathological behavior is exhibited.
After diving into the code, once the error state is reached, it seems like the Callable being handed off to Room's invalidation tracker is never being called. As an experiment, I created an observeForever on the profileStream to see if I could engage a different codepath. This resulted in the Callable being executed, but it returns stale data. ObserveForever being an unsuitable solution anyway, I didn't want to further debug this option.
Suspicious of lifecycle-related issues, I swapped out "this" on my observe calls for the Fragments' viewLifecycleOwners. This didn't seem to change anything.
viewModel.viewState.observe(viewLifecycleOwner, Observer { render(it) })
VIEWMODEL
init {
val profileStream = profileRepo.getProfile(id)
viewState.addSource(mapPhotoStream()) { photos -> viewState.value =
currentViewState().copy(photos = photos) }
viewState.addSource(profileStream) { profile ->
if (profile.name == null) {
Log.w(TAG, "Name of a profile should not be null.
UserId=${profile.userId}")
}
viewState.value = currentViewState().copy(
name = profile.name ?: ""
)
}
}
FRAGMENT
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupBottomSheetBehavior()
viewModel.viewState.observe(viewLifecycleOwner, Observer { render(it) })
}