Home · Android & Kotlin Tutorials

Coroutines with Lifecycle and LiveData

In this tutorial, you’ll build an Android app that uses coroutines with LiveData objects and lifecycle-aware CoroutineScopes.

4.6/5 5 Ratings

Version

  • Kotlin 1.3, Android 5.0, Android Studio 3.6

Android architecture components provide first-class support for Kotlin coroutines. They let you write asynchronous code defined within CoroutineScopes tied to lifecycles and LiveData.

Because an Android app runs on its UI thread by default, it’s constrained to the lifecycle of its components. To move work off the UI thread, you need to write asynchronous code that runs within the lifecycle’s bounds. Otherwise, you could face memory leaks, power inefficiency and battery over-consumption.

In this tutorial, you’ll build a trading app with a user profile screen. This screen simulates displaying both static and real-time user information. A real app would fetch this data from a server, but in this tutorial, you’ll use dummy data.

By building this app, you’ll learn about:

  • Coroutines with LiveData builder.
  • Lifecycle-aware CoroutineScopes: ViewModelScope and LifecycleScope.
Note: This tutorial assumes you have previous experience using both Android Architecture Components and Kotlin Coroutines. If you’re new to Android Architecture Components, check out our Android Architecture Components: Getting Started tutorial, especially the sections on ViewModels and LiveData. Also, if you’re unfamiliar with Kotlin Coroutines, take a look at Kotlin Coroutines Tutorial for Android: Getting Started.

Getting Started

Click the Download Materials button at the top or bottom of the page to access the starter and final projects for this tutorial.

Next, open the starter project in Android Studio.

Take a moment to familiarize yourself with the code. You’ll see the following classes.

  • MainActivity.kt: Activity that wraps the user profile screen.
  • ProfileFragment.kt: Fragment that displays the user information.
  • ProfileViewModel.kt: ViewModel that provides the user information through LiveData using use case classes.
  • ProfileViewModelFactory.kt: ViewModelFactory that constructs ProfileViewModel instances.
  • GetUserInformationUseCase.kt: Use case that provides the user’s name, account number and phone number.
  • GetTotalValueUseCase.kt: Use case that provides the user’s total amount of money in real-time.
  • GetStocksUseCase.kt: Use case that provides the stocks the user is currently investing in.
  • GetRecommendedStockUseCase.kt: Use case that provides the user with a stock recommendation. It also provides a method to refresh it.

Build and run the app. You’ll see the profile screen, but the information fields are all empty. You’ll display the user’s information in no time!

Coroutine with Lifecycle and LiveData

Using Coroutines With LiveData

Generally speaking, when using LiveData, you might need to perform an asynchronous computation before returning a value, or many values. For this scenario, you’ll use the LiveData builder. It bridges the gap between LiveData and coroutines, where the asynchronous computation is wrapped in a suspend function and starts running once the LiveData instance becomes active.

fun <T> liveData(
  // 1
  context: CoroutineContext,
  
  // 2
  timeoutInMs: Long,

  // 3
  block: suspend LiveDataScope<T>.() -> Unit): LiveData<T>

The LiveData builder has three parameters:

  1. context: A CoroutineContext that defines the context for running the block. It defaults to the MainCoroutineDispatcher.
  2. timeoutInMs: The duration in milliseconds after which the block is cancelled if there are no active observers on the LiveData. It defaults to five seconds.
  3. block: A suspend function that runs an asynchronous operation when the LiveData is active. If the block completes successfully or cancelles due to reasons other than the LiveData becoming inactive, it won’t re-execute even after the LiveData goes through the active/inactive cycle.

    When the block cancels because the LiveData becomes inactive, and if or when it becomes active again, the block is re-executed from the beginning. If you need to continue the operations based on where it last stopped, you can call LiveDataScope.lastValue to get the last emitted value.

Adding the LiveData Builder Dependency

To use the LiveData builder in your trading app, add the following dependency to the project’s app/build.gradle.

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

Emitting Single Values Through LiveData With Coroutines

