I have a StateFlow coroutine that is shared amongst various parts of my application. When I cancel the CoroutineScope of a downstream collector, a JobCancellationException is propagated up to the StateFlow, and it stops emitting values for all current and future collectors.
The StateFlow:
val songsRelay: Flow<List<Song>> by lazy {
MutableStateFlow<List<Song>?>(null).apply {
CoroutineScope(Dispatchers.IO)
.launch { songDataDao.getAll().distinctUntilChanged().collect { value = it } }
}.filterNotNull()
}
A typical 'presenter' in my code implements the following base class:
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
var view: T? = null
private val job by lazy {
Job()
}
private val coroutineScope by lazy { CoroutineScope( job + Dispatchers.Main) }
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
job.cancel()
view = null
}
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return coroutineScope.launch(block = block)
}
}
A BasePresenter implementation might call launch{ songsRelay.collect {...} }
When the presenter is unbound, in order to prevent leaks, I cancel the parent job. Any time a presenter that was collecting the songsRelay StateFlow is unbound, the StateFlow is essentially terminated with a JobCancellationException, and no other collectors/presenters can collect values from it.
I've noticed that I can call job.cancelChildren() instead, and this seems to work (StateFlow doesn't complete with a JobCancellationException). But then I wonder what the point is of declaring a parent job, if I can't cancel the job itself. I could just remove job altogether, and call coroutineScope.coroutineContext.cancelChildren() to the same effect.
If I do just call job.cancelChildren(), is that sufficient? I feel like by not calling coroutineScope.cancel(), or job.cancel(), I may not be correctly or completely cleaning up the tasks that I have kicked off.
I also don't understand why the JobCancellationException is propagated up the hierarchy when job.cancel() is called. Isn't job the 'parent' here? Why does cancelling it affect my StateFlow?
UPDATE:
Are you sure your songRelay is actually getting cancelled for all presenters? I ran this test and "Song relay completed" is printed, because onCompletion also catches downstream exceptions. However Presenter 2 emits the value 2 just fine, AFTER song relay prints "completed". If I cancel Presenter 2, "Song relay completed" is printed again with a JobCancellationException for Presenter 2's job.
I do find it interesting how the one flow instance will emit once each for each collector subscribed. I didn't realize that about flows.
val songsRelay: Flow<Int> by lazy {
MutableStateFlow<Int?>(null).apply {
CoroutineScope(Dispatchers.IO)
.launch {
flow {
emit(1)
delay(1000)
emit(2)
delay(1000)
emit(3)
}.onCompletion {
println("Dao completed")
}.collect { value = it }
}
}.filterNotNull()
.onCompletion { cause ->
println("Song relay completed: $cause")
}
}
#Test
fun test() = runBlocking {
val job = Job()
val presenterScope1 = CoroutineScope(job + Dispatchers.Unconfined)
val presenterScope2 = CoroutineScope(Job() + Dispatchers.Unconfined)
presenterScope1.launch {
songsRelay.onCompletion { cause ->
println("Presenter 1 Completed: $cause")
}.collect {
println("Presenter 1 emits: $it")
}
}
presenterScope2.launch {
songsRelay.collect {
println("Presenter 2 emits: $it")
}
}
presenterScope1.cancel()
delay(2000)
println("Done test")
}
I think you need to use SupervisorJob in your BasePresenter instead of Job. In general using Job would be a mistake for the whole presenter, because one failed coroutine will cancel all coroutines in the Presenter. Generally not what you want.
OK, so the problem was some false assumptions I made when testing this. The StateFlow is behaving correctly, and cancellation is working as expected.
I was thinking that between Presenters, StateFlow would stop emitting values, but I was actually testing the same instance of a Presenter - so its Job had been cancelled and thus it's not expected to continue collecting Flow emissions.
I also mistakenly took CancellationException messages emitted in onCompletion of the StateFlow to mean the StateFlow itself had been cancelled - when actually it was just saying the downstream Collector/Job had been cancelled.
I've come up with a better implementation of BasePresenter that looks like so:
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T>, CoroutineScope {
var view: T? = null
private var job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun bindView(view: T) {
if (job.isCancelled) {
job = Job()
}
this.view = view
}
override fun unbindView() {
job.cancel()
view = null
}
}
Related
I am reading this article to fully understand the dos and donts of using Flow while comparing it to my implementation, but I can't grasp clearly how to tell if you are wasting resource when using Flow or flow builder. When is the time a flow is being release/freed in memory and when is the time that you are wasting resource like accidentally creating multiple instances of flow and not releasing them?
I have a UseCase class that invokes a repository function that returns Flow. In my ViewModel this is how it looks like.
class AssetViewModel constructor(private val getAssetsUseCase: GetAssetsUseCase) : BaseViewModel() {
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
}
The idea here is we are fetching data from remote every 10 seconds while also allowing on demand fetch of data via UI.
Just a typical useless UseCase class
class GetAssetsUseCase #Inject constructor(
private val repository: AssetsRepository // Passing interface not implementation for fake test
) {
operator fun invoke(baseUrl: String, query: String, limit: String): Flow<RequestStatus<AssetDomain>> {
return repository.fetchAssets(baseUrl, query, limit)
}
}
The concrete implementation of repository
class AssetsRepositoryImpl constructor(
private val service: CryptoService,
private val mapper: AssetDtoMapper
) : AssetsRepository {
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
) = flow {
try {
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
}
}
After reading this article which says that using stateIn or sharedIn will improve the performance when using a flow, it seems that I am creating new instances of the same flow on-demand. But there is a limitation as the stated approach only works for variable and not function that returns Flow.
stateIn and shareIn can save resources if there are multiple observers, by avoiding redundant fetching. And in your case, you could set it up to automatically pause the automatic re-fetching when there are no observers. If, on the UI side you use repeatOnLifecycle, then it will automatically drop your observers when the view is off screen and then you will avoid wasted fetches the user will never see.
I think it’s not often described this way, but often the multiple observers are just observers coming from the same Activity or Fragment class after screen rotations or rapidly switching between fragments. If you use WhileSubscribed with a timeout to account for this, you can avoid having to restart your flow if it’s needed again quickly.
Currently you emit to from an external coroutine instead of using shareIn, so there’s no opportunity to pause execution.
I haven't tried to create something that supports both automatic and manual refetching. Here's a possible strategy, but I haven't tested it.
private val refreshRequest = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets() {
refreshRequest.trySend(Unit)
}
val assetState = flow {
while(true) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).map {
when(it){
is RequestStatus.Loading -> AssetState.FetchLoading
is RequestStatus.Success -> AssetState.FetchSuccess(it.data.assetDataDomain)
is RequestStatus.Failed -> AssetState.FetchFailed(it.message)
}
}.emitAll()
withTimeoutOrNull(100L) {
// drop any immediate or pending manual request
refreshRequest.receive()
}
// Wait until a fetch is manually requested or ten seconds pass:
withTimeoutOrNull(10000L - 100L) {
refreshRequest.receive()
}
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(4000L), replay = 1)
To this I would recommend not using flow as the return type of the usecase function and the api call must not be wrapped inside a flow builder.
Why:
The api call actually is happening once and then again after an interval it is triggered by the view model itself, returning flow from the api caller function will be a bad usage of powerful tool that is actually meant to be called once and then it must be self-reliant, it should emit or pump in the data till the moment it has a subscriber/collector.
One usecase you can consider when using flow as return type from the room db query call, it is called only once and then the room emits data into it till the time it has subscriber.
.....
fun fetchAssets() {
viewModelScope.launch {
// loading true
val result=getusecase(.....)
when(result){..process result and emit on state..}
// loading false
}
}
.....
suspend operator fun invoke(....):RequestStatus<AssetDomain>{
repository.fetchAssets(baseUrl, query, limit)
}
.....
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
):RequestStatus {
try {
//RequestStatus.Loading()//this can be managed in viewmodel itself
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
RequestStatus.Success(domainModel)
} catch (e: HttpException) {
RequestStatus.Failed(e)
} catch (e: IOException) {
RequestStatus.Failed(e)
}
}
I have two fragments: GeneralInformationFrament and HomeFragment, and I have one ViewModel associated for each of them. Inside both of them I have a method like:
private fun getInstallationSiteInformation() {
launch(Dispatchers.IO) {
currentFragment.getInstallationSiteOfUser(employeeId).collect {
when(it.status){
Resource.Status.LOADING -> {
withContext(Dispatchers.Main){
//Code
}
}
Resource.Status.SUCCESS -> {
withContext(Dispatchers.Main){
//code
}
}
Resource.Status.ERROR -> {
withContext(Dispatchers.Main) {
//more code
}
}
}
}
}
}
Inside each viewModel I have:
fun getInstallationSiteOfUser(employeeId: Int): Flow<Resource<InstallationSiteEntity?>> =
installationSiteRepository.getInstallationSiteOfUser(employeeId).map{
installationSiteResponse ->
when(installationSiteResponse.status){
Resource.Status.LOADING -> {
Resource.loading(null)
}
Resource.Status.SUCCESS -> {
val installationSite = installationSiteResponse.data
Resource.success(installationSite)
}
Resource.Status.ERROR -> {
Resource.error(installationSiteResponse.message!!, null)
}
}
}
and in the InstallationSiteRepository I have:
fun getInstallationSiteOfUser(employeeId: Int): Flow<Resource<InstallationSiteEntity>> = flow{
emit(Resource.loading(null))
val installationSiteOfEmployee = employeesDao.getEmployeeDetailed(employeeId)
remoteApiService.getInstallationSiteOfEmployee(employeeId).collect {apiResponse ->
when(apiResponse){
is ApiSuccessResponse -> {
apiResponse.body?.let {
installationSitesDao.insertInstallationSite(it.installationSite)}
emitAll(installationSitesDao.getInstallationSiteFromEmployeeId(employeeId).map { installationData ->
Resource.success(installationData)
})
}
is ApiErrorResponse -> {
emitAll(installationSitesDao.getInstallationSiteFromEmployeeId(employeeId).map { installationData ->
Resource.error(apiResponse.errorMessage, installationData)
})
}
}
}
}
Soon after the transition between GeneralInformationFragment to HomeFragment the method getInstallationSiteInformation() is called in onViewCreated() so the behavior that I am encountering is that the flows are being collected in both fragments one after another and because one of the fragments is not available anymore I am getting a NullPointerException. My question is: When a flow source emits, every target collecting it gets the values? Is it possible what I am describing? Shouldn't the flow inside GeneralInformationFragment have been canceled and stopped receiving it ?
[EDIT 1]
In the top of my Fragments there is:
#AndroidEntryPoint
class GeneralInformationFragment : Fragment(), CoroutineScope {
private var job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
And in OnDestroy() of the Fragments:
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
When a flow source emits, every target collecting it gets the values?
Yes. Flows are cold streams. They do nothing until a receiver calls collect. There is no restriction on multiple calls from receivers. Flow will emit its values for each caller.
Shouldn't the flow inside GeneralInformationFragment have been canceled and stopped receiving it ?
You need to cancel the Flow collection. In getInstallationSiteInformation(), launch(Dispatchers.IO) {/**/} will return a Job. You can keep a reference to that Job and cancel when you want.
Alternatively, you can cancel the CoroutineScope.
Your CoroutineScope in your code is unclear, does your Fragment implement CoroutineScope maybe?
There are "out the box" CoroutineScopes for a Fragment you could use instead: lifecycleScope and viewLifecycleOwner.lifecycleScope.
They will handle the lifecycle (cancellation) for you. lifecycleScope is tied to the Fragment lifecyle, whereas viewLifecycleOwner.lifecycleScope is tied to the Fragment View lifecycle, onViewCreated to onDestroyView
I hope this helps at least a bit.
I have a retrofit service
interface Service {
#PUT("path")
suspend fun dostuff(#Body body: String)
}
It is used in android view model.
class VM : ViewModel(private val service: Service){
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
val state = MutableLiveData<String()
init {
uiScope.launch {
service.doStuff()
state.value = "lol"
}
}
override fun onCleared(){
viewModelJob.cancel()
}
}
I would like to write a test for the cancelling of the view model. This will be done mocking service and delaying so that the co routine does not complete. Whilst blocking, we invoke onCleared to cancel the co routine. This should prevent state getting set...
#Test
fun `on cleared - cancels request`() = runBlocking {
//given
`when`(service.doStuff()).thenAnswer { launch { delay(1000) } }
val vm = ViewModel(service)
// when
vm.cleared()
//then
assertThat(vm.state, nullValue())
}
However it seems that vm.state always gets set??? What is the best way to test when clearing a scope that a co routine gets cancelled?
The problem here is in thenAnswer { launch { delay(1000) } }, which effectively makes your doStuff method look like that:
suspend fun doStuff() {
launch { delay(1000) }
}
As you can see, this function does not actually suspend, it launches a coroutine and returns immediately. What would actually work here is thenAnswer { delay(1000) }, which does not work, because there is no suspend version of thenAnswer in Mockito (as far as I know at least).
I would recommend to switch to Mokk mocking library, which supports kotlin natively. Then you can write coEvery { doStuff() } coAnswers { delay(1000) } and it will make your test pass (after fixing all the syntax errors ofc).
I'm trying to find a way to nicely implement an IdlingResource that will poll a CoroutineDispatcher's isActive property. However, from debugging, there never seems to be an active Job when checking this property.
So far I've tried using AsyncTask's THREAD_POOL_EXECUTOR for built-in idling, but it doesn't seem to work when using the asCoroutineDispatcher extension function and using that resulting CoroutineDispatcher to launch my ViewModel's job. I've attempted writing a custom IdlingResource
ViewModel
fun authenticate(username: String, password: String) = viewModelScope.launch(Dispatchers.Default) {
if (_authenticateRequest.value == true) {
return#launch
}
_authenticateRequest.postValue(true)
val res = loginRepo.authenticate(username, password)
_authenticateRequest.postValue(false)
when {
res is Result.Success -> {
_authenticateSuccess.postValue(res.item)
}
res is Result.Failure && res.statusCode.isHttpClientError -> {
_authenticateFailure.postValue(R.string.invalid_password)
}
else -> {
_authenticateFailure.postValue(R.string.network_error)
}
}
}
IdlingResource
class CoroutineDispatcherIdlingResource(
private val resourceName: String,
private val dispatcher: CoroutineDispatcher
) : IdlingResource {
private var callback: IdlingResource.ResourceCallback? = null
override fun getName() = resourceName
override fun isIdleNow(): Boolean {
if (dispatcher.isActive) { return false }
callback?.onTransitionToIdle()
return true
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}
}
Espresso Test
#RunWith(AndroidJUnit4::class)
class LoginIntegrationTest {
#get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)
var idlingResource: CoroutineDispatcherIdlingResource? = null
#Before
fun before() {
idlingResource = CoroutineDispatcherIdlingResource(this.javaClass.simpleName, Dispatchers.Default)
IdlingRegistry.getInstance().register(idlingResource)
}
#Test
fun loginFailure() {
onView(withId(R.id.username))
.perform(clearText()).perform(typeText("aslkdjqwe"))
onView(withId(R.id.password))
.perform(clearText()).perform(typeText("oxicjqwel"))
onView(withId(R.id.login_button))
.perform(click())
onView(withId(com.google.android.material.R.id.snackbar_text))
.check(matches(withText(R.string.invalid_password)))
}
}
I'm expecting the isActive property to be true once the ViewModel 'authenticate' function is called, but this doesn't seem to be the case. It always appears to be false, since there's never an active Job in the CoroutineDispatcher.
Figured out a solution! It turns out that AsyncTask's THREAD_POOL_EXECUTOR was actually working fine for this. What I was missing was having an IdlingResource for Retrofit/OkHttp.
My initial assumption was that the coroutine running on the THREAD_POOL_EXECUTOR would implicitly be waited on while the HTTP client goes off, but I've used the IdlingResource here to accomplish everything nicely.
CoroutineContext.isActive is misleading because it checks if the context has a Job object and whether it is active. A CoroutineDispatcher is a context with no other elements like Job so it will always return false.
In order to track continuations you will probably need some sort of custom ContinuationInterceptor which keeps track of in-progress and cancelled continuations.
Setup:
In our project (at work - I cannot post real code), we have implemented clean MVVM. Views communicate with ViewModels via LiveData. ViewModel hosts two kinds of use cases: 'action use cases' to do something, and 'state updater use cases'. Backward communication is asynchronous (in terms of action reaction). It's not like an API call where you get the result from the call. It's BLE, so after writing the characteristic there will be a notification characteristic we listen to. So we use a lot of Rx to update the state. It's in Kotlin.
ViewModel:
#PerFragment
class SomeViewModel #Inject constructor(private val someActionUseCase: SomeActionUseCase,
someUpdateStateUseCase: SomeUpdateStateUseCase) : ViewModel() {
private val someState = MutableLiveData<SomeState>()
private val stateSubscription: Disposable
// region Lifecycle
init {
stateSubscription = someUpdateStateUseCase.state()
.subscribeIoObserveMain() // extension function
.subscribe { newState ->
someState.value = newState
})
}
override fun onCleared() {
stateSubscription.dispose()
super.onCleared()
}
// endregion
// region Public Functions
fun someState() = someState
fun someAction(someValue: Boolean) {
val someNewValue = if (someValue) "This" else "That"
someActionUseCase.someAction(someNewValue)
}
// endregion
}
Update state use case:
#Singleton
class UpdateSomeStateUseCase #Inject constructor(
private var state: SomeState = initialState) {
private val statePublisher: PublishProcessor<SomeState> =
PublishProcessor.create()
fun update(state: SomeState) {
this.state = state
statePublisher.onNext(state)
}
fun state(): Observable<SomeState> = statePublisher.toObservable()
.startWith(state)
}
We are using Spek for unit tests.
#RunWith(JUnitPlatform::class)
class SomeViewModelTest : SubjectSpek<SomeViewModel>({
setRxSchedulersTrampolineOnMain()
var mockSomeActionUseCase = mock<SomeActionUseCase>()
var mockSomeUpdateStateUseCase = mock<SomeUpdateStateUseCase>()
var liveState = MutableLiveData<SomeState>()
val initialState = SomeState(initialValue)
val newState = SomeState(newValue)
val behaviorSubject = BehaviorSubject.createDefault(initialState)
subject {
mockSomeActionUseCase = mock()
mockSomeUpdateStateUseCase = mock()
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(behaviorSubject)
SomeViewModel(mockSomeActionUseCase, mockSomeUpdateStateUseCase).apply {
liveState = state() as MutableLiveData<SomeState>
}
}
beforeGroup { setTestRxAndLiveData() }
afterGroup { resetTestRxAndLiveData() }
context("some screen") {
given("the action to open the screen") {
on("screen opened") {
subject
behaviorSubject.startWith(initialState)
it("displays the initial state") {
assertEquals(liveState.value, initialState)
}
}
}
given("some setup") {
on("some action") {
it("does something") {
subject.doSomething(someValue)
verify(mockSomeUpdateStateUseCase).someAction(someOtherValue)
}
}
on("action updating the state") {
it("displays new state") {
behaviorSubject.onNext(newState)
assertEquals(liveState.value, newState)
}
}
}
}
}
At first we were using an Observable instead of the BehaviorSubject:
var observable = Observable.just(initialState)
...
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(observable)
...
observable = Observable.just(newState)
assertEquals(liveState.value, newState)
instead of the:
val behaviorSubject = BehaviorSubject.createDefault(initialState)
...
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(behaviorSubject)
...
behaviorSubject.onNext(newState)
assertEquals(liveState.value, newState)
but the unit test were being flaky. Mostly they would pass (always when ran in isolation), but sometime they would fail when running the whole suit. Thinking it is to do with asynchronous nature of the Rx we moved to BehaviourSubject to be able to control when the onNext() happens. Test are now passing when we run them from AndroidStudio on the local machine, but they are still flaky on the build machine. Restarting the build often makes them pass.
The tests which fail are always the ones where we assert the value of LiveData. So the suspects are LiveData, Rx, Spek or their combination.
Question: Did anyone have similar experiences writing unit tests with LiveData, using Spek or maybe Rx, and did you find ways to write them which solve these flakiness issues?
....................
Helper and extension functions used:
fun instantTaskExecutorRuleStart() =
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) {
runnable.run()
}
override fun isMainThread(): Boolean {
return true
}
override fun postToMainThread(runnable: Runnable) {
runnable.run()
}
})
fun instantTaskExecutorRuleFinish() = ArchTaskExecutor.getInstance().setDelegate(null)
fun setRxSchedulersTrampolineOnMain() = RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
fun setTestRxAndLiveData() {
setRxSchedulersTrampolineOnMain()
instantTaskExecutorRuleStart()
}
fun resetTestRxAndLiveData() {
RxAndroidPlugins.reset()
instantTaskExecutorRuleFinish()
}
fun <T> Observable<T>.subscribeIoObserveMain(): Observable<T> =
subscribeOnIoThread().observeOnMainThread()
fun <T> Observable<T>.subscribeOnIoThread(): Observable<T> = subscribeOn(Schedulers.io())
fun <T> Observable<T>.observeOnMainThread(): Observable<T> =
observeOn(AndroidSchedulers.mainThread())
I didn't used Speck for unit-testing. I've used java unit-test platform and it works perfect with Rx & LiveData, but you have to keep in mind one thing. Rx & LiveData are async and you can't do something like someObserver.subscribe{}, someObserver.doSmth{}, assert{} this will work sometimes but it's not the correct way to do it.
For Rx there's TestObservers for observing Rx events. Something like:
#Test
public void testMethod() {
TestObserver<SomeObject> observer = new TestObserver()
someClass.doSomethingThatReturnsObserver().subscribe(observer)
observer.assertError(...)
// or
observer.awaitTerminalEvent(1, TimeUnit.SECONDS)
observer.assertValue(somethingReturnedForOnNext)
}
For LiveData also, you'll have to use CountDownLatch to wait for LiveData execution. Something like this:
#Test
public void someLiveDataTest() {
CountDownLatch latch = new CountDownLatch(1); // if you want to check one time exec
somethingTahtReturnsLiveData.observeForever(params -> {
/// you can take the params value here
latch.countDown();
}
//trigger live data here
....
latch.await(1, TimeUnit.SECONDS)
assert(...)
}
Using this approach your test should run ok in any order on any machine. Also the wait time for latch & terminal event should be as low as possible, the tests should run fast.
Note1: The code is in JAVA but you can change it easily in kotlin.
Note2: Singleton are the biggest enemy of unit-testing ;). (With static methods by their side).
The issue is not with LiveData; it is the more common problem - singletons. Here the Update...StateUseCases had to be singletons; otherwise if observers got a different instance they would have a different PublishProcessor and would not get what was published.
There is a test for each Update...StateUseCases and there is a test for each ViewModel into which Update...StateUseCases is injected (well indirectly via the ...StateObserver).
The state exists within the Update...StateUseCases, and since it is a singleton, it gets changed in both tests and they use the same instance becoming dependent on each other.
Firstly try to avoid using singletons if possible.
If not, reset the state after each test group.