i have an Android Fragment that injects a model for data binding. 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().
the Fragment is injected in the Activity via field injection, and the ViewModel is injected into the Fragment also via field injection. however, the ViewModel itself injects its dependencies via constructor injection.
this works fine when the Fragment is first instantiated --- when savedInstanceState is 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.
storing the ViewModel state shouldn't be an issue, but i'm having difficulty seeing how to restore it afterward. 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. below i provide a reduced code example for the Activity, Fragment, and ViewModel.
as of yet, i haven't provided any means of saving the ViewModel state when the Fragment is saved. 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. when restoring the ViewModel via the Parcel interface --- particularly the Parcelable.Creator<> interface --- it seems i have to directly instantiate my 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.
here is the 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...
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...
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
}
thanks so much David Rawson for your helpful post. 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...
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. this is based on if savedInstanceState is null (or not) --- though one probably could also check if one (or all) dependencies are null. i went with the former check, since the semantics were arguably more clear. note the explicit checks in onCreate() and onCreateView().
when savedInstanceState is null, then the assumption is that the Fragment is being instantiated from scratch through injection; LoginViewModel will not be null. conversely, when savedInstanceState is non-null, then the class is being rebuilt rather than injected. in this case, the Fragment has to inject its dependencies itself and, in turn, those dependencies need to reformulate themselves with savedInstanceState.
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. there are many ways to solve this but all follow the standard Android approach.
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. then, i added corresponding marshalling methods to the ViewModels. in my implementation, i have base classes that handle this for all ViewModels, but below is a simplified example without base class support.
to ease save / restore of instance state, i employ Parceler. here is my example LoginViewModelState class. 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...
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);
}
}