8

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?

Viktor Vostrikov
  • 1,322
  • 3
  • 19
  • 36

3 Answers3

5

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)
        }
    }
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thanks for the informative answer. I did not understand how can I generate a new scope that is dependent on viewModelScope in that execute method? – Viktor Vostrikov May 05 '20 at 13:28
  • 4
    Looks like I've contradicted the recommendation of the team lead of the coroutines library here at the end: https://medium.com/@elizarov/coroutine-context-and-scope-c8b255d59055 He actually suggests putting the scope as a function parameter or receiver as a way to indicate in the signature that the function launches a "fire and forget" type of coroutine. – Tenfour04 May 05 '20 at 13:41
  • But when working in Android there is already a paradigm of always having a main scope (`viewModelScope` or `lifecycleScope`) that's set up to cancel its jobs when the current ViewModel, Fragment, or Activity goes away, so I feel that with Android it's cleaner to encapsulate any other scopes you create. I did add an example of creating a child scope. – Tenfour04 May 05 '20 at 13:44
  • 1
    Since Fragments and ViewModels already encapsulate a chunk of your app's functionality, I think in most cases you don't need to create child scopes because the Fragment/ViewModel itself has already encapsulated a scope of jobs that should all be cancelled if it goes away. You can track individual Jobs within this scope if you need to micro-manage their cancellation. – Tenfour04 May 05 '20 at 13:47
  • `coroutineScope(Dispatchers.IO)` doesn't appear to be able to accept `Dispatchers.IO` as a parameter as of kotlinx.coroutines version 1.6.4. (It only accepts a lambda now.) – Philip Guin Feb 07 '23 at 23:57
  • @PhilipGuin, use `withContext` for that. `withContext` and `coroutineScope` have the same behavior except that the former also lets you modify the context. – Tenfour04 Mar 25 '23 at 16:42
3

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.

Marko Novakovic
  • 141
  • 1
  • 2
  • 3
  • Thanks!. Just a few more questions. I got some other answers and they say that execute(coroutineScope: CoroutineScope, bitmap: Bitmap) is same as CoroutineScope.execute(bitmap: Bitmap). Why then I can't call that second function method in VM? Perhaps I need to call it somehow differentely? Regarding passing coroutineContext. I don't do that stuff myself too often, but in this case it makes sense. That execute function is actually much larger, it even creates async await blocks, so it is easier and more maintainable to pass coroutineScope. – Viktor Vostrikov May 05 '20 at 06:14
  • Inside `ViewModel` you can do `viewModelScope.execute(bitmap) {}` – Marko Novakovic May 07 '20 at 17:39
2

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.

Octavia Togami
  • 4,186
  • 4
  • 31
  • 49