Using the LiveData builder, you can emit single values which are caught by the LiveData’s active observers. To do this, use LiveDataScope.emit().

val aLiveData: LiveData<String> = liveData {
  // 1
  val result: String = computeResult()

  // 2
  emit(result)
}

Here’s what’s going on with the code above:

  1. computeResult() can be a suspend function that performs an asynchronous operation and returns a String. For example, it can fetch a value from a server over the network, or read a value from a local database.
  2. Once the result is ready, you emit it using the emit() function. This calls LiveData.setValue() internally.

Displaying User Information

Ideally, your user could view and edit their personal information on the profile screen, but that’s out of the scope of this tutorial. Instead, you’ll focus on displaying the user information.

Open GetUserInformationUseCase.kt. To return the user’s information using LiveData builder, implement get() by adding this:

fun get(): LiveData<UserInformation> = liveData {
  // 1
  delay(GET_USER_INFORMATION_DELAY)

  // 2
  emit(UserInformation(USER_NAME, ACCOUNT_NUMBER, PHONE_NUMBER))
}

In the function above, you:

  1. Pause the execution of the function for the duration of GET_USER_INFORMATION_DELAY. This simulates fetching the data asynchronously.
  2. Emit a UserInformation instance with dummy data. Any active observers of this LiveData get this result.

Build and run the app. The screen now displays the user’s name, account number and phone number.

Coroutines and LiveData: Emitting Single Values

Next, you’ll show the user’s total investment value in real-time!

Emitting Values From a LiveData Source With Coroutines

LiveData builder doesn’t limit you to just emitting a single value. It also allows you to emit a different source from which data can be returned using LiveDataScope.emitSource(). Whenever its value changes, the LiveData builder emits that same value. Generally speaking, this looks as follows:

val aLiveData: LiveData<String> = liveData {
  // 1
  emitSource(source1)

  // 2
  emit("value")

  // 3
  emit(source2)
}

val source1: LiveData<String> = ...
val source2: LiveData<String> = ...

The block above:

  1. Sets source1 as a source for the LiveDara builder. Whenever source1 has a new value, aLiveData receives and emits it.
  2. Emits a single value to the LiveData’s active observers. It also removes source1 as a source. This means even when source1‘s value changes, aLiveData no longer emits that new value.
  3. Sets source2 as a source for the LiveData builder. aLiveData now listens to changes in source2. Whenever its value updates, aLiveData emits the new value.

You’ll use this technique to update your user’s total investments in real-time!

Displaying the User’s Total Investment Amount

During trading hours, stock prices gain and lose value. Since stock prices often fluctuate, it would be helpful to display the user’s total investment amount of money in real-time.

In a real app, you might implement this using webhooks. For this tutorial, you’ll simulate this fluctuation using a random change in prices.

Open GetTotalValueUseCase.kt. Replace the get() to return the total investment value for display using the following code:

fun get(): LiveData<Double> = liveData {
  // 1
  emit(0.00)

  // 2
  delay(TOTAL_VALUE_INITIAL_DELAY_MS)

  // 3
  emitSource(getTotalValue())
}

private fun getTotalValue(): LiveData<Double> = liveData {
  var total = INITIAL_TOTAL_VALUE

  // 4
  while (true) {
    // 5
    delay(TOTAL_VALUE_UPDATE_RATE_MS)

    // 6
    total = total.moreOrLessWithMargin(TOTAL_VALUE_DIFF_RANGE)
    emit(total)    
  }
}

In the two functions above, you:

  1. Emit a value to display while the total amount computes asynchronously.
  2. Pause for one second to simulate the asynchronous changes of the amount.
  3. Set getTotalValue() as a source for this LiveData builder.
  4. As long as the LiveData has an active observer, this loop keeps running and updating the total amount. This simulates real-time updates.
  5. Pause for two seconds before each update to the total amount. This lets you simulate the initial time it takes to fetch the result, perhaps from a server, and also sets the rate at which the total investment amount updates.
  6. Update the total amount using the extension function moreOrLessWithMargin() and then emit it. You notify the get() function observing this LiveData of this change, and also emit this newly computed value.

