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:
- Coroutines On Android (part III): Real work
- Lessons learnt using Coroutines Flow in the Android Dev Summit 2019 app
Finally, welcome to WeChat public: Paladin Wind.