I would like to try and use the MVP pattern for a simple Android application I am writing. This is my first time using the MVP pattern, as I'm still learning, so please be gentle ;)
I have a fragment that I would like to use in conjunction with 4 different presenters. The problem I have is, how can I pass a different presenter to each instance?
I would like to pass the presenter to a constructor, however when Android recreates the fragment, it will call the default constructor. Does this mean it will no longer hold a reference to the presenter?
If so, how else can I pass in the presenter?
I've included some psuedo code of what I'd like to do below. Please note I've just typed this straight into the browser so there may be some silly mistakes, but hopefully you get a rough idea of what I mean.
My 2 interfaces:
public interface IClickableListPresenter {
ListAdapter createListAdapter();
void onListItemClick(int position);
}
public interface ITabbable {
String getTitle();
Fragment getFragment();
}
2 example presenters:
public class ArtistPresenter implements IClickableListPresenter {
public ListAdapter createListAdapter(){
// Create a ListAdapter containing a list of artists
}
public void onListItemClick(int position){
// Handle the click event
}
}
public class TitlePresenter implements IClickableListPresenter {
public ListAdapter createListAdapter(){
// Create a ListAdapter containing a list of song titles
}
public void onListItemClick(int position){
// Handle the click event in a completely different way
// to the ArtistPresenter
}
}
My fragment:
public class ClickableListFragment extends ListFragment
implements ITabbable {
private IClickableListPresenter presenter;
private String title;
// What can I do instead of this constructor?
public ClickableListFragment(
String title, IClickableListPresenter presenter){
this.title = title;
this.presenter = presenter;
}
#Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setListAdapter(presenter.createListAdapter());
}
#Override
public void onListItemClick(ListView l, View v, int position, long id){
presenter.onListItemClick(position);
}
public Fragment getFragment(){
return this;
}
public String getTitle(){
return title;
}
}
And finally, the class that instantiates the fragments:
public class TabsPagerAdapter extends FragmentPagerAdapter{
private ITabbable tabs[] = {
new ClickableListFragment("Artist", new ArtistPresenter()),
new ClickableListFragment("Title", new TitlePresenter()),
//...
};
//...
}
You could do something like this
public class TabsPagerAdapter extends FragmentPagerAdapter{
private ITabbable tabs[] = {
ClickableListFragment.newInstance(ClickableListFragment.Type.ARTIST),
ClickableListFragment.newInstance(ClickableListFragment.Type.TITLE),
//...
};
}
In ClickableListFragment
public enum Type {
ARTIST,
TITLE
}
public static MyFragment newInstance(final Type fragmentType) {
final MyFragment fragment = new MyFragment();
Bundle args = new Bundle();
args.putSerializable(TYPE, fragmentType);
fragment.setArguments(args);
return fragment;
}
and in onCreate
#Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Type tempType = (Type) getArguments().getSerializable(TYPE);
if(tempType == Type.ARTIST){
variable = new ArtistPresenter();
.... what you want with that variable
.
.
.
}
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 created application using FragmentPagerAdapter with 3 fragments inner static class inside let say MainActivity. Basically android studio template of tabbed activity generate the code that way, also in android documentation use that inner static class too just for example
creating swipe view
But it will be bad for performance because I have to make all variables and functions become static to make it work, also its not the right thing to do.
So I try to split the Fragment to its own class, and from this article
communicate between fragment
it tell us how to sending variable/action callback from fragment to parent activity (in my case is MainActivity)
And after some research, sending data from activity to fragment can be done using interface too. But it becomes both are implementing each other and I get
cyclic inheritance error
Most of the topic on the internet discussing how to pass data from activity to fragment are happened when creating the fragment, using bundle and etc.
For dynamic application that request data to server that approach seems like cannot be used.
Is there any correct way how to pass data (in my case is List Object) from activity to fragment at certain time when the request to server is finished (the fragment already created before)? Thank you.
Here is some of my code
MainActivity
public class MainActivity extends AppCompatActivity implements BlankFragment.OnFragmentInteractionListener {
//On create
ViewPager viewPager = findViewById(R.id.vpPager);
MyPagerAdapter adapterViewPager = new MyPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapterViewPager);
//adapter
public static class MyPagerAdapter extends FragmentPagerAdapter {
private static int NUM_ITEMS = 3;
private String tabTitles[] = new String[]{"Home", "History", "My Account"};
public MyPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
// Returns total number of pages
#Override
public int getCount() {
return NUM_ITEMS;
}
// Returns the fragment to display for that page
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return BlankFragment.newInstance();
case 1:
return FragmentHistory.newInstance();
case 2:
return FragmentAccount.newInstance();
default:
return BlankFragment.newInstance();
}
}
// Returns the page title for the top indicator
#Override
public CharSequence getPageTitle(int position) {
return tabTitles[position];
}
}
#Override
public void onFragmentInteraction() {
getKegiatan();
}
}
BlankFragment
public class BlankFragment extends Fragment {
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
private String mParam1;
private String mParam2;
private OnFragmentInteractionListener mListener;
private final Context context = getContext();
private SharedPreferences sharedPreferences;
private int mRole;
private ImageView ivNoData;
private ExpandableListView elvKegiatan;
private ListView lvKegiatan;
private EditText etFrom, etTo;
private String dateFrom, dateTo;
private List<Kegiatan> listKegiatan;
private List<Kegiatan> superKegiatanSaya;
private List<Kegiatan> superKegiatanAnak;
public BlankFragment() {
// Required empty public constructor
}
public static BlankFragment newInstance() { //String param1, String param2
BlankFragment fragment = new BlankFragment();
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
sharedPreferences = context.getSharedPreferences(
getResources().getString(R.string.myspkey), Context.MODE_PRIVATE);
mRole = sharedPreferences.getInt(getString(R.string.splogin_role), 0);
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_kegiatan, container, false);
FloatingActionButton fabAddKeg = view.findViewById(R.id.fabAddKegiatan);
fabAddKeg.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
startActivity(new Intent(context, TambahKegiatanActivity.class));
}
});
lvKegiatan = view.findViewById(R.id.listViewKegiatan);
elvKegiatan = view.findViewById(R.id.elvKegiatan);
View buttonSearch = view.findViewById(R.id.buttonSearch);
buttonSearch.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onButtonPressed();
}
});
etFrom = view.findViewById(R.id.editTextFrom);
etTo = view.findViewById(R.id.editTextTo);
populateList();
prepareDate();
LinearLayout llRangeKeg = view.findViewById(R.id.linearLayoutRangeKeg);
switch (mRole){
case UserConst.ROLE_USER:
break;
case UserConst.ROLE_SUPER:
case UserConst.ROLE_CEO:
case UserConst.ROLE_CFO:
llRangeKeg.setVisibility(View.GONE);
break;
case UserConst.ROLE_ADMIN:
case UserConst.ROLE_FINANCE:
llRangeKeg.setVisibility(View.GONE);
fabAddKeg.setVisibility(View.GONE);
break;
}
return view;
}
// TODO: Rename method, update argument and hook method into UI event
public void onButtonPressed() {
if (mListener != null) {
mListener.onFragmentInteraction();
}
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnFragmentInteractionListener) {
mListener = (OnFragmentInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnFragmentInteractionListener");
}
}
#Override
public void onDetach() {
super.onDetach();
mListener = null;
}
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
* <p>
* See the Android Training lesson <a href=
* "http://developer.android.com/training/basics/fragments/communicating.html"
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction();
}
1 )Define a listener interface. I usually do this as an inner interface within the activity:
public interface DataUpdateListener {
void onDataUpdate(List<Object> mData);
}
2 )make object of interface
DataUpdateListener dataListener;
3 )Add the register methods to the activity:
public void registerDataUpdateListener(DataUpdateListener listener) {
dataListener=listener;
}
4 )Have your fragments implement DataUpdateListener:
public class MyFragment extends Fragment implements DataUpdateListener {
5 )implement the method
#Override
public void onDataUpdate(List<Object> mData) {
// put your UI update logic here
}
6 )Override onAttach() and onDestroy() in the fragments to register/unregister:
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
((MainActivity) activity).registerDataUpdateListener(this);
}
7 ) When you update UI in Fragment
dataListener.onDataUpdate(mData);
I think you can use EventBus for this situation
Create a class for the event (for exemple Data)
public class Data {
public List objects;
//getter & setter
}
In your Fragment put a subscriber. Add :
#Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(Data data) {
//you can get List of objects by data.getObjects();
};
Register and unregister your subscriber (in the Fragment)
#Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
#Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
In your Activity post the event when data is ready
Data data = new Data();
data.setObject(objects);
EventBus.getDefault().post(data);
Do not forget to add on grade dependencies :
implementation 'org.greenrobot:eventbus:3.1.1'
More information here
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 need to transmit data from my activity layer to a view (or at least its fragment) that is not a child of AdapterView.
For a ListView, I could do this very easily with its adapter, but I am stuck on how to reproduce this behavior for a non AdapterView widget (for clarity, let's say a TextView).
I don't want to keep a reference to my fragment (or worse, the view) at Activity level.
Any ideas ?
One way to do this is to use java.util.Observable/Observer :
import java.util.Observable;
import java.util.Observer;
public class MyTextView extends View implements Observer{
#Override
public void update(Observable observable, Object data) {
this.setText((String)data);
}
}
Then, you need an Observable class :
import java.util.Observable;
public class MyObservable extends Observable {
public void setText(String text){
notifyObservers(text);
}
}
Activity :
public class MainActivity extends Activity {
TextView tv;
#Override
public void onCreate(Bundle savedInstanceState) {
...
MyObservable mtv = new MyTextView(getApplicationContext());
MyTextViewModel mm = new MyTextViewModel(10);
mm.addObserver(mtv);
mm.setText("test");
// demonstrated in an activity to shorten the sample, but ideally you would
// keep the observer at activity level and manage the view in the fragment
}
}
------------------------------------------------
Another way to do this is through android.database.DataSetObservable to implement a more traditional Adapter like object :
public class CustomAdapter extends DataSetObservable {
String mText;
public String getText() {
return mText;
}
public void setText(String text) {
mText = text;
}
}
You manipulate it like any other adapter at Activity level :
public class MyActivity extends Activity {
private CustomAdapter mCustomAdapter;
#Override
protected void onCreate() {
...
mCustomAdapter = new CustomAdapter();
}
private void initializeFragment (Fragment fragment) {
// this or whatever method you use to setup your fragments
((MyFragment) fragment).setCustomAdapter(mCustomAdapter);
}
private void onDataLoaded (Stg data) {
// callback method invoked when the network thread has finished loading data
mCustomAdapter.setText(data.text);
mCustomAdapter.notifyChanged();
}
Finally, the only thing missing is the link between your fragment and the view :
public class MyFragment extends Fragment {
private CustomAdapter mCustomAdapter;
public setCustomAdapter(CustomAdapter adapter) {
// this method allows to setup the adapter at startup
mCustomAdapter = adapter;
}
protected DataSetObserver mMyViewObserver = new MyObserver();
private class MyObserver extends DataSetObserver {
#Override
public void onChanged() {
mUpdateHandler.sendEmptyMessage(0);
}
}
private Handler mUpdateHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
updateMyView();
}
};
private void updateMyView() {
if (mView == null) {
return;
}
mView.setMainTextViewText(mCustomAdapter.getText());
}
}
And here you have it. Each time you call notifyChanged(), your observer gets called. In return, he invokes the handler that update the view.
Here you have it, leak free, thread safe custom adapter for any kind of view.