zak.taccardi
04/13/2020, 6:18 PMCoroutineDispatcher
for LifecycleCoroutineScope
in production?
Seems it’s hardcoded to Dispatchers.Main.immediate
and SupervisorJob
which is something I desire flexibility around
https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescopewasyl
04/13/2020, 6:24 PMzak.taccardi
04/13/2020, 6:24 PMzak.taccardi
04/13/2020, 6:25 PMDispatchers.Main.immediate
is a violation of thatwasyl
04/13/2020, 6:26 PMwe provide the ability to inject the dispatchersYes but why 😄
wasyl
04/13/2020, 6:27 PMzak.taccardi
04/13/2020, 6:27 PMCoroutineName
for debugging purposes, who knowsfatih
04/13/2020, 6:34 PMzak.taccardi
04/13/2020, 6:35 PMif you are using daggerno
fatih
04/13/2020, 6:35 PMzak.taccardi
04/13/2020, 6:36 PMcreating custom annotation for each dispatcherNot using reflection, java 8, or annotation processing
wasyl
04/13/2020, 6:37 PMzak.taccardi
04/13/2020, 6:38 PMzak.taccardi
04/13/2020, 6:39 PMwasyl
04/13/2020, 6:42 PMzak.taccardi
04/13/2020, 6:49 PMFragment
, you can inject your own ViewModel
zak.taccardi
04/13/2020, 6:50 PM.viewModelScope
is an anti-pattern because it prevents injecting a CoroutineScope
, so I don’t use itzak.taccardi
04/13/2020, 6:50 PMviewModelScope
to work when under test is a jokezak.taccardi
04/13/2020, 6:52 PMLifecycleScope
are very useful, like launchWhenStarted
zak.taccardi
04/13/2020, 7:07 PMrkeazor
04/14/2020, 6:26 AMzak.taccardi
04/14/2020, 2:33 PMViewModel
to construct your own non-jetpack ViewModel
(without onCleared()
) with a CoroutineScope
that is cancelled in the jetpack’s onCleared()
.
Injecting a CoroutineScope
means you have no need to leverage onCleared()
because CoroutineScope
provides its own method for completion callback via CompletionHandler
.
There’s no better, cleaner way to do it.rkeazor
04/14/2020, 7:06 PMzak.taccardi
04/14/2020, 7:07 PMrkeazor
04/14/2020, 7:08 PMzak.taccardi
04/14/2020, 7:08 PMCoroutineScope
from runBlocking { .. }
rkeazor
04/14/2020, 7:09 PMrkeazor
04/14/2020, 7:10 PMfun CoroutineScope.addNumbers(...)
zak.taccardi
04/14/2020, 7:10 PMrkeazor
04/14/2020, 7:10 PMzak.taccardi
04/14/2020, 7:10 PMrkeazor
04/14/2020, 7:10 PMsuspend fun addNumnbers() { coroutineScope {
rkeazor
04/14/2020, 7:11 PMrkeazor
04/14/2020, 7:11 PMzak.taccardi
04/14/2020, 7:11 PM.viewModelScope
ignores the runBlocking
you are wrapping your tests with because you violate structured concurrencyrkeazor
04/14/2020, 7:11 PMzak.taccardi
04/14/2020, 7:12 PMJob
instance is when you are doing something very complexzak.taccardi
04/14/2020, 7:14 PMrkeazor
04/14/2020, 7:14 PMrkeazor
04/14/2020, 7:14 PMrkeazor
04/14/2020, 7:15 PMfun addSomething(x: Int, y: Int) {
launch { x + y }
.....
}
rkeazor
04/14/2020, 7:15 PMrkeazor
04/14/2020, 7:16 PMzak.taccardi
04/14/2020, 7:16 PMrkeazor
04/14/2020, 7:16 PMzak.taccardi
04/14/2020, 7:16 PMzak.taccardi
04/14/2020, 7:17 PMzak.taccardi
04/14/2020, 7:17 PMDispatchers.Default
in production and the blocking dispatcher from runBlocking { .. }
when testing (or runBlockingTest(..)
)zak.taccardi
04/14/2020, 7:18 PMCoroutineScope
injectionzak.taccardi
04/14/2020, 7:18 PMCoroutineScope
into a class over extending Jetpack’s ViewModel
achievesrkeazor
04/14/2020, 7:20 PMrkeazor
04/14/2020, 7:21 PMrkeazor
04/14/2020, 7:22 PMrkeazor
04/14/2020, 7:23 PMrkeazor
04/14/2020, 7:23 PMzak.taccardi
04/14/2020, 7:24 PMCoroutineScope
. That CoroutineScope
is then provided via the actual Jetpack ViewModel
, which is abstracted via a single function call. This function is a standalone module that uses the Jetpack ViewModel
as an implementation dependency so the rest of the codebase is not coupled to the Jetpack `ViewModelzak.taccardi
04/14/2020, 7:25 PMa rule is like one lineOne line per test. And if you forget to use it, test fails. Also breaks the ability to execute tests in parallel because you now rely on shared static state
rkeazor
04/14/2020, 7:26 PMrkeazor
04/14/2020, 7:28 PMzak.taccardi
04/14/2020, 7:28 PMMyViewModel
is scoped to. I usually scope to user flows rather than UI components like Fragments because then I can’t share that MyViewModel
with other fragments. But this is abstracted by how the MyViewModel
is providedrkeazor
04/14/2020, 7:29 PMzak.taccardi
04/14/2020, 7:29 PMzak.taccardi
04/14/2020, 7:29 PMrkeazor
04/14/2020, 7:30 PMrkeazor
04/14/2020, 7:30 PMrkeazor
04/14/2020, 7:31 PMzak.taccardi
04/14/2020, 7:32 PMClass<T>
, it’s Class<T>
+ CoroutineScope
rkeazor
04/14/2020, 7:33 PMzak.taccardi
04/14/2020, 7:34 PMrkeazor
04/14/2020, 7:34 PMrkeazor
04/14/2020, 7:35 PMzak.taccardi
04/14/2020, 7:35 PMzak.taccardi
04/14/2020, 7:35 PMDispatchers.setMain(..)
anywhere because we inject our dispatcherszak.taccardi
04/14/2020, 7:35 PMrkeazor
04/14/2020, 7:36 PMrkeazor
04/14/2020, 7:37 PMzak.taccardi
04/14/2020, 7:38 PMDispatchers.Main
for performance reasons for our ViewModel
s. So this would mean that every viewModelScope.launch { }
would be incorrect because it would need to be viewModelScope.launch(Dispatchers.Default) { .. }
every time.
and to guarantee consistency there, that would mean to write a lint rule that would be unnecessaryrkeazor
04/14/2020, 7:38 PMzak.taccardi
04/14/2020, 7:39 PMdispatchers.main
vs Dispatchers.Main
is straight forwardzak.taccardi
04/14/2020, 7:39 PMrkeazor
04/14/2020, 7:43 PMzak.taccardi
04/14/2020, 7:44 PMState
, exposed as Flow<State>
, does not require the UI thread to build it fullyrkeazor
04/14/2020, 7:46 PMrkeazor
04/14/2020, 7:46 PMrkeazor
04/14/2020, 7:47 PMzak.taccardi
04/14/2020, 7:48 PMzak.taccardi
04/14/2020, 7:49 PMAndroidSchedulers.MAIN
outside of the place you construct your dispatchersrkeazor
04/14/2020, 7:51 PMzak.taccardi
04/14/2020, 7:51 PMzak.taccardi
04/14/2020, 7:51 PMinternal class ViewModelHolder<T>(
private val coroutineScope: CoroutineScope,
val viewModel: T
) : AndroidXViewModel() {
override fun onCleared() {
coroutineScope.cancel()
}
}
rkeazor
04/14/2020, 7:51 PMzak.taccardi
04/14/2020, 7:52 PMzak.taccardi
04/14/2020, 7:52 PMrkeazor
04/14/2020, 7:52 PMrkeazor
04/14/2020, 7:53 PMrkeazor
04/14/2020, 7:53 PMzak.taccardi
04/14/2020, 7:54 PMzak.taccardi
04/14/2020, 7:55 PMzak.taccardi
04/14/2020, 7:57 PMrkeazor
04/14/2020, 7:59 PMzak.taccardi
04/14/2020, 8:00 PMrkeazor
04/14/2020, 8:00 PMrkeazor
04/14/2020, 8:01 PMrkeazor
04/14/2020, 8:02 PMrkeazor
04/14/2020, 8:02 PMrkeazor
04/14/2020, 8:02 PMrkeazor
04/14/2020, 8:03 PMzak.taccardi
04/14/2020, 8:03 PMViewModel
is nothing but a scoping mechanism. You can take that scoping mechanism and add additional responsibility to it, like building states for a view to consume. Of course, while minor, this violates single responsibilityzak.taccardi
04/14/2020, 8:03 PMzak.taccardi
04/14/2020, 8:04 PMBoth ViewModels are already testable and ViewModelScopeEasy to test and difficult to test are two different things
zak.taccardi
04/14/2020, 8:05 PMi cant see the benefitYou want to have some `ViewModel`s do background work in
Dispatchers.Default
. While others have a requirement to use Dispatchers.Main
. How do you achieve that with .viewModelScope
in a non-error prone way?rkeazor
04/14/2020, 8:09 PMrkeazor
04/15/2020, 12:27 AMrkeazor
04/15/2020, 12:33 AMrkeazor
04/15/2020, 12:42 AMzak.taccardi
04/15/2020, 4:59 PMViewmodelscope, is just a coroutineScope that is already created for you and cleared for you in Viewmodel.onClear.... There is no more complexity pass that.this is becomes very complex if you need to: 1. change which dispatcher is used - in prod:
Dispatchers.Main
vs Dispatchers.Default
- in test: TestCoroutineDispatcher
2. Cancellation semantics:
- in prod: ViewModel.onCleared(..)
- in test: when @Test fun testFunction()
completeszak.taccardi
04/15/2020, 5:01 PM