Best Practices For Coroutines
Best Practices For Coroutines
Android
This page presents several best practices that have a positive impact by making your app
more scalable and testable when using coroutines.
Note: These tips can be applied to a broad spectrum of apps. However, you should treat them as
guidelines and adapt them to your requirements as needed.
Inject Dispatchers
Don't hardcode Dispatchers when creating new coroutines or calling withContext .
// DO inject Dispatchers
class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}
This dependency injection pattern makes testing easier as you can replace those
dispatchers in unit and instrumentation tests with a test dispatcher
(#test-coroutine-dispatcher) to make your tests more deterministic.
Note: While you might have seen hardcoded dispatchers in code snippets across this site and our
codelabs, this is only to keep the sample code concise and simple. In your application, you should
inject dispatchers.
// This use case fetches the latest news and the associated author.
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) {
// This method doesn't need to worry about moving the execution of the
// coroutine to a different thread as newsRepository is main-safe.
// The work done in the coroutine is lightweight as it only creates
// a list and add elements to it
suspend operator fun invoke(): List<ArticleWithAuthor> {
val news = newsRepository.fetchLatestNews()
This pattern makes your app more scalable, as classes calling suspend functions don't
have to worry about what Dispatcher to use for what type of work. This responsibility
lies in the class that does the work.
fun loadNews() {
viewModelScope.launch {
val latestNewsWithAuthors = getLatestNewsWithAuthors()
_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
}
}
}
// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
// DO NOT do this. News would probably need to be refreshed as well.
// Instead of exposing a single value with a suspend function, news shoul
// be exposed using a stream of data as in the code snippet above.
suspend fun loadNews() = getLatestNewsWithAuthors()
}
Views shouldn't directly trigger any coroutines to perform business logic. Instead, defer
that responsibility to the ViewModel . This makes your business logic easier to test as
ViewModel objects can be unit tested, instead of using instrumentation tests that are
required to test views.
In addition to that, your coroutines will survive configuration changes automatically if the
work is started in the viewModelScope . If you create coroutines using lifecycleScope
instead, you'd have to handle that manually. If the coroutine needs to outlive the
ViewModel 's scope, check out the Creating coroutines in the business and data layer
section (#create-coroutines-data-layer).
Note: Views should trigger coroutines for UI-related logic. For example, fetching an image from the
Internet or formatting a String.
/* ... */
}
/* ... */
}
This best practice makes the caller, generally the presentation layer, able to control the
execution and lifecycle of the work happening in those layers, and cancel when needed.
If the work to be done in those coroutines is relevant only when the user is present on the
current screen, it should follow the caller's lifecycle. In most cases, the caller will be the
ViewModel, and the call will be cancelled when the user navigates away from the screen
and the ViewModel is cleared. In this case, coroutineScope
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-
scope.html)
or supervisorScope
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-
scope.html)
should be used.
class GetAllBooksAndAuthorsUseCase(
private val booksRepository: BooksRepository,
private val authorsRepository: AuthorsRepository,
) {
suspend fun getBookAndAuthors(): BookAndAuthors {
// In parallel, fetch books and authors and return when both requests
// complete and the data is ready
return coroutineScope {
val books = async { booksRepository.getAllBooks() }
val authors = async { authorsRepository.getAllAuthors() }
BookAndAuthors(books.await(), authors.await())
}
}
}
If the work to be done is relevant as long as the app is opened, and the work is not bound
to a particular screen, then the work should outlive the caller's lifecycle. For this
scenario, an external CoroutineScope should be used as explained in the Coroutines &
Patterns for work that shouldn’t be cancelled blog post
(https://medium.com/androiddevelopers/coroutines-patterns-for-work-that-shouldnt-be-cancelled-
e26c40f142ad)
.
class ArticlesRepository(
private val articlesDataSource: ArticlesDataSource,
private val externalScope: CoroutineScope,
) {
// As we want to complete bookmarking the article even if the user moves
// away from the screen, the work is done creating a new coroutine
// from an external scope
suspend fun bookmarkArticle(article: Article) {
externalScope.launch { articlesDataSource.bookmarkArticle(article) }
.join() // Wait for the coroutine to complete
}
}
externalScope should be created and managed by a class that lives longer than the
current screen, it could be managed by the Application class or a ViewModel scoped to
a navigation graph.
Inject TestDispatchers in tests
An instance of TestDispatcher
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-
dispatcher/index.html)
should be injected into your classes in tests. There are two available implementations in
the kotlinx-coroutines-test library
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/):
StandardTestDispatcher
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-
standard-test-dispatcher.html)
: Queues up coroutines started on it with a scheduler, and executes them when the
test thread is not busy. You can suspend the test thread to let other queued
coroutines run using methods such as advanceUntilIdle
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-
test/kotlinx.coroutines.test/advance-until-idle.html)
.
UnconfinedTestDispatcher
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-
unconfined-test-dispatcher.html)
: Runs new coroutines eagerly, in a blocking way. This generally makes writing tests
easier, but gives you less control over how coroutines are executed during the test.
class ArticlesRepositoryTest {
@Test
fun testBookmarkArticle() = runTest {
// Pass the testScheduler provided by runTest's coroutine scope to
// the test dispatcher
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
val articlesDataSource = FakeArticlesDataSource()
val repository = ArticlesRepository(
articlesDataSource,
testDispatcher
)
val article = Article()
repository.bookmarkArticle(article)
assertThat(articlesDataSource.isBookmarked(article)).isTrue()
}
}
All TestDispatchers should share the same scheduler. This allows you to run all your
coroutine code on the single test thread to make your tests deterministic. runTest will
wait for all coroutines that are on the same scheduler or are children of the test
coroutine to complete before returning.
Note: The above works best if no other Dispatchers are used in the code under test. This is why it's
not recommended to hardcode Dispatchers in your classes.
Note: The coroutine testing APIs changed significantly in kotlinx.coroutines 1.6.0. See the migration
guide
(https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md) if
you need to migrate from the previous testing APIs.
Avoid GlobalScope
This is similar to the Inject Dispatchers best practice. By using GlobalScope
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-
scope/index.html)
, you're hardcoding the CoroutineScope that a class uses bringing some downsides with
it:
You can't have a common CoroutineContext to execute for all coroutines built into
the scope itself.
Instead, consider injecting a CoroutineScope for work that needs to outlive the current
scope. Check out the Creating coroutines in the business and data layer section
(#create-coroutines-data-layer) to learn more about this topic.
For example, if you're reading multiple files from disk, before you start reading each file,
check whether the coroutine was cancelled. One way to check for cancellation is by
calling the ensureActive
(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-
active.html)
function.
someScope.launch {
for(file in files) {
ensureActive() // Check for cancellation
readFile(file)
}
}
All suspend functions from kotlinx.coroutines such as withContext and delay are
cancellable. If your coroutine calls them, you shouldn't need to do any additional work.
For more information about cancellation in coroutines, check out the Cancellation in
coroutines blog post
(https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629).
class LoginViewModel(
private val loginRepository: LoginRepository
) : ViewModel() {
For more information, check out the blog post Exceptions in coroutines
(https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c), or Coroutine
exceptions handling (https://kotlinlang.org/docs/exception-handling.html) in the Kotlin
documentation.
Content and code samples on this page are subject to the licenses described in the Content License
(/license). Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.