带有savedInstanceState的Android片段正在执行Dagger2注入的带有构造函数注入的模型



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

片段通过字段注入被注入到活动中,以及ViewModel也通过字段注入被注入到片段中。然而,ViewModel本身通过构造函数注入来注入其依赖项。

当Fragment第一次被实例化时(当savedInstanceState为null时),这种方法可以很好地工作。然而,在还原Fragment时它不起作用:目前,ViewModel为null,因为在保存Fragment状态时我还没有对它进行分组。

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

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

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

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

这是登录活动。。。

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

这是登录碎片。。。

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
}

在您的特定用例中,最好将ViewModel注入Fragment内部,而不是将ViewModel从Activity传递到Fragment中带有依赖项的Fragment。您之所以要这样做,是为了更好地协调ViewModel与Fragment的生命周期。

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

这意味着每次创建Fragment时,都会注入一个新的ViewModel

然而,我怀疑仅此一项对于您的特定用例来说是不够的。在某个阶段,您可能需要提取一个用于创建ViewModel的轻量级工厂类,以便将其与依赖项解耦,并允许使用相同的saveInstanceState

像这样的东西可能会起作用:

public class LoginViewModelFactory {
private final Dependency dependency;
public LoginViewModelFactory(Dependency dependency) {
this.dependency = dependency;
}
public LoginViewModel create() {
return new LoginViewModel(dependency);
}
}

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

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的帮助。我需要一点额外的时间来解决你的建议和我正在做的事情,并想出了一个更简单的解决方案。也就是说,如果没有你提供的,我不可能到达那里,所以再次感谢!以下是解决方案,使用与我在初始查询中提供的示例代码相同的代码。

登录活动保持不变。。。

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是否为null(或不为null)——尽管也可以检查一个(或所有)依赖项是否为null。我选择了前一个检查,因为语义可以说更清晰。请注意onCreate()和onCreateView()中的显式检查。

当savedInstanceState为null时,则假设Fragment是通过注入从头开始实例化的;LoginViewModel不会为null。相反,当savedInstanceState为非null时,则重新生成类,而不是注入类。在这种情况下,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类,该类只封装将保存到片段中的Bundle或从中恢复的状态。然后,我向ViewModels添加了相应的编组方法。在我的实现中,我有用于处理所有ViewModel的基类,但下面是一个没有基类支持的简化示例。

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

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

最新更新