I have been facing an issue unit testing our business logic. The problem is when I wanted to unit test all the emissions of the livedata. Right now I only get the last emitted value but I wanted to unit test all the values that get emitted one by one.
Here is my rough setup.
Here is how my viewModel onSaveEvent click event looks like
disposable.add(finalSource
.observeOn(schedulerProvider.ui())
.doOnSubscribe {
// show spinner
_myLiveData.value = Loading(toShow = true, loadingMessage = "Loading")
}
.doAfterTerminate {
// remove spinner
_myLiveData.value = Loading(toShow = false)
}
.subscribe { resource ->
// Update either on success or error.
_myLiveData.value = resource
}
)
My view model test looks something like this.
#Test
fun testOnSaveEvent() {
// Other setup code.
Mockito.`when`(myRepo.onSave()).thenReturn(mockSingle)
myViewModel.onSaveEvent(salesTaxCompanyInfo)
testScheduleProvider.ui().triggerActions()
// I see that the value is of type Loading
val firstValue = myViewModel.myLiveData.getOrAwaitValue()
// The next value is also Loading, although I would like to see the success event.
val secondValue = myViewModel.myLiveData.getOrAwaitValue()
}
Ideally, I am looking for something on the lines TestCoroutineDispatcher listed here: https://medium.com/androiddevelopers/testing-two-consecutive-livedata-emissions-in-coroutines-5680b693cbf8
While I understand that LiveData only holds the last value it receives I wanted to know if there is any we can unit test the emissions one by one.
Related
I need to transfer a stream of values from a publisher / producer to one or multiple consumers / subscribers / observers. The exact requirements are:
the "stream" has a default value
consumers receive the last published value when they subscribe (the last value is replayed)
the last published value can also be retrieved via a value property
two consecutive published values are distinct (no duplicates)
all published values need to be received by the consumer (values cannot be conflated)
consumption happens via a FlowCollector
the implementation must be multiplatform (Android, iOS, JS)
MutableStateFlow is meeting almost all of these requirements except item #5:
Updates to the value are always conflated. So a slow collector skips fast updates, but always collects the most recently emitted value
(https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/).
So something like this:
val flow = MutableStateFlow(0)
// runs in co-routine 1
flow.collect {
println("Collect: $it")
delay(100)
}
// runs in co-routine 2
repeat(10) {
flow.emit(it + 1)
}
will print 0 and 10 but not the numbers in between because the collector is slow and emitted values are conflated
I could use MutableSharedFlow:
val flow = MutableSharedFlow<Int>(replay = 1, extraBufferCapacity = 1)
MutableSharedFlow doesn't conflate values but 1) has no value property (there's last() but that's a suspend function), 2) allows duplicates and 3) has no initial values.
While it's possible to add these three requirements, adding the value property and the duplicate check isn't trivial.
I could use a BehaviorSubject:
val subject = BehaviorSubject(0)
val flow = subject.asFlow().distinctUntilChanged()
That would work perfectly but I'd have to add https://github.com/badoo/Reaktive just for this.
So here's my question: is there a Kotlin Flow solution that meets all the requirements without having to add the missing pieces "manually" (like with MutableSharedFlow)?
It's not so bad to add the missing pieces you want to MutableSharedFlow to have exactly the syntax you want:
class MutableBehaviorFlow<T : Any?>(
private val initialValue: T,
private val _backingSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(
replay = 1,
extraBufferCapacity = Int.MAX_VALUE,
onBufferOverflow = BufferOverflow.SUSPEND
)
) : MutableSharedFlow<T> by _backingSharedFlow {
init {
tryEmit(initialValue)
}
val value: T
get() = try {
replayCache.last()
} catch (_: NoSuchElementException) {
initialValue
}
override suspend fun emit(value: T) {
if (value != this.value) _backingSharedFlow.emit(value)
}
override fun tryEmit(value: T): Boolean =
if (value != this.value) _backingSharedFlow.tryEmit(value)
else true
}
I am currently trying to combine two streams and use that result to display into a Compose view in Android. I currently initialize it like this:
private var flowA: MutableStateFlow<String?> = MutableStateFlow(null)
private var flowB: MutableStateFlow<String?> = MutableStateFlow(null)
var combinedFlows: Flow<Pair<String?, String?>> =
flowA.combine(flowB) {a, b ->
Pair(a, b)
}
Emit like so:
// Some logic to create string
flowA.emit("New String")
// More logic to create another string
flowB.emit("Another New String")
In the compose view I am collecting like this:
var pairStrings = viewModel.pairStrings.collectAsState()
The combined stream is never emiting any data when I emit from flowA and flowB. Is there something I am missing here?
--Edit--
This is in the view model:
val pairStrings: StateFlow<Pair<String?, String?>> = combinedFlows
.stateIn(
scope = viewModelScope,
initialValue = Pair(null,null),
started = SharingStarted.Eagerly,
)
You didn't provide much information, but you need to run/subscribe to the combined flow. I think you need to do the following:
val combinedFlows = flowA.combine(flowB) { a, b ->
a to b // will emit a Pair with the latest value of flowA and flowB
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed())
The shareIn operator runs the upstream flow in a separate coroutine. Doing this, you'll be able to consume the flow.
You need to pass a CoroutineScope, assuming you're doing this call in a ViewModel (which should be the best place), you can use the viewModelScope. You also need to pass the SharingState which will determine the strategy that controls when sharing is started and stopped. In this case, I'm using WhileSubscribed which immediately stops the flow when the last subscriber disappears.
In my viewModel I have:
private val setEntitiesList = mutableStateListOf<Exercise>()
val exercisesFromDB = exerciseDao.getAllExercisesWithSetNo(trainingId)
val exercises =
exercisesFromDB.combine(setEntitiesList.asFlow()) { exercises, setEntitiesList ->
Pair(exercises, setEntitiesList)
}.mapLatest { (exercises, setEntitiesList) ->
//I am altering exercises list here
exercises
}
In my fragment I have:
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED){
addTrainingViewModel.exercises.collectLatest {
exercisesAdapter.submitList(it)
}
}
}
It doesnt work. Nothing is collected in fragment
If I change in fragment to collect "addTrainingViewModel.exercisesFromDB" it works - values are emited and collected.
What I would like to achieve: new list of exercises is emitted when setEntitiesList or exercisesFromDB are changed and I am able to do sth with a list of exercises before it is emitted
I assume that setEntitiesList variable is of type Iterable. In that case when you convert it to Flow like setEntitiesList.asFlow() you create a cold Flow, it means that when you add new elements to the setEntitiesList iterable they won't be emitted to the combined Flow.
On the other hand, by calling addTrainingViewModel.exercisesFromDB you get a hot Flow, so when you tested it separately you got the right result.
So you can't use a converted iterable object setEntitiesList as hot Flow. You need somehow to rewrite your logic and use a hot Flow instead of cold Flow. You can use MutableSharedFlow for that. The code will be something like the following:
private val setEntitiesListFlow = MutableSharedFlow<Exercise>(extraBufferCapacity = 64)
val exercisesFromDB = exerciseDao.getAllExercisesWithSetNo(trainingId)
val exercises =
exercisesFromDB.combine(setEntitiesListFlow) { exercises, setEntitiesList ->
Pair(exercises, setEntitiesList)
}.mapLatest { (exercises, setEntitiesList) ->
//I am altering exercises list here
exercises
}
// somewhere else in your code emitting Exercise:
setEntitiesListFlow.tryEmit(Exercise(...))
I am working my through a new android application using Jetpack Compose (1.0.0-alpha08) and RxJava2 to manage the flow of data from my model (Realm 10 in this case. For a given screen, I have a view model that defines the data that will be subscribed to by the top level Compostable view. So, for example:
ViewModel...
class ListItemViewModel: ViewModel() {
val items: Flowable<Item>
get() {
val data1 = userRealm.where<Item1>()
.also(query).findAllAsync().asFlowable()
.onBackpressureLatest().doOnNext{System.out.println("Realm on Next")}
.observeOn(
Schedulers.single()
).filter{it.isLoaded}.map{ result ->
System.out.println("Maping Realm")
result
}.doOnSubscribe {System.out.println("Subscribe")}
val data2 == //same as above but with a different item
return Flowable.combineLatest(data1, data2, combineFunction)
.onBackpressureLatest()
.doOnNext{System.out.println("Hello")}
.doOnComplete {System.out.println("Complete")}
.subscribeOn(AndroidSchedulers.mainThread())
}
}
View
#Compostable
fun List(List<Item> items) {
val viewModel: ListItemViewModel = viewModel()
val list by viewModel.items.subscribeAsState(initial = listOf())
ItemList(list = list)
}
#Compostable
fun ItemList(List<Item> items {
LazyColumnFor(...) {
.......
}
}
Everything works as I would expect and the list renders on the screen as I want. However, what I assume would happen here is that the subscribe would only happen once and the Flowable would only push out new data as new data was emitted. As a result, I would only expect the various onNext methods to be triggered when new data was present in the stream, e.g. something changed in the realm db. As I am not adding/deleting any data to/from the Realm, once I have the first set of results, I would expect the flowable to go "silent".
However, when I run the above, the subscribe message related to the realm subscription is logged over and over. The same for the "Hello" and the other logging statements in the onNext methods. Also, if I add any logging in my combine function, I see those log statements in the same fashion as I see the "Hello" log. From this it seems like each time the List composable is being rendered, it resubscribes to the Flowable from my viewmodel and triggers the full process. As I said, I was expecting that this subscription would only happen once.
This is perhaps correct behaviour, but mentally, it feels like I am burning CPU cycles for no reason as my methods are being called over and over when no data has change. Am I setting things up correctly, or is there something flawed in how I have configured things?
I ultimately worked around the problem and took a hybrid approach where I used Realm/RXJava to handle the data flow and when things have changed, update a LiveData object.
View Model
private val internalItemList = MutableLiveData(listOf<Item>())
val itemList: LiveData<List<Item>> = internalItemList
//capture the subscription so you can dispose in onCleared()
val subscription = items.observeOn(AndroidSchedulers.mainThread()).subscribe {
this.internalItemList.value = it
}
View
val list by viewModel.itemList.observeAsState(listOf())
This is must less chatty and works as I want it to. Not sure if it is the correct way to do this, but it seems to be working
Given the following setup:
I have 2 repositories: Repository A and Repository B both of them return live data.
I have a ViewModel that uses both of these repositories.
I want to extract something from Repository A and depending on the result I want to grab something from Repository B and then transform the result before returning to UI.
For this I have been looking at the LiveData Transformation classes. The examples show a single transformation of the result however I want something along the lines of chaining two transformations. How can I accomplish this?
I have tried setting something up like this but get a type mismatch on the second transformation block:
internal val launchStatus: LiveData<String> = Transformations
.map(respositoryA.getData(), { data ->
if (data.isValid){
"stringA"
} else {
//This gives a type mismatch for the entire block
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
}
})
(Also please let me know if there is an alternate/recommended approach for grabbing something for chaining these call i.e. grab something from A and then grab something from B depending on result of A and so on)
Your lambda sometimes returns the String "stringA", and sometimes returns the LiveData<String> given by:
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
This means that your lambda doesn't make sense - it returns different things in different branches.
As others have mentioned, you could write your own MediatorLiveData instead of using the one given by Transformations. However, I think it's easier to do the following:
internal val launchStatus: LiveData<String> = Transformations
.switchMap(respositoryA.getData(), { data ->
if (data.isValid) {
MutableLiveData().apply { setValue("stringA") }
} else {
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
}
})
All I've done is made the first code branch also return a LiveData<String>, so now your lambda makes sense - it's a (String) -> LiveData<String>. I had to make one other change: use switchMap instead of map. This is because map takes a lambda (X) -> Y, but switchMap takes a lambda (X) -> LiveData<Y>.
I used MediatorLiveData to solve this problem.
MediatorLiveData can observer other LiveData objects and react to them.
Instead of observing either of the repositories. I created myData (instance of MediatorLiveData) in my ViewModel class and have my view observe this object. Then I add Repository A as the initial source and observe that and only add Repository B if the result of A requires it. This allows me to keep the transformations associated with the live data of each of the repo and still process each result in the correct order. See below for implementation:
internal val myData: MediatorLiveData<String> = MediatorLiveData()
private val repoA: LiveData<String> = Transformations.map(
respositoryA.getData(), { data ->
if (data.isValid) "stringA" else ""
})
private val repoB: LiveData<String> = Transformations.map(
repositoryB.getData(), { data -> "stringB"
})
fun start() {
myData.addSource(repoA, {
if (it == "stringA") {
myData.value = it
} else {
myData.addSource(repoB, {
myData.value = it
})
}
})
}
Note: The solution does not cover the case where repoB might be added multiple times but it should be simple enough to handle.
I would try using switchMap instead of map:
Similar to map(), applies a function to the value stored in the LiveData object and unwraps and dispatches the result downstream. The function passed to switchMap() must return a LiveData object.
You can nest transformations.
val finalLiveData = Transformations.switchMap(liveData1){
val search = it
Transformations.switchMap(liveData2) {
db(context).dao().all(search, it)
}
}
You can transform the data by using switchmap. Here's an example documentation.