Manual CoroutineScopes: how to avoid GlobalScope
Usually, when you need to run a suspend
method while developing your Android app, you just use viewModelScope
out of habit. No need to think about which CoroutineScope
to use since all the logic happens in the View Model most of the time.
(Note: Check this out for a quick recap of the Coroutines "vocabulary")
Well, sometimes you need to run async work outside of the View Model. For instance, it's quite common for an external SDK to require an instance of a class for performing an app-specific operation.
In these cases, a common anti-pattern is to use the GlobalScope
coroutine scope. But this API is quite easy to be misused and to create memory leaks. For instance, if you are making a network request (quite a common use-case for an app-specific async operation) and there's a network issue it will stay alive and waste resources.
The ideal approach in these cases is to manually create and manage a CoroutineScope
. Remember that you are responsible for canceling the async work when it's no longer needed to avoid any memory leaks and unnecessary resources allocation.
class AppRelatedProvider @Inject constructor(
private val repository: Repository
) : SdkInterfaceForAppRelatedProvider {
private val scope = CoroutineScope(Dispatchers.IO) // 1.
override fun provideSomething(callback: Callback) {
try {
scope.launch { // 2.
val response = repository.fetchAppRelatedSomething()
callback.onSuccess(response)
}
} catch (e: RepositoryException) {
callback.onFailure(e)
}
}
override fun destroy() {
scope.cancel() // 3.
}
}
- Manually create the
CoroutineScope
. Remember that you can explicitly set the "dispatchers" (i.e. threads) that the async work will run. In case of background/network workDispatchers.IO
should be set instead of the default main thread. - Run your
suspend
function using the manually createdCoroutineScope
. - Don't forget to stop any async work when it's no longer needed. In this case, the
SdkInterfaceForAppRelatedProvider
provides a function for when the object should be destroyed.
Note: In my original example, I initially usedfinalize()
which is called just before the garbage collector destroys an object. Ideally, you should have a more "specific" point in which your async work is no longer needed. Be aware thatfinalize()
might never be called (thanks @broot__) so it's not guaranteed that no leaks will occur.
Hopefully, it's a bit clearer now how to create and manage your own CoroutineScope
s (and to avoid GlobalScope
whenever possible).
Happy coding!