Run coroutines concurrently without waiting for completion

124 views Asked by At

I'm struggling to understand why this is so difficult to figure out. This is in Android if it matters. Please see the code below

private val _myFlow = MutableStateFlow<Result<Flow<ResponseDataClass>>>(Result.Success(emptyFlow()))

suspend fun getSomethingFromPreferences(): Result<Flow<ResponseDataClass>> {
    Log.d("Coroutines", "getSomethingFromPreferences")
    
    prefs.myDataInPreferences?.takeIf { it.requiredList.isNotEmpty() }?.let {
        Log.d("Coroutines", "prefs.myDataInPreferences is not empty")
        _myFlow.emit(Result.Success(flowOf(it)))
    }
    
    coroutineScope {
        async(Dispatchers.IO) {
            getDataFromNetwork()
        }
    }
    
    Log.d("Coroutines", "finished")
    return _myFlow.value
}

private suspend fun getDataFromNetwork() {
    Log.d("Coroutines", "getDataFromNetwork")

    // val result = Call network API

    if (result is Result.Success) {
        Log.d("Coroutines", "result is Success")
        prefs.myDataInPreferences = result.data
        _myFlow.value = Result.Success(flowOf(result.data))
    } else {
        Log.d("Coroutines", "result is Error")
        _myFlow.value = Result.Error(Error(result.error.message))
    }
}

What I'd like to see in my logs is:

2023-07-04 13:07:36.339 D/Application: getSomethingFromPreferences
2023-07-04 13:07:36.339 D/Application: prefs.myDataInPreferences is not empty
2023-07-04 13:07:36.339 D/Application: getDataFromNetwork
2023-07-04 13:07:36.339 D/Application: finished
2023-07-04 13:07:36.339 D/Application: Received data in viewModel
2023-07-04 13:07:36.339 D/Application: result is Success

But what I keep seeing is:

2023-07-04 13:07:36.339 D/Application: getSomethingFromPreferences
2023-07-04 13:07:36.339 D/Application: prefs.myDataInPreferences is not empty
2023-07-04 13:07:36.339 D/Application: getDataFromNetwork
2023-07-04 13:07:36.339 D/Application: result is Success
2023-07-04 13:07:36.339 D/Application: finished
2023-07-04 13:07:36.339 D/Application: Received data in viewModel

Basically, I'd like

  • the flow to emit from local data first which I will receive in my viewModel
  • call the network in parallel
  • if network call was successful, update local data and emit the new data to the flow.

What keeps happening is even if local data is present the coroutine waits for network call to complete and only then emits to the flow.

I've tried many things other than what you see in the code above, everything works exactly the same way.

1

There are 1 answers

0
broot On

To be honest, your current solution has so many flaws that it is hard for me to understand what was the idea behind it and how was it even supposed to work.

For some reason you have a flow of flows. In getSomethingFromPreferences() you return the inner flow, then in getDataFromNetwork() you replace this inner flow with another one. That means the caller of the first function doesn't even know about that new flow, so how could they collect a new value? You need to add a new item to the same flow you returned, but you can't do that because you returned a flow created with a fixed contents: flowOf(it).

Also, if you use a suspend function and inside you start async operations without providing another scope, then that means these async operations are subtasks of the current function. Current function can't finish before its subtasks are done. This is by design, according to the structured concurrency concept.

Instead, you should simply return a flow, which first emits the first value, then it makes a network call and emits the second value. It won't be easy to provide a working code, but it should be something along lines:

fun getSomethingFromPreferences(): Flow<Result<ResponseDataClass>> = flow {
    prefs.myDataInPreferences?.takeIf { it.requiredList.isNotEmpty() }?.let {
        emit(Result.Success(it))
    }

    emit(getDataFromNetwork())
}

private suspend fun getDataFromNetwork(): Result<ResponseDataClass> {
    // val result = Call network API

    return if (result is Result.Success) {
        Result.Success(result.data)
    } else {
        Result.Error(Error(result.error.message))
    }
}

Also, this line seems a little "smelly" to me: prefs.myDataInPreferences = result.data. getDataFromNetwork() is strictly for acquiring the data from the network, so it seems wrong it has any side effects. Maybe moving this to the getSomethingFromPreferences() would help, as this function is already reading from this property, so then it would handle both saving and loading to/from the cache.