Android: Transformations.switchMap not triggered when quick call - android

I'm attempting to learn MVI. I am updating a state event live data that is being observed by a Transformations switch map. This is in my viewmodel.
fun setStateEvent(event: StateEvent) {
Timber.d("SetStateEvent: [$event]")
_stateEvent.value = event
}
val dataState: LiveData<DataState<CustomViewState>> = Transformations.switchMap(_stateEvent) { stateEvent ->
Timber.d("Got state event [$stateEvent]")
stateEvent?.let {
handleStateEvent(it)
}
}
Now in my view, I am trying to perform two actions:
viewmodel.setStateEvent(CustomStateEvent.ActionOne())
viewmodel.setStateEvent(CustomStateEvent.ActionTwo())
These are my logs:
SetStateEvent: [CustomStateEvent$ActionOne]
SetStateEvent: [CustomStateEvent$ActionTwo]
Got state event [CustomStateEvent$ActionTwo]
...
For some reason, the first one is getting cancelled/ignored. What am I doing wrong?

LiveData is a state holder and it's designed to serve this purpose. So it guarantees that all active (whose state is at least STARTED) observers will eventually receive the latest value of this LiveData, but all intermediate values can be conflated. If you need all the posted values to be delivered without conflation, you should use another abstraction - e.g kotlin channel.

Related

Use observe for a variable that updated inside another observe in Kotlin

I am trying first handle the response from API by using observe. Later after observing the handled variable I want to save it to database.
The variable tokenFromApi is updated inside tokenResponseFromApi's observer. Is it possible to observe tokenFromApi outside the observer of tokenResponseFromApi? When debugged, the code did not enter inside tokenFromApi observer when the app started.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var tokenResponseFromApi: LiveData<String>? = MutableLiveData<String>()
var tokenFromApi: LiveData<TokenEntity>? = MutableLiveData<TokenEntity>()
tokenResponseFromApi?.observe(viewLifecycleOwner, Observer {
tokenResponseFromApi ->
if (tokenResponseFromApi != null) {
tokenFromApi = viewModel.convertTokenResponseToEntity(tokenResponseFromApi, dh.asDate)
}
})
tokenFromApi?.observe(viewLifecycleOwner, Observer {
tokenFromApi ->
if (tokenFromApi != null) {
viewModel.saveTokenToDB(repo, tokenFromApi)
}
})
}
Your problem is that you're registering the observer on tokenFromApi during setup, and when you get your API response, you're replacing tokenFromApi without registering an observer on it. So if it ever emits a value, you'll never know about it. The only observer you have registered is the one on the discarded tokenFromApi which is never used by anything
Honestly your setup here isn't how you're supposed to use LiveData. Instead of creating a whole new tokenFromApi for each response, you'd just have a single LiveData that things can observe. When there's a new value (like an API token) you set that on the LiveData, and all the observers see it and react to it. Once that's wired up, it's done and it all works.
The way you're doing it right now, you have a data source that needs to be taken apart, replaced with a new one, and then everything reconnected to it - every time there's a new piece of data, if you see what I mean.
Ideally the Fragment is the UI, so it reacts to events (by observing a data source like a LiveData and pushes UI events to the view model (someone clicked this thing, etc). That API fetching and DB storing really belongs in the VM - and you're already half doing that with those functions in the VM you're calling here, right? The LiveDatas belong in the VM because they're a source of data about what's going on inside the VM, and the rest of the app - they expose info the UI needs to react to. Having the LiveData instances in your fragment and trying to wire them up when something happens is part of your problem
Have a look at the App Architecture guide (that's the UI Layer page but it's worth being familiar with the rest), but this is a basic sketch of how I'd do it:
class SomeViewModel ViewModel() {
// private mutable version, public immutable version
private val _tokenFromApi = MutableLiveData<TokenEntity>()
val tokenFromApi: LiveData<TokenEntity> get() = _tokenFromApi
fun callApi() {
// Do your API call here
// Whatever callback/observer function you're using, do this
// with the result:
result?.let { reponse ->
convertTokenResponseToEntity(response, dh.asDate)
}?.let { token ->
saveTokenToDb(repo, token)
_tokenFromApi.setValue(token)
}
}
private fun convertTokenResponseToEntity(response: String, date: Date): TokenEntity? {
// whatever goes on in here
}
private fun saveTokenToDb(repo: Repository, token: TokenEntity) {
// whatever goes on in here too
}
}
so it's basically all contained within the VM - the UI stuff like fragments doesn't need to know anything about API calls, whether something is being stored, how it's being stored. The VM can update one of its exposed LiveData objects when it needs to emit some new data, update some state, or whatever - stuff that's interesting to things outside the VM, not its internal workings. The Fragment just observes whichever one it's interested in, and updates the UI as required.
(I know the callback situation might be more complex than that, like saving to the DB might involve a Flow or something. But the idea is the same - in its callback/result function, push a value to your LiveData as appropriate so observers can receive it. And there's nothing wrong with using LiveData or Flow objects inside the VM, and wiring those up so a new TokenEntity gets pushed to an observer that calls saveTokenToDb, if that kind of pipeline setup makes sense! But keep that stuff private if the outside world doesn't need to know about those intermediate steps

Flow emits different values when collecting it multiple times

I created a Flow from which I emit data. When I collect this flow twice, there are 2 different sets of data emitted from the same variable instead of emitting the same values to both collectors.
I have a simple Flow that I created myself. The text will be logged twice a second
val demoFlow: Flow<String> = flow {
while (true) {
val text = "Flow ${(0..100).random()}"
Log.d("TAG", text)
emit(text)
delay(1000)
}
}
In my viewModel I have a simple function that gets the previous Flow
fun getSimpleFlow() = FlowRepository.demoFlow
And in my Fragment I collect and display my Flow
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.getSimpleFlow().collect {
binding.tv1.text = it
}
}
launch {
viewModel.getSimpleFlow().collect {
binding.tv2.text = it
}
}
}
}
If I transform the Flow to a StateFlow or a SharedFlow, I no longer have this problem.
I don't understand how or why this happens since I'm using the same 'demoFlow' variable.
Is there a way to get the same values from 'demoFlow' without converting to a StateFlow or a SharedFlow?
Regular Flows are cold, this behaviour is by design.
The demoFlow is the same, so you have the same Flow instance. However, collecting the flow multiple times actually runs the body inside the flow { ... } definition every time from the start. Each independent collection has its own variable i etc.
Using a StateFlow or a SharedFlow allows to share the source of the flow between multiple collectors. If you use shareIn or stateIn on some source flow, that source flow is only collected once, and the items collected from this source flow are shared and sent to every collector of the resulting state/shared flow. This is why it behaves differently.
In short, reusing a Flow instance is not sufficient to share the collection. You need to use flow types that are specifically designed for this.

