简体   繁体   English

带有saveInstanceStateState的Android Fragment通过构造函数注入来解析Dagger2注入模型

[英]Android Fragment with savedInstanceState unparcelling Dagger2 injected model with constructor injection

i have an Android Fragment that injects a model for data binding. 我有一个Android Fragment,可为数据绑定注入模型。 more specifically, i inject a ViewModel (defined in the Fragment's xml via a tag) and, call ViewDataBinding.setViewModel() to initiate the binding in onCreateView(). 更具体地说,我注入一个ViewModel(通过标签在Fragment的xml中定义),然后调用ViewDataBinding.setViewModel()以在onCreateView()中启动绑定。

the Fragment is injected in the Activity via field injection, and the ViewModel is injected into the Fragment also via field injection. 片段通过字段注入被注入到Activity中,而ViewModel也通过字段注入被注入到Fragment中。 however, the ViewModel itself injects its dependencies via constructor injection. 但是,ViewModel本身通过构造函数注入来注入其依赖项。

this works fine when the Fragment is first instantiated --- when savedInstanceState is null. 当Fragment第一次实例化时,这很好用---当saveInstanceState为null时。 however, it doesn't work when the Fragment is being restored: currently, the ViewModel is null because i haven't parceled it when the Fragment state is being saved. 但是,在还原Fragment时它不​​起作用:当前,ViewModel为null,因为在保存Fragment状态时我尚未将其拆分。

storing the ViewModel state shouldn't be an issue, but i'm having difficulty seeing how to restore it afterward. 存储ViewModel状态不应该是一个问题,但是我很难看到以后如何还原它。 the state will be in the Parcel but not the (constructor) injected dependencies. 状态将在包裹中,但不在(构造函数)注入的依赖项中。

as an example, consider a simple Login form, which contains two fields, User Name and Password. 例如,考虑一个简单的登录表单,其中包含两个字段:用户名和密码。 the LoginViewModel state is simply two strings, but it also has various dependencies for related duties. LoginViewModel状态仅是两个字符串,但是对于相关职责也有各种依赖性。 below i provide a reduced code example for the Activity, Fragment, and ViewModel. 下面我提供了Activity,Fragment和ViewModel的简化代码示例。

as of yet, i haven't provided any means of saving the ViewModel state when the Fragment is saved. 到目前为止,在保存Fragment时,我还没有提供任何保存ViewModel状态的方法。 i was working on this, with the basic Parcelable pattern, when i realized that conceptually i did not see how to inject the ViewModel's dependencies. 当我意识到从概念上讲我没有看到如何注入ViewModel的依赖项时,我正在使用基本的Parcelable模式进行此工作。 when restoring the ViewModel via the Parcel interface --- particularly the Parcelable.Creator<> interface --- it seems i have to directly instantiate my ViewModel. 当通过Parcel接口还原ViewModel时---特别是Parcelable.Creator <>接口---看来我必须直接实例化ViewModel。 however, this object is normally injected and, more importantly, its dependencies are injected in the constructor. 但是,通常会注入此对象,更重要的是,它的依赖项会注入到构造函数中。

this seems like a specific Android case that is actually a more general Dagger2 case: an injected object is sometimes restored from saved state but still needs its dependencies injected via the constructor. 这似乎是一个特定的Android案例,实际上是一个更一般的Dagger2案例:注入的对象有时会从保存的状态恢复,但仍然需要通过构造函数注入其依赖项。

here is the LoginActivity... 这是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();
        }
    }
}

here is the LoginFragment... 这是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();
    }
}

and, finally, here is an abstracted version of the LoginViewModel... 最后,这是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
}

In your particular use case, it may be better to inject inside the Fragment rather than pass the ViewModel from the Activity to the Fragment with the dependency inside it. 在您的特定用例中,最好将其插入Fragment内,而不是将ViewModel从Activity传递到具有内部依赖项的Fragment。 The reason you would want to do this is to better co-ordinate the ViewModel with the lifecycle of the 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);
    }
}

This will mean that every time your Fragment gets created, it will be injected with a new ViewModel . 这意味着每次创建您的Fragment时,都会将其注入新的ViewModel

However, I suspect that this alone will not be enough for your particular use case. 但是,我怀疑仅此一项不足以满足您的特定用例。 At some stage you will probably have to extract a lightweight factory class for creating the ViewModel to decouple it from the dependency and allow saveInstanceState of the same. 在某个阶段,您可能必须提取一个轻量级工厂类以创建ViewModel,以将其与依赖项分离并允许相同的saveInstanceState

