Cronet integration with LiveData and Kotlin Coroutines on Android

Kelvin Watson
5 min readNov 16, 2019

Cronet is a Chromium library used to handle network requests. It’s used on a number of Google products, including YouTube and Google Maps.

Purpose

While there are many working Android code examples of usage of Kotlin coroutines, LiveData and the network library Retrofit, there isn’t an example of the specific combination of Cronet in the context of LiveData and coroutines.

The Cronet documentation’s sample code shows a traditional callback structure (see `Url ). However, not included in that documentation is how Cronet can be integrated with Kotlin Coroutines and LiveData. The following example bridges this gap.

Prerequisites

In addition to modern Android development paradigms, it is assumed that you have some knowledge of Dagger 2/Dagger-Android dependency injection. Basic information about Cronet can be found here.

Setup

For this particular example, we’ll want to include the Cronet library (as well as its less performant fallback in the case that it can’t be loaded from Google Play Services), as well as the latest LiveData-Ktx library. More details here.

// Cronet
implementation 'com.google.android.gms:play-services-cronet:16.0.0'
// fallback
implementation group: 'org.chromium.net', name: 'cronet-fallback', version: '76.3809.111'

// LiveData builder
implementation group: 'androidx.lifecycle', name: 'lifecycle-livedata-ktx', version: '2.2.0-rc02'

Architecture

Following current Android architecture component guidelines and patterns for use of LiveData and coroutines, we have the following code samples showing a standard fragment, Android ViewModel and repository .

CustomFragment.kt

In the fragment, we instantiate our view model and observe the LiveData object for changes.

class CustomFragment : Fragment() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val customViewModel: CustomViewModel by viewModels {
viewModelFactory
}

override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
customViewModel.viewModelListLiveData.observe(this,
Observer {
onViewModelListChanged(it)
})
}
}

CustomViewModel.kt

In the view model, we call the repository to get the LiveData. Note the emitSource call. This is similar to calling MediatorLiveData.addSource, where one liveData object observes another. In this case, myItemList is observing myItemLiveData. When myItemLiveData changes (e.g. is fetched), this triggers the Transformations.map callback.

class CustomViewModel @Inject constructor(private val repository: Repository) : ViewModel() {

private var myItemsLiveData: LiveData<List<MyItem>>

init {
myItemsLiveData = liveData(Dispatchers.IO) {
val myItemsLiveData = repository.getMyItemsLiveData()
emitSource(myItemsLiveData)
}
}

val viewModelListLiveData: LiveData<ArrayList<MyItem>> =
Transformations.map(myItemsLiveData) { myItems ->
//transform to view models
val viewModels = ArrayList<MyItem>()
for (mItem in myItems) {
viewModels.add(MyItemViewModel(mItem))
}
viewModels
}
}

CustomRepository.kt

Following the Android developer documentation on how to send a simple network request using Cronet, we have the following repository implementation.

The Cronet documentation’s sample code shows a traditional callback structure (see UrlRequest.Callback). However, not included in that documentation is how Cronet can be integrated with Kotlin Coroutines and LiveData. The following example bridges this gap.

The repository snippet below makes the network call look synchronous. That’s the beauty of Kotlin coroutines. Internally, it uses the latest LiveData builder and wraps the UrlRequest.Callback implementation in a suspendCoroutine. Don’t worry, we’ll go through how all of that works.

class CustomRepository @Inject constructor(private val context: Context, private val gson: Gson) : Repository {
private val coroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

override suspend fun getMyItemsLiveData() = liveData(coroutineDispatcher) {
val result: List<MyItem> = getMyItemsLiveDataInternal()
emit(result) // emit the result back to the view model
}

Analysis of CustomRepository.kt

Looking at the Cronet documentation for performing a simple network request, we see a traditional callback structure, where we can act on the result of the network calls (function bodies are omitted for brevity):

class MyUrlRequestCallback : UrlRequest.Callback() {    override fun onRedirectReceived(request: UrlRequest?, info: UrlResponseInfo?, newLocationUrl: String?) { //... }

override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) { //... }

override fun onReadCompleted(request: UrlRequest?, info: UrlResponseInfo?, byteBuffer: ByteBuffer?) { //... }

override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) { //... }
}

How can we convert this into a structure where we can emit the final result from the LiveData builder back to our Android ViewModel?

Notice in the Cronet official documentation that the request runs on a single thread Executor.

val executor: Executor = Executors.newSingleThreadExecutor()

We can use the asCoroutineDispatcher extension function to convert this to a coroutineDispatcher to run the request as a coroutine.

val coroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

override suspend fun getMyItemsLiveData():
LiveData<List<MyItem>> = liveData(coroutineDispatcher) { // ... }

Now we follow the Cronet documentation example of creating and starting the network request. This part is straight forward, except for the MyUrlRequestCallback()as noted below.

val requestBuilder = cronetEngine.newUrlRequestBuilder(
"https://www.example.com",
MyUrlRequestCallback(), // how can we call emit from here?
executor
)

val request: UrlRequest = requestBuilder.build()
request.start()

This traditional callback structure presents an issue. In CoroutineLiveData, we need to call emit to send our result back to the ViewModel. Unfortunately, if we look at the signature for emit, we see that it is a suspend function, which must be run from another suspend function or a coroutine.

/**
* Set's the [LiveData]'s value to the given [value]. If you've called [emitSource] previously,
* calling [emit] will remove that source.
*
* Note that this function suspends until the value is set on the [LiveData].
*
*
@param value The new value for the [LiveData]
*
*
@see emitSource
*/
suspend fun emit(value: T)

Thankfully, we can “flatten” the callback structure by using suspendCoroutine, abstracting the Cronet invocation and callback into an internal function.

private suspend fun getMyItemsLiveDataInternal() =     suspendCoroutine<List<MyItem>> { continuation ->

val executor = Executors.newSingleThreadExecutor()
val cronetEngineBuilder = CronetEngine.Builder(context)
val cronetEngine = cronetEngineBuilder.build()
val requestBuilder = cronetEngine.newUrlRequestBuilder(
"http://example-api.com/example",
object : UrlRequest.Callback() {

override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {
val httpStatusCode = info?.httpStatusCode
val myBuffer: ByteBuffer = ByteBuffer.allocateDirect(32 * 1024)
if (httpStatusCode == 200) {
request?.read(myByteBuffer)
}
}

override fun onReadCompleted(request: UrlRequest?, info: UrlResponseInfo?, buffer: ByteBuffer?) {
buffer?.flip()
buffer?.let {
val bytes = ByteArray(it.remaining())
it.get(bytes)
String(bytes, Charset.forName("UTF-8"))
}.apply {
val myItems = gson.fromJson(this, MyItem::class.java)
continuation.resume(myItems)
}
buffer?.clear()
request?.read(buffer)
}

// other callbacks omitted for brevity
},
executor
)
val request: UrlRequest = requestBuilder.build()
request.start()
}
}

The key here is the continuation callback parameter. Execution of the coroutine is suspended while the network call is in flight. Once the we have our network data, we can resume the coroutine by calling continuation.resume, thereby emitting our result.

Our original problem was that emit could not be called without a surrounding coroutine/suspending function. This flattening process of converting a traditional callback to a coroutine has also solved that problem since emit now lives within the LiveData builder.

override suspend fun getMyItemsLiveData() = liveData(dispatcher) {
val result: List<MyItem> = getMyItemsLiveDataInternal()
emit(result)
}

I hope you found this article helpful. Please leave comments below if anything requires further clarification. I will be updating this article as I continue developing in Kotlin.

Please consider supporting me so I can bring you more content:

--

--