ga('send', 'pageview');
Categories
Mjukvaruhantverk Teknik

Lifecycle & Fragments backstack

LiveData and ViewModel are two awesome new additions to the Android toolbox. The examples are straight forward, but unfortunately they don’t cover every aspect of the Android universe. Working with the FragmentManager backstack I encountered a problem. Here is how I solved it.

LiveData and ViewModel are two awesome new additions to the Android toolbox. The examples are straight forward, but unfortunately they don’t cover every aspect of the Android universe. Working with the FragmentManager backstack I encountered a problem. Here is how I solved it.

At the time of writing this article, I missed one key part. Instead of passing in this to observe, you should pass in getLifeCycleOwner() which solves the problem described in this article. Thank you for the feedback!

While the problem described below still exists, there are now better solutions to solving the problem (if you’re using Android Support Libraries 28.0.0 or AndroidX 1.0.0.)

You can read more about it here: 5 common mistakes when using Architecture Components

Lifecycle

There are tons of blogs on the subject already. If you are interested to learn more then here is a really good blog by Lyla Fujiwara on ViewModels and when you should use them. Google has also created the android-lifecycles codelab. I recommend you try it out if you haven’t already.

ViewModel & LiveData

These are two very powerful classes. The ViewModel can hold your view state and can be accessed from both Activities and Fragments. Your view state can be plain java objects or you can use LiveData to observe the view state. Here is an example to show how you can observe LiveData in an Activity.

Your ViewModel

public class CounterViewModel extends ViewModel {
    private final MutableLiveData<Integer> counter = 
        new MutableLiveData<>();

    public LiveData<Integer> getCounter() {
        return counter;
    }

    public void incCounter() {
        Integer value = counter.getValue();
        counter.setValue(value != null ? (value + 1) : 1);
    }
}

This ViewModel is responsible for holding a counter value. Initially the value is not set and calling counter.getValue() will return null. Once incCounter is called the counter value will be set to 1 then 2 then 3 and so on.

Your Activity

public class MyActivity extends LifecycleActivity {

    private CounterViewModel counterViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity);

        counterViewModel= ViewModelProviders.of(this)
            .get(CounterViewModel.class);
        counterViewModel.getCounter()
            .observe(this, this::onCounterUpdate);
        
        findViewById(R.id.page)
            .setOnClickListener(v -> counterViewModel.incCounter());
    }

    private void onCounterUpdate(Integer val) {
        Timber.i("Counter value: %d", val);
        findViewById(R.id.text)
            .setText("Counter value: " + val);
    }
}

The CounterViewModel is bound to the lifecycle of the Activity. This means it will live as long as the Activity isn’t destroyed and it will survive orientation changes.

The counter we observe also uses the Activity lifecycle. We don’t need to unobserve/unsubscribe as we do in RxJava, the observer will be removed in onDestroy automatically. Event though we observe the counter state in onCreate we will only get callbacks to onCounterUpdate when the Activity is in a started state. (Also, you won’t get any callback to onCounterUpdate unless the counter value has been set, but that’s beside this point in this example).

If the activity is paused then resumed the view should be the same as before and onCounterUpdate won’t be called since the activity never left the started state.

If orientation changes the Activity will be destroyed but the CounterViewModel will survive and will be bound to the activity once again in onCreate. Once the Activity come to a started state (after onStart) onCounterUpdate will be called and the view will be updated.

If you run this example, you’ll see that once we start the application onCounterUpdate will not be called. If the user touches the page, incCounter will be called setting the LiveData counter to 1 and onCounterUpdate will be called with value being 1. If the activity gets destroyed (rotate screen) the ViewModel survives and will still have the value 1 and once the Activity state become started (after onStart is called) onCounterUpdate will be called with the latest value and the view will be updated to the correct state again.

Log from orientation change

onPause
onStop
onDestroy
onCreate
onStart
Counter value: 1
onResume

Fragments with ViewModel

If you only have one fragment there isn’t much difference from the activity example above.

public class MyFragment extends LifecycleFragment {

