androidandroid-jetpack-composekotlin-coroutinesandroid-mvvmcoroutinescope

How can I call multiple APIs that are depending to each other's data, each response data must be used on the following API call?


//From Kotlin code (Compose View), using CoroutineScope

scope.launch {
    viewModel.getDataOne(key)
    viewModel.data_one.collect{ one ->
        when(one){
            is DataState.Success->{
                viewModel.getDataTwo(one.data.value1, one.data.value2)
                viewModel.data_two.collect{ two ->
                    when(two){
                        is DataState.Success->{
                            viewModel.getDataThree(two.data.valueID, two.data.valueName)
                            viewModel.data_three.collect{ three ->
                                when(three){
                                    is DataState.Success->{
                                        //Continue with your calls
                                        /**
                                         * But then, this doesn't look clean beacuse of the nested api calls.
                                         * So is the a clean way to write this calls on your Views?
                                         */
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

// Function from ViewModel

fun getDataOne(key:String){
    viewModelScope.launch {
        repository.getDataOne(key = key).collect{
            valueDataState.tryEmit(it)
        }
    }
}

Please help me to write a clean Code than having this long nested calls, if there's a proper way or a cleaner way to write that. Thanks in advance .....


Solution

  • Yes, this can be flattened out. You have a lot of nested collect calls, which can be flattened using flat mapping. You need to do this anyway if any of those inner flows are SharedFlows/StateFlows/from repositories. Those kinds of flows are infinite, so if you do a nested collect, it will be stuck on the first value of the outer flow forever. The "latest" part of flatMapLatest gives it an opportunity to restart the inner flow if the outer flow has a new value.

    scope.launch {
        viewModel.getDataOne(key)
        viewModel.data_one
            .filter { it is DataState.Success }
            .flatMapLatest { one ->
                viewModel.getDataTwo(one.data.value1, one.data.value2)
                viewModel.data_two
            }
            .filter { it is DataState.Success }
            .flatMapLatest { two ->
                viewModel.getDataThree(two.data.valueID, two.data.valueName)
                viewModel.data_three
            }
            .filter { it is DataState.Success }
            .collect { three ->
                //...
            }
    }
    

    But to me it looks like this should all be in done in the view model and exposed as a single flow for the UI to consume. This stuff is working with the data, not displaying it.


    The following is something else you can do to make it fit conventions better and make the code easier to read.

    Instead of the getDataOne() function, I would change it to a dataOneKey property, because that's all this function really does. It sets the key that's used as the dependency of the data_one flow. Functions with get at the start usually return the thing they are describing. This is kind of a universal OOP convention, although in Kotlin there are properties, so you usually only see getter functions if they are suspend functions. So, here's how I would change the view model. I have to make some guesses here because I don't really know what the other flows in your view model are currently designed as. You could convert each of your get functions something like this:

    private val dataOneKeyFlow = MutableStateFlow<String?>(null)
    var dataOneKey: String?
        get() = dataOneKeyFlow.value
        set(value) { dataOneKeyFlow.value = value }
    val valueDataState = dataOneKeyFlow
        .flatMapLatest { key -> if (key == null) emptyFlow() else repository.getDataOne(key) }
        .shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), replay = 1)
    

    Then on the other end it would look like:

    scope.launch {
        viewModel.dataOneKey = key
        viewModel.data_one
            .filter { it is DataState.Success }
            //...