CoroutineLiveDataKt

Added in 2.2.0

public final class CoroutineLiveDataKt


Summary

Public methods

static final @NonNull LiveData<@NonNull T>
<T extends Object> liveData(
    @NonNull CoroutineContext context,
    long timeoutInMs,
    @ExtensionFunctionType @NonNull SuspendFunction1<@NonNull LiveDataScope<@NonNull T>, Unit> block
)

Builds a LiveData that has values yielded from the given block that executes on a LiveDataScope.

static final @NonNull LiveData<@NonNull T>
@RequiresApi(value = 26)
<T extends Object> liveData(
    @NonNull Duration timeout,
    @NonNull CoroutineContext context,
    @ExtensionFunctionType @NonNull SuspendFunction1<@NonNull LiveDataScope<@NonNull T>, Unit> block
)

Builds a LiveData that has values yielded from the given block that executes on a LiveDataScope.

Public methods

public static final @NonNull LiveData<@NonNull T> <T extends Object> liveData(
    @NonNull CoroutineContext context,
    long timeoutInMs,
    @ExtensionFunctionType @NonNull SuspendFunction1<@NonNull LiveDataScope<@NonNull T>, Unit> block
)

Builds a LiveData that has values yielded from the given block that executes on a LiveDataScope.

The block starts executing when the returned LiveData becomes active. If the LiveData becomes inactive while the block is executing, it will be cancelled after timeoutInMs milliseconds unless the LiveData becomes active again before that timeout (to gracefully handle cases like Activity rotation). Any value LiveDataScope.emited from a cancelled block will be ignored.

After a cancellation, if the LiveData becomes active again, the block will be re-executed from the beginning. If you would like to continue the operations based on where it was stopped last, you can use the LiveDataScope.latestValue function to get the last LiveDataScope.emited value.

If the block completes successfully or is cancelled due to reasons other than LiveData becoming inactive, it will not be re-executed even after LiveData goes through active inactive cycle.

As a best practice, it is important for the block to cooperate in cancellation. See kotlin coroutines documentation for details https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html.

The timeoutInMs can be changed to fit different use cases better, for example increasing it will give more time to the block to complete even if LiveData is inactive. It is good for cases when block is finite (meaning it can complete successfully) and is costly to restart. Otherwise if a block is cheap to restart, decreasing the timeoutInMs value will allow to yield less values that aren't consumed by anything.

// a simple LiveData that receives value 3, 3 seconds after being observed for the first time.
val data : LiveData<Int> = liveData {
delay(3000)
emit(3)
}


// a LiveData that fetches a `User` object based on a `userId` and refreshes it every 30 seconds
// as long as it is observed
val userId : LiveData<String> = ...
val user = userId.switchMap { id ->
liveData {
while(true) {
// note that `while(true)` is fine because the `delay(30_000)` below will cooperate in
// cancellation if LiveData is not actively observed anymore
val data = api.fetch(id) // errors are ignored for brevity
emit(data)
delay(30_000)
}
}
}

// A retrying data fetcher with doubling back-off
val user = liveData {
var backOffTime = 1_000
var succeeded = false
while(!succeeded) {
try {
emit(api.fetch(id))
succeeded = true
} catch(ioError : IOException) {
delay(backOffTime)
backOffTime *= minOf(backOffTime * 2, 60_000)
}
}
}

// a LiveData that tries to load the `User` from local cache first and then tries to fetch
// from the server and also yields the updated value
val user = liveData {
// dispatch loading first
emit(LOADING(id))
// check local storage
val cached = cache.loadUser(id)
if (cached != null) {
emit(cached)
}
if (cached == null || cached.isStale()) {
val fresh = api.fetch(id) // errors are ignored for brevity
cache.save(fresh)
emit(fresh)
}
}

// a LiveData that immediately receives a LiveData<User> from the database and yields it as a
// source but also tries to back-fill the database from the server
val user = liveData {
val fromDb: LiveData<User> = roomDatabase.loadUser(id)
emitSource(fromDb)
val updated = api.fetch(id) // errors are ignored for brevity
// Since we are using Room here, updating the database will update the `fromDb` LiveData
// that was obtained above. See Room's documentation for more details.
// https://developer.android.com/training/data-storage/room/accessing-data#query-observable
roomDatabase.insert(updated)
}
Parameters
@NonNull CoroutineContext context

