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
Search the entire web