Android retrofit concurrent coroutine with Deferred

dependencies for retrofit, okhttp, json converter, livedata and viewmodel

def retrofit_version = '2.9.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:retrofit-mock:$retrofit_version"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation "com.squareup.okhttp3:logging-interceptor:4.7.2"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2")

implementation "androidx.compose.runtime:runtime-livedata:1.2.0-alpha05"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'

GithubService.kt, the service interface, data models and function for creating the retrofit service.

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import java.util.*

interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    suspend fun getOrgRepos(
        @Path("org") org: String
    ): Response>

    @GET("repos/{owner}/{repo}/contributors?per_page=100")
    suspend fun getRepoContributors(
        @Path("owner") owner: String,
        @Path("repo") repo: String
    ): Response>
}

@Serializable
data class Repo(
    val id: Long,
    val name: String
)

@Serializable
data class User(
    val login: String,
    val contributions: Int
)

@Serializable
data class RequestData(
    val username: String,
    val password: String,
    val org: String
)

@OptIn(ExperimentalSerializationApi::class)
fun createGitHubService(username: String, password: String): GitHubService {
    val authToken = "Basic " + Base64.getEncoder().encode("$username:$password".toByteArray()).toString(Charsets.UTF_8)
    val loggingInterceptor = HttpLoggingInterceptor()
    loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

    val httpClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val original = chain.request()
            val builder = original.newBuilder()
                .header("Accept", "application/vnd.github.v3+json")
                .header("Authorization", authToken)
            val request = builder.build()
            chain.proceed(request)
        }
        .addInterceptor(loggingInterceptor)
        .build()

    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.github.com")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())

        .client(httpClient)
        .build()
    return retrofit.create(GitHubService::class.java)
}

ConcurrentRequest.kt, the function for implementing the concurrent requests with coroutine Deferred. In this example, all of the api call for getRepoContributors are collected in a list of Deferred, and then they are being executed concurrently.

import kotlinx.coroutines.*

suspend fun loadContributorsConcurrent(service: GitHubService, req: RequestData, updateResults: (msg: String)->Unit): List = coroutineScope {
    val repos = service
        .getOrgRepos(req.org)
        .also { updateResults(constructRepoLog(req, it)) }
        .bodyList()

    val deferreds: List>> = repos.map { repo ->
        async {
            updateResults("starting loading for ${repo.name}")
            delay(3000)
            service.getRepoContributors(req.org, repo.name)
                .also { updateResults(constructUsersLog(repo, it)) }
                .bodyList()
        }
    }
    deferreds.awaitAll().flatten().aggregate()
}

MainViewModel.kt, the view model for launching the api calls and handling the results.

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.codexpedia.kotlincorountine.service.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {
    private val reqData: RequestData = RequestData("your-github-username", "your-github-token-settings->developer-settings->personal-access-token", "kotlin")
    private val service = createGitHubService(reqData.username, reqData.password)

    val logMsg: LiveData get() = _logMsg
    private val _logMsg = MutableLiveData()

    val logMsgs: LiveData> get() = _logMsgs
    private val _logMsgs = MutableLiveData>()

    private var job: Job? = null

    private fun handleUpdateMsg(tag: String, msg: String) {
        Log.d(tag, msg)
        _logMsg.postValue(msg)
        if (_logMsgs.value === null) {
            _logMsgs.postValue(mutableListOf(msg))
        } else {
            _logMsgs.value!!.add(0,msg)
            _logMsgs.postValue(_logMsgs.value)
        }
    }

    fun onAction() {
        _logMsgs.value = mutableListOf()
        job = viewModelScope.launch(Dispatchers.IO) {
            loadContributorsConcurrent(service, reqData) {
                handleUpdateMsg("CONCURRENT", it)
            }
        }
    }
}

MainActivity.kt, the main activity class with the UI.

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codexpedia.kotlincorountine.ui.theme.KotlinCorountineTheme
import com.google.accompanist.flowlayout.FlowRow

class MainActivity : ComponentActivity() {
    private val mainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            KotlinCorountineTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    val logMsg by mainViewModel.logMsg.observeAsState("")
                    val logMsgs by mainViewModel.logMsgs.observeAsState(emptyList())

                    MainContent(logMsg, logMsgs) {
                        mainViewModel.onAction()
                        Toast.makeText(this, "Action type: $it", Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
    }
}

@Composable
fun MainContent(logMsg: String, logMsgs: List, onClick: () -> Unit) {
    Column() {
        LabelText("Click the button to start download")
          ActionBtn("Concurrent") {
            onClick()
          }
        Logs(logMsgs)
    }
}

@Composable
fun LabelText(text: String) {
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = text,
            modifier = Modifier.padding(16.dp)
        )
    }
}

@Composable
fun ActionBtn(text: String, onClick: () -> Unit) {
    val shape = CircleShape
    Button(
        onClick = onClick,
        modifier = Modifier
            .padding(8.dp)
            .background(MaterialTheme.colors.primary, shape)
    )
    {
        Text(text = text)
    }
}

@Composable
fun Logs(messages: List) {
    LazyColumn {
        items(messages) { message ->
            Column(
                modifier = Modifier.fillMaxWidth(),
            ) {
                Text(
                    text = message,
                    modifier = Modifier.padding(2.dp)
                )
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun MainContentPreview() {
    KotlinCorountineTheme {
        MainContent("", listOf("Hello")) {

        }
    }
}

Complete example in Github
https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/01_Introduction
https://github.com/kotlin-hands-on/intro-coroutines

Search within Codexpedia

Custom Search

Search the entire web

Custom Search