    private CounterViewModel counterViewModel;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        counterViewModel= ViewModelProviders.of(this)
            .get(CounterViewModel.class);
        counterViewModel.getCounter()
            .observe(this, this::onCounterUpdate);
    }

    ...

    @Override
    public void onViewCreated(...) {
        ...

        findViewById(R.id.page)
            .setOnClickListener(v -> counterViewModel.incCounter());
    }

    private void onCounterUpdate(Integer val) {
        Timber.i("Counter value: %d", val);
        findViewById(R.id.text)
            .setText("Counter value: " + val);
    }
}

This example works pretty much exactly as the Activity example above. The ViewModel can either be bound to the Activity or the Fragment lifecycle (depending on your needs).

So whats the problem?

If you add a second Fragment on the backstack to replace the first Fragment the first Fragment doesn’t get destroyed, but its view will be destroyed (onDestroyView). If the backstack is then popped the second Fragment is destroyed and the first fragment will be started again after its view is recreated (onCreateView & onViewCreated). But for some reason onCounterUpdate will not be called in the first Fragment and its view will not display the counter value 🙁

How can we fix this

There are of course several way to fix this:

Manually checking

Once the view is created (onViewCreated) check if the counter is set and call onCounterUpdate manually. If we do this we’ll get 2 calls to onCounterUpdate on orientation change if the value is set. Also we’re not really observing the counter state anymore with this code 🙁

Observe in onStart

Observing the counter in onStart seems to work. Every time onStart is called we set the observer. Because we observe again in onStart, onCounterUpdate will get an update if the counter value has been set. Yay!

@Override
public void onStart() {
    super.onStart();
    viewModel.getCounter().observe(this, this::onCounterUpdate);
}

Can you spot the problem? The documentation for observe tells us it is pretty safe to observe multiple times as it will not add the same observer again if it is already observing. However this::onCounterUpdate will create a new method reference/observable every time we call this code.

We can instead create a field to reference the observer

private Observer<Integer> onCounterUpdate = this::onCounterUpdate;
@Override
public void onStart() {
    super.onStart();

    viewModel.getCounter().observe(this, onCounterUpdate);
}

This is neither pretty nor does it work as we want it to. We are now back to the same problem we had when we observed the counter in onCreate. But at least we are not creating multiple observers anymore.

We can combine this solution with removing the observer in onStop.

@Override
public void onStop() {
    viewModel.getCounter().removeObserver(onCounterUpdate);
    super.onStop();
}

This does solve all our problems, but having the observer reference as a field isn’t very nice. It wold be great if we could do the following:

viewModel.getCounter().observe(this, this::onCounterUpdate);
...
viewModel.getCounter().removeObserver(this::onCounterUpdate);

But as we already know each call to this::onCounterUpdate will create a new reference. It the example we observe with one observer reference and try to remove another reference. So we’ll end up with multiple observers again ;(

The best approach I’ve found this far is to use removeObservers which will remove all observers. Using this we end up with an approach very similar to RxJava.

@Override
public void onStart() {
    super.onStart();
    viewModel.getCounter().observe(this, this::onCounterUpdate);
}

@Override
public void onStop() {
    viewModel.getCounter().removeObservers(this);
    super.onStop();
}

Problem solved 🙂

Conclusion

As we are still in alpha9 this may simply be a bug, but since the first fragment is stopped then come back to started state, I would expect my observer to be called again. For now, this fix seems to work OK at least. Hopefully this will be fixed in a later release of the Lifecycle components and I hope you’ll find this guide helpful in the mean time.

If you find a better alternative or have any suggestion for improvements then please post a comment below.

I’ve also created a Github project for this blog where you can see how to work with ViewModel & LiveData in these scenarios (Activity, Fragment and the Backstack).

Thanks for reading!

By Peter Törnhult

Android utvecklare, Techcoach, Scrum master

One reply on “Lifecycle & Fragments backstack”

In fragment, instead of passing “this” as first argument to the observer you are supposed to pass getLifeCycleOwner(). It will look this way:

counterViewModel.getCounter()
.observe(getLifeCycleOwner(), this::onCounterUpdate);

This way, you don’t need to re-observe the LiveData in onStart() and remove the observer.

Tha framework will handle your observer appropriately.

Leave a Reply to Coggy Borris Cancel reply

Your email address will not be published. Required fields are marked *