Kotlin flow (or something similar) that can be collected with multiple collectors

I tried using Kotlin Flow to be some kind of message container which should pass this message to all observers (collectors). I do not want to use LiveData on purpose because it need to be aware of lifecycle.
Unfortunately I have noticed that if one collector collects message from flow no one else can receive it.
What could I use to achieve "one input - many output".
You can use StateFlow or SharedFlow, they are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers.
From the documentation, available here:
StateFlow: is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property.
SharedFlow: a hot flow that emits values to all consumers that collect from it. A SharedFlow is a highly-configurable generalization of StateFlow.
A simple example using state flow with view model:
class myViewModel() : ViewModel() {
val messageStateFlow = MutableStateFlow("My inicial awesome message")
}
You can emit a new value using some scope:
yourScope.launch {
messageStateFlow.emit("My new awesome message")
}
You can collect a value using some scope:
yourScope.launch {
messageStateFlow.collect {
// do something with your message
}
}
Attention: Never collect a flow from the UI directly from launch or the launchIn extension function to update UI. These functions process events even when the view is not visible. You can use repeatOnLifecycle as the documentation sugests.
You can try BehaviorSubject from rxJava. Is more comfortable to use than poor kotlin.flow. Seems like this link is for you: BehaviorSubject vs PublishSubject
val behaviorSubject = BehaviorSubject.create<MyObject> {
// for example you can emit new item with it.onNext(),
// finish with error like it.onError() or just finish with it.onComplete()
somethingToEmit()
}
behaviorSubject.subscribe {
somethingToHandle()
}

Issue collecting a coroutine flow when applying a combine transformation on fragment created

I am having a strange issue with coroutine flows. I have two flows that i need to observe and if one of them emits a value, collect both flows and perform some transformations based on business logic to emit one result which is the result i am interested in collecting from the fragment. Currently, i am doing this combine in the viewmodel like this
//Done in viewmodel
val someComparisonValue = 2
val combinedFlow = firstFlow.combineTransform(secondFlow) { value1, value2 ->
if (value1 != someComparisonValue) {
emit(someValueComputed)
}else if (value1 == value2){
emit(AnotherValue)
}
// if none of these conditions are met, emit the first value instead
emit(value1)
}
// done in fragment
lifecycleScope.launchWhenCreated {
view.combinedFlow.collect {
Log.e("Logged", it.toString())
}
My issue is that this does not get collected when the fragment gets created even though the two flows already have values emitted as they are connected to a broadcast channel. The transformation does however happen when the fragment has already been created and another emission has been sent. The weird part is without the conditional logic within the combineTransform, the fragment will always get the value on create. It seems the doing the conditional logic is not something it executes on create. Does someone have a clue on how to fix this?
Found the solution, somehow using broadcast channels was the root cause of the issue. For some reason, emissions were probably dropped so the flow would not be triggered. Switching to ConflatedBroadcastChannels solved this issue for me

Notifying LiveData observers but without passing them any data

I have a case where I have LiveData observer that monitors a condition that indicates if the user is signed in. The observer will only get notified when the user is signed in. I don't need to pass any data to the observer. When the observer gets called, it simply means that the user is signed in:
val observer = Observer<String> { signedIn ->
// The user is signed in. Do something...
}
model.isSignedIn.observe(this, observer)
In my viewmodel I believe I'm suppose to update the observer as follows:
isSignedIn.setValue()
Is this the proper way to update an observer that doesn't require any data sent to it? LiveData is really about notifying observers about data changes. But in my example, I'm using it to notify about an event change. It's a subtle difference and maybe using LiveData for this case is not the best way of doing it.
In that case you can use LiveData, it has no restrictions, especially if you want to be lifecycle aware.
If you want to have more clear API for that case you can use extension function mechanism. And in your case, suggest to use Unit type for live data variable.
typealias NoValueLiveData = MutableLiveData<Unit>
fun NoValueLiveData.setValue() {
this.value = Unit
}

Categories

Resources