9

TL;DR: If a layout used with data binding has an EditText, and there is a binding expression for android:text, the binding expression overwrites the saved instance state value... even if we do not explicitly trigger a binding evaluation. What the user typed in before the configuration change gets wiped out. How do we work around this, so that on a configuration change, the saved instance state value is used?


We have a silly Model:

public class Model {
  public String getTitle() {
    return("Title");
  }
}

And we have a layout that references that Model:

<?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"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  </android.support.constraint.ConstraintLayout>
</layout>

Note that this layout has no binding expressions; we'll get to that in a bit.

The layout is used in a dynamic fragment:

public class FormFragment extends Fragment {
  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater,
                           @Nullable ViewGroup container,
                           @Nullable Bundle savedInstanceState) {
    return(MainBinding.inflate(inflater, container, false).getRoot());
  }
}

Note that we are not calling setModel() anywhere to push a Model into the binding. The MainBinding (for the main.xml layout shown above) is just used to inflate the layout.

This code (with a suitable FragmentActivity to set up the FormFragment) correctly uses the saved instance state. If the user types something into the EditText, then rotates the screen, the newly-recreated EditText shows the entered-in text.

Now, let's change the layout to add a binding expression for android:text:

<?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>

Now, if the user types something into the EditText and rotates the screen, the newly-recreated EditText is empty. The binding expression overwrites whatever the framework restored from the saved instance state.

This comes despite the fact that I am not calling setModel() on the binding. I can certainly see where if I called setModel() on the binding where that would replace the EditText contents with the data from the model. But I am not doing that.

I can reproduce this behavior on both official devices (Google Pixel, Android 8.0) and ecosystem devices (Samsung Galaxy S8, Android 7.1).

This can be worked around "manually" by saving the state ourselves and restoring it at some point. For example, multiple comments have suggested two-way binding, but that runs counter to other design objectives (e.g., immutable model objects). This seems like a rather fundamental limitation of data binding, so I am hoping that there's something that I missed that I can configure to have the saved instance state be used automatically.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • in `android:text="@{model.title}"` you are using one-way data binding or it is a typo and you meant two-way databinding? – pskink Oct 09 '17 at 16:25
  • @pskink: I am using one-way binding. Two-way binding would be another possible workaround, though one that I would prefer not to use in the real app where I ran into this issue. Two-way binding is another variation on "saving the state ourselves and restoring it at some point". – CommonsWare Oct 09 '17 at 16:26
  • 3
    See the [relevant answer](https://stackoverflow.com/a/46086436/1676363) of using automatic state restoration with two way binding. – ianhanniballake Oct 11 '17 at 04:41

1 Answers1

5

I thought that ianhanniballake had a reference to a relevant answer, but maybe there is more to it. Here is my interpretation of how that reference can be applied to these circumstances.

Using the XML that you presented, the following code will alternately restore from the saved instance state and restore from the model. When the saved instance state is restored then, presumably, there is not model instantiated to restore from. That is when mCount is even. If a model exists, then the saved instance state is basically ignored and the binding takes over. There is a little more logic here than we want, but it is less than saving and restoring explicitly.

mCount is just an artifice for the sake of the argument. A flag or other indication of whether the model exists or not would be used.

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    private int mCount;

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

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        mCount = (savedInstanceState == null) ? 0 : savedInstanceState.getInt("mCount", 0);
        if (mCount % 2 == 1) {
            // 1st, 3rd, 5th, etc. rotations. Explicitly execute the bindings and let the framework
            // restore from the saved instance state.
            binding.executePendingBindings();
        } else {
            // First creation and 2nd, 4th, etc. rotations. Set up our model and let the
            // framework restore from the saved instance state then overwrite with the bindings.
            // (Or maybe it just ignores the saved instance state and restores the bindnings.)
            Model model = new Model();
            binding.setModel(model);
        }
        mCount++;
    }

    @Override
    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
        bundle.putInt("mCount", mCount);
    }
}
Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • "I thought that ianhanniballake had a reference to a relevant answer" -- I do not want two-way binding, as I stated in a comment, in an edit to the question, and in the bounty. "When the saved instance state is restored then, presumably, there is not model instantiated to restore from" -- that is not the case. With one-way binding, we may have initially populated the widgets from the model, but then the saved instance state should take over, so that we do not lose modifications that the user enters while the device undergoes a configuration change. – CommonsWare Oct 11 '17 at 18:58
  • But, I can confirm that `executePendingBindings()`, at least when called from an activity's `onCreate()` method, does seem to improve matters, allowing normal saved instance state processing to work. I'll need to see how I can adapt it to the scenario from my question, where data binding is being done in a fragment. Many thanks! – CommonsWare Oct 11 '17 at 18:59
  • @CommonsWare I understand that you don't want the two-way binding in the "relevant response." So, my "interpretation" was for the one-way binding. – Cheticamp Oct 11 '17 at 20:02
  • Yes, and I had not considered the possibility that `executePendingBindings()` would be relevant in this case. After all, in my sample, I'm not actually binding *anything* (no `setModel()` call), and yet the saved instance state was still broken. I'll spend more time on your solution in a few days and will have a better sense of how well it works. Thanks again! – CommonsWare Oct 11 '17 at 20:15
  • If you are using fragments, if you create your binding in `onCreateView()`, call `executePendingBindings()` immediately thereafter, and the saved instance state will be applied to the widgets. You then need to make sure that you do not clobber that state by binding model data later. So, if the `savedInstanceState` is `null`, you bind normally; if the `savedInstanceState` is not null, skip the normal binding and rely on the instance state. – CommonsWare Oct 14 '17 at 14:15
  • @Cheticamp Can please answer this question ? https://stackoverflow.com/questions/60669372/retain-edittext-with-screen-rotation-using-databinding-library – NullByte08 Mar 13 '20 at 12:16