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.
    }
}
  1. 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 work Dispatchers.IO should be set instead of the default main thread.
  2. Run your suspend function using the manually created CoroutineScope.
  3. 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 used finalize() 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 that finalize() 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 CoroutineScopes (and to avoid GlobalScope whenever possible).

Happy coding!