我们如何获取数据绑定以使用保存的实例状态?

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

TL;DR:如果与数据绑定一起使用的布局具有 EditText,并且存在 android:text 的绑定表达式,则绑定表达式会覆盖已保存的实例状态值...甚至如果我们没有明确触发绑定评估。用户在配置更改之前输入的内容将被清除。我们如何解决这个问题,以便在配置更改时使用保存的实例状态值?


我们有傻Model:

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

我们有一个布局引用 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>

请注意,此布局没有绑定表达式;我们稍后会讲到。

在动态片段中使用布局:

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());
  }
}

请注意,我们并没有在任何地方调用 setModel() 来将 Model 推入绑定。 MainBinding(对于上面显示的 main.xml 布局)仅用于扩充布局。

此代码(用合适的 FragmentActivity 来设置 FormFragment)正确使用保存的实例状态。如果用户在 EditText 中输入内容,然后旋转屏幕,新创建的 EditText 会显示输入的文本。

现在,让我们更改布局以添加 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>

现在,如果用户在 EditText 中键入内容并旋转屏幕,则新创建的 EditText 是空的。绑定表达式会覆盖框架从保存的实例状态恢复的任何内容。

尽管我没有在绑定上调用 setModel(),但还是出现了这种情况。我当然可以看到如果我在绑定上调用 setModel() 时会用模型中的数据替换 EditText 内容。但我不会那样做。

我可以在官方设备(Google Pixel,Android 8.0)和生态系统设备(Samsung Galaxy S8,Android 7.1)上重现此行为。

这可以通过我们自己保存状态并在某个时候恢复它来解决 "manually"。例如,多个评论建议双向绑定,但这与其他设计目标(例如,不可变模型对象)背道而驰。这似乎是数据绑定的一个相当基本的限制,所以我希望有一些我错过的东西可以配置为自动使用保存的实例状态。

我以为 ianhanniballake 提到了一个相关的答案,但也许还有更多。以下是我对如何将该参考应用于这些情况的解释。

使用您提供的 XML,以下代码将交替从保存的实例状态和从模型恢复。当保存的实例状态被恢复时,大概没有模型实例化来恢复。那就是 mCount 是偶数的时候。如果模型存在,则保存的实例状态基本上被忽略,绑定接管。这里的逻辑比我们想要的要多一点,但比显式保存和恢复要少。

mCount 只是为了争论的一种技巧。将使用标志或其他指示模型是否存在。

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);
    }
}