Android 带有 savedInstanceState unparcelling Dagger2 注入模型和构造函数注入的片段

Android Fragment with savedInstanceState unparcelling Dagger2 injected model with constructor injection

我有一个 Android 片段可以注入数据绑定模型。更具体地说,我注入了一个 ViewModel(通过标签在 Fragment 的 xml 中定义),然后调用 ViewDataBinding.setViewModel() 以在 onCreateView() 中启动绑定。

片段通过字段注入注入到Activity中,并且 ViewModel 也通过字段注入注入到 Fragment 中。然而,ViewModel 本身通过构造函数注入来注入它的依赖项。

当 Fragment 首次实例化时,这工作正常 --- 当 savedInstanceState 为 null 时。但是,在恢复 Fragment 时它不起作用:目前,ViewModel 为 null,因为在保存 Fragment 状态时我没有将它打包。

存储 ViewModel 状态应该不是问题,但我很难看到之后如何恢复它。状态将在 Parcel 中,但不在(构造函数)注入的依赖项中。

例如,考虑一个简单的登录表单,其中包含两个字段,用户名和密码。 LoginViewModel 状态只是两个字符串,但它也有各种相关职责的依赖项。下面我为 Activity、Fragment 和 ViewModel 提供了简化的代码示例。

到目前为止,我还没有提供任何在保存 Fragment 时保存 ViewModel 状态的方法。当我意识到在概念上我没有看到如何注入 ViewModel 的依赖项时,我正在使用基本的 Parcelable 模式来处理这个问题。通过 Parcel 接口恢复 ViewModel 时——尤其是 Parcelable.Creator<> 接口——看来我必须直接实例化我的 ViewModel。然而,这个对象通常被注入,更重要的是,它的依赖项被注入到构造函数中。

这似乎是一个特定的 Android 案例,实际上是一个更普遍的 Dagger2 案例:注入的对象有时会从保存的状态恢复,但仍然需要通过构造函数注入其依赖项。

这里是登录Activity...

public class LoginActivity extends Activity {

    @Inject /* default */ Lazy<LoginFragment> loginFragment;

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

        setContentView(R.layout.login_activity);

        ActivityComponent.Creator.create(getAppComponent(), this).inject(this);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.activity_container, loginFragment.get())
                    .commit();
        }
    }
}

这是 LoginFragment...

public class LoginFragment extends Fragment {

    @Inject /* default */ LoginViewModel loginViewModel;

    @Nullable
    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
        final LoginFragmentBinding binding = setViewDataBinding(LoginFragmentBinding.inflate(inflater, container, false));

        binding.setViewModel(loginViewModel);

        // ... call a few methods on loginViewModel

        return binding.getRoot();
    }
}

最后,这是 LoginViewModel 的抽象版本...

public class LoginViewModel {
    private final Dependency dep;

    private String userName;
    private String password;

    @Inject
    public LoginViewModel(final Dependency dep) {
        this.dep = dep;
    }

    @Bindable
    public String getUserName() {
        return userName;
    }

    public void setUserName(final String userName) {
        this.userName = userName;
        notifyPropertyChanged(BR.userName);
    }

    // ... getter / setter for password
}

在您的特定用例中,最好在 Fragment 内部注入而不是将 ViewModel 从 Activity 传递到具有其中依赖项的 Fragment。您想要这样做的原因是 co-ordinate 具有片段生命周期的 ViewModel。

public class LoginFragment extends Fragment {

    @Inject /* default */ LoginViewModel loginViewModel;

    @Nullable
    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
        final LoginFragmentBinding binding = setViewDataBinding(LoginFragmentBinding.inflate(inflater, container, false));

        return binding.getRoot();
    }

    @Override
    public void onActivityCreated(View v) {
          FragmentComponent.Creator.create((LoginActivity) getActivity(), this).inject(this);
          binding.setViewModel(loginViewModel);
    }
}

这意味着每次您的片段被创建时,它都会被注入一个新的 ViewModel

但是,我怀疑仅此一项不足以满足您的特定用例。在某个阶段,您可能必须提取一个轻量级工厂 class 来创建 ViewModel 以将其与依赖项分离并允许 saveInstanceState 相同。

像这样的东西可能会成功:

public class LoginViewModelFactory {

     private final Dependency dependency;

     public LoginViewModelFactory(Dependency dependency) {
         this.dependency = dependency;
     }

     public LoginViewModel create() {
          return new LoginViewModel(dependency);
     }
}

那么你现在只需要在你的 Fragment 中注入工厂:

public class LoginFragment extends Fragment {

    @Inject LoginViewModelFactory loginViewModelFactory;

    private LoginViewModel loginViewModel;

    @Override
    public void onActivityCreated(Bundle b) {
          FragmentComponent.Creator.create((LoginActivity) getActivity(), this).inject(this);
          loginViewModel = loginViewModelFactory.create();
          binding.setViewModel(loginViewModel);
    }
}

因为ViewModel现在已经和依赖解耦了,你可以很容易的实现Parcelable:

public class LoginViewModel {

    private String userName;
    private String password;

    public LoginViewModel(Parcel in) {
        userName = in.readString();
        password = in.readString();
    }

    @Bindable
    public String getUserName() {
        return userName;
    }

    public void setUserName(final String userName) {
        this.userName = userName;
        notifyPropertyChanged(BR.userName);
    }

