Saturday, May 14, 2022

 

5 Uses of KTX LiveData Coroutine Builder

Connecting LiveData to Kotlin Coroutine Flow And Other Uses

Photo by Jackson David on Unsplash

LiveData was introduced by Google as a means to connect the View (Activity/Fragment) and its ViewModel. It’s like a simplified reactive component (e.g. RxJava or Kotlin’s Flow) that also aware of the View’s LifeCycle

With the recent introduction of Kotlin’s Coroutine and Kotlin’s Flow, now Google introduced a way to connect Kotlin’s Flow to LiveData using LiveData Coroutine Builder.

Benefits of using Kotlin’s Coroutine/Flow

The benefit of using Kotlin’s Coroutine/Flow connecting to LiveData is to ensure the underlying component (e.g. Repository, Domain layer) be done fully in the background.

This will help overcome the below challenges

  1. The postValue of LiveData does drops value if the main thread is busy
  2. The LiveData transformation functions are all done in the main thread.

Pre-requisite

To use LiveData Coroutine Builder, firstly we need to include the KTX library e.g.

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"

Then one can use it using

liveData {
emit(data) // OR
emitSource(liveData)
}
// ORstateFlow.asLiveData()

Let’s get to the usage of LiveData Coroutine Builder

1. Connect Kotlin Coroutine to LiveData

If we have a coroutine that we need to call to fetch some data to LiveData, we can do the following.

val someTypeLiveData: LiveData<SomeType> = liveData {
// get data from is a suspend function
val data = aSuspedFunction()
emit(data)
}

Once the LiveData connects to any observer, it will then call and admit the data from the suspend function.

We can also ensure it is done on Background Thread using

val someTypeLiveData: LiveData<SomeType> =
liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
// get data from is a suspend function
val data = aSuspedFunction()
emit(data)
}

2. Connect Kotlin Flow (or StateFlow) to LiveData

The above is a one-off fetch. But if we have a flow, where data is continuously emitted, we can use

val someTypeLiveData: LiveData<SomeType> =   
stateFlow.asLiveData(
viewModelScope.coroutineContext + Dispatchers.IO
)

Once the LiveData connects to any observer, it will be pending on the stateFlow to emit its data. Internally for asLiveData is actually also a liveData {...}

public fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}

3. Transformation on Background

As we have shared previously, the LiveData Transformation is done on the main thread. This makes such transformation an issue if the transformation logic is compute-intensive.

To move it to the background, we have use LiveData's switchMap and liveData coroutine builder as seen below.

val liveData: LiveData<String> =
Transformations.switchMap(sourceliveData) {
liveData
(viewModelScope.coroutineContext + Dispatchers.IO)
val data = someTranformFunction(it)
emit(data)
}
}

Thanks to 

 for the coding guide.

You can also get the code sample here. (note in the code example, the repository uses StateFlow to avoid using setPost of LiveData)

4. Connecting Multiple LiveData Source Emission

In the event, we have multiple LiveDatas source emissions, where we want to control the logic of the emission, we can also use Coroutine Builder.

A very lame example below

liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emitSource(repository.liveDataSourceA)
delay(2000)
emitSource(repository.liveDataSourceB)
delay(2000)
emitSource(repository.liveDataSourceC)
}

You can also get the code sample here.

5. Delay and Keep Coroutine Alive Temporarily

One special feature we have in liveData coroutine builder is, it can be configured to keep the coroutine alive for a specific duration of the LiveData is not active.

This is useful in the event if the user change configuration or temporarily pauses the activity for a short amount of time, while we want to keep the coroutine alive to complete the job. But if it went exceed the time threshold, then we want to restart the entire coroutine operation.

The below is an exact description of the condition

The liveData building block serves as a structured concurrency primitive between coroutines and LiveData. The code block starts executing when LiveData becomes active and is automatically canceled after a configurable timeout when the LiveData becomes inactive. If it is canceled before completion, it is restarted if the LiveData becomes active again. If it completed successfully in a previous run, it doesn't restart. Note that it is restarted only if canceled automatically. If the block is canceled for any other reason (e.g. throwing a CancellationException), it is not restarted.

If we look at the code, we’ll see we have timeoutInMs, which is defaulted to 5s.

@OptIn(ExperimentalTypeInference::class)
public fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

What this means is, when we put the Activity (that observers the LiveData) on the background and the Activity onPause/onStop (note: not with don’t keep activity), the Coroutine will be kept alive for the timeoutInMs duration.

  • If the timeout is completed before the coroutine run finishes, the coroutine will restart when the Activity resume back active
  • If the timeout is not completed when the Activity resumes back active, given the coroutine didn’t finish, it will just continue till completion.
  • If the coroutine is completed before the timeout, even though the Activity has yet to resume, the coroutine will not be restarted, but instead, just emit the last value it has.

With the liveData coroutine builder, we can not bridge between Kotlin Flow and LiveData if we wanted to. This provides greater flexibility of combining both tech stuff together for our usage. i.e. the ability of LiveData to observe the Android lifecycle, and the better Kotlin Flor Reactive operation and Threading handling.

No comments:

Post a Comment