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:
- ViewModelScope →
androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
or higher - LifecycleScope →
androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
or higher - liveData →
androidx.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:
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