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: 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) {
            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

Custom Search

Search the entire web

Custom Search