Build and run the app. You’ll see the total amount now displays on the screen, and updates every two seconds!

Coroutines and LiveData: Emitting Values From a Source

Now the app is coming along! Next, you’ll display the list of stocks in which your user is currently investing.

Transforming LiveData With Coroutines

Just like you can apply a transformation to LiveData using Transformations.switchMap(), you can do the same with the LiveData builder by using liveData.switchMap(transform), which applies transform() on each element it receives. Generally speaking, the code might looks like this:

val aLiveData = LiveData<String> = ...
val transformedLiveData: LiveData<Int> = aLiveData.switchMap { element ->
  liveData {
    val transformedElement = transform(element)
    emit(transformedElement)
  }
}

Whenever aLiveData‘s value changes, this new value is received inside the switchMap() function. It’s transformed using transform() and then emitted to its active observers.

You’ll use this approach to show your user’s investments.

Displaying the User’s Investments

In addition to the total amount of their investments, the user might want to see all the stocks in which they’re investing.

Open GetStocksUseCase.kt and add the following method:

private fun getStocks(): LiveData<List<String>> = liveData {
  delay(GET_STOCKS_DELAY)
  emit(STOCKS)
}

Using a LiveData builder, this above code gets a list of stocks in which the user is currently investing. This operation is asynchronous to simulate fetching the information from somewhere else, such as a server.

Next, in order to transform this list into a String that’s displayable on the screen, you’ll use a switchMap() transformation. Replace the get function in GetStocksUseCase with the following:

fun get(): LiveData<String> = getStocks().switchMap { stocks ->
  liveData {
    emit(stocks.joinToString())
  }
}

The get() function observes getStocks(). Once it returns a value, it transforms it and emits a new value.

If getStocks() returns [“stock1”, “stock2”, “stock3”], then get() transforms this list by joining its elements, returning the String “stock1, stock2, stock3”.

Build and run the app. You’ll see the list of the user’s stocks.

Coroutines and LiveData: Transformations

You’re almost there! The last piece of information missing from the user profile screen is the user-curated stock recommendation. Before you add that feature, you’ll first need to explore ViewModelScopes.

Using ViewModelScope

Use the ViewModelScope when you’re performing an operation inside a ViewModel and need to make sure it cancels when the ViewModel isn’t active. It’s defined for every ViewModel and ensures any coroutine launched within it cancels when the ViewModel clears.

Generally speaking, to use the ViewModelScope, access it through the viewModelScope attribute inside a ViewModel. The code to do this might looks as follows:

class MyViewModel: ViewModel() {
  fun aFunction() {
    viewModelScope.launch {
      performOperation()
    }
  }
}

performOperation() runs inside a coroutine that automatically cancels when the ViewModel clears. You’ll use this technique to fetch stock recommendations in your app.

Adding the ViewModelScope Dependency

To use ViewModelScope, add the following dependency to the project’s app/build.gradle:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

This will add the necessary KTX extensions to the ViewModel.

Displaying a Stock Recommendation

You can help your user make better investment choices, especially if they’re a novice stock investor, by recommending a stock for them to invest in.

Open GetRecommendedStockUseCase.kt and replace the contents of get() with the following code:

// 1
suspend fun get() = withContext(ioDispatcher) {
  delay(GET_RECOMMENDED_STOCK_DELAY)

  // 2
  _recommendedStock.postValue(RECOMMENDED_STOCKS.random())
}

In this code, you return a stock recommendation for display after a delay. The code will:

  1. Use the IO dispatcher as the function’s coroutine context, since fetching the recommendation would probably involve a network call.
  2. Randomly return any of the stocks in RECOMMENDED_STOCKS.
Note: Use postValue() to set the new value of the recommended stock instead of setValue() since this operation takes place on a thread other than the main thread.

Finally, you’ll need to call GetRecommendedStockUseCase.get() from ProfileViewModel. Replace getRecommendedStock() in ProfileViewModel with the following:

