I have MainActivity which is containing 5 Fragments (FragmentA, FragmentB, FragmentC....E) with ViewPager. FragmentA has viewModel and observing a MutableLiveData called "showPopupSuccess" which set to true after doing a task.
The problem is when i go to FragmentC and then get back to FragmentA. The popup is showing again because the observer looks like "reactivated". How to get rid of this? I want the mutableLiveData get resetted. So it has no value in it and not showing a popup
This is the video of the bug if you want to see further
https://www.youtube.com/watch?v=Ay1IIQgOOtk
The easiest way to solve your issue: Use the Event wrapper
Instead of "resetting" the LiveData, you can mark its content as handled when you observe it for the first time. Then your repeated observations know it has already been handled and can ignore it.
To create a better answer according to the guidelines I'm copying over the relevant information from the linked article:
The wrapper:
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
In the ViewModel:
// Instead of Boolean the type of Event could be popup
// parameters or whatever else.
private val _showSuccess = MutableLiveData<Event<Boolean>>()
val showSuccess : LiveData<Event<Boolean>>
get() = _showSuccess
In the Fragment:
myViewModel.showSuccess.observe(viewLifecycleOwner, Observer {
it.getContentIfNotHandled()?.let {
// This is only executed if the event has never been handled
showSuccess(...)
}
})
Related
I have a LiveData in my ViewModel:-
private val _toastMessage = MutableLiveData<Long>()
val toastMessage
get() = _toastMessage
And this is the only way I am changing it's value(on click of a submit button in the fragment):-
fun onSubmitClicked(<params>){
Log.i(LOG_TAG, "submit button clicked")
uiScope.launch {
if(!myChecksForEditTextValuesSucceeded())
{
_toastMessage.value = 0
}else{
_toastMessage.value = 1
}
}
}
And in the fragment, I have an observer for this LiveData:-
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { it->
when{
(it.compareTo(0) == 0) -> Toast.makeText(context, resources.getString(R.string.toast_msg_transaction_not_inserted), Toast.LENGTH_SHORT).show()
else -> Toast.makeText(context, resources.getString(R.string.toast_msg_transaction_inserted), Toast.LENGTH_SHORT).show()
}
})
Ideally, I am expecting the onChange of this Observer to be called only on clicking the submit button on my fragment. But, as I can see, it is also getting called even on onCreateView of my fragment.
What could be the possible reasons for this?
The issue is that LiveData pushes new values while you're observing it, but it also pushes the most recent value when you first observe it, or if the observer's Lifecycle resumes and the data has changed since it was paused.
So when you set toastMessage's value to 1, it stays that way - and ViewModels have a longer lifetime than Fragments (that's the whole point!) so when your Fragment gets recreated, it observes the current value of toastMessage, sees that it's currently 1, and shows a Toast.
The problem is you don't want to use it as a persistent data state - you want it to be a one-shot event that you consume when you observe it, so the Toast is only shown once in response to a button press. This is one of the tricky things about LiveData and there have been a bunch of workarounds, classes, libraries etc built around making it work
There's an old post here from one of the Android developers discussing the problem with this use case, and the workarounds available and where they fall short - in case anyone is interested! But like it says at the top, that's all outdated, and they recommend following the official guidelines.
The official way basically goes:
something triggers an event on the ViewModel
the VM updates the UI state including a message to be displayed
the UI observes this update, displays the message, and informs the VM it's been displayed
the VM updates the UI state with the message removed
That's not the only way to handle consumable events, but it's what they're recommending, and it's fairly simple. So you'd want to do something like this:
// making this nullable so we can have a "no message" state
private val _toastMessage = MutableLiveData<Long?>(null)
// you should specify the type here btw, as LiveData instead of MutableLiveData -
// that's the reason for making the Mutable reference private and having a public version
val toastMessage: LiveData<Long?>
get() = _toastMessage
// call this when the current message has been shown
fun messageDisplayed() {
_toastMessage.value = null
}
// make a nice display function to avoid repetition
fun displayToast(#StringRes resId: Int) {
Toast.makeText(context, resources.getString(resId), Toast.LENGTH_SHORT).show()
// remember to tell the VM it's been displayed
transactionViewModel.messageDisplayed()
}
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { it->
// if the message code is null we just don't do anything
when(it) {
0 -> displayToast(R.string.toast_msg_transaction_not_inserted)
1 -> displayToast(R.string.toast_msg_transaction_inserted)
}
})
You also might want to create an enum of Toast states instead of just using numbers, way more readable - you can even put their string IDs in the enum:
enum class TransactionMessage(#StringRes val stringId: Int) {
INSERTED(R.string.toast_msg_transaction_inserted),
NOT_INSERTED(R.string.toast_msg_transaction_not_inserted)
}
private val _toastMessage = MutableLiveData<TransactionMessage?>(null)
val toastMessage: LiveData<TransactionMessage?>
get() = _toastMessage
uiScope.launch {
if(!myChecksForEditTextValuesSucceeded()) toastMessage.value = NOT_INSERTED
else _toastMessage.value = INSERTED
}
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { message ->
message?.let { displayToast(it.stringId) }
// or if you're not putting the string resource IDs in the enum:
when(message) {
NOT_INSERTED -> displayToast(R.string.toast_msg_transaction_not_inserted)
INSERTED -> displayToast(R.string.toast_msg_transaction_inserted)
}
})
It can be a bit clearer and self-documenting compared to just using numbers, y'know?
I'm implementing the viewModel and for communicate between the viewModel and fragment I'm doing this :
public class SplashViewModel extends AndroidViewModel {
private LiveData<Boolean> actions;
public SplashViewModel(#NonNull Application application) {
super(application);
actions= new MutableLiveData<>();
}
public void aViewModelMethod() {
//doing some stuff
if (stuff == X){
//I need to hide a view for exemple, I'm doing this
actions.postValue(true);
}
}
Now inside my Fragment I have an observable who is trigger when actions.postValue(true) is reached
viewModel.actions.observe(getViewLifecycleOwner(), new Observer<Boolean>() {
#Override
public void onChanged(Boolean action) {
if (action){
databinding.myView.setVisibility(View.VISIBLE);
}
}
});
This work fine but if I have a lot of communication I need to implement each time a new variable, and observe it ?
It's ok when they are 4 or 5 but what I am suppose to do when they are hundreds ?
I try to change boolean by an integer with a switch and a list of actions, but when the viewModel is initialize it's possible that several postValue are trigger and when I created the observable I'm only get the last one, that make sense.
Usually, I have two observable live data in my view model. First is represent the state of the whole screen. Second I use for "single-shot" events like toasts, navigation, showing dialogs.
My view model:
class PinCreateViewModel(...) : ViewModel() {
val event = MutableLiveData<BaseEvent<String>>()
val state = MutableLiveData<PinCreateViewState>()
}
I have a single state object for the whole screen:
sealed class PinCreateViewState {
object FirstInput : PinCreateViewState()
data class SecondInput(val firstEnteredPin: String) : PinCreateViewState()
object Error : PinCreateViewState()
object Loading : PinCreateViewState()
}
I think with this approach it's easy to think about my screen states, easy to design my screen as a finite state machine, and easy to debug. Especially, I like this approach to very complex screens. In this case, I have a single source of truth for my whole screen state.
But sometimes I want to show dialogs, toast or open new screens. These things are not part of my screen state. And this is why I want to handle them separately. And in this case, I use Events:
sealed class BaseEvent(private val content: String) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): String? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): String = content
}
class ErrorEvent(content: String) : BaseEvent(content)
class MessageEvent(content: String) : BaseEvent(content)
And my Fragment interaction with ViewModel looks like this:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
observe(viewModel.event, this::onEvent)
observe(viewModel.state, this::render)
}
private fun render(state: PinCreateViewState) {
when (state) {
PinCreateViewState.FirstInput -> setFirstInputState()
is PinCreateViewState.SecondInput -> setSecondInputState()
PinCreateViewState.Error -> setErrorState()
PinCreateViewState.Loading -> setLoadingState()
}
}
fun onEvent(event: BaseEvent<String>) {
event.getContentIfNotHandled()?.let { text ->
when (event) {
is MessageEvent -> showMessage(text)
is ErrorEvent -> showError(text)
}
}
}
I really like Kotlin Sealed classes because it forces me to handle all possible cases. And I can find unhandled states even before compilation.
PostValue method Posts a task to the main thread to set the given value. If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.
If you will have hundreds of communication between your fragment and viewModel so you just have to deduce your fragment logic like if you have to show view on some conditions then just observe one non mutable live data in your fragment and use two live data's mutable and another non mutable .... use non mutable to set that boolean on every sort of stuff and checks in your viewModel and in the beginning assign that live data to your non mutable one.
private val _liveData = MutableLiveData<Boolean>()
internal val liveData: LiveData<Boolean> = _liveData
It's the better approach ,i hope i understand you question better if not please elaborate it more so that i can help .
I have the following ViewModel with MutableLiveData data and another LiveData ones that is derived from data in a way that it updates its value only if the data.number is equal to 1.
class DummyViewModel : ViewModel() {
private val data = MutableLiveData<Dummy>()
val ones = data.mapNotNull { it.takeIf { it.number == 1 } }
init {
data.value = Dummy(1, "Init")
doSomething()
}
fun doSomething() {
data.value = Dummy(2, "Do something")
}
}
data class Dummy(val number: Int, val text: String)
fun <T, Y> LiveData<T>.mapNotNull(mapper: (T) -> Y?): LiveData<Y> {
val mediator = MediatorLiveData<Y>()
mediator.addSource(this) { item ->
val mapped = mapper(item)
if (mapped != null) {
mediator.value = mapped
}
}
return mediator
}
I observe ones in my fragment. However, If I execute doSomething, I don't receive any updates in my fragment. If I don't execute doSomething, the dummy Init is correctly present in ones and I receive an update.
What is happening here? Why is ones empty and how can I overcome this issue?
Maybe I'm missing something, but the behavior seems like expected to me...
Lets' try to reproduce both cases sequentially.
Without doSomething() :
Create Livedata
Add Dummy(1, "Init")
Start listening in the fragment: Because number is 1, it passes your filter and the fragment receives it
With doSomething():
Create Livedata
Add Dummy(1, "Init")
Add Dummy(2, "Do something") (LiveData keeps only the last value, so if nobody observes, the first value is getting lost)
Start listening in the fragment: Because number is 2, the value gets filtered and the fragment receives nothing
A little offtopic: it's always good to write tests for ViewModel cases like this, because you'll be able to isolate the problem and find the real reason quickly.
EDIT: also be aware that your filter is only working on observing, it isn't applied when putting the value into LiveData.
In my fragment I observe dbQuestionsList field:
viewModel.dbQuestionsList.observe(viewLifecycleOwner, Observer { list ->
Log.i("a", "dbQuestionsList inside fragment = $list ")
})
In my fragment I have few buttons and depending on which one is pressed I call method on viewModel passing the string which was set as tag to the button.
viewModel.onFilterChanged(button.tag as String)
My ViewMode:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("")
}
private fun onFilterChanged(filter: String) {
dbQuestionsList = mRepository.getDbQuestionsForCategory(filter)
}
Repository method:
fun getDbQuestionsForCategory(categoryName: String): LiveData<List<DatabaseQuestion>> {
return database.dbQuestionsDao().getDbQuestionsByCategory(categoryName)
}
Dao method:
#Query("SELECT * FROM db_questions_database WHERE category = :categoryId")
fun getDbQuestionsByCategory(categoryId: String): LiveData<List<DatabaseQuestion>>
When I press button, viewModel method is called with argument which should be used to update LiveData by searching through room database, but NOTHING gets updated for no reason. Database is not empty so there is no reason to return null and not trigger observer in main Fragment.
But when I do this in my viewModel:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("travel")
}
where I hardcode parameter, the room will return list and observer in fragment will be triggered, so it works like that but doesn't work when arguments is passed when button is pressed, Please explain because this thing doesn't make sense. I tried with mutable live data, with using .setValue and .postValue but NOTHING works.
The reason you aren't getting updates is because onFilterChanged() is reassigning dbQuestionsList, not updating it. So the variable you observe initially is never actually modified.
I would probably implement this using a Transformation:
val filter = MutableLiveData<String>().apply { value = "" }
val dbQuestionsList = Transformations.switchMap(filter) {
mRepository.getDbQuestionsForCategory(filter)
}
Then in your fragment just set the filter when your button is clicked:
viewModel.filter.value = button.tag as String
Try this:
dbQuestionsList.value = mRepository.getDbQuestionsForCategory(filter)
or
dbQuestionsList.postValue(mRepository.getDbQuestionsForCategory(filter))
I created live data which emits a single event as in this example.
My question is next:
How to notify only last subscribed observer when the value in the LiveData changes?
What comes to my mind is to store observers in the linked list in SingleLiveData class and then to call super.observe only if a passed observer is the same as the last element of the list.
I'm not sure if this is the best approach.
I want to use this mechanism to propagate FAB click events from activity to the fragments which are shown inside of the ViewPager. Fragments are dynamically added to view pager adapter, so let's say that we know the order of the fragments.
In the end, I found a workaround for this problem. I had to move away from the live data that emits a single event since it couldn't behave the way I needed it to behave.
Instead of this, I used simple mutable live data which emits an event object which wraps a data as in the last paragraph of this article by Jose Alcérreca.
I'm showing fragments in a view pager so I have only one visible fragment at the time.
So my view model looks like this:
class ActionViewModel : ViewModel() {
private val onCreateLiveData: MutableLiveData<Event<String>> = MutableLiveData()
fun observeOnCreateEvent(): LiveData<Event<String>> = onCreateLiveData
fun onCreateCollectionClick(message: String) {
this.onCreateLiveData.value = Event(message)
}
}
Event wrapper class implementation looks like this:
/*Used as a wrapper for data that is exposed via a LiveData that represents an
event.*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
In fragments now we can observe events like this:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionViewModel::class.java)
actionViewModel.observeOnCreateEvent()
.observe(this, Observer {
it?.takeIf { userVisibleHint }?.getContentIfNotHandled()?.let {
//DO what ever is needed
}
})
}
Fragment userVisibleHint property will return true if the fragment is currently visible to the user. Since we are only showing one fragment at the time this works for us. This means that the fragment will only access the event data if it is visible.
Also, implementation of the Event wrapper allows only one read of the value, so that every next time Observer gets this event, its value will be null and we'll ignore it.
Conclusion: This way we are simulating a single event live data which notifies only last subscribed observer.
If you're using Kotlin, you can replace LiveData with Flow. StateFlow can be used to replace regular LiveData, while SharedFlow can be used for stateless events. It will also provide you null safety and all the operators and configurations that come with Flow.
The migration is described here among other places. Here's a basic example:
ViewModel:
interface MyViewModel {
val myData: StateFlow<MyData>
val myEvents: SharedFlow<MyEvent>
}
class MyViewModelImpl: MyViewModel {
override val myData = MutableStateFlow(MyData())
override val myEvents = MutableSharedFlow<MyEvent>(replay = 0, extraBufferCapacity = 1, BufferOverflow.DROP_OLDEST)
/*
* Do stuff
*/
}
Activity:
lifecycleScope.launch {
myData.collect {
// handle stateful data
}
}
lifecycleScope.launch {
myEvents.collect {
// handle stateless events
}
}
Note that lifecycleScope requires the appropriate ktx dependency.
Herer's some more reading about Flow in Android.
I found solution for me in LD extension:
fun <T> LiveData<T>.observeAsEvent(owner: LifecycleOwner, observer: Observer<in T>) {
var previousKey: Any? = value?: NULL
observe(owner) { value ->
if (previousKey == NULL || previousKey != value) {
previousKey = value
observer.onChanged(value)
}
}
}
private const val NULL = "NULL"
Usage for this:
viewModel.resultLiveData.observeAsEvent(viewLifecycleOwner) {
...
}
I crafted a solution, feel free to take a look
https://github.com/ueen/LiveEvent
I've created a library to handle the most common cases that we might encounter while working with event-driven data scenarios
https://github.com/javaherisaber/LiveX
It contains the following types of classes:
LiveData
LiveEvent
OneShotLiveEvent
SingleLiveEvent
Multiple observers can register, all of them receive the event based on lifecycle
Multiple observers can register, each one receive the event only once
Only one observer can register and receive the event only once
Multiple observers can register, only the first one receive the event