0% found this document useful (0 votes)
33 views

Best Practices For Kotlin Coroutine in Android

The document discusses best practices for using coroutines in Android applications. It recommends injecting dispatchers to make code more testable, creating coroutines in the view model layer to prevent crashes, exposing suspend functions and flows from the data layer to allow callers to control execution, avoiding exposing mutable types, using an external coroutine scope for long-running background tasks, avoiding GlobalScope due to potential issues, making coroutines cancellable to allow stopping operations, and handling exceptions appropriately using jobs and coroutine exception handlers.

Uploaded by

morsi5038
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
33 views

Best Practices For Kotlin Coroutine in Android

The document discusses best practices for using coroutines in Android applications. It recommends injecting dispatchers to make code more testable, creating coroutines in the view model layer to prevent crashes, exposing suspend functions and flows from the data layer to allow callers to control execution, avoiding exposing mutable types, using an external coroutine scope for long-running background tasks, avoiding GlobalScope due to potential issues, making coroutines cancellable to allow stopping operations, and handling exceptions appropriately using jobs and coroutine exception handlers.

Uploaded by

morsi5038
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

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!

You might also like