Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How Do We Get Data Binding To Use Saved Instance State?

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.

like image 912
CommonsWare Avatar asked Oct 09 '17 16:10

CommonsWare


People also ask

How can I save an activity state using the Save instance state?

When the activity goes into the background, the system calls onSaveInstanceState() . You should save the search query in the onSaveInstanceState() bundle. This small amount of data is easy to save. It's also all the information you need to get the activity back into its current state.

How would you enable view binding for a module in your application so you can reference views from layout files without using Findviewbyid?

To enable view binding, configure viewBinding in your module-level build. gradle file. Once enabled for a project, view binding will generate a binding class for all of your layouts automatically. You don't have to make changes to your XML — it'll automatically work with your existing layouts.


1 Answers

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);
    }
}
like image 164
Cheticamp Avatar answered Oct 05 '22 18:10

Cheticamp