Coroutines in Android - One Shot and Multiple Values

Keywords: Android network Database Retrofit

In Android, the data we use may be one-time or multiple values

This paper introduces how to deal with these two situations with MVVM mode of coroutines in Android. It focuses on the application of coroutine Flow in Android

One-shot vs multiple values

The data used in practical application may be one shot, multiple values, or stream

For example, in a microblog application:

  • Weibo information: obtained upon request, and the result returned is completed. - > one shot
  • Reading and likes: you need to observe the constantly changing data source. The first result return does not mean completion. - > multiple values, stream

Data types in MVVM architecture

How does it look different architecturally to operate at once and observe data with multiple values (streams)?

  • One shot operation: livedata in ViewModel, suspend fun d in repository and Data source
class MyViewModel {
    val result = liveData {
        emit(repository.fetchData())
    }
}

There are two options for implementing multiple values:

  • Multiple values with LiveData: ViewModel, repository, and data source all return LiveData. But LiveData is not designed for streaming, so it's a little strange to use
  • Streams with Flow: livedata is in ViewModel, and Flow is returned from repository and Data source

It can be seen that the main difference between the two methods is the data form consumed by ViewModel, which is LiveData or Flow

It will be explained in the following three aspects: ViewModel, Repository and Data source

What is Flow

Now that Flow is mentioned, let's talk about what it is first, so that you can see it on the same page

Multiple values in Kotlin can be stored in a collection, such as list, or generated by calculation. However, if the values are generated asynchronously, the method needs to be marked suspend to avoid blocking the main thread

Flow is similar to sequence, but flow is non blocking

Take this example:

fun foo(): Flow<Int> = flow {
    // flow builder
    for (i in 1..3) {
        delay(1000) // pretend we are doing something useful here
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> {
    // Launch a concurrent coroutine to check if the main thread is blocked
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(1000)
        }
    }
    // Collect the flow
    foo().collect { value -> println(value) }
}

This code is output after execution:

I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3
  • The flow method used to build flow here is a builder function. The code in the builder block can be suspend ed
  • The emit method is responsible for sending values
  • cold stream: only terminal operation can be activated. The most common is collect()

If you are familiar with Reactive Streams or have used RxJava, you can feel that Flow design looks similar

ViewModel level

The case of sending a single value is relatively simple and typical. There is no need to talk about it here. It mainly talks about the case of sending multiple values. Each time, it is divided into two cases: the type of ViewModel consumption is LiveData or Flow

Launch N values

LiveData -> LiveData

val currentWeather: LiveData<String> = dataSource.fetchWeather()

Flow -> LiveData

val currentWeatherFlow: LiveData<String> = liveData {
    dataSource.fetchWeatherFlow().collect {
        emit(it)
    }
}

To reduce the boilerplate code, simplify the writing method:

val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow().asLiveData()

This simplified form is used directly in the back

Launch 1+N values

LiveData -> LiveData

val currentWeather: LiveData<String> = liveData {
    emit(LOADING_STRING)
    emitSource(dataSource.fetchWeather())
}

emitSource() sends a LiveData

Flow -> LiveData

When using Flow, you can use the same form as above:

val currentWeatherFlow: LiveData<String> = liveData {
    emit(LOADING_STRING)
    emitSource(
        dataSource.fetchWeatherFlow().asLiveData()
    )
}

It seems a little strange to write like this, and its readability is not good, so you can use the Flow API to write like this:

val currentWeatherFlow: LiveData<String> = 
dataSource.fetchWeatherFlow()
    .onStart{emit(LOADING_STRING)}
    .asLiveData()

Suspend transformation

If you want to do some transformation in ViewModel

LiveData -> LiveData

val currentWeatherLiveData: LiveData<String> = dataSource.fetchWeather().switchMap {
    liveData { emit(heavyTransformation(it)) }
    
}

It is not suitable to use map for transformation, because it is in the main thread

Flow -> LiveData

It is convenient to use Flow for conversion:

val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow()
    .map{ heavyTransformation(it) }
    .asLiveData()

Repository layer

The Repository layer is often used to assemble and transform data
LiveData is not designed to do these transformations
Flow provides many useful operators, so it is obviously a better choice:

val currentWeatherFlow: Flow<String> =
    dataSource.fetchWeatherFlow()
        .map { ... }
        .filter { ... }
        .dropWhile { ... }
        .combine { ... }
        .flowOn(Dispatchers.IO)
        .onCompletion { ... }

Data Source layer

The Data Source layer is a network and database, which usually uses some third-party libraries
If you use libraries that support orchestration, such as Retrofit and Room, you only need to mark the method as suspend

  • Retrofit supports coroutines from 2.6.0
  • Room supports coroutines from 2.1.0

One-shot operations

For one-time operation, data layer as long as the suspend method return value

suspend fun doOneShot(param: String) : String = retrofitClient.doSomething(param)

If the network or database used does not support cooperation, is there a way? The answer is yes
Use suspend coroutine to solve this problem

For example, the third-party library you use is based on callback. You can use suspend cancelable coroutine to transform one shot operation:

suspend fun doOneShot(param: String): Result<String> = 
suspendCancellableCoroutine { continuation -> 
    api.addOnCompleteListener { result -> 
        continuation.resume(result)
    }.addOnFailureListener { error -> 
        continuation.resumeWithException(error)
    }.fetchSomething(param)
}

If the process is cancelled, resume will be ignored

After verifying that the code works as expected, further refactoring can be done to abstract this part

Data source with Flow

The data layer returns Flow. You can use flow builder:

fun fetchWeatherFlow(): Flow<String> = flow {
    var counter = 0
    while(true) {
        counter++
        delay(2000)
        emit(weatherConditions[counter % weatherConditions.size])
    }
}

If you use a library that doesn't support Flow, but uses callbacks, the callbackFlow builder can be used to transform the Flow

fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow {
    val callback = object: Callback {
        override fun onNextValue(value: T) {
            offer(value)
        }
        
        override fun onApiError(cause: Throwable) {
            close(cause)
        }
        
        override fun onCompleted() = close()
    }
    api.register(callback)
    awaitClose { api.unregister(callback) }
}

LiveData may not be required

In the above example, ViewModel still keeps the data exposed to the UI as LiveData type. Is it possible not to use LiveData?

lifecycleScope.launchWhenStarted {
    viewModel.flowToFlow.collect {
        binding.currentWeather.text = it
    }
}

In fact, this is the same effect as using LiveData

Reference resources

Video:

File:

Blog:

Finally, welcome to WeChat public: Paladin Wind.

Posted by itguysam on Sun, 08 Dec 2019 01:50:03 -0800