Search code examples
androidkotlinkotlin-flow

How can I combine Flows when I use Kotlin?


The Code B comes from the official sample project , it generate a UI state with combine Flows.

I hope to create a UI state using Code A, but it fails, how can I fix it ?

The most differences between Code A and Code B is fun listAll(eSortBy: ESortBy): Flow<EResult<List<MInfo>>> which requires a MutableStateFlow parameter ESortBy , and I have to collect Flow within combine().

Code A

    private val _audioRecordState= MutableStateFlow(ERecordState.STOPPED)
    private val _listSortBy = MutableStateFlow(ESortBy.START_PRIORITY)
    private val _listMInfo = _listSortBy.map { handelMInfo.listAll(it)}   // It returns  Flow<Flow<EResult<List<MInfo>>>>

    val homeUIState: StateFlow<HomeUIState> =  combine(
        _audioRecordState, _listSortBy, _listMInfo
    )
    {    audioRecordState, listSortBy ,listMInfo->

         log("A: ")
         val temp= listMInfo.last()
         log("B: ")  // It doesn't fire

         when (temp) {
             is EResult.LOADING -> {
                 HomeUIState(audioRecordState, listSortBy)
             }
             is EResult.SUCCESS -> {
                 log("C: "+ temp.data.size)
                 HomeUIState(audioRecordState, listSortBy, temp.data)
             }
             is EResult.ERROR -> {
                 HomeUIState(audioRecordState, listSortBy)
             }
         }
    }
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(),
            HomeUIState(audioRecordState = ERecordState.STOPPED)
        )


data class HomeUIState(
    val audioRecordState: ERecordState = ERecordState.STOPPED,
    val listSortBy: ESortBy = ESortBy.START_PRIORITY,
    val listMInfo: List<MInfo> = listOf<MInfo>()
)

fun listAll(eSortBy: ESortBy): Flow<EResult<List<MInfo>>>

Code B

private val _savedFilterType =
        savedStateHandle.getStateFlow(TASKS_FILTER_SAVED_STATE_KEY, ALL_TASKS)

    private val _filterUiInfo = _savedFilterType.map { getFilterUiInfo(it) }.distinctUntilChanged()
    private val _userMessage: MutableStateFlow<Int?> = MutableStateFlow(null)
    private val _isLoading = MutableStateFlow(false)
    private val _filteredTasksAsync =
        combine(taskRepository.getTasksStream(), _savedFilterType) { tasks, type ->
            filterTasks(tasks, type)
        }
            .map { Async.Success(it) }
            .catch<Async<List<Task>>> { emit(Async.Error(R.string.loading_tasks_error)) }

    val uiState: StateFlow<TasksUiState> = combine(
        _filterUiInfo, _isLoading, _userMessage, _filteredTasksAsync
    ) { filterUiInfo, isLoading, userMessage, tasksAsync ->
        when (tasksAsync) {
            Async.Loading -> {
                TasksUiState(isLoading = true)
            }
            is Async.Error -> {
                TasksUiState(userMessage = tasksAsync.errorMessage)
            }
            is Async.Success -> {
                TasksUiState(
                    items = tasksAsync.data,
                    filteringUiInfo = filterUiInfo,
                    isLoading = isLoading,
                    userMessage = userMessage
                )
            }
        }
    }
        .stateIn(
            scope = viewModelScope,
            started = WhileUiSubscribed,
            initialValue = TasksUiState(isLoading = true)
        )

Solution

  • Calling last() on a Flow makes it suspend so it can collect the entire flow until it declares itself complete, and then it returns the last value collected. If you call last on a SharedFlow or StateFlow or any infinite cold Flow, it will never return because these flows never complete.

    In my opinion, you should not be passing around Flows of Flows. A Flow of Flows is just a temporary object you use to build a more final flow by flattening it somehow, by calling something like flatMapConcat or flatMapLatest or transformLatest on it, etc. It's confusing to pass the Flow of Flows around before flattening it, because then it's unclear in other parts of the code how you should properly handle it.

    If you flatten your Flow before combining it, then it will be much easier to work with inside the combine lambda.

    You should properly flatten your flow right here:

    private val _listMInfo = _listSortBy.map { handelMInfo.listAll(it)}   // It returns  Flow<Flow<EResult<List<MInfo>>>>
        .foo(...) // some flow operator here to flatten it appropriately, depending on your situation.
    

    I don't know the design of your app to be able to tell you what is the proper way to flatten this particular flow.