The CoroutineContext to run the given block in. Defaults to EmptyCoroutineContext combined with Dispatchers.Main.immediate

long timeoutInMs

The timeout in ms before cancelling the block if there are no active observers (LiveData.hasActiveObservers. Defaults to DEFAULT_TIMEOUT.

@ExtensionFunctionType @NonNull SuspendFunction1<@NonNull LiveDataScope<@NonNull T>, Unit> block

The block to run when the LiveData has active observers.

@RequiresApi(value = 26)
public static final @NonNull LiveData<@NonNull T> <T extends Object> liveData(
    @NonNull Duration timeout,
    @NonNull CoroutineContext context,
    @ExtensionFunctionType @NonNull SuspendFunction1<@NonNull LiveDataScope<@NonNull T>, Unit> block
)

Builds a LiveData that has values yielded from the given block that executes on a LiveDataScope.

The block starts executing when the returned LiveData becomes active. If the LiveData becomes inactive while the block is executing, it will be cancelled after the timeout duration unless the LiveData becomes active again before that timeout (to gracefully handle cases like Activity rotation). Any value LiveDataScope.emited from a cancelled block will be ignored.

After a cancellation, if the LiveData becomes active again, the block will be re-executed from the beginning. If you would like to continue the operations based on where it was stopped last, you can use the LiveDataScope.latestValue function to get the last LiveDataScope.emited value.

If the block completes successfully or is cancelled due to reasons other than LiveData becoming inactive, it will not be re-executed even after LiveData goes through active inactive cycle.

As a best practice, it is important for the block to cooperate in cancellation. See kotlin coroutines documentation for details https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html.

The timeout can be changed to fit different use cases better, for example increasing it will give more time to the block to complete even if LiveData is inactive. It is good for cases when block is finite (meaning it can complete successfully) and is costly to restart. Otherwise if a block is cheap to restart, decreasing the timeout value will allow to yield less values that aren't consumed by anything.

// a simple LiveData that receives value 3, 3 seconds after being observed for the first time.
val data : LiveData<Int> = liveData {
delay(3000)
emit(3)
}


// a LiveData that fetches a `User` object based on a `userId` and refreshes it every 30 seconds
// as long as it is observed
val userId : LiveData<String> = ...
val user = userId.switchMap { id ->
liveData {
while(true) {
// note that `while(true)` is fine because the `delay(30_000)` below will cooperate in
// cancellation if LiveData is not actively observed anymore
val data = api.fetch(id) // errors are ignored for brevity
emit(data)
delay(30_000)
}
}
}

// A retrying data fetcher with doubling back-off
val user = liveData {
var backOffTime = 1_000
var succeeded = false
while(!succeeded) {
try {
emit(api.fetch(id))
succeeded = true
} catch(ioError : IOException) {
delay(backOffTime)
backOffTime *= minOf(backOffTime * 2, 60_000)
}
}
}

// a LiveData that tries to load the `User` from local cache first and then tries to fetch
// from the server and also yields the updated value
val user = liveData {
// dispatch loading first
emit(LOADING(id))
// check local storage
val cached = cache.loadUser(id)
if (cached != null) {
emit(cached)
}
if (cached == null || cached.isStale()) {
val fresh = api.fetch(id) // errors are ignored for brevity
cache.save(fresh)
emit(fresh)
}
}

// a LiveData that immediately receives a LiveData<User> from the database and yields it as a
// source but also tries to back-fill the database from the server
val user = liveData {
val fromDb: LiveData<User> = roomDatabase.loadUser(id)
emitSource(fromDb)
val updated = api.fetch(id) // errors are ignored for brevity
// Since we are using Room here, updating the database will update the `fromDb` LiveData
// that was obtained above. See Room's documentation for more details.
// https://developer.android.com/training/data-storage/room/accessing-data#query-observable
roomDatabase.insert(updated)
}
Parameters
@NonNull Duration timeout

The timeout duration before cancelling the block if there are no active observers (LiveData.hasActiveObservers.

@NonNull CoroutineContext context

The CoroutineContext to run the given block in. Defaults to EmptyCoroutineContext combined with Dispatchers.Main.immediate.

@ExtensionFunctionType @NonNull SuspendFunction1<@NonNull LiveDataScope<@NonNull T>, Unit> block

The block to run when the LiveData has active observers.