0

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
}
Steve Yohanan
  • 757
  • 5
  • 17

2 Answers2

2

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. The reason you would want to do this is to better co-ordinate the ViewModel with the lifecycle of the 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.

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.

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:

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:

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:

@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);
    }
David Rawson
  • 20,912
  • 7
  • 88
  • 124
0

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);
    }
}
Steve Yohanan
  • 757
  • 5
  • 17