private fun getRecommendedStock() {
  viewModelScope.launch {
    getRecommendedStockUseCase.get()
  }
}

Since there’s no guarantee fetching the recommended stock will complete before the ViewModel clears, launch this operation from the ViewModelScope.

Build and run the app. Now you’ll see the recommended stock on the screen!

ViewModelScope

Click the refresh button next to the recommended stock. Notice nothing happens. You’ll fix that in a moment.

Refreshing the Stock Recommendation

The stock market is unpredictable. Since stock prices rise and fall the recommended stock may change with time. It may even change abruptly, which is why you need to let your user refresh it when they want.

Open GetRecommendedStockUseCase.kt and replace the contents of refresh() with the following:

suspend fun refresh() = withContext(ioDispatcher) {
  _recommendedStock.postValue(REFRESHING)
  get()
}

In the code above, you first update the UI to let your user know the recommended stock is being refreshed because getting the refreshed value may take some time. Then you call get() again to return a random stock.

Next, update refreshRecommendedStock() in ProfileViewModel to use viewModelScope to call the refresh() method since it is now a suspending function:

fun refreshRecommendedStock() {
  viewModelScope.launch {
    getRecommendedStockUseCase.refresh()
  }
}

Build and run the app. Click the refresh button. Notice the message “Refreshing…” before a new recommended stock shows.

ViewModelScope

The app looks great! The total investment amount updates in real-time, the user information is on the screen and the user can interact with the UI to update the stock recommendation.

How can it get better, you ask? Scroll to the next section and find out!

Using Lifecycle-Aware Coroutine Scopes

Every Lifecycle comes with a LifecycleScope, which lets you launch coroutines that are automatically cancelled once the Lifecycle reaches the DESTROYED state.

You can access the LifecycleScope from a Lifecycle using the coroutineScope attribute. You could also access it from a LifecycleOwner, like an Activity or Fragment, using the lifecycleScope attribute.

The LifecycleScope provides convenient methods that are lifecycle-aware and run a block of code. Each method only runs the block once the lifecycle reaches a certain state, based on the name of the method.

  • launchWhenCreated(block)
  • launchWhenStarted(block)
  • launchWhenResumed(block)

Adding the LifecycleScope Dependency

To use LifecycleScope, add the following dependency to the project’s app/build.gradle:

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"

Displaying a Dialog on the Screen’s First Launch

You’re proud of the stock recommendation feature your app provides, and you want to encourage the user to use it. So, display an informative dialog the first time the profile screen launches.

Open ProfileFragment.kt and add the following block, which launches a Dialog the first time the fragment’s launched:

// 1
init {

  // 2
  lifecycleScope.launchWhenResumed {
    if (isFirstProfileLaunch()) {
      displayTipDialog()
    }
  }
}

In the code above, you’re attempting to display the Dialog. This logic runs on the Fragment’s construction.

  1. You use the init() block of the Fragment even though it shouldn’t launch the dialog before it starts and its UI displays. It’s safe because you’re using the ProfileFragment’s LifecycleScope.
  2. Use launchWhenResumed() to ensure the dialog isn’t displayed until ProfileFragment is at least in the resumed state. You also could use launchWhenCreated() or launchWhenStarted(), since displaying the Dialog only requires a Context.

Build and run the app. You’ll see a dialog pop up once the screen displays.

If you close the dialog by clicking the Ok button, it won’t show when you launch the app again. However, if you dismiss it by clicking the back button or elsewhere on the screen, it’ll show the next time you open the app.

LifecycleScope

And there you have it, your awesome trading app’s profile screen!

Where to Go From Here?

Download the final project using the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you learned how to use coroutines with LiveData with the help of the LiveData builder. You also learned how to use lifecycle-aware CoroutineScopes.

This project is a beginning but there’s so much more you can add. For instance, you could replace the dummy data with real data from a server or local database. Or you could make the user information fields editable.

I hope you enjoyed this tutorial! If you have any questions or comments, please join the forum discussion below.

Average Rating

4.6/5

Add a rating for this content

5 ratings

More like this

Contributors

Comments