On Last Google IO, Google released a preview of some new arch components, one of which, ViewModel.
In the docs google shows one of the possible uses for this component:
It is very common that two or more fragments in an activity need to
communicate with each other. This is never trivial as both fragments
need to define some interface description, and the owner activity must
bind the two together. Moreover, both fragments must handle the case
where the other fragment is not yet created or not visible.
This common pain point can be addressed by using ViewModel objects.
Imagine a common case of master-detail fragments, where we have a
fragment in which the user selects an item from a list and another
fragment that displays the contents of the selected item.
These fragments can share a ViewModel using their activity scope to
handle this communication.
And shows a implementation example:
public class SharedViewModel extends ViewModel {
private final SavedStateHandle state;
public SharedViewModel(SavedStateHandle state) {
this.state = state;
}
private final MutableLiveData<Item> selected = state.getLiveData("selected");
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
#Override
protected void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = new ViewModelProvider(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
#Override
protected void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SharedViewModel model = new ViewModelProvider(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// update UI
});
}
}
I was quite excited about the possibility of not needing those interfaces used for fragments to communicate through the activity.
But Google's example does not show exactly how would I call the detail fragment from master.
I'd still have to use an interface that will be implemented by the activity, which will call fragmentManager.replace(...), or there is another way to do that using the new architecture?
Updated on 6/12/2017,
Android Official provide a simple, precise example to example how the ViewModel works on Master-Detail template, you should take a look on it first.Share data between fragments
As #CommonWare, #Quang Nguyen methioned, it is not the purpose for Yigit to make the call from master to detail but be better to use the Middle man pattern. But if you want to make some fragment transaction, it should be done in the activity. At that moment, the ViewModel class should be as static class in Activity and may contain some Ugly Callback to call back the activity to make the fragment transaction.
I have tried to implement this and make a simple project about this. You can take a look it. Most of the code is referenced from Google IO 2017, also the structure.
https://github.com/charlesng/SampleAppArch
I do not use Master Detail Fragment to implement the component, but the old one ( communication between fragment in ViewPager.) The logic should be the same.
But I found something is important using these components
What you want to send and receive in the Middle man, they should be sent and received in View Model only
The modification seems not too much in the fragment class. Since it only change the implementation from "Interface callback" to "Listening and responding ViewModel"
View Model initialize seems important and likely to be called in the activity.
Using the MutableLiveData to make the source synchronized in activity only.
1.Pager Activity
public class PagerActivity extends AppCompatActivity {
/**
* The pager widget, which handles animation and allows swiping horizontally to access previous
* and next wizard steps.
*/
private ViewPager mPager;
private PagerAgentViewModel pagerAgentViewModel;
/**
* The pager adapter, which provides the pages to the view pager widget.
*/
private PagerAdapter mPagerAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pager);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
mPager.setAdapter(mPagerAdapter);
pagerAgentViewModel = new ViewModelProvider(this).get(PagerAgentViewModel.class);
pagerAgentViewModel.init();
}
/**
* A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in
* sequence.
*/
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
...Pager Implementation
}
}
2.PagerAgentViewModel (It deserved a better name rather than this)
public class PagerAgentViewModel extends ViewModel {
private final SavedStateHandle state;
private final MutableLiveData<String> messageContainerA;
private final MutableLiveData<String> messageContainerB;
public PagerAgentViewModel(SavedStateHandle state) {
this.state = state;
messageContainerA = state.getLiveData("Default Message");
messageContainerB = state.getLiveData("Default Message");
}
public void sendMessageToB(String msg)
{
messageContainerB.setValue(msg);
}
public void sendMessageToA(String msg)
{
messageContainerA.setValue(msg);
}
public LiveData<String> getMessageContainerA() {
return messageContainerA;
}
public LiveData<String> getMessageContainerB() {
return messageContainerB;
}
}
3.BlankFragmentA
public class BlankFragmentA extends Fragment {
private PagerAgentViewModel viewModel;
public BlankFragmentA() {
// Required empty public constructor
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(getActivity()).get(PagerAgentViewModel.class);
textView = (TextView) view.findViewById(R.id.fragment_textA);
// set the onclick listener
Button button = (Button) view.findViewById(R.id.btnA);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewModel.sendMessageToB("Hello B");
}
});
//setup the listener for the fragment A
viewModel.getMessageContainerA().observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(#Nullable String msg) {
textView.setText(msg);
}
});
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank_a, container, false);
return view;
}
}
4.BlankFragmentB
public class BlankFragmentB extends Fragment {
public BlankFragmentB() {
// Required empty public constructor
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(getActivity()).get(PagerAgentViewModel.class);
textView = (TextView) view.findViewById(R.id.fragment_textB);
//set the on click listener
Button button = (Button) view.findViewById(R.id.btnB);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewModel.sendMessageToA("Hello A");
}
});
//setup the listener for the fragment B
viewModel.getMessageContainerB().observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(#Nullable String msg) {
textView.setText(msg);
}
});
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank_b, container, false);
return view;
}
}
As written in the official Google tutorial now you may obtain a shared view model with by activityViewModels()
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
I have found a similar solution as others according to google codelabs example.
I have two fragments where one of them wait for an object change in the other and continues its process with updated object.
for this approach you will need a ViewModel class as below:
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import yourPackage.YourObjectModel;
public class SharedViewModel extends ViewModel {
public MutableLiveData<YourObjectModel> item = new MutableLiveData<>();
public YourObjectModel getItem() {
return item.getValue();
}
public void setItem(YourObjectModel item) {
this.item.setValue(item);
}
}
and the listener fragment should look like this:
public class ListenerFragment extends Fragment{
private SharedViewModel model;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.item.observe(getActivity(), new Observer<YourObjectModel>(){
#Override
public void onChanged(#Nullable YourObjectModel updatedObject) {
Log.i(TAG, "onChanged: recieved freshObject");
if (updatedObject != null) {
// Do what you want with your updated object here.
}
}
});
}
}
finally, the updater fragment can be like this:
public class UpdaterFragment extends DialogFragment{
private SharedViewModel model;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
}
// Call this method where it is necessary
private void updateViewModel(YourObjectModel yourItem){
model.setItem(yourItem);
}
}
It is good to mention that the updater fragment can be any form of fragments(not DialogFragment only) and for using these architecture components you should have these lines of code in your app build.gradle file. source
dependencies {
def lifecycle_version = "1.1.1"
implementation "android.arch.lifecycle:extensions:$lifecycle_version"
}
Before you are using a callback which attaches to Activity which is considered as a container.
That callback is a middle man between two Fragments.
The bad things about this previous solution are:
Activity has to carry the callback, it means a lot of work for
Activity.
Two Fragments are coupled tightly, it is difficult to update or change logic later.
With the new ViewModel (with support of LiveData), you have an elegant solution. It now plays a role of middle man which you can attach its lifecycle to Activity.
Logic and data between two Fragments now lay out in ViewModel.
Two Fragment gets data/state from ViewModel, so they do not need to know each other.
Besides, with the power of LiveData, you can change detail Fragment based on changes of master Fragment in reactive approach instead of previous callback way.
You now completely get rid of callback which tightly couples to both Activity and related Fragments.
I highly recommend you through Google's code lab. In step 5, you can find an nice example about this.
I implemented something similar to what you want, my viewmodel contains LiveData object that contains Enum state, and when you want to change the fragment from master to details (or in reverse) you call ViewModel functions that changing the livedata value, and activity know to change the fragment because it is observing livedata object.
TestViewModel:
public class TestViewModel extends ViewModel {
private MutableLiveData<Enums.state> mState;
public TestViewModel() {
mState=new MutableLiveData<>();
mState.setValue(Enums.state.Master);
}
public void onDetail() {
mState.setValue(Enums.state.Detail);
}
public void onMaster() {
mState.setValue(Enums.state.Master);
}
public LiveData<Enums.state> getState() {
return mState;
}
}
Enums:
public class Enums {
public enum state {
Master,
Detail
}
}
TestActivity:
public class TestActivity extends LifecycleActivity {
private ActivityTestBinding mBinding;
private TestViewModel mViewModel;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding=DataBindingUtil.setContentView(this, R.layout.activity_test);
mViewModel=ViewModelProviders.of(this).get(TestViewModel.class);
mViewModel.getState().observe(this, new Observer<Enums.state>() {
#Override
public void onChanged(#Nullable Enums.state state) {
switch(state) {
case Master:
setMasterFragment();
break;
case Detail:
setDetailFragment();
break;
}
}
});
}
private void setMasterFragment() {
MasterFragment masterFragment=MasterFragment.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout, masterFragment,"MasterTag").commit();
}
private void setDetailFragment() {
DetailFragment detailFragment=DetailFragment.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout, detailFragment,"DetailTag").commit();
}
#Override
public void onBackPressed() {
switch(mViewModel.getState().getValue()) {
case Master:
super.onBackPressed();
break;
case Detail:
mViewModel.onMaster();
break;
}
}
}
MasterFragment:
public class MasterFragment extends Fragment {
private FragmentMasterBinding mBinding;
public static MasterFragment newInstance() {
MasterFragment fragment=new MasterFragment();
return fragment;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_master, container, false);
mBinding.btnDetail.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final TestViewModel viewModel=ViewModelProviders.of(getActivity()).get(TestViewModel.class);
viewModel.onDetail();
}
});
return mBinding.getRoot();
}
}
DetailFragment:
public class DetailFragment extends Fragment {
private FragmentDetailBinding mBinding;
public static DetailFragment newInstance() {
DetailFragment fragment=new DetailFragment();
return fragment;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_detail, container, false);
mBinding.btnMaster.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final TestViewModel viewModel=ViewModelProviders.of(getActivity()).get(TestViewModel.class);
viewModel.onMaster();
}
});
return mBinding.getRoot();
}
}
I end up using the own ViewModel to hold up the listener that will trigger the Activity method. Similar to the old way but as I said, passing the listener to ViewModel instead of the fragment. So my ViewModel looked like this:
public class SharedViewModel<T> extends ViewModel {
private final MutableLiveData<T> selected = new MutableLiveData<>();
private OnSelectListener<T> listener = item -> {};
public interface OnSelectListener <T> {
void selected (T item);
}
public void setListener(OnSelectListener<T> listener) {
this.listener = listener;
}
public void select(T item) {
selected.setValue(item);
listener.selected(item);
}
public LiveData<T> getSelected() {
return selected;
}
}
in StepMasterActivity I get the ViewModel and set it as a listener:
StepMasterActivity.class:
SharedViewModel stepViewModel = ViewModelProviders.of(this).get("step", SharedViewModel.class);
stepViewModel.setListener(this);
...
#Override
public void selected(Step item) {
Log.d(TAG, "selected: "+item);
}
...
In the fragment I just retrieve the ViewModel
stepViewModel = ViewModelProviders.of(getActivity()).get("step", SharedViewModel.class);
and call:
stepViewModel.select(step);
I tested it superficially and it worked. As I go about implementing the other features related to this, I will be aware of any problems that may occur.
For those using Kotlin out there try the following approach:
Add the androidx ViewModel and LiveData libraries to your gradle file
Call your viewmodel inside the fragment like this:
class MainFragment : Fragment() {
private lateinit var viewModel: ViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// kotlin does not have a getActivity() built in method instead we use activity, which is null-safe
activity?.let {
viemModel = ViewModelProvider(it).get(SharedViewModel::class.java)
}
}
}
The above method is a good practice since it will avoid crashes due to null pointer exceptions
Edit: As btraas complemented: activity is compiled into getActivity() which is marked as #Nullable in the android SDK. activity and getActivity() are both accessible and equivalent.
You can set values from Detail Fragment to Master Fragment like this
model.selected.setValue(item)
Related
After updating to the latest support repository,
compile 'com.android.support:appcompat-v7:24.2.0'
compile 'com.android.support:design:24.2.0'
compile 'com.android.support:percent:24.2.0'
compile 'com.android.support:recyclerview-v7:24.2.0'
I'm getting the weird exception.
java.lang.IllegalStateException: Fragment null must be a public static class to be properly recreated from instance state.
at android.support.v4.app.BackStackRecord.doAddOp(BackStackRecord.java:435)
at android.support.v4.app.BackStackRecord.add(BackStackRecord.java:414)
at android.support.v4.app.DialogFragment.show(DialogFragment.java:154)
at com.androidapp.base.BaseActivity.showDialogFragment(BaseActivity.java:78)
at com.androidapp.MainActivity.showNewDialog(MainActivity.java:304)
at com.androidapp.MainActivity$6.onClick(MainActivity.java:228)
In my BaseActivity class, I've created a re-usable fragment which can be used in activity class that extends the BaseActivty
public void showDialogFragment(DialogFragment newFragment) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment prev = getSupportFragmentManager().findFragmentByTag("dialog");
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack("dialog");
newFragment.show(ft, "dialog");
}
Back to the MainActivty I've used the fragment like this,
public class MainActivity extends BaseActivity {
#SuppressLint("ValidFragment")
public void showNewDialog(int type, String title, String message) {
final DialogNew dialog = new DialogNew() {
#Override
public void success(boolean isLandscape) {
.......
}
#Override
public void cancel() {
}
};
dialog.setArgs(title, message);
super.showDialogFragment(dialog);
}
}
The DialogNew class is below,
public abstract class DialogNew extends DialogFragment {
private View rootView;
private String title;
private String message;
public void setArgs(String title, String message) {
Bundle args = new Bundle();
args.putString("title", title);
args.putString("message", message);
setArguments(args);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_TITLE, 0);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_new_dialog, container, false);
init();
setListeners();
return rootView;
}
public abstract void success(boolean isLandscape);
public abstract void cancel();
}
PS: The same code works with older support repository.
The error is not especially weird. If you were not getting this error before, that was weird.
Android destroys and recreates fragments as part of a configuration change (e.g., screen rotation) and as part of rebuilding a task if needed (e.g., user switches to another app, your app's process is terminated while it is in the background, then the user tries to return to your app, all within 30 minutes or so). Android has no means of recreating an anonymous subclass of DialogNew.
So, make a regular public Java class (or a public static nested class) that extends DialogNew and has your business logic, replacing the anonymous subclass of DialogNew that you are using presently.
I recreated my fragment from scratch, it's solved the problem for me.
New -> Fragment -> Fragment (Blank) and you uncheck the 2nd box before confirming.
The reason for this error is very well explained on Android Developers guides.
When the system issues a configuration change, it needs to be able to create a new instance of your fragment. In order to do so, it relies on a default constructor of the fragment which takes no arguments and therefore cannot have any dependencies. If your Fragment class is not a static public class, the system is unable to reflectively find this default constructor and the error indicates just that.
To get around the problem, you will have to override the default implementation of the FragmentFactory of the FragmentManager instance which will handle creation of your fragment. This is explained by code in the link I provided.
Edit: You probably don't want to do this... See the comments.
The code sample looks similar to what I had suggested over here, and I also recently discovered that the solution I had there was not working anymore. I've updated my answer there for Java7, but if you have Java8 the solution is super easy:
(I haven't tested this yet)
public class DialogNew extends DialogFragment {
private View rootView;
private String title;
private String message;
// Do nothing by default
private Consumer mSuccess = (boolean b) -> {};
private Runnable mCancel = () -> {};
public void setArgs(String title, String message) {
Bundle args = new Bundle();
args.putString("title", title);
args.putString("message", message);
setArguments(args);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_TITLE, 0);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_new_dialog, container, false);
// use mSuccess.accept(boolean) when needed
init();
setListeners();
return rootView;
}
public void setSuccess(Consumer success) {
mSuccess = success;
}
public void setCancel(Runnable cancel) {
mCancel = cancel;
}
}
Then in the Main activity:
public class MainActivity extends BaseActivity {
public void showNewDialog(int type, String title, String message) {
final DialogNew dialog = new DialogNew();
dialog.setArgs(title, message);
dialog.setSuccess((boolean isLandscape) -> {
//....
});
super.showDialogFragment(dialog);
}
}
Create Fragment from new >Fragment> Blank Fragment
it works for me ♥♥♥
This error was occurred because of virtual methods is used in creating an instance of fragment.
Virtual methods must be removed from declaration and a handler class to should be used for listening to DialogNew class events.
public class MainActivity extends BaseActivity {
#SuppressLint("ValidFragment")
public void showNewDialog(int type, String title, String message) {
final DialogNew dialog = new DialogNew(
// use DialogHandler for manage success or cancel click
new DialogHandler() {
#Override
public void success(boolean isLandscape) {
}
#Override
public void cancel() {
}
}
);
dialog.setArgs(title, message);
super.showDialogFragment(dialog);
}
}
I discover the new android architecture component and I want to test the couple ViewModel / LiveData through a small test application. The latter has two fragments (in a ViewPager), the first creates/updates a list of cards (via an EditText) and the second displays all the cards.
My ViewModel:
public class CardsScanListViewModel extends AndroidViewModel {
private MutableLiveData> cardsLiveData = new MutableLiveData();
private HashMap cardsMap = new HashMap();
public CardsScanListViewModel(#NonNull Application application) {
super(application);
}
public MutableLiveData> getCardsLiveData() {
return this.cardsLiveData;
}
public void saveOrUpdateCard(String id) {
if(!cardsMap.containsKey(id)) {
cardsMap.put(id, new Card(id, new AtomicInteger(0)));
}
cardsMap.get(id).getCount().incrementAndGet();
this.cardsLiveData.postValue(cardsMap);
}
}
My second fragment:
public class CardsListFragment extends Fragment {
CardsAdapter cardsAdapter;
RecyclerView recyclerCardsList;
public CardsListFragment() {}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final CardsScanListViewModel viewModel =
ViewModelProviders.of(this).get(CardsScanListViewModel.class);
observeViewModel(viewModel);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_cards_list, container, false);
recyclerCardsList = v.findViewById(R.id.recyclerCardsList);
recyclerCardsList.setLayoutManager(new LinearLayoutManager(getActivity()));
cardsAdapter = new CardsAdapter(getActivity());
recyclerCardsList.setAdapter(cardsAdapter);
return v;
}
private void observeViewModel(CardsScanListViewModel viewModel) {
viewModel.getCardsLiveData().observe(this, new Observer > () {
#Override
public void onChanged(#Nullable HashMap cards) {
if (cards != null) {
cardsAdapter.setCardsList(cards.values());
}
}
});
}
}
TheHashMap, like my MutableLiveData, update well but my second fragment doesn't receive the information via observer.
You are observing the new instance of ViewModel instead of observing the same ViewModel used by your First Fragment.
final CardsScanListViewModel viewModel =
ViewModelProviders.of(this).get(CardsScanListViewModel.class);
Above code initialize new instance of CardsScanListViewModel for your second fragment CardsListFragment, because you passed this as context.
If you update any data from this fragment it will update in this instance of ViewModel.
It works in your first Fragment because it updates data and observes data from same instance of ViewModel
To keep data common among ViewModels initiate view model by passing activity context instead of fragment context in both the fragments.
final CardsScanListViewModel viewModel =
ViewModelProviders.of(getActivity()).get(CardsScanListViewModel.class);
This will create single instance of CardsScanListViewModel and data will be shared between fragments as they are observing LiveData from single instance of ViewModel.
For confirmation, you need to add notifyDataSetChanged() after updating the list if you haven't done that in adapter itself
private void observeViewModel(CardsScanListViewModel viewModel) {
viewModel.getCardsLiveData().observe(this, new Observer > () {
#Override
public void onChanged(#Nullable HashMap cards) {
if (cards != null) {
cardsAdapter.setCardsList(cards.values());
cardsAdapter.notifyDataSetChanged();
}
}
});
}
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
}
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);
}
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);
}
}
I am reinventing my app using a classic MVP approach. In order to to this I read many many articles and tutorials, and what I came out with is that the best way is to :
create an interface for the presenter and one for the view
make fragments and activities implements view interfaces
create an implementation of the presenter interface, which takes in the constructor an instance of the the view it manages, and hold a reference to the presenter inside the view's implementation.
So I have created this classes
VIEW INTERFACE
public interface SignupEmailView extends BaseView {
void fillEmail(String email);
void onEmailInvalid(String error);
void onDataValidated();
}
PRESENTER INTERFACE
public interface SignupEmailPresenter {
void initData(Bundle bundle);
void validateData(String email);
}
VIEW IMPLEMENTATION
public class FrSignup_email extends BaseSignupFragmentMVP implements IBackHandler, SignupEmailView {
public static String PARAM_EMAIL = "param_email";
#Bind(R.id.signup_step2_new_scrollview)
ScrollView mScrollview;
#Bind(R.id.signup_step2_new_lblTitle)
SuperLabel mLblTitle;
#Bind(R.id.signup_step2_new_lblSubtitle)
TextView mLblSubtitle;
#Bind(R.id.signup_step2_new_txtEmail)
EditText mTxtEmail;
#Bind(R.id.signup_step2_new_btnNext)
Button mBtnNext;
protected SignupActivityView mActivity;
SignupEmailPresenter mPresenter;
public FrSignup_email() {
// Required empty public constructor
}
public static FrSignup_email newInstance(String email) {
FrSignup_email fragment = new FrSignup_email();
Bundle b = new Bundle();
b.putString(PARAM_EMAIL, email);
fragment.setArguments(b);
return fragment;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mActivity = (SignupActivityView) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement IResetPasswordBridge");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = loadView(inflater, container, savedInstanceState, R.layout.fragment_signup_email);
mPresenter = new SignupEmailPresenterImpl(this);
ButterKnife.bind(this, view);
return view;
}
#Override
public final void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
applyCircularReveal();
mPresenter.initData(this.getArguments());
mTxtEmail.setImeOptions(EditorInfo.IME_ACTION_NEXT);
mTxtEmail.setOnEditorActionListener(new TextView.OnEditorActionListener() {
#Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
mPresenter.validateData(mTxtEmail.getText().toString());
return true;
}
return false;
}
});
mTxtEmail.setOnTouchListener(new OnTouchCompoundDrawableListener_NEW(mTxtEmail, new OnTouchCompoundDrawableListener_NEW.OnTouchCompoundDrawable() {
#Override
public void onTouch() {
mTxtEmail.setText("");
}
}));
mBtnNext.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mPresenter.validateData(mTxtEmail.getText().toString());
}
});
}
#Override
public void fillEmail(String email) {
mTxtEmail.setText(email);
}
#Override
public void onEmailInvalid(String error) {
displayError(error);
}
#Override
public void onDataValidated() {
changeFieldToValid(mTxtEmail);
setEmail(mTxtEmail.getText().toString());
// the activity shows the next fragment
mActivity.onEmailValidated();
}
#Override
public boolean doBack() {
if (!isLoading()) {
mActivity.onEmailBack();
}
return true;
}
#Override
public void displayError(String error) {
changeFieldToInvalid(mTxtEmail);
mLblSubtitle.setText(error);
mLblSubtitle.setTextColor(ContextCompat.getColor(getActivity(), R.color.field_error));
}
}
PRESENTER IMPLEMENTATION
public class SignupEmailPresenterImpl implements SignupEmailPresenter {
private SignupEmailView mView;
public SignupEmailPresenterImpl(SignupEmailView view) {
mView = view;
}
#Override
public void initData(Bundle bundle) {
if (bundle != null) {
mView.fillEmail(bundle.getString(FrSignup_email.PARAM_EMAIL));
}
}
#Override
public void validateData(String password) {
ValidationUtils_NEW.EmailStatus status = ValidationUtils_NEW.validateEmail(password);
if (status != ValidationUtils_NEW.EmailStatus.VALID) {
mView.onEmailInvalid(ValidationUtils_NEW.getEmailErrorMessage(status));
} else {
mView.onDataValidated();
}
}
}
Now the fragment is held by an activity which implements this view interface and has its own presenter
public interface SignupActivityView extends BaseView {
void onEmailValidated();
void onPhoneNumberValidated();
void onPasswordValidated();
void onUnlockCodeValidated();
void onResendCodeClick();
void onEmailBack();
void onPhoneNumberBack();
void onPasswordBack();
void onConfirmCodeBack();
void onSignupRequestSuccess(boolean resendingCode);
void onSignupRequestFailed(String errorMessage);
void onTokenCreationFailed();
void onUnlockSuccess();
void onUnlockError(String errorMessage);
void showTermsAndConditions();
void hideTermsAndConditions();
}
My idea is to have a unit test for each project unit, so for each view and presenter implementation I want a unit test, so I want to unit test my fragment with roboletric, and for example I want to test that if I click the "NEXT" button and the email is correct, the hosting Activity's onEmailValidated()method is called. This is my test class
public class SignupEmailViewTest {
private SignupActivity_NEW mActivity;
private SignupActivity_NEW mSpyActivity;
private FrSignup_email mFragment;
private FrSignup_email mSpyFragment;
private Context mContext;
#Before
public void setUp() {
final Context context = RuntimeEnvironment.application.getApplicationContext();
this.mContext = context;
mActivity = Robolectric.buildActivity(SignupActivity_NEW.class).create().visible().get();
mSpyActivity = spy(mActivity);
mFragment = FrSignup_email.newInstance("");
mSpyFragment =spy(mFragment);
mSpyActivity.getFragmentManager()
.beginTransaction()
.replace(R.id.signupNew_fragmentHolder, mSpyFragment)
.commit();
mSpyActivity.getFragmentManager().executePendingTransactions();
}
#Test
public void testEmailValidation() {
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblTitle).isShown());
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle).isShown());
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
assertTrue(((SuperLabel) mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle)).getText().equals(mContext.getString(R.string.email_empty)));
((EditText) mSpyActivity.findViewById(R.id.signup_step2_new_txtEmail)).setText("aaa#bbb.ccc");
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
verify(mSpyFragment).onDataValidated();
verify(mSpyActivity).onEmailValidated();
}
}
everything works well, is just the last verify which doesn't work. Note that the previous verify works, so onEmailValidated is called for sure.
Aside from this specific case, I have some point to discuss:
If with roboeletric I am forced to use an activity to instantiate a fragment, how can I test the fragment in complete isolation (which would be the unit tests goal)? I mean, if I use Robolectric.setupActivity(MyActivity.class) and the activity instantiates somewhere a fragment, it will load the activity and the fragment, which is good, but what if the activity manages a flow of fragments? How can I test the second or third fragment without manually navigating to it? Someone can say to use a dummy activity and use FragmentTestUtil.startFragment, but what in the fragment's onAttach() method is implemented the bridging with the parent activity? Is it me going on the wrong way or are this problems still unsolved?
thanks
Actually you don't even require Roboelectric to do any of those tests.
If each fragment/activity implements a different view interface you could implement fake views and instantiate those instead of the activity/fragment. In this way you could have isolated tests.
If you don't want to implement all the methods of the view interface you could use Mockito and stub only the ones that your unit test requires.
Let me know if you need sample code.
After updating to the latest support repository,
compile 'com.android.support:appcompat-v7:24.2.0'
compile 'com.android.support:design:24.2.0'
compile 'com.android.support:percent:24.2.0'
compile 'com.android.support:recyclerview-v7:24.2.0'
I'm getting the weird exception.
java.lang.IllegalStateException: Fragment null must be a public static class to be properly recreated from instance state.
at android.support.v4.app.BackStackRecord.doAddOp(BackStackRecord.java:435)
at android.support.v4.app.BackStackRecord.add(BackStackRecord.java:414)
at android.support.v4.app.DialogFragment.show(DialogFragment.java:154)
at com.androidapp.base.BaseActivity.showDialogFragment(BaseActivity.java:78)
at com.androidapp.MainActivity.showNewDialog(MainActivity.java:304)
at com.androidapp.MainActivity$6.onClick(MainActivity.java:228)
In my BaseActivity class, I've created a re-usable fragment which can be used in activity class that extends the BaseActivty
public void showDialogFragment(DialogFragment newFragment) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment prev = getSupportFragmentManager().findFragmentByTag("dialog");
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack("dialog");
newFragment.show(ft, "dialog");
}
Back to the MainActivty I've used the fragment like this,
public class MainActivity extends BaseActivity {
#SuppressLint("ValidFragment")
public void showNewDialog(int type, String title, String message) {
final DialogNew dialog = new DialogNew() {
#Override
public void success(boolean isLandscape) {
.......
}
#Override
public void cancel() {
}
};
dialog.setArgs(title, message);
super.showDialogFragment(dialog);
}
}
The DialogNew class is below,
public abstract class DialogNew extends DialogFragment {
private View rootView;
private String title;
private String message;
public void setArgs(String title, String message) {
Bundle args = new Bundle();
args.putString("title", title);
args.putString("message", message);
setArguments(args);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_TITLE, 0);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_new_dialog, container, false);
init();
setListeners();
return rootView;
}
public abstract void success(boolean isLandscape);
public abstract void cancel();
}
PS: The same code works with older support repository.
The error is not especially weird. If you were not getting this error before, that was weird.
Android destroys and recreates fragments as part of a configuration change (e.g., screen rotation) and as part of rebuilding a task if needed (e.g., user switches to another app, your app's process is terminated while it is in the background, then the user tries to return to your app, all within 30 minutes or so). Android has no means of recreating an anonymous subclass of DialogNew.
So, make a regular public Java class (or a public static nested class) that extends DialogNew and has your business logic, replacing the anonymous subclass of DialogNew that you are using presently.
I recreated my fragment from scratch, it's solved the problem for me.
New -> Fragment -> Fragment (Blank) and you uncheck the 2nd box before confirming.
The reason for this error is very well explained on Android Developers guides.
When the system issues a configuration change, it needs to be able to create a new instance of your fragment. In order to do so, it relies on a default constructor of the fragment which takes no arguments and therefore cannot have any dependencies. If your Fragment class is not a static public class, the system is unable to reflectively find this default constructor and the error indicates just that.
To get around the problem, you will have to override the default implementation of the FragmentFactory of the FragmentManager instance which will handle creation of your fragment. This is explained by code in the link I provided.
Edit: You probably don't want to do this... See the comments.
The code sample looks similar to what I had suggested over here, and I also recently discovered that the solution I had there was not working anymore. I've updated my answer there for Java7, but if you have Java8 the solution is super easy:
(I haven't tested this yet)
public class DialogNew extends DialogFragment {
private View rootView;
private String title;
private String message;
// Do nothing by default
private Consumer mSuccess = (boolean b) -> {};
private Runnable mCancel = () -> {};
public void setArgs(String title, String message) {
Bundle args = new Bundle();
args.putString("title", title);
args.putString("message", message);
setArguments(args);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_TITLE, 0);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_new_dialog, container, false);
// use mSuccess.accept(boolean) when needed
init();
setListeners();
return rootView;
}
public void setSuccess(Consumer success) {
mSuccess = success;
}
public void setCancel(Runnable cancel) {
mCancel = cancel;
}
}
Then in the Main activity:
public class MainActivity extends BaseActivity {
public void showNewDialog(int type, String title, String message) {
final DialogNew dialog = new DialogNew();
dialog.setArgs(title, message);
dialog.setSuccess((boolean isLandscape) -> {
//....
});
super.showDialogFragment(dialog);
}
}
Create Fragment from new >Fragment> Blank Fragment
it works for me ♥♥♥
This error was occurred because of virtual methods is used in creating an instance of fragment.
Virtual methods must be removed from declaration and a handler class to should be used for listening to DialogNew class events.
public class MainActivity extends BaseActivity {
#SuppressLint("ValidFragment")
public void showNewDialog(int type, String title, String message) {
final DialogNew dialog = new DialogNew(
// use DialogHandler for manage success or cancel click
new DialogHandler() {
#Override
public void success(boolean isLandscape) {
}
#Override
public void cancel() {
}
}
);
dialog.setArgs(title, message);
super.showDialogFragment(dialog);
}
}