zak.taccardi
10/25/2019, 8:21 PMviewModelScope
being pushed by the Android team? It requires a bunch of workarounds that would be completely unnecessary if the CoroutineScope
was just injected into the ViewModel
CoroutineScope
was designed so there’s no need to inject dispatchers either, so this type of nasty scope.launch(dispatcher)
is not neededPablichjenkov
10/25/2019, 9:45 PMDragan Stanojević - Nevidljivi
10/25/2019, 9:46 PMzak.taccardi
10/25/2019, 9:46 PMviewModelScope
seems to be an anti-patternCoroutineScope
that is cancelled appropriatelyDragan Stanojević - Nevidljivi
10/25/2019, 9:50 PMviewModelScope
to something different.Dustin Lam
10/25/2019, 9:51 PMzak.taccardi
10/25/2019, 9:51 PMDustin Lam
10/25/2019, 9:51 PMDragan Stanojević - Nevidljivi
10/25/2019, 9:54 PMzak.taccardi
10/25/2019, 9:57 PMclass MyViewModel(scope: CoroutineScope)
The reason viewModelScope
exists so you can get a scope that is associated with a ViewModel
. But what if it was provided to you from a CoroutineViewModelFactory
or something?
instead of:
create(modelClass: Class<T>): ViewModel
you would have:
create(modelClass: Class<T>, scope: CoroutineScope): ViewModel
Dragan Stanojević - Nevidljivi
10/25/2019, 10:01 PMzak.taccardi
10/25/2019, 10:02 PMscope: CoroutineScope
would be cancelled for you, the same way onCleared()
is called for youDragan Stanojević - Nevidljivi
10/25/2019, 10:04 PMzak.taccardi
10/25/2019, 10:05 PMDragan Stanojević - Nevidljivi
10/25/2019, 10:05 PMzak.taccardi
10/25/2019, 10:05 PMCoroutineScope
- it would be created for youClass<T>
or call factory.create
yourselfDragan Stanojević - Nevidljivi
10/25/2019, 10:13 PMviewModelScope
.
If you want to create coroutineScope and be able to pass it to ViewModel through constructor using DI or whatever, framework doesn't know what you intend to do with it or how long it should live. You can freely do it from the introduction of ViewModelProvider.Factory, and you do not need use viewModelScope... no one is forcing you to use viewModelScope the framework gives you.
If you want ViewModel to accept coroutineScope by default through constructor, but create the coroutineScope for you and pass it using that constructor, and manage it, then I don't follow you at all...
let's wait for others to chime in...zak.taccardi
10/25/2019, 10:14 PMthe framework doesn’t know what you intend to do with it or how long it should live.Yes it does. The framework knows when to cancel the scope for you because it knows when to call
viewModel.onCleared()
for youCoroutineScope
was provided for you by the framework factory.
override create(modelClass: Class<T>, scope: CoroutineScope): ViewModel {
if (modelClass == MyViewModel::class.java){
return MyViewModel(scope)
}
// ..
]
scope
above would be cancelled for you at the same time onCleared()
would currently be calleditnoles
10/25/2019, 10:25 PMzak.taccardi
10/25/2019, 10:26 PMitnoles
10/25/2019, 10:28 PMDragan Stanojević - Nevidljivi
10/25/2019, 10:30 PMviewModelScope
. That's fine...
If I passed coroutineScope, I want to be responsible for it's own lifecycle. I created it, I own it, I'm responsible for it.
Maybe particular ViewModel is used for logical portion of UI, say not even a screen's fragment but a nested fragment. If I passed scope which I'd like to reuse, I definitively don't want ViewModel to kill it on it's onCleared()
Therefore my argument, framework cannot know when to cancel it. It's passed by constructor. ViewModel doesn't know if it was instantiated with ViewModelProvider.Factory, or manually, or in test or with DI, or...
Only edge case classes should be responsible for objects passed to it. Otherwise they should stay clear...
If I have Car and Engine objects, and instantiate the Car with Car(instanceOfEngine), I don't want resulting instance of car to kill/sell/do whatever with Engine instance. I'm responsible for it, not Car. What you're saying is Car knows exactly when to kill the Engine instance. I don't really see how it can know it...zak.taccardi
10/25/2019, 10:30 PMCoroutineScope
that is scoped to the lifecycle of the ViewModel
CoroutineScope
, not developerviewModelScope
possible. This complex workaround would be unnecessary if the scope from runBlockingTest { }
could be injected into the ViewModel
Dragan Stanojević - Nevidljivi
10/25/2019, 10:37 PMviewModelScope
.
What's with the constructor passing if the framework will create it anyway?
It's like forcing framework's Car class to require Engine instance in it's constructor, but framework will always create this Engine instance for you. What's the point of passing by constructor then?zak.taccardi
10/25/2019, 10:47 PMparticular ViewModel is used for logical portion of UI, say not even a screen's fragment but a nested fragment. If I passed scope which I'd like to reuse, I definitively don't want ViewModel to kill it on it's onCleared()If your
ViewModel
outlives onCleared()
due to your UI being permanently destroyed , then you shouldn’t be using the `ViewModel`from Jetpack. You should define your own. In fact, this is what I do.
Jetpack’s ViewModel
uses inheritance (bad), where if you just inject a CoroutineScope
into a custom interface ViewModel
(not the Jetpack ViewModel), then you have a lot more flexibility because the lifetime of the ViewModel
becomes an implementation detail of how you provide the ViewModel and the CoroutineScope
injected into it.
I was requesting that a new CoroutineViewModelProvider.Factory
be created that would allow us to scope a ViewModel
to the UI (like ViewModelProvider.Factory
currently does), but instead of just providing a Class<T>
, also provide us with a CoroutineScope
that would be cancelled when the activity/fragment is permanently destroyed (when .onCleared()
) is called. We would never be calling scope.cancel()
ourselves, because that would be managed by the framework due us using CoroutineViewModelProvider.Factory
What's with the constructor passing if the framework will create it anyway?because then you can easily swap out the framework provided
CoroutineScope
when testing.
runBlockingTest {
val viewModel = MyViewModel(scope = this)
// now test your view model
}
Dragan Stanojević - Nevidljivi
10/25/2019, 10:55 PMManuel Vivo
10/25/2019, 11:04 PMviewModelScope
reduces the boilerplate code of having to create and manage your own scope in the ViewModel. Not only reduces the boilerplate code but also reduces the chances of making a mistake (e.g. configuring it with a Job
instead of a SupervisorJob
). If you know what you're doing, you don't need it but there's a lot of people who find it useful.
If you have other use case in which your coroutines need to run on a lifecycle different than the VM, then it's fine to get it injected (but this is a code smell, that code shouldn't be triggered by the VM). If not, you still need some sort of logic to get that scope cancelled when the VM is destroyed (which you get for free with vmScope)Dragan Stanojević - Nevidljivi
10/25/2019, 11:09 PMIf yourWhat you missed here is my explanation that ViewModel and coroutineScope passed through constructor don't need to have the same lifetime. See at around 2:58 for a relevant slide. We have many ViewModels and hence related coroutineScopes. Again, nobody is forcing you to useoutlivesViewModel
due to your UI being permanently destroyed , then you shouldn’t be using the `ViewModel`from Jetpack. You should define your own. In fact, this is what I do.onCleared()
viewModelScope
provided by library. You can create your own, pass it through the constructor, be it in app or in test, and use only that one.
But for the n-th time, if you pass any object through constructor, it's your responsibility to end it. With tests it's easy, at the end of the test you can cancel it, but you can't do it from Activities and Fragments because they may have shorter lifespan then the ViewModel itself.
What they described in the testing session explains why they use TestCoroutineDispatcher(), Dispatchers.setMain(), Dispatchers.resetMain() and how testDispatcher.cleanupTestCoroutine() weather entered manually or through JUnit4 Rule, catches leaked and/or unfinished coroutine scopes.
You can manually do all that manually. I personally would rather not, and thus prefer frameworks viewModelScope
and testing Rule how it's proposed.zak.taccardi
10/25/2019, 11:12 PMDragan Stanojević - Nevidljivi
10/25/2019, 11:15 PMviewModelScope
which is a convenience coroutineScope which is managed by library. You can provide your own, but then you need to manage it on your own... What you want you already can do anyway. It's just that you don't get library to hold your hand and cancel the passed coroutineScope for you.zak.taccardi
10/25/2019, 11:16 PMBut for the n-th time, if you pass any object through constructor, it's your responsibility to end it.No, it's not. It's not your responsibility to call
onCleared()
. Take the runBlocking
example below.
runBlocking {
val scope: CoroutineScope = this@runBlocking
}
scope
is provided to you as this
and you do not need to manage manually calling scope.cancel()
yourself
When you use runBlocking { }
, it's not your responsibility to cancel the scope provided to youManuel Vivo
10/25/2019, 11:18 PMDragan Stanojević - Nevidljivi
10/25/2019, 11:20 PMIt's not your responsibility to call!= You're responsible for objects you've created and passed through constructoronCleared()
zak.taccardi
10/25/2019, 11:20 PMWhat you want you already can do anyway. It's just that you don't get library to hold your hand and cancel the passed coroutineScope for you.Correct. I'm requesting that a library is provided to make achieving this as easy as possible, because it's the best solution.
viewModelScope
is great when you aren't testing, but when you do need to test it has issues because you cannot constructor inject a different Coroutine scope easilyCoroutineScope
. It would be provided by `CoroutineViewModelProvider.Factory`in production, or runBlockingTest { }
while under testviewModelScope
definitely makes sense.
But I'm advocating that a separate CoroutineViewModelProvider.Factory
could exist that would provide the proper scope at instantiation time
I'll create an issue to track this at some point in the futureDragan Stanojević - Nevidljivi
10/25/2019, 11:33 PMclass MyViewModel(val testingScope: CoroutineScope?): ViewModel() {
val myScope = testingScope ?: viewModelScope
...
in every ViewModel + ViewModelProvider.FactoryViewModelFactory
vs
@get:Rule val coroutineRule = MainCoroutineRule()
Manuel Vivo
10/25/2019, 11:37 PMzak.taccardi
10/26/2019, 12:30 AMviewModelScope
for those that want to not leverage DI, but a special solution to inject a CoroutineScope
with a special factory would be awesomestreetsofboston
10/26/2019, 12:44 AMrkeazor
10/26/2019, 1:36 PMstreetsofboston
10/26/2019, 2:00 PMrkeazor
10/26/2019, 2:22 PMzak.taccardi
10/26/2019, 2:27 PMTestCoroutineDispatcher
)streetsofboston
10/26/2019, 2:27 PMzak.taccardi
10/26/2019, 2:28 PMstreetsofboston
10/26/2019, 2:29 PMzak.taccardi
10/26/2019, 2:30 PMstreetsofboston
10/26/2019, 2:32 PMrkeazor
10/26/2019, 2:32 PMzak.taccardi
10/26/2019, 2:32 PMstreetsofboston
10/26/2019, 2:34 PMrkeazor
10/26/2019, 2:34 PMzak.taccardi
10/26/2019, 2:35 PMstreetsofboston
10/26/2019, 2:35 PMrkeazor
10/26/2019, 2:36 PMzak.taccardi
10/26/2019, 2:36 PM.setMain(..)
is used to change the dispatcher to one that works for testing so it's not the main
dispatcher anymorerkeazor
10/26/2019, 2:36 PMzak.taccardi
10/26/2019, 2:37 PMrkeazor
10/26/2019, 2:37 PMstreetsofboston
10/26/2019, 2:37 PMrkeazor
10/26/2019, 2:38 PMstreetsofboston
10/26/2019, 2:39 PMrkeazor
10/26/2019, 2:40 PMzak.taccardi
10/26/2019, 2:41 PMCoroutineScope
and dispatchers and haven't had any difficulty testing Coroutines associated with thatstreetsofboston
10/26/2019, 2:41 PMrkeazor
10/26/2019, 2:41 PMzak.taccardi
10/26/2019, 2:42 PMrkeazor
10/26/2019, 2:42 PMstreetsofboston
10/26/2019, 2:42 PMrkeazor
10/26/2019, 2:42 PMzak.taccardi
10/26/2019, 2:42 PMviewModel.onCleared()
rkeazor
10/26/2019, 2:42 PMstreetsofboston
10/26/2019, 2:43 PMrkeazor
10/26/2019, 2:43 PMzak.taccardi
10/26/2019, 2:43 PM.onCleared()
. I also use the default dispatcher to parallelize workstreetsofboston
10/26/2019, 2:43 PMMainScope()
rkeazor
10/26/2019, 2:44 PMzak.taccardi
10/26/2019, 2:44 PMstreetsofboston
10/26/2019, 2:44 PMzak.taccardi
10/26/2019, 2:46 PMViewModel
can be used as an object graph for dependencies associated with the associated activity or fragment's full lifetime across config changesstreetsofboston
10/26/2019, 2:46 PMclass MyViewModel(private val scope: CoroutineScope) : ViewModel() {
...
override fun onCleared () { scope.cancel() }
}
zak.taccardi
10/26/2019, 2:47 PMCoroutineScope
, there's actually no reason to use onCleared()
, because you can leverage scope.coroutineContext[Job]!!.invokeOnCompletion { }
So there becomes no need to extend Jetpack's ViewModel
streetsofboston
10/26/2019, 2:48 PMrkeazor
10/26/2019, 2:50 PMstreetsofboston
10/26/2019, 2:52 PMrkeazor
10/26/2019, 2:53 PMstreetsofboston
10/26/2019, 2:54 PMrkeazor
10/26/2019, 2:56 PMstreetsofboston
10/26/2019, 2:59 PM@Inject @MainScope val scope: CoroutineScope
in your ViewModel constructor.rkeazor
10/26/2019, 3:00 PMstreetsofboston
10/26/2019, 3:01 PMzak.taccardi
10/26/2019, 3:06 PMCoroutineScope
is very scalable (composition/delegation instead of inheritance)rkeazor
10/26/2019, 3:07 PMzak.taccardi
10/26/2019, 3:07 PMrkeazor
10/26/2019, 3:10 PMzak.taccardi
10/26/2019, 3:11 PMrkeazor
10/26/2019, 3:11 PMzak.taccardi
10/26/2019, 3:11 PMrkeazor
10/26/2019, 3:13 PMzak.taccardi
10/26/2019, 3:21 PMrkeazor
10/26/2019, 3:22 PMzak.taccardi
10/26/2019, 3:28 PMrkeazor
10/26/2019, 3:31 PMjw
10/26/2019, 4:19 PMthere is a complex workaround to make unit testing ... possibleit's The Jetpack Way™