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: LiveDataget() = _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