Rohit Paneliya
Android Developer
Best practices for
coroutines
in Android
Inject Dispatcher
Injecting Dispatcher makes code as you can replace those dispatchers
in unit and instrumentation tests with a test dispatcher to make your
tests more deterministic.
// Good Practice
class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
){
withContext(defaultDispatcher) { /* ... */ }
suspend fun loadNews() = withContext(defaultDispatcher)
}
// Bad Practice
class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
){
withContext(Dispatchers.Default) { /* ... */ }
suspend fun loadNews() = withContext(Dispatchers.Default)
}
ViewModel layer should create coroutines
The UI layer should be dumb; it should not be directly attached to any
business logic. Once the ViewModel gets destroyed, all the coroutines
within the ViewModel scope will be cleared, preventing crashes and
making unit testing easier.
// Good Practice
class LatestNewsViewModel (
private val getLatestNewsUsecase: GetLatestNewsUseCase
): ViewModel() {
fun loadNews() {
viewModelScope.launch
viewModelScope.launch{{ // Launch coroutine within viewModel scope
val latestNewsWithAuthors = getLatestNewsWithAuthors()
}
}
}
// Bad Practice
class LatestNewsViewModel (
private val getLatestNewsUsecase: GetLatestNewsUseCase
): ViewModel() {
fun loadNews() {.........} // View should not trigger coroutine directly
suspendfun
suspend
}
Data and business layer should expose
suspend functions and Flows
The caller (generally the ViewModel layer) can control the execution
and lifecycle of the work happening in those layers, being able to
cancel when needed.
suspend function - for one-shot calls
Flow - to notify about the data changes
// Good Practice
class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
){
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
fun getExamples(): Flow<Example> { /* ... */ }
}
Don't expose mutable types
Prefer exposing immutable types to other classes. This way, all
changes to the mutable type are centralized in one class, making it
easier to debug when something goes wrong.
// Good Practice
class LatestNewsViewModel : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Loading)
val uiState: StateFlow<LatestNewsUiState> = _uiState
/**** Code ****/
}
// Bad Practice
class LatestNewsViewModel : ViewModel() {
// DO NOT expose mutable types
val uiState = MutableStateFlow(LatestNewsUiState.Loading)
/**** Code ****/
}
Creating coroutines in the business and
data layer
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 which
should be managed by a class that lives longer than the current screen
(Application class or a ViewModel scoped to a navigation graph).
// Example
class ArticlesRepository(
private val articlesDataSource: ArticlesDataSource,
private val externalScope: CoroutineScope, // Application class scope
){
// 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
}
}
Avoid GlobalScope
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.
Why we should avoid:
It might be tempting to hardcode Dispatchers if you use
GlobalScope straight away. That is bad practice!
As your code is going to be executed in an uncontrolled scope, the
testing becomes very hard.
GlobalScope operates on the whole application so if not handled
carefully the app crash or memory leak may happen.
// Example
class ArticlesRepository(
private val articlesDataSource: ArticlesDataSource
){
// 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 with GlobalScope
suspend fun bookmarkArticle(article: Article) {
GlobalScope.launch { articlesDataSource.bookmarkArticle(article) }
.join() // Wait for the coroutine to complete
}
}
Make your coroutine cancellable
For non-cancellable coroutines, these problems may happen:
You won’t be able to stop those operations in tests.
An endless loop that uses delay won’t be able to cancel any more.
Collecting a Flow within it makes the Flow non-cancellable from
the outside.
// Bad Practice
class NewsRepository(private val defaultDispatcher = Dispatchers.Default) {
suspend fun loadNews() = withContext(defaultDispatcher) {
withContext(NonCancellable) {
// We are not sure how much time it will take to complete
veryImportantOperation()
}
}
}
AVOID using GlobalScope and withContext(NonCancellable) for any
long-running operation that goes beyond the scope.
Instead, use CoroutineScope from the Application() Class.
Watch out for exceptions
Use Job() - when you want if any single child coroutine fails, it
should also fail the parent coroutine.
Use SupervisorJob() or supervisorScope - when you want the
failure of a child not to affect other children.
Use CoroutineExceptionHandler on a root coroutine to use as a
generic catch block for this room and all of its children.
Uncaught exceptions will always be thrown regardless of the kind
of Job you use.
launch {...} throws the exception as soon as it happens.
async {...} throws the exception only when you call .await().
Was this helpful?
SAVE THIS POST!
Follow me for more Android content!