I am experimenting with coroutines and feel unsure about passing coroutineScope to plain Kotlin UseCase. Can such approach create memory leaks?
Suppose we are initialising our UseCase in VM and will try to pass viewModelScope:
class UploadUseCase(private val imagesPreparingForUploadUseCase: ImagesPreparingForUploadUseCase){
fun execute(coroutineScope: CoroutineScope, bitmap: Bitmap) {
coroutineScope.launch {
val resizedBitmap = withContext(Dispatchers.IO) {
imagesPreparingForUploadUseCase.getResizedBitmap(bitmap, MAX_SIZE)
}
}
}
}
Is it safe code? No difference if I would declare this exact code in VM instead?If no, that means I could pass coroutineScope as constructor argument....Now I initially thought that I should create my execute method in a following way:
fun CoroutineScope.execute(bitmap: Bitmap) {
launch {
val resizedBitmap = withContext(Dispatchers.IO) {
imagesPreparingForUploadUseCase.getResizedBitmap(bitmap, MAX_SIZE)
}
}
}
}
As far as I understand we use extension function in order for method to use parent coroutineScope. That means, I don't need to pass coroutineScope as argument and just change method to use extension function.
However, in my surprise VM cannot see this method available! Why this method is not available from VM to call?
This is marked as red in VM:
private fun uploadPhoto(bitmap: Bitmap, isImageUploaded: Boolean) {
prepareDataForUploadingUseCase.execute(bitmap)
}
This is not marked red from VM:
private fun uploadPhoto(bitmap: Bitmap, isImageUploaded: Boolean) {
prepareDataForUploadingUseCase.execute(viewModelScope, bitmap)
}
If my understanding is wrong, why would I use CoroutineScope as extension function instead of passing coroutineScope as function argument?
Kotlin coroutines provide an API that enables you to write asynchronous code. With Kotlin coroutines, you can define a CoroutineScope , which helps you to manage when your coroutines should run. Each asynchronous operation runs within a particular scope.
Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely. Active coroutines launched in GlobalScope do not keep the process alive. They are like daemon threads.
The difference between a context and a scope is in their intended purpose. It is defined as extension function on CoroutineScope and takes a CoroutineContext as parameter, so it actually takes two coroutine contexts (since a scope is just a reference to a context).
It will not finish the entire uiScope [ Exceptional Exceptions for Coroutines made easy…? ]. In other words; the calling Coroutine will be canceled, but the CoroutineScope in which it runs remains active.
The main difference between these two scopes is that the MainScope () uses Dispatchers.Main for its coroutines, making it perfect for UI components, and the CoroutineScope () uses Dispatchers.Default by default. Another difference is that CoroutineScope () takes in a CoroutineContext as a parameter.
How can we use CoroutineScopes in Kotlin? Kotlin’s Coroutines allow the use of suspend functions, Channels and Flows and they all operate in the context of a so-called CoroutineScope. How can we tie it to the lifecycle management of our own components?
I hope you learned a bit more how CoroutineScopes can be leveraged to manage the lifecycles of the asynchronous tasks of your application’s components. The fact that CoroutineScopes implement Structured Concurrency alleviates a lot of the burden dealing with cancelations, exception handling and avoiding runtime- and memory-leaks.
Passing it as a parameter vs using it as an extension function receiver is effectively the same in the end result. Extension function receivers are basically another parameter that you are passing to the function, just with rearranged syntax for convenience. So you can't use an extension function as a "cheat" to avoid passing a receiver.
But either way, I see it as kind of a clumsy design to have to provide a scope and then hiding the coroutine setup inside the function. This results in spreading coroutine scope manipulation across both sides of the function barrier. The function that calls this function has to be aware that some coroutine is going to get called on the scope it passes, but it doesn't know whether it needs to worry about how to handle cancellation and what it's allowed to do with the scope that it passed.
In my opinion, it would be cleaner to either do this:
suspend fun execute(bitmap: Bitmap) = withContext(Dispatchers.IO) {
imagesPreparingForUploadUseCase.getResizedBitmap(bitmap, MAX_SIZE)
}
so the calling function can launch the coroutine and handle the entire coroutine in one place. Or pass no coroutine scope, but have the execute
function internally generate its own scope (that is dependent on lifecycleScope
or viewModelScope
if applicable), and handle its own cancellation behavior. Here's an example of creating a child scope of the lifecycle scope and adding it to some collection of jobs that you might want to cancel under certain circumstances.
fun execute(bitmap: Bitmap) {
lifecycleScope.launch {
bitmapScopes += coroutineScope(Dispatchers.IO) {
imagesPreparingForUploadUseCase.getResizedBitmap(bitmap, MAX_SIZE)
}
}
}
I am answering this specific question: "Why this method is not available from VM to call?"
The method is not available because it takes a receiver (CoroutineScope
), but you already have an implicit receiver due to being inside a type declaration: UploadUseCase
. Therefore, you cannot just call the second form of the method, because you would somehow have to specify two receivers.
Luckily, Kotlin provides an easy way to do exactly that, the with
method.
private fun uploadPhoto(bitmap: Bitmap, isImageUploaded: Boolean) {
with(prepareDataForUploadingUseCase) {
viewModelScope.execute(bitmap)
}
}
However, I would say that this is quite weird, and agree with @Marko Novakovic that you should remove this responsibility from UseCase
.
You can pass CoroutineScope
as a function parameter, no problem with that. However I would advise you to remove that responsibility from UseCase
. Launch coroutines from ViewModel
, Presenter
etc.
Extension functions are to be called on the instance of extension type. You don't need to call launch {}
and withContext
inside same function. Do either. launch(Dispatchers.IO) {}
.
Extension functions are not just to access parent scope, you can use them for whatever you need them for, you choose.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With