Observer registered after LiveEvent already has value is not executed
Closed this issue ยท 21 comments
If the LiveEvent's setValue
is called before observe
and there is no observer to consume the event the next registered observer is not receiving the event as well because pending is by default false
.
This is maybe a deliberate decision but I find it a huge weakness of the design.
Thanks @bio007 for you raising your concern, but the value should be an event, and this implementation don't want to keep events by definition. Try to call observe
before setting the value. It would be the correct architecture design by the way.
I understand your point but sometimes it can be handy to have default value set, or value set during initialization (of VM). In that case no observer is there to listen and it is not bad pattern or violating MVVM in any way IMHO.
LiveData and "SingleLiveData" (from Google example project) support this "feature" same way as behavior subjects or channels in Flow.
Anyway if you don't want to support this behavior I can live without it and use custom LiveData :)
Thanks! But default value for an event!?
You are comparing this implementation with behavior subject, where it's comparable with a PublishSubject
. The BehaviorSubject
is comparable with LiveData
itself. So everything is on its place.
Regarding the Kotlin channels, you need to consider suspension into account, where we don't have anything to compare to it here! Which means in Kotlin channels the send
will suspend until somebody receive
the event, so the send
will not be executed at all! Therefore, it's not a default! It's a little bit like in Rx we publish the value in onSubscribe
method, which again is not a default!
In the end, you're a free man/woman so please use whatever works for you :)
I faced the same issue today but I think when I tried using postValue
then the observer received the value. I am not sure how this is working.
@hadilq Can you please give me any hint regarding this behavior?
@nikhiljainlive You need to observe the LiveEvent
, or generally any LiveData
implementation, in the first callback you have. For instance, in Activity and Fragments you need to observe it in onCreate
method. Remember LiveData
is lifecycle aware so it will not call onChange
in where you call observe
, in this case onCreate
.
@hadilq I am observing the LiveEvent in my Activity's onCreate
method only. Refer below for a detailed explanation of my issue.
I have a ViewModel class in which there are two properties like shown below:
private val _currentUser = LiveEvent<CurrentUserState>()
val currentUser = _currentUser as LiveData<CurrentUserState>
And, in ViewModel init
block, I am loading the user by calling the loadCurrentUser
function from some data storage. This later on sets the Success
event or Failure
event when the user is loaded or any failure occurs. Refer to the code snippet shown below:
init {
_currentUser.value = Loading
viewModelScope.launch {
loadCurrentUser()
}
}
The currentUser
Livedata is then observed at MainActivity onCreate
method.
Issue:
The problem is the Loading
event is never emitted when I set the value to LiveEvent by calling _currentUser.value = Loading
(setValue method). But when I set the value in LiveEvent by calling _currentUser.postValue(Loading)
(postValue method), the observer receives the Loading value even if it was not registered when the value was posted.
So, I see different behaviors when using setValue
and postValue
with the LiveEvent.
This is related to the rejected PR I posted a while back: #24
I still consider it to be a bug, especially given how many people continue to run into this situation. The emitter shouldn't care about the order of observers but ยฏ\_(ใ)_/ยฏ
@hadilq I am observing the LiveEvent in my Activity's
onCreate
method only. Refer below for a detailed explanation of my issue.
I have a ViewModel class in which there are two properties like shown below:private val _currentUser = LiveEvent<CurrentUserState>() val currentUser = _currentUser as LiveData<CurrentUserState>And, in ViewModel
init
block, I am loading the user by calling theloadCurrentUser
function from some data storage. This later on sets theSuccess
event orFailure
event when the user is loaded or any failure occurs. Refer to the code snippet shown below:init { _currentUser.value = Loading viewModelScope.launch { loadCurrentUser() } }The
currentUser
Livedata is then observed at MainActivityonCreate
method.Issue:
The problem is theLoading
event is never emitted when I set the value to LiveEvent by calling_currentUser.value = Loading
(setValue method). But when I set the value in LiveEvent by calling_currentUser.postValue(Loading)
(postValue method), the observer receives the Loading value even if it was not registered when the value was posted.
So, I see different behaviors when usingsetValue
andpostValue
with the LiveEvent.
I also think this should be supported (that's why I created the PR) but well... I understand the other points as well. Anyway post is working for you because it is asynchronous and your observer make it to register in the meantime. But I wouldn't take this "luck" for granted as it can be unpredictable.
@nikhiljainlive Thank you for sharing details. This problem happens because in init
of VM no LiveData
is obsereved yet. Personally I would post
it as you did, but it may really be a problem. I need to think more guys.
@hadilq Thanks for the response.
@nikhiljainlive I tried to reproduce your problem with tests, then I remember I had this trouble before!
Above I claimed that I understood your problem by saying "This problem happens because in init of VM no LiveData is obsereved yet.", but I didn't! It's not the case, because we have a test in the test suite like
@Test
fun `observe after start`() {
// Given
owner.create()
liveEvent.observe(owner, observer)
val event = "event"
// When
liveEvent.value = event
// Then
verify(observer, never()).onChanged(event)
// When
owner.start()
// Then
verify(observer, times(1)).onChanged(event)
}
It explicitly mentioned that the event should be kept by the LiveEvent
until we observe it after start
.
In you example, you have called loadCurrentUser()
method. Maybe it responses faster than you expect, before onStart
, and that's why you cannot see the loading!
Can you take a time to create a test method that simulate your case and fails, so I can understand what's going wrong! Thanks man.
@hadilq I tried running the above test and it's passing. But I think this is not the case with me and I created and run another test and it's failing. Refer to the below test:
@Test
fun `observe after start`() {
// Given
owner.create()
val event = "event"
// When
liveEvent.value = event
// Then
verify(observer, never()).onChanged(event)
liveEvent.observe(owner, observer)
// When
owner.start()
// Then
verify(observer, times(1)).onChanged(event)
}
In my case, refer to the below steps:
lifecycleOwner
gets created (for example -MainActivity onCreate gets called
)- ViewModel gets created which calls the init method and sets the Loading state on LiveEvent
- lifecycleOwner starts observing the LiveEvent
- And finally the lifecycleOwner starts.(for example -
MainActivity onStart gets called
)
The same scenario is reflected in the test I have shared. Refer to the below example for my code:
// MainActivity onCreate
// initializing viewmodel in onCreate which calls the init of ViewModel (refer to the VM code)
viewModel = ViewModelProvider(this, viewModelFactory)
.get(MainActivitySharedViewModel::class.java)
// observing user state after init is called
sharedViewModel.currentUser.observe(this, Observer { userState ->
when (userState) {
is Loading -> {
// not emitted
}
// other states
}
}
Refer to this comment for VM code.
Please note that the event emitted in the init method of VM i.e Loading is never emitted using setValue.
@nikhiljainlive
Is there any reason you use LiveEvent
to handle the CurrentUserState
? I mean it's clearly a "state" and should be handled by LiveData
, not LiveEvent
. Your test is clearly setting an event that should be observed in the future, which makes it being not-event! I had a long discussion with @fergusonm in his #24 about a pending event and multi observer situations. Let me know if it helped.
@hadilq I handle the navigation logic based on the CurrentUserState
. In brief, I show the progress bar when CurrentUserState
is Loading
, navigate to the login screen when Failure
, and navigate to HomeScreen when CurrentUserState
is Success
.
So as you see, It's clear that I don't want to reset my navigation and lose my navigation state in case of a configuration change (where LiveData
emits the value again) and that's why here LiveEvent
fulfills my purpose.
Your test is clearly setting an event that should be observed in the future, which makes it being not-event!
I would disagree that it's not an event in the definition of "single live event". I would also argue that's actually desirable to send an event for a future observer to see. Otherwise there is a coupling of the lifecycles between the observer and the emitter that I don't think should exist. (The emitter has to know that there's no observer yet and it's not safe to emit.)
Say during view model initialization you want to show a toast, send a broadcast or a push notification or any other action that should happen just once. (Perhaps some early error condition was noticed.) It's very conceivable that the view model could emit an event well before the observer has started to consume the events.
This situation can happen not only at view model initialization time but also during configuration change if things are timed just right. The observers stop observing and for a very brief moment it's possible to emit a value before the new observers start observing.
The first condition, emitting during view model initialization, is "solved" by holding a pending event until the first observer consumes it. The second condition, emitting during a brief configuration change window, is not solvable without moving to something like channels.
I strongly agree with @fergusonm. I have always used on-time event (e.g. SingleLiveEvent as an event which should be processed exactly one time and then discarded. But that doesn't mean it should be discarded before anyone got a chance to process it.
For those interested, I wrote an article on an alternative implementation of the single live event pattern using Kotlin channels and flows.
I want to be clear in that it's just an alternative to LiveEvent and I'm in no way implying that Hadi's solution bad. It's just a different choice. (For example my solution can and must only have a single observer for events as the emitter is backed by a channel rather than LiveData.) It was his implementation and the discussions I had with him that really got me thinking about what requirements my particular projects needed.
@fergusonm Thanks for your review. I'll merge it soon and release it.
Also I have read your article. Nice job. I never had a chance to say thank you for including my solution in your article. Thanks man ;)
Regarding this solution, you and I know LiveData
is deprecated, so I would not recommend it if you already are not depending on it. For those who have legacy code to support, I continue supporting this library because I still have time and there is a demand.
Regarding the single observer for single event, it looks like a good decision to make. Here we are not in the position to change the behavior of this library to support only one observer, but if I had to do it again, I would go for the single observer too.
Hey no worries. Your library was the entire reason I did a full deep dive on single live events. Both your medium article and the library solved many issues for me. The single live event "issue" is hard. Even my solution doesn't fully address all use cases.
I think the single observer is really only needed because there isn't something like RxJava's connectable observables with Kotlin flow. That is, something where you can stop the stream from emitting while you take time to set up the observers. It's also a side effect of channels since channels have a fan-out behaviour.
At one point I even wrote an RxJava "ValveSubject" to test out a stream that seemed it would solve the problem of multiple observers. If the "valve" was closed nothing was emitted from the stream. That would allow observers time to get set up and ready. Then the valve could be opened and events could be observed. On config change the valve could be closed momentarily and then reopened when the new fragment or activity was ready.
Maybe one day I'll re-write it for Kotlin flows...
@fergusonm ValveSubject is a nice idea. Looking forward to see its Kotlin flow implementation ;)