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);
}
}
Related
I'm trying the new Saved State module but without result, at the moment. This is the OnCreate of my Fragment:
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppCompatActivity activity = (AppCompatActivity) requireActivity();
viewModel = new SavedStateViewModelFactory(requireActivity().getApplication(), requireActivity()).create(QueryViewModel.class);
//The following is code for debugging purposes
List<SingleNote> list = viewModel.getState().get(KEY);
Log.i("ONCREATE", "ONCREATE");
if (list != null) {
Log.i("NUMBER OF ELEMENTS", String.valueOf(list.size()));
}
}
and this is my ViewModel class:
public class QueryViewModel extends ViewModel {
private SavedStateHandle state = new SavedStateHandle();
public QueryViewModel(SavedStateHandle savedStateHandle) {
state = savedStateHandle;
}
public SavedStateHandle getState() {
return state;
}
I save a List<> of Serializable object inside the SavedStatehandle and it works. The problem came when I start a new activity with StartActivity (it is an Activity of my app, not an external one) because when I return back the SavedStateHandle is empty.
Any suggest? I have added the correct dependencies and I've already tried the ViewModelProvider constructor.
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);
}
}
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)
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 am working on an app which uses the NavigationDrawer. Different fragments are placed into the content view of the MainActivity whenever a menu item in the drawer is selected.
To inform the MainActivity that a Fragment successfully attached the following callback is executed:
public class CustomFragment extends Fragment {
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
((MainActivity) activity).onSectionAttached();
}
}
Since I am started using Otto with Dagger in the project I am curious how I can substitute the callback with a .post() event such as:
mBus.post(new CustomFragmentAttachedEvent);
The problem is that mBus is null in onAttach(). It gets initialized in onCreate().
#Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((MyApp) getActivity().getApplication()).getObjectGraph().inject(this);
}
Here is an example of such a Fragment class.
References:
Complete Android Fragment & Activity Lifecycle
You can easily try out the example yourself: Create a new project from the NavigationDrawer template available in Android Studio, add Dagger and Otto and try to substitute the mentioned callback.
Working solution:
Here is the example Fragment in the final working version.
You can create a provider for your Otto bus with Dagger as follows:
#Module(injects = {
YourFragment.class,
MainActivity.class
},complete = true)
public class EventBusModule {
#Provides
#Singleton
Bus provideBus() {
return new Bus();
}
}
Then you register the EventBusModule when you create your ObjectGraph. You can create your graph in the Application's onCreate():
public class MyApplication extends Application{
public void onCreate() {
Object[] modules = new Object[]{new EventBusModule()};
Injector.init(modules);
}
}
You would need to create an Injector that has some static methods and a reference to the ObjectGraph so you can manipulate it without having a reference to the Application. Something like this:
public final class Injector {
private static ObjectGraph objectGraph = null;
public static void init(final Object... modules) {
if (objectGraph == null) {
objectGraph = ObjectGraph.create(modules);
}
else {
objectGraph = objectGraph.plus(modules);
}
// Inject statics
objectGraph.injectStatics();
}
public static final void inject(final Object target) {
objectGraph.inject(target);
}
public static <T> T resolve(Class<T> type) {
return objectGraph.get(type);
}
}
Then you just use #Inject in your Fragment to let Dagger give you a Bus instance and inject the Fragment:
public class MyFragment extends Fragment{
#Inject Bus mBus;
public void onAttach(){
Injector.inject(this);
}
}