Search code examples
androidkotlinandroid-livedata

How to collect multiple liveDatas as one single Flow?


So I have a specific problem.

We have started refactor the legacy code, and in this point, we need to adopt to a legacy class.

So the legacy class does some network calls, and based on the response it sets up some liveData objects.

In the feature it will be refactored, and instead of liveData it will do everything inside a suspend fun and returns with the respective data, but right now we are not able to use this approach.

So I was thinking about some kind of hybrid solution.

I would like to collect all of these liveDatas in one flow which will be a UseCase. Theoretically just one emit will happen, because just one of the liveDatas will get value.

In the viewModel I subscribe to it and do everything. After the first collection I would like to destroy this collection job. Maybe I can use the takeWhile() operator in the flow.

Is it good? Is it will cause memory leaks? The takeWhile will

This is how it looks like now:

class UseCase(){
    operator fun invoke(a: LiveData<String>, b: LiveData<String>, c: LiveData<Int>) =
            flow<Result<String>> {
                a.asFlow().collect{
                    emit(Resource.Error)
                }

                b.asFlow().collect{
                    emit(Resource.Error)
                }

                c.asFlow().collect{
                    emit(Resource.Success)
                }
            }
}

class TestViewModel(): ViewModel(){

    fun getData(){
        var shouldCollect = true

        viewModelScope.launch {
            UseCase().invoke(MutableLiveData(""),MutableLiveData(""),MutableLiveData(1)).takeWhile { shouldCollect }.collect{
                shouldCollect = false
            }
        }
    }
}

I know it isn't a good approach, but it will replaced as soon as the dependency class will be refactored.

So now I tried it out, and it doesn't work. Only just the first liveData in to list updates the flow, so if the second liveData gets value, it doesn't emit it for some reason. I try to merge/zip/combine them


Solution

  • I'm not entirely sure what you want to do with it, but you can merge multiple flows of the same type into a single flow:

    operator fun invoke(
        a: LiveData<String>,
        b: LiveData<String>,
        c: LiveData<Int>,
    ): Flow<Result<String>> = merge(
        a.asFlow().map { Resource.Error },
        b.asFlow().map { Resource.Error },
        c.asFlow().map { Resource.Success },
    )
    

    This will emit at least 3 values, with no guarantee to the order in which the elements are emitted.

    If you want to combine the values of the flows into a single value instead, it can be done like this:

    operator fun invoke(
        a: LiveData<String>,
        b: LiveData<String>,
        c: LiveData<Int>,
    ): Flow<Result<String>> = combine(
        a.asFlow(),
        b.asFlow(),
        c.asFlow(),
    ) { valueA, valueB, valueC ->
        if (...) {
            Resource.Success
        } else {
            Resource.Error
        }
    }
    

    I'm also unsure about what you try to achieve in the view model. If the underlying LiveDatas are not changed anymore, it doesn't hurt still collecting the flow; nothing will happen anyways. If you are only interested in the first value(s), and want to ignore any changes afterwards, you could use something similar to your approach, but I'm not sure how robust this is in your scenario: Everytime getData() is called the flows are recreated and you effectively get the then current values of the LiveDatas, not what it was in the beginning.

    But then again, you shouldn't even collect flows in the view model. The general idea is for the flows to be generated at the lowest possible level (in the data layer) and be collected at the latest possible moment, in the UI. Everything in between, including your view model, should only transform the flows (like the merge and the combine above). The view model as the last line before collection should then transform all flows into a flow of ui state objects and make it a StateFlow with:

    stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = /* some initial value to use until the flows produce their first value */,
    )
    

    This works especially well if you use compose for you UI and collect the resulting flow with collectAsStateWithLifecycle.