    // ... getter / setter for password

        @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(userName);
        dest.writeString(password);
    }

    public static final Creator<LoginViewModel> CREATOR = new Creator<LoginViewModel>() {
        @Override
        public LoginViewModel createFromParcel(Parcel in) {
            return new LoginViewModel(in) {};
        }

        @Override
        public LoginViewModel[] newArray(int size) {
            return new LoginViewModel[size];
        }
    };
}

因为它现在是可打包的,你可以把它保存在片段的outbundle中:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(LoginViewModel.PARCELABLE_LOGIN_VIEW_MODEL, loginViewModel);
}

然后您需要检查它是否正在使用您的一种创建方法进行恢复:

    @Override
    public void onActivityCreated(Bundle b) {
          FragmentComponent.Creator.create((LoginActivity) getActivity(), this).inject(this);
          loginViewModel = bundle.getParcelable(LoginViewModel.PARCELABLE_LOGIN_VIEW_MODEL);
          if (loginViewModel == null) {
              loginViewModel = loginViewModelFactory.create();
          }
          binding.setViewModel(loginViewModel);
    }

非常感谢 David Rawson 的帮助 post。我需要一些额外的时间来解决您的建议以及我正在做的事情,并提出了一个更简单的解决方案。那就是说,如果没有你提供的东西,我不可能到达那里,所以再次感谢!以下是解决方案,使用我在初始查询中提供的相同示例代码。

LoginActivity 保持不变...

public class LoginActivity extends Activity {

    @Inject /* default */ Lazy<LoginFragment> loginFragment;

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

        setContentView(R.layout.login_activity);

        ActivityComponent.Creator.create(getAppComponent(), this).inject(this);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.activity_container, loginFragment.get())
                    .commit();
        }
    }
}

然而,LoginFragment 的主要变化在于它有选择地注入其依赖项,即 LoginViewModel。这是基于 savedInstanceState 是否为空(或不为空)——尽管人们可能也可以检查一个(或所有)依赖项是否为空。我选择了前一张支票,因为语义可以说更清晰。请注意 onCreate() 和 onCreateView() 中的显式检查。

当 savedInstanceState 为 null 时,则假设 Fragment 正在通过注入从头开始实例化; LoginViewModel 不会为空。相反,当 savedInstanceState 为 non-null 时,class 将被重建而不是注入。在这种情况下,Fragment 必须自己注入它的依赖项,反过来,这些依赖项需要用 savedInstanceState 重新表述自己。

在我最初的询问中,我没有理会保存状态的示例代码,但为了完整性我将其包含在这个解决方案中。

public class LoginFragment extends Fragment {

    private static final String INSTANCE_STATE_KEY_VIEW_MODEL_STATE = "view_model_state";

    @Inject /* default */ LoginViewModel loginViewModel;

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

        if (savedInstanceState != null) {
            ActivityComponent.Creator.create(((BaseActivity) getActivity()).getAppComponent(),
                    getActivity()).inject(this);
        }
    }

    @Nullable
    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
        final LoginFragmentBinding binding = setViewDataBinding(LoginFragmentBinding.inflate(inflater, container, false));

        if (savedInstanceState != null) {
            loginViewModel.unmarshallState(
                    savedInstanceState.getParcelable(INSTANCE_STATE_KEY_VIEW_MODEL_STATE));
        }

        binding.setViewModel(loginViewModel);

        // ... call a few methods on loginViewModel

        return binding.getRoot();
    }

    @Override
    public void onSaveInstanceState(final Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putParcelable(INSTANCE_STATE_KEY_VIEW_MODEL_STATE, loginViewModel.marshallState());
    }
}

然后,最后的更改是让 ViewModel 根据需要从 Fragment 保存/恢复其状态。有很多方法可以解决这个问题,但都遵循标准 Android 方法。

在我的例子中,因为我有越来越多的 ViewModels --- 每个都有(注入)依赖关系、状态和行为 --- 我决定创建一个单独的 ViewModelState class 来封装只有将要保存和恢复的状态 to/from 片段中的捆绑包。然后,我向 ViewModels 添加了相应的编组方法。在我的实现中,我有 base classes 可以为所有 ViewModel 处理这个问题,但下面是一个没有 base class 支持的简化示例。

为了简化实例状态的保存/恢复,我使用了Parceler。这是我的示例 LoginViewModelState class。是的,没有样板文件!

@Parcel
/* default */ class LoginViewModelState {

    /* default */ String userName;
    /* default */ String password;

    @Inject
    public LoginViewModelState() { /* empty */ }
}

这是更新后的 LoginViewModel 示例,主要展示了 LoginViewModelState 的使用以及底层的 Parceler 辅助方法...

public class LoginViewModel {

    private final Dependency dep;
    private LoginViewModelState state;

    @Inject
    public LoginViewModel(final Dependency dep,
                          final LoginViewModelState state) {
        this.dep = dep;
        this.state = state;
    }

    @Bindable
    public String getUserName() {
        return state.userName;
    }

    public void setUserName(final String userName) {
        state.userName = userName;
        notifyPropertyChanged(BR.userName);
    }

    // ... getter / setter for password

    public Parcelable marshallState() {
        return Parcels.wrap(state);
    }

    public void unmarshallState(final Parcelable parcelable) {
        state = Parcels.unwrap(parcelable);
    }
}