The Saved Instance State Situation
Using LiveData
and ViewModel
exacerbates a problem with how the data binding framework interacts with the saved instance state Bundle
.
In addition to data that you might put in that Bundle
, the built-in onSavedInstanceState()
logic saves obvious user-mutable state of widgets in the UI to the Bundle
. So, for example, if the user types something into an EditText
, or toggles the state of a Switch
, that information goes in the Bundle
. If you have matching widgets in the new configuration, the state is applied to those widgets automatically.
Except if you are using the data binding framework, in which case, things get complicated.
The General/DataBindingState
sample project illustrates the problem and the workaround.
This app has a trivial UI, consisting mostly of an EditText
. It uses the data binding framework, so the main.xml
layout resource has a <layout>
element and so forth:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="model"
type="com.commonsware.databindingstate.Model" />
</data>
<android.support.constraint.ConstraintLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.commonsware.databindingstate.MainActivity">
<EditText android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="text"
android:text="@{model.title}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>
Of note, the android:text
attribute of the EditText
has a binding expression, pulling the title
from a Model
object. Model
is as trivial of a model as you can imagine:
package com.commonsware.databindingstate;
public class Model {
public String getTitle() {
return("Title");
}
}
However, we do not actually wind up using that Model
. In fact, we never bind anything to the layout.
One might reasonably expect that this would result in the same flow as if we did not use data binding at all:
- The UI shows our layout
- The user types something into the
EditText
- The user rotates the screen, and the saved instance state
Bundle
populates the newly-created replacementEditText
for the new configuration
Instead, if you try it, you will find that what you type in gets lost on a configuration change.
However, you will notice that there is an action bar overflow menu, with an “Apply Workaround” checkable item in it. If you check that, what you type into the EditText
is properly retained across the configuration change.
The difference is a call to executePendingBindings()
in onCreateView()
of the FormFragment
that is showing our limited UI:
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (savedInstanceState!=null) {
savedWorkaround=savedInstanceState.getBoolean(STATE_WORKAROUND);
if (workaround!=null) {
workaround.setChecked(savedWorkaround);
}
}
MainBinding binding=MainBinding.inflate(inflater, container, false);
if (savedWorkaround) {
binding.executePendingBindings();
}
return(binding.getRoot());
}
Our layout is main.xml
, so our binding class is MainBinding
. We call the static inflate()
method on it to inflate our layout, and use getRoot()
to return the View
from onCreateView()
.
If, however, our checkable MenuItem
is checked (or, rather, was checked in the previous configuration), we call executePendingBindings()
. Otherwise, we do not. That makes the difference.
(the author would like to thank Stack Overflow user Cheticamp for pointing out the workaround)
This sample is very artificial. Using data binding without binding any data would seem atypical. However, it does illustrate a general problem with data binding that using reactive UIs — LiveData
, ViewModel
, etc. — exacerbate. Simply put, when do we bind, to get the correct results?
Let’s examine two common cases: the user is editing an existing model object, or the user is creating a new model object.
Existing Model
We inflate our layout using our binding object, plus we observe a LiveData
to get the model object when it is ready. Once it is ready, we call the appropriate setter on the binding to populate the widgets.
Right?
Unfortunately, not always.
That is what we do at the outset, the first time our activity or fragment is displayed to edit this particular model. However, if we undergo a configuration change, we have a problem: we do not want to lose changes that the user made already. If the user started typing something into an EditText
, then rotated the screen (e.g., to switch to the landscape keyboard for easier typing), we do not want to lose the changes already made in that EditText
.
Ordinarily, the saved instance state Bundle
would handle that… but if we turn around and call our binding setter in the new activity or fragment, we will replace what the user typed in with whatever is in the model object.
There are two main solutions here:
- Use two-way binding, so the UI immediately updates the model object as the user makes changes to it. If the
ViewModel
holds onto that model object and can give it back to us after the configuration change, we can safely call the setter method on the binding object, as our model has the appropriate data. - Only bind the model object on the initial creation of the activity or fragment, not on configuration changes. Instead, let the normal saved instance state logic handle the form contents. This requires you to call
executePendingBindings()
shortly after inflating the layout via the binding class, so that the saved instance state is applied properly.
New Model
In the new-model scenario, you have the additional question of: is there anything to bind, anyway?
If you are using two-way data binding, then you need to bind something to collect the input from the user. Similarly, if you are using a newly created model object to supply starter data to the form, you will need to create such a model object and bind it. In these cases, the new-model scenario is largely the same as the existing-model scenario, with the primary difference being where the model comes from.
If you have no need for a model at the outset, though, you could skip binding anything, much as the sample app skipped binding anything. Then, so long as you call executePendingBindings()
shortly after inflating the layout via the binding class, you should be in fine shape.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.