Android retrofit coroutine channels
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) }
ConcurrentChannelsRequest.kt, the function for implementing the requests with coroutine channels. the requests runs concurrently, and shows intermediate progress at the same time. In this example, all of the api call for getRepoContributors are launched with their own coroutine and the result is send via the channel. Each result is then received on channel.receive()
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch suspend fun loadContributorsChannels( service: GitHubService, req: RequestData, updateResults: (msg: String)->Unit ) { coroutineScope { val repos = service .getOrgRepos(req.org) .also { updateResults(constructRepoLog(req, it)) } .bodyList() val channel = Channel>() for (repo in repos) { launch { val users = service.getRepoContributors(req.org, repo.name) .also { updateResults(constructUsersLog(repo, it)) } .bodyList() channel.send(users) } } var allUsers = emptyList
() repeat(repos.size) { val users = channel.receive() allUsers = (allUsers + users).aggregate() updateResults(allUsers.toString()) } } }
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) { loadContributorsChannels(service, reqData) { handleUpdateMsg('channels', 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("Channels") { 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