Joaquim Ley
Joaquim Ley

Reputation: 4127

MVVM // ViewModel event being fired on Activity rotation (recreated)

Reading the Google docs I found (sort of) an example of using a selectedItem in order to propagate an event being fired to other observers, this is my current implementation:

ViewModel

public void onListItemClicked(Item item) {
    if (selectedItem.getValue() == item) {
        return;
    }
    selectedItem.postValue(item);
}


public LiveData<Item> getSelectedItem() {
    if (selectedItem == null) {
        selectedItem = new MutableLiveData<>();
    }

    return selectedItem;

}

View

ListViewModel viewModel = ViewModelProviders.of(this).get(ListViewModel.class);

viewModel.getSelectedItem().observe(this, new Observer<Item>() {
    @Override
    public void onChanged(@Nullable Item item) {
        if (item != null) {
            openDetailActivity(item);
        }
    }
});

And when the user clicks the list:

@Override
public void onItemClicked(Item item) {
    viewModel.onListItemClicked(item);
}

All good and all it works, the problem is when the user rotates the screen and the ListActivity is re-created detects a change and will open the DetailActivity when subscribing.

I found a workaround which is adding selectedItem.postValue(null); on the getSelectedItem() but it's a little hacky.

Ofc one could argue that the opening the details activity and propagating the even should be separate, but I was wondering if someone has a better implementation/suggestion.

Upvotes: 0

Views: 832

Answers (1)

Joaquim Ley
Joaquim Ley

Reputation: 4127

EDIT

Using the SingleLiveEvent is the way to go. This makes sure your ViewModel only fires the event once.

Here's the reference article:

I've created a gist with the Kotlin class. I've been using with success for these use-cases:

I'll keep the gist up-to-date, but I'll leave the code here also (FYI this might be outdated as I won't be editing this answer every time I make a change to the gist):


package YOUR_PACKAGE


import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean

/**
 * A lifecycle-aware observable that sends only new updates after subscription, used for events like
 * navigation and Snackbar messages.
 * <p>
 * This avoids a common problem with events: on configuration change (like rotation) an update
 * can be emitted if the observer is active. This LiveData only calls the observable if there's an
 * explicit call to setValue() or call().
 * <p>
 * Note that only one observer is going to be notified of changes.
 */
class SingleLiveEvent<T> : MutableLiveData<T>() {

    private val mPending = AtomicBoolean(false)

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        // Observe the internal MutableLiveData
        super.observe(owner, Observer<T> { t ->
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    @MainThread
    override fun setValue(@Nullable t: T?) {
        mPending.set(true)
        super.setValue(t)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }
}


Old answer:

So after researching quite a bit and getting with contact with a Google dev. the recommended solution is to have separate responsibilities. Opening an activity should the response to the click event and not the actual change, this type of selectedItem scenarios is especially useful for decoupled communication to other listening Views. e.g another fragment in the same activity

Upvotes: 1

Related Questions