3

I need to run 6 API calls Simultaneously and need to update the UI for each when corresponding request finishes

Currently I am using kotlin coroutines parallel execution using the following code

    suspend fun getAllData() : List<String>{
    return withContext(Dispatchers.IO) {

        lateinit var getObject1Task: Deferred<Response<String>>
        lateinit var getObject2Task: Deferred<Response<String>>
        lateinit var getObject3Task: Deferred<Response<String>>

        lateinit var getObject4Task: Deferred<Response<String>>
        lateinit var getObject5Task: Deferred<Response<String>>
        lateinit var getObjec6Task: Deferred<Response<String>>

        launch {
            getObject1Task = dataApiService.getData()
            getObject2Task = dataApiService.getData()
            getObject3Task = dataApiService.getData()
            getObject4Task = dataApiService.getData()
            getObject5Task = dataApiService.getData()
            getObject6Task = dataApiService.getData()
        }

          var stringList = ArrayList<String >()

        stringList.add(getObject1Task.await().body()!!) /// add All to the list
        stringList
    }
}

I am unable to find a way a way to get data for each string as soon as that API finishes. I also tried LiveData but some how that was making any sense.

Each String has no link with the other so it not essential to add all strings in a list

Syeda Zunaira
  • 5,191
  • 3
  • 38
  • 70
  • are you using retrofit for APIs? – Pavan Varma Dec 05 '19 at 16:40
  • where you able to find any solution for this scenario, If yes sharing adding it here as answer will help many of those like me. – Rekha Jun 19 '20 at 16:17
  • @Rekha it was a mix of @ nulldroid answer and some comments, basically, you need to use suspend function which is always sequential and then I used runBlocking which is not the best choice but at least it make it work. – Syeda Zunaira Jun 23 '20 at 07:41

2 Answers2

1

Using coroutines, there are multiple ways to achieve this. Here are 2 examples:

  1. Use launch without returning a value and directly update UI from within the coroutine, once the string is ready.
  2. Similar to your approach, you can also use async, wait for the future response return value and then update UI.

Example 1: Fire and forget, update UI directly when each element is ready

When updating UI elements from within a coroutine, you should use Dispatchers.Main as coroutine context.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)


    repeat(6){index ->
        val id = resources.getIdentifier("tv${index+1}", "id", packageName)
        val textView = findViewById<TextView>(id)
        textViews.add(textView)
    }

    repeat(6){ index ->
        GlobalScope.launch(Dispatchers.Main) { // launch coroutine in the main thread
            val apiResponseTime = Random.nextInt(1000, 10000)
            delay(apiResponseTime.toLong())
            textViews[index].text = apiResponseTime.toString()
        }
    }
}

Note: Here, every TextView gets updated as soon as the string is ready, without blocking the main thread. I used 6 example TextViews in a LinearLayout with IDs "tv1", "tv2"...

Example 2: Using parallel async + await(), update UI when all jobs are finished (similar to yours)

Here we are launching 6 async in parallel and add the results to the list as soon as they are ready. When the last result is added, we return the list and update the TextViews in a loop.

val textViews = mutableListOf<TextView>()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    repeat(6){index ->
        val id = resources.getIdentifier("tv${index+1}", "id", packageName)
        val textView = findViewById<TextView>(id)
        textViews.add(textView)
    }
    // note: again we use Dispatchers.Main context to update UI
    GlobalScope.launch(Dispatchers.Main) {
        val strings = updateUIElementAfterThisFinishes()
        repeat(6){index ->
            textViews[index].text = strings[index]
        }
    }

}

 // for API calls we use Dispatchers.IO context, this function will finish at 10 seconds or less
suspend fun updateUIElementAfterThisFinishes(): List<String> = withContext(Dispatchers.IO){
    val strings = mutableListOf<String>()
    val jobs = Array(6){GlobalScope.async {
        val apiResponseTime = Random.nextInt(1000, 10000)
        delay(apiResponseTime.toLong())
        apiResponseTime.toString()
    }}
    jobs.forEach {
        strings.add(it.await())
    }
    return@withContext strings
}
nulldroid
  • 1,150
  • 8
  • 15
  • but I have a separate API class for that. I want to achieve MVVM and this way I will lose that plus will that still be SIMULTANEOUS calls? – Syeda Zunaira Dec 05 '19 at 19:11
  • Yes it will be simultaneous, for both ways. These are just examples. There is no issue using MVVM, you won't lose anything. Just put these functions (i.e. the suspend fun ) in your ViewModel and/or call dataApiService.getData() from your repository – nulldroid Dec 05 '19 at 19:35
  • but the ViewModel or dataApiService has no idea of TextView how can I update textView from there? – Syeda Zunaira Dec 05 '19 at 19:38
  • No, you update TextView in your Activity, you just fetch data in ViewModel/Repository – nulldroid Dec 05 '19 at 19:39
  • "update UI when all jobs are finished " is not what I want I want to update UI after every response. – Syeda Zunaira Dec 05 '19 at 19:40
  • Then you should state that in your question. You can use the first example then. – nulldroid Dec 05 '19 at 19:43
  • I have a little confusion here, you said "you just fetch data in ViewModel/Repository" but in answer you suggest "Use launch without returning a value and directly update UI from within the coroutine, once the string is ready." ?? – Syeda Zunaira Dec 05 '19 at 19:45
  • Yes it is mentioned in Question "update the UI for each when corresponding request finishes" – Syeda Zunaira Dec 05 '19 at 19:46
0

If APIs are not related to each other, then you launch a new coroutine for every API. Coroutines are lite weight, so you can launch as many as you want.

Since you have 6 APIs, launch 6 coroutines at once and handle their responses in there corresponding coroutines.

and you also want to follow MVVM, so you can use LiveData to pass data from viewModel to your fragment or activity.


In ViewModel, it will look something like this

// liveDatas for passing data to your fragment or activity
// each liveData should be observed and update their respective views
val object1LiveData = MutableLiveData<String>()
val object2LiveData = MutableLiveData<String>()
val object3LiveData = MutableLiveData<String>()
val object4LiveData = MutableLiveData<String>()
val object5LiveData = MutableLiveData<String>()
val object6LiveData = MutableLiveData<String>()

private val listOfLiveData = listOf(
    object1LiveData,
    object2LiveData,
    object3LiveData,
    object4LiveData,
    object5LiveData,
    object6LiveData
)

fun fetchData(){
    listOfLiveData.forEach { liveData->
        // launching a new coroutine for every liveData
        // for parellel execution
        viewModelScope.launch{
            callApiAndUpdateLiveData(liveData)
        }
    }  
}

private suspend fun callApiAndUpdateLiveData(liveData: MutableLiveData<String>){
    val response = dataApiService.getData().await()
    liveData.value = response.body()!!
}
Pavan Varma
  • 1,199
  • 1
  • 9
  • 21