https://kotlinlang.org logo
Title
u

ubu

05/01/2020, 9:30 AM
Hi guys. There this problem with
viewModelScope
from Android Architecture Components’
ViewModel
that i’ve been thinking over lately. I run a lot of background operations inside some
ViewModel
, because I use its
ViewModelScope
. These operations are
use-cases
injected into constructor of this
ViewModel
. For a better separation of concerns I would like to extract some of these operations in some other class that I would then inject in this
ViewModel
, but in order to run these operations, I need that
ViewModelScope
. Is there a way to provide it to injected components without passing the scope every time in some function signature?
e

Erik

05/01/2020, 9:36 AM
Make the injected components' functions
suspend fun
, so the view model can call these from its
viewModelScope
. Then there's no need to pass a
CoroutineScope
, because these functions can only be called from a coroutine scope. Does this fit your case?
👍 1
u

uli

05/01/2020, 9:36 AM
A common pattern would be to pass the scope as receiver to every method that creates new coroutines. i.e. make your 'launching' methods extensions on
CoroutineScope
.
u

ubu

05/01/2020, 9:39 AM
@Erik, unfortunately, in our current implementation, in order to invoke a use case, we need to pass a
CoroutineScope
abstract class BaseUseCase<out Type, in Params>(
    private val context: CoroutineContext = <http://Dispatchers.IO|Dispatchers.IO>
) where Type : Any {

    abstract suspend fun run(params: Params): Either<Throwable, Type>

    open operator fun invoke(
        scope: CoroutineScope,
        params: Params,
        onResult: (Either<Throwable, Type>) -> Unit = {}
    ) {
        val job = scope.async(context) { run(params) }
        scope.launch { onResult(job.await()) }
    }

    object None
}
e

Erik

05/01/2020, 9:39 AM
Or what Uli says, depending on if your components' can return quickly or return values
u

ubu

05/01/2020, 9:40 AM
and an example of usage:
private fun proceedWithGettingAccount() {
    getCurrentAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
        result.either(
            fnL = { Timber.e(it, "Error while getting account") },
            fnR = { account ->
                _profile.postValue(ProfileView(name = account.name))
                loadAvatarImage(account)
            }
        )
    }
}
e

Erik

05/01/2020, 9:41 AM
The
invoke
operator could be a
suspend fun
too, and it can return any type. It would then run asynchronously, depending on the scope's context, but you can switch context e.g. using
withContext(context) { ... }
u

ubu

05/01/2020, 9:42 AM
@uli, could you give an example?
e

Erik

05/01/2020, 9:43 AM
fun CoroutineScope.myBuilder() = async { ... }
u

uli

05/01/2020, 9:53 AM
private fun CoroutineScope.proceedWithGettingAccount() {
    getCurrentAccount.invoke(BaseUseCase.None) {
        ...
    }
}

fun CoroutineScope.getCurrentAccount(....
viewModelScope.proceedWithGettingAccount()
u

ubu

05/01/2020, 9:57 AM
thanks! as far as i understand, this means that i have to put all these extensions inside
ViewModel
, whereas I would like to abstract threading logic in my
Domain
module.
e

Erik

05/01/2020, 9:59 AM
The extensions can go in your domain
The coroutine scope is provided by the view model
u

ubu

05/01/2020, 10:03 AM
in @uli’s example, there is a use case instance already available (because it’s injected in vm), i actually don’t get where i would put this extension in use-case implementation.
/**
 * Use-case for starting downloading files.
 * @see Params
 */
class DownloadFile(
    private val downloader: Downloader,
    context: CoroutineContext
) : BaseUseCase<Unit, Params>(context) {

    override suspend fun run(params: Params) = try {
        downloader.download(
            url = params.url,
            name = params.name
        ).let { Either.Right(it) }
    } catch (t: Throwable) {
        Either.Left(t)
    }

    /**
     * Params for downloading file.
     * @property name file name
     * @property url url of the file to download
     */
    data class Params(
        val name: String,
        val url: Url
    )
}
abstract class BaseUseCase<out Type, in Params>(
    private val context: CoroutineContext = <http://Dispatchers.IO|Dispatchers.IO>
) where Type : Any {

    abstract suspend fun run(params: Params): Either<Throwable, Type>

    open operator fun invoke(
        scope: CoroutineScope,
        params: Params,
        onResult: (Either<Throwable, Type>) -> Unit = {}
    ) {
        val job = scope.async(context) { run(params) }
        scope.launch { onResult(job.await()) }
    }

    object None
}
invocation:
downloadFile.invoke(
    scope = viewModelScope,
    params = DownloadFile.Params(
        url = urlBuilder.file(file.hash),
        name = file.name.orEmpty()
    )
) { result ->
    result.either(
        fnL = { Timber.e(it, "Error while trying to download file: $file") },
        fnR = { Timber.d("Started download file: $file") }
    )
}
as you can see, execution context is injected, but vm calls method from BaseUseCase class.
u

uli

05/01/2020, 10:32 AM
open operator fun invoke(
        scope: CoroutineScope,
        params: Params,
        onResult: (Either<Throwable, Type>) -> Unit
Would become
open operator fun CoroutineScope.invoke(
        params: Params,
        onResult: (Either<Throwable, Type>) -> Unit
You can do that inside
BaseUseCase
btw, just as a side note. why do you use a callback in
invoke
? Instead you could also make it suspend and just return
Either
.
👆 2
u

ubu

05/01/2020, 10:38 AM
thanks, guys!
u

uli

05/01/2020, 11:06 AM
If you don't want to make
invoke
suspend, you can still simplify:
{
         scope.launch {        val result = withContext(context) { run(params) }
onResult(result) }
    }
`async`is primarily for starting multiple jobs in paralel, then waiting for all
u

ubu

05/01/2020, 11:17 AM
@uli, I used callback because I don’t know how to return type, not a coroutine job.
u

uli

05/01/2020, 11:39 AM
open suspend operator fun invoke(params: Params) : Either<Throwable, Type> = withContext(context) {
    run(params)
}
👍 1
Use like this:
viewModelScope.launch {
    var result = downloadFile.invoke(
        params = DownloadFile.Params(
            url = urlBuilder.file(file.hash),
            name = file.name.orEmpty()
				)
    )
    result.either(
        fnL = { Timber.e(it, "Error while trying to download file: $file") },
        fnR = { Timber.d("Started download file: $file") }
    )
}
👍 1
This way, you also eliminate the original issue of needing the VM-scope in BaseUsecase
because you are now launching inside the VM
u

ubu

05/01/2020, 1:56 PM
thanks a lot, @uli, this seems to be what i’ve been looking for, great!
this way I also no longer need
CoroutineScope
extensions
👍 1