Ningan
Ningan

Reputation: 171

How to stop LiveData event being triggered more than Once

I am using MutableLiveData within my application for event based communication. I have single activity two fragments architecture.

With the help of ViewModel, I'm consuming the LiveData events in Fragment-1. But, when I replace this Fragment-1 with Fragment-2 using Menu bar and finally come back to Fragment-1, old values of LiveData are captured again.

How to avoid this problem? Any help/suggestions are highly appreciated! Thank you.

Upvotes: 7

Views: 4493

Answers (6)

Denis Luttcev
Denis Luttcev

Reputation: 385

@Ningan, my idea is to provide a base class of one-time events to be inherited into client code, and wrap each observer received from the client code into an internal one that operates on the base class.

OnceEventLiveData.java (also available as Github Gist):

public class OnceEventLiveData<T extends OnceEventLiveData.OnceEvent> extends MutableLiveData<T> {
    @Override
    public void observe(@NonNull final LifecycleOwner owner,
                        @NonNull final Observer<? super T> observer) {
        super.observe(owner, new OnceEventObserver<>(observer));
    }

    @Override
    public void observeForever(@NonNull final Observer<? super T> observer) {
        super.observeForever(new OnceEventObserver<>(observer));
    }

    @Override
    public void removeObserver(@NonNull final Observer<? super T> observer) {
        super.removeObserver(new OnceEventObserver<>(observer));
    }

    private static class OnceEventObserver<T extends OnceEvent> implements Observer<T> {
        @NonNull
        private final Observer<OnceEvent> mObserver;

        OnceEventObserver(@NonNull final Observer<? super T> observer) {
            //noinspection unchecked
            mObserver = (Observer<OnceEvent>) observer;
        }

        @Override
        public void onChanged(@NonNull OnceEvent event) {
            if (!event.mIsConsumed.get()) {
                mObserver.onChanged(event);
                event.mIsConsumed.getAndSet(true);
            }
        }

        @Override
        public boolean equals(@Nullable final Object other) {
            if (this == other) return true;
            if (other == null || getClass() != other.getClass()) return false;

            OnceEventObserver<?> that = (OnceEventObserver<?>) other;

            return mObserver.equals(that.mObserver);
        }

        @Override
        public int hashCode() {
            return mObserver.hashCode();
        }
    }

    /**
     * Declares a base class for any consumer data that be intended for use into this LiveData
     * implementation.
     */
    public abstract static class OnceEvent {
        @NonNull
        private final AtomicBoolean mIsConsumed = new AtomicBoolean(false);
    }
}

Abstract OnceEvent class encapsulates it own "is consumed" status. It makes necessary to inherit the base class and create separate classes for each type of event.

On the other hand, the View and ViewModel do not change compared to use regular LiveData, you can add and remove any needed observers; all one-time processing logic is well-encapsulated into nested classes. It allows you to use OnceEventLiveData as any others LiveData, and to be focused only on the meaning of your custom event.

Usage in ViewModel :

class EventViewModel extends ViewModel {


    public static class CustomEvent extends OnceEventLiveData.OnceEvent {
        // any needed event's payload here
    }


    @NonNull
    private final OnceEventLiveData<CustomEvent> mEvent = new OnceEventLiveData<>();

    @NonNull
    public LiveData<CustomEvent> getEvent() {
        return mEvent;
    }

    public void doSomething() {
        // business logic that beget the event
        mEvent.setValue(new CustomEvent());
    }

}

Usage in Activity/Fragment :

class EventActivity extends AppCompatActivity implements Observer<CustomEvent> {


    @Override
    protected void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        EventViewModel eventViewModel = new ViewModelProvider(this).get(EventViewModel.class);
        eventViewModel.getEvent().observe(this, this);
    }

    @Override
    public void onChanged(@NonNull CustomEvent event) {
        // eevent handling logic here
    }


}

Upvotes: -1

rojarand
rojarand

Reputation: 644

Simple, clean, reusable:

class Event<T>(val payload: T, var broadcasted: Boolean = false)

class MutableEventLiveData<T>: MutableLiveData<Event<T>>() {
    fun postEvent(value: T) {
        super.postValue(Event(value))
    }
}

typealias EventLiveData<T> = LiveData<Event<T>>

class EventObserver<T>(private val broadcastCallback: (t: T)->Unit): Observer<Event<T>> {

    override fun onChanged(e: Event<T>) {
        if (!e.broadcasted) {
            broadcastCallback(e.payload)
            e.broadcasted = true
        }
    }
}

Sample usage:

class YourViewModel : ViewModel() {
    private val _errorEvent = MutableEventLiveData<String>()
    val errorEvent: EventLiveData<String>
        get() = _errorEvent

    fun fireErrorEvent(errorMessage: String) {
        _errorEvent.postEvent(errorMessage)
    }
    ...
}

class YourActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        //Note!!! EventObserver handles events not Observer
        viewModel.errorEvent.observe(this, EventObserver { 
            errorMessage -> showErrorMessage(errorMessage)
        })
    }
    ...
}

Upvotes: 0

ueen
ueen

Reputation: 692

I faced the same problem and came up with this library to solve it https://github.com/ueen/LiveEvent Hope this helps, enjoy!

Upvotes: 0

Darko Martinović
Darko Martinović

Reputation: 169

Problem with accepted answer is that you can only have one observer. This article describes solution with multiple observers.

Upvotes: 1

Kashish Sharma
Kashish Sharma

Reputation: 769

Wherever you're observing the liveData, in onChanged method remove the observers by calling myLiveDataObject.removeObservers(this); This will remove the observer after first-time data is observed.

Upvotes: 0

Metwalli
Metwalli

Reputation: 1891

You can use Event to wrap LiveData values to handle consuming its values as in the following article: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

Event class would be like:

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
}

Let us say that your LiveData value is a String then the LiveData of single event would be like:

val navigateToDetails = MutableLiveData<Event<String>>()

Upvotes: 5

Related Questions