Android Fragment with savedInstanceState unarcelling Dagger2注入模型与构造函数注入

时间:2017-01-20 18:02:04

标签: android android-fragments dependency-injection parcelable dagger-2

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

通过现场注入将片段注入活动中,并且 ViewModel也通过场注入注入Fragment。但是,ViewModel本身通过构造函数注入注入其依赖项。

当Fragment首次实例化时 - 当savedInstanceState为null时,这可以正常工作。但是,在恢复Fragment时它不​​起作用:当前,ViewModel为null,因为我在保存Fragment状态时没有对它进行分区。

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

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

到目前为止,我还没有提供保存片段时保存ViewModel状态的任何方法。我正在研究这个,使用基本的Parcelable模式,当我意识到概念上我没有看到如何注入ViewModel的依赖项。当通过Parcel接口恢复ViewModel时 - 特别是Parcelable.Creator<>接口---似乎我必须直接实例化我的ViewModel。但是,通常会注入此对象,更重要的是,它的依赖项将在构造函数中注入。

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

这是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 ...

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
}

2 个答案:

答案 0 :(得分:1)

在您的特定用例中,最好在Fragment中注入,而不是将ViewModel从Activity传递给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。{/ p>

这样的事情很可能会成功:

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

由于它现在是可以分割的,你可以将它保存在片段的分类中:

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

答案 1 :(得分:0)

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

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为null(或不是)---尽管可能还可以检查一个(或所有)依赖项是否为null。我使用前一张支票,因为语义可以说更清楚。请注意onCreate()和onCreateView()中的显式检查。

当savedInstanceState为null时,则假设片段是通过注入从头开始实例化的; 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方法。

在我的情况下,因为我有越来越多的ViewModel--每个都有(注入)依赖项,状态和行为---我决定创建一个单独的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);
    }
}