Something like this would probably do the trick: 这样的事情可能会解决问题:

public class LoginViewModelFactory {

     private final Dependency dependency;

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

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

Then you just need to inject the factory inside your Fragment now: 然后,您只需要立即将工厂注入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);
    }
}

Because the ViewModel is now decoupled from the dependency, you can easily implement Parcelable: 由于现在将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];
        }
    };
}

Since it is now parcelable, you can save it in the outbundle of the Fragment: 由于它现在可以拆分,因此可以将其保存在Fragment的捆绑包中:

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

Then you need to check if it's being restored in one of your creation methods: 然后,您需要检查您的创建方法之一是否正在还原它:

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

thanks so much David Rawson for your helpful post. 非常感谢David Rawson的有用帖子。 i needed a little extra time to resolve your suggestion with what exactly i am doing and came up with a more simple solution. 我需要一些额外的时间来解决您的建议,确切地说我在做什么,并提出了一个更简单的解决方案。 that said, i couldn't have gotten there without what you provided, so thanks again! 就是说,如果没有您提供的信息,我将无法到达那里,所以再次感谢! following is the solution, using the same example code i provided in the initial inquiry. 以下是解决方案,使用在初始查询中提供的相同示例代码。

the LoginActivity remains the same... 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();
        }
    }
}

the major change to LoginFragment, however, is that it selectively injects its dependencies, namely the LoginViewModel. 但是,对LoginFragment的主要更改是,它选择性地注入了其依赖项,即LoginViewModel。 this is based on if savedInstanceState is null (or not) --- though one probably could also check if one (or all) dependencies are null. 这是基于saveInstanceState是否为null(或不是)---尽管也可以检查一个(或所有)依赖项是否为null。 i went with the former check, since the semantics were arguably more clear. 我接受了前一种检查,因为语义可以说更加清晰。 note the explicit checks in onCreate() and onCreateView(). 注意onCreate()和onCreateView()中的显式检查。

when savedInstanceState is null, then the assumption is that the Fragment is being instantiated from scratch through injection; 当saveInstanceState为null时,则假设是通过注入从头开始实例化Fragment; LoginViewModel will not be null. LoginViewModel不能为null。 conversely, when savedInstanceState is non-null, then the class is being rebuilt rather than injected. 相反,当saveInstanceState为非null时,则将重建该类而不是注入该类。 in this case, the Fragment has to inject its dependencies itself and, in turn, those dependencies need to reformulate themselves with savedInstanceState. 在这种情况下,Fragment必须自己注入其依赖项,而这些依赖项又需要使用saveedInstanceState重新构造自己。

in my original inquiry, i didn't bother with sample code that saves state, but i included in this solution for completeness. 在我最初的询问中,我没有理会保存状态的示例代码,但是为了完整性起见,我将其包含在此解决方案中。

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

the final change, then, is to have the ViewModel save / restore its state on demand from the Fragment. 然后,最后的更改是让ViewModel根据片段的需要保存/恢复其状态。 there are many ways to solve this but all follow the standard Android approach. 有很多方法可以解决此问题,但都遵循标准的Android方法。

in my case, since i have a growing number of ViewModels --- each of which has (injected) dependencies, state, and behaviors --- i decided to create a separate ViewModelState class that encapsulates solely the state that will be saved and restored to/from a Bundle in the Fragment. 就我而言,由于我的ViewModels数量越来越多---每个都有(注入的)依赖项,状态和行为---我决定创建一个单独的ViewModelState类,该类仅封装将保存和恢复的状态到/来自片段中的捆绑包。 then, i added corresponding marshalling methods to the ViewModels. 然后,我在ViewModels中添加了相应的编组方法。 in my implementation, i have base classes that handle this for all ViewModels, but below is a simplified example without base class support. 在我的实现中,我有可以为所有ViewModel处理的基类,但是下面是一个没有基类支持的简化示例。

to ease save / restore of instance state, i employ Parceler . 为了简化实例状态的保存/恢复,我使用了Parceler here is my example LoginViewModelState class. 这是我的示例LoginViewModelState类。 Yay, no boilerplate! 是的,没有样板!

@Parcel
/* default */ class LoginViewModelState {

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

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

and here is the updated LoginViewModel example, mainly showing the use of LoginViewModelState as well as the Parceler helper methods under the hood... 这是更新的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);
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM