Use Kotlin coroutines with lifecycle-aware components

CoroutinesLifecycleLiveData

Kotlin coroutines let you write asynchronous code more easily. You can use a CoroutineScope to control when your coroutines start and stop. Each asynchronous task runs within a specific scope.

Lifecycle-aware components support coroutines and work well with LiveData. This guide explains how to use coroutines with these components effectively.

Add KTX Dependencies

To use the built-in coroutine scopes, add the required KTX extensions to your project:

  • ViewModelScopeandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 or higher
  • LifecycleScopeandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0 or higher
  • liveDataandroidx.lifecycle:lifecycle-livedata-ktx:2.4.0 or higher

Lifecycle-Aware Coroutine Scopes

Lifecycle-aware components provide built-in coroutine scopes for better resource management in your app.

ViewModelScope

Each ViewModel has a ViewModelScope. Coroutines started in this scope are automatically canceled when the ViewModel is cleared.

This is useful for tasks that should only run while the ViewModel is active. For example, if you’re calculating data for a layout, running it in ViewModelScope ensures that the work stops when the ViewModel is no longer needed, preventing unnecessary resource use.

You can access ViewModelScope using the viewModelScope property inside your ViewModel. Here’s an example:

class MyViewModel: ViewModel() { 
    init { 
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared. 
        } 
    } 
}

LifecycleScope

Each Lifecycle object has a LifecycleScope. Coroutines started in this scope are automatically canceled when the Lifecycle is destroyed.

You can access this scope using:

  • lifecycle.coroutineScope
  • lifecycleOwner.lifecycleScope

The example below shows how to use lifecycleOwner.lifecycleScope to create precomputed text asynchronously:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

Restartable Lifecycle-Aware Coroutines

The lifecycleScope helps cancel long-running tasks when the Lifecycle is DESTROYED, but sometimes you need more control.

For example, you may want to start collecting a flow when the Lifecycle is STARTED and stop when the Lifecycle is STOPPED. This ensures the flow runs only when the UI is visible, saving resources and preventing crashes.

To handle this, Lifecycle and LifecycleOwner provide the repeatOnLifecycle API. It runs a code block whenever the Lifecycle reaches the STARTED state and cancels it when the Lifecycle is STOPPED.

Here’s an example:

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

Lifecycle-Aware Flow Collection

If you need to collect data from a single flow while following the Lifecycle, use Flow.flowWithLifecycle() to make your code simpler.

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

If you need to collect multiple flows at the same time, each flow should run in a separate coroutine. In this case, using repeatOnLifecycle() is more efficient.

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

Use Coroutines with LiveData

Sometimes, when using LiveData, you need to calculate values asynchronously. For example, retrieving a user’s preferences before displaying them in the UI.

In these cases, you can use the liveData builder to call a suspend function and return the result as a LiveData object.

In the example below, loadUser() is a suspend function. The liveData builder runs it asynchronously and uses emit() to send the result:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

How liveData Works

The liveData builder connects coroutines with LiveData and manages execution automatically:

  • It starts running only when the LiveData is active.
  • If the LiveData becomes inactive, the coroutine stops after a timeout.
  • If it was canceled automatically, it restarts when LiveData becomes active again.
  • If it finished successfully before, it won’t restart.
  • If canceled manually (e.g., due to an exception), it won’t restart.

You can also emit multiple values. Each emit() call pauses the coroutine until the LiveData value is updated on the main thread.

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

You can also combine liveData with Transformations, as shown in the following example:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

You can send multiple values in LiveData using emitSource(). Call it whenever you need to update the value.

Note: Each time you use emit() or emitSource(), it replaces the previous value.

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

Conclusion

Kotlin coroutines make it easier to write asynchronous code in Android apps by providing structured concurrency and lifecycle awareness. By using ViewModelScope and LifecycleScope, you can ensure coroutines run only when needed, preventing memory leaks and unnecessary resource usage.

For Flow and LiveData, lifecycle-aware collection methods like repeatOnLifecycle() and flowWithLifecycle() help optimize performance by only running tasks when the UI is visible. Additionally, the liveData builder simplifies integrating coroutines with LiveData, allowing you to emit values efficiently.

By following these best practices, you can build more responsive, efficient, and maintainable Android apps while leveraging the full power of Kotlin coroutines.

Leave a Reply

Your email address will not be published. Required fields are marked *