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);
}
}
My project use MVP architecture and RxJava to get data from a remote JSON Api.
I have a MainActivity, it has 2 roles. The first one is to be a fragment container, the second one is to get data from the JSON api and transmit it to my fragment (I only have one fragment for now but will have another one later using the same data).
For now, I'm getting the data in my MainActivity. I'm trying to get the data from my fragment by calling a method in my MainActivity (using an interface for decoupling).
The problem is the data in my fragment is always empty, I suppose it's because my activity inflate my fragment so fast that when my fragment calls my activity method to get the data this data is still empty since the request didn't receive the answer yet and this request is called asynchronously using RxJava.
So I want to wait for the data being loaded to open my fragment,or open my fragment and wait the data being loaded in the activity before get it (showing a visual progress to the user). The problem is not really how to do this but when and where. Thank you for your help.
I moved my loadData() method and the transaction to open my fragment several times in different positions in the lifecycle, nothing worked. For now everything is in in MainActivity.onStart() :
#Override
protected void onStart() {
super.onStart();
presenter.setView(this);
// Load data from JSON API
presenter.loadData(city, authToken);
// Load fragments
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
fm.beginTransaction()
.add(R.id.ll_container, fragment)
.commit();
}
}
The data is retrieve in the loadData() method of my presenter :
public class MainPresenter implements MainActivityMVP.Presenter {
final static String TAG = MainPresenter.class.getCanonicalName();
private MainActivityMVP.View view;
private MainActivityMVP.Model model;
private Subscription subscription = null;
public MainPresenter(MainActivityMVP.Model model) { this.model = model; }
#Override
public void loadData(String city, String authToken) {
subscription = model.result(city, authToken)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Aqicn>() {
#Override
public void onCompleted() {
Log.i(TAG,"completed");
}
#Override
public void onError(Throwable e) {
e.printStackTrace();
}
#Override
public void onNext(Aqicn aqicn) {
Data data = aqicn.getData();
Iaqi iaqi = data.getIaqi();
ViewModel viewModel = new ViewModel(data.getAqi(),
data.getDominentpol(),
iaqi.getCo().getV(),
iaqi.getH().getV(),
iaqi.getNo2().getV(),
iaqi.getO3().getV(),
iaqi.getP().getV(),
iaqi.getPm10().getV(),
iaqi.getPm25().getV(),
iaqi.getR().getV(),
iaqi.getSo2().getV(),
iaqi.getT().getV(),
iaqi.getW().getV());
Log.d(TAG,data.getCity().getName());
if (view != null) {
view.updateData(viewModel);
}
}
});
}
#Override
public void rxUnsubscribe() {
if (subscription != null) {
if (!subscription.isUnsubscribed()) {
subscription.unsubscribe();
}
}
}
#Override
public void setView(MainActivityMVP.View view) {
this.view = view;
}
}
When the response to the request is received the presenter call the updateData() method in MainActivity (see in my presenter code above). This is where I initialize the ArrayList pollutionLevels that is supposed to contain the data I try to get from my fragment :
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
pollutionLevels = viewModel.getAllPolluants();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
}
This is the method in my MainActivity called from my fragment to get data :
#Override
public ArrayList<PollutionLevel> getPollutionLevels() {
return pollutionLevels;
}
In my fragment I try to get the data in onAttach() but it's always empty :
public interface PollutionLevelsListener{
ArrayList<PollutionLevel> getPollutionLevels();
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
try {
pollutionLevelsListener = (PollutionLevelsListener) context;
ArrayList<PollutionLevel> levels = pollutionLevelsListener.getPollutionLevels();
for(PollutionLevel l:levels) {
Log.d(TAG,l.getName());
}
} catch (ClassCastException castException){
castException.printStackTrace();
}
}
EDIT : add ViewModel.getAllPolluants() method
This is the method in my ViewModel that returns the ArrayList :
public ArrayList<PollutionLevel> getAllPolluants() {
ArrayList<PollutionLevel> allLevels = new ArrayList();
allLevels.add(new PollutionLevel("Co",Double.toString(co)));
allLevels.add(new PollutionLevel("H",Double.toString(h)));
allLevels.add(new PollutionLevel("No2",Double.toString(no2)));
allLevels.add(new PollutionLevel("o3",Double.toString(o3)));
allLevels.add(new PollutionLevel("p",Double.toString(p)));
allLevels.add(new PollutionLevel("o3",Double.toString(o3)));
allLevels.add(new PollutionLevel("pm10",Integer.toString(pm10)));
allLevels.add(new PollutionLevel("pm25",Integer.toString(pm25)));
allLevels.add(new PollutionLevel("r",Double.toString(r)));
allLevels.add(new PollutionLevel("so2",Double.toString(so2)));
allLevels.add(new PollutionLevel("t",Double.toString(t)));
allLevels.add(new PollutionLevel("w",Double.toString(w)));
return allLevels;
}
EDIT : Add new modified MainActivity class and PollutionLevelListener interface, trying to apply #cricket_007 answer
public class MainActivity extends AppCompatActivity implements MainActivityMVP.View, PollutionLevelsListener {
final static String TAG = MainActivity.class.getCanonicalName();
#BindString(R.string.city)
String city;
#BindString(R.string.aqicn_token)
String authToken;
#Inject
MainActivityMVP.Presenter presenter;
ArrayList<PollutionLevel> pollutionLevels;
PollutionLevelsListener pollutionListener;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
((App) getApplication()).getComponent().injectPollutionLevels(this);
}
#Override
public void updateData(ViewModel viewModel) {
pollutionLevels = viewModel.getAllPolluants();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
//===== NullPointerException
pollutionListener.onPollutionLevelsLoaded(pollutionLevels);
}
#Override
protected void onStart() {
super.onStart();
presenter.setView(this);
presenter.loadData(city, authToken);
}
#Override
public void onPollutionLevelsLoaded(List<PollutionLevel> levels) {
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
};
// Load fragments
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
fm.beginTransaction()
.add(R.id.ll_container, fragment)
.commit();
}
}
#Override
protected void onDestroy() {
super.onDestroy();
presenter.rxUnsubscribe();
}
}
Interface
public interface PollutionLevelsListener {
void onPollutionLevelsLoaded(List<PollutionLevel> levels);
}
#################### EDIT ########################
After a lot of doubt with what solution to adopt I follow the answer and recommendations of #yosriz. This is the code I ended with. Be aware that I still need to implement a cache management feature as for now the JSON resquest is made for both fragment.
As a result I have a common repository used by my both fragment. The MainActivity became only a fragment container, it doesn't get any data. it doesn't even have a MVP structure since I think It's now useless.
My both fragment (so my both features) get their data from PollutionLevelRepository :
public interface Repository {
Observable<Aqicn> getPollutionLevelsFromNetwork(String city, String authToken);
Observable<Aqicn> getPollutionLevels(String city, String authToken);
}
public class PollutionLevelsRepository implements Repository {
private PollutionApiService pollutionApiService;
private static Observable<Aqicn> pollutionData = null;
public PollutionLevelsRepository(PollutionApiService pollutionApiService) {
this.pollutionApiService = pollutionApiService;
}
#Override
public Observable<Aqicn> getPollutionLevelsFromNetwork(String city, String authToken) {
pollutionData = pollutionApiService.getPollutionObservable(city, authToken);
return pollutionData;
}
#Override
public Observable<Aqicn> getPollutionLevels(String city, String authToken) {
return getPollutionLevelsFromNetwork(city, authToken);
}
}
The Model of my first fragment (Donut feature) :
public class DonutModel implements DonutFragmentMVP.Model {
final static String TAG = DonutModel.class.getSimpleName();
private Repository repository;
public DonutModel(Repository repository) {
this.repository = repository;
}
#Override
public Observable<Aqicn> getPollutionLevels(String city, String authToken) {
Observable<Aqicn> aqicnObservable = repository.getPollutionLevels(city, authToken);
return aqicnObservable;
}
}
The Model of my second fragment (Pollution level feature) :
public class PollutionLevelsModel implements PollutionLevelsFragmentMVP.Model {
private Repository repository;
public PollutionLevelsModel(Repository repository) {
this.repository = repository;
}
#Override
public Observable<Aqicn> result(String city, String authToken) {
Observable<Aqicn> aqicnObservable = repository.getPollutionLevels(city, authToken);
return aqicnObservable;
}
}
Well, you probably have timing issue, the model.result is async IO operation that will update data on activity in async fashion when it will finish, while your fragment call to get the data is happening as soon as the fragment attached the activity (which is still async as you call fragment commit() and not commitNow()) but if you compare it to the probably network call of model.result it will be probably always faster.
Actually I think your approach is wrong, when you're using reactive fashion with Rx you should push the data, here at the end, you're pulling it at the fragment side from the Activity, while you don't know if this data is already available.
The data that is loaded from the presenter should update immediately the fragment, meaning either your Activity.updateData() will update the fragment, or more correct approach to my opinion is that the presenter will be tied to the fragment itself as this is the actual View it's updating, so the view.UpdateData() at the presenter will notify the fragment directly.
Did you tried to make an method inside the fragment and you can hit it once updateData(ViewModel viewModel) called ?
for example (try to add this method in you fragment):
public class YourFragmentName extends Fragment {
public YourFragmentName(StepsHandler stepsHandler){
this.stepsHandler = stepsHandler;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_supplier_registrtion_first, container, false);
return rootView;
}
public void dataLoaded() {
// Do what you need after data finish loading..
}
}
From your Activity :
public class MainActivity extends Activity implements StepsHandler {
YourFragmentName fragmentName;
//onCreate ()
fragmentName = new YourFragmentName(this);
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
pollutionLevels = viewModel.getAllPolluants();
fragmentName.dataLoaded();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
}
}
I'm trying to get the data from my fragment by calling a method in my MainActivity
It seems your interface is only returning the field, which could very possibly be before the request has finished. Which you seem to understand...
didn't receive the answer yet and this request is called asynchronously using RxJava
I wouldn't suggest you wait, and instead do
open my fragment and wait the data being loaded in the activity before get it (showing a visual progress to the user).
However you want to implement that, you can try a new ProgressDialog() and show / hide that.
Your issue is that onAttach gets immediately called and the request is still going on indefinitely.
You need to "subscribe" for that data from the Fragment.
A "listener" is not typically written to implement a "getter", so let's rewrite that
public interface PollutionLevelsListener {
void onPollutionLevelsLoaded(List<PollutionLevel> levels);
}
Then, you can use that instead to start your Fragment rather than immediately when the Activity starts
// The Activity
class ... implements PollutionLevelsListener {
#Override
public void onPollutionLevelsLoaded(List<PollutionLevel> levels) {
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
};
// Moved this section here
// Load fragments
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
// If your object is Parcelable
/*
* Bundle args = new Bundle();
* args.putParcelableArrayList(levels);
* fragment.setArguments(args);
*/
ft.add(R.id.ll_container, fragment).commit();
}
}
And now that you have that method,
the presenter call the updateData() method in MainActivity
Well, there's where the list comes from, so just pass it to that new method where the Fragment is then loaded
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
if (pollutionLevels == null) {
pollutionsLevels = new ArrayList<>();
}
pollutionLevels.clear();
pollutionLevels.addAll(viewModel.getAllPolluants());
this.onPollutionLevelsLoaded(pollutionsLevels);
}
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);
}
}
Sorry for my English
I started to learn how to use TDD in Android Development.
I need to test a Fragment in isolation. I used this tutorial, but I got exception when I tried to commit transaction (to add fragment in activity) in test. I found solution - using transaction.commitAllowingStateLoss() instead of transaction.commit(). But I'm not sure that this solution will always work correctly.
So, now I use something like this:
in application package (not test package) I have package helper_to_test_fragments with next classes:
FragmentBuilder.java
public interface FragmentBuilder {
Fragment build();
}
CurrentTestedFragmentBuilder.java
public class CurrentTestedFragmentBuilder {
private static class DummyFragmentBuilder implements FragmentBuilder {
#Override
public Fragment build() {
Fragment dummyFragment = new Fragment() {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// inflate empty FrameLayout
return inflater.inflate(R.layout.fragment_dummy, container, false);
}
};
return dummyFragment;
}
}
private static FragmentBuilder currentFragmentBuilder = new DummyFragmentBuilder();
public static void setCurrentBuilder(FragmentBuilder fragmentBuilder) {
if (fragmentBuilder == null) {
throw new IllegalArgumentException("fragmentBuilder should be not null");
}
currentFragmentBuilder = fragmentBuilder;
}
public static Fragment build() {
return currentFragmentBuilder.build();
}
}
ActivityHelperToTestFragments.java
// This activity declared in application Manifest.xml file
public class ActivityHelperToTestFragments extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_helper_to_test_fragments);
if (savedInstanceState == null) {
Fragment currentTestedFragment = CurrentTestedFragmentBuilder.build();
getFragmentManager()
.beginTransaction()
.add(R.id.containerOfCurrentTestedFragment, currentTestedFragment)
.commit();
}
}
public Fragment getTestedFragment() {
return getFragmentManager().findFragmentById(R.id.containerOfCurrentTestedFragment);
}
}
activity_helper_to_test_fragments.xml - layout for helper activity above
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/containerOfCurrentTestedFragment" />
Using:
public class TestSomeFragment
extends ActivityInstrumentationTestCase2<ActivityHelperToTestFragments> {
private ActivityHelperToTestFragments activity;
private Fragment testedFragment;
public TestSomeFragment() {
super(ActivityHelperToTestFragments.class);
}
#Override
protected void setUp() throws Exception {
super.setUp();
CurrentTestedFragmentBuilder.setCurrentBuilder(new FragmentBuilder() {
#Override
public Fragment build() {
return new SomeFragment();
}
});
activity = getActivity();
getInstrumentation().waitForIdleSync();
testedFragment = activity.getTestedFragment();
}
public void testPreconditions() {
assertNotNull(activity);
}
public void testFragment() {
assertNotNull(testedFragment);
}
}
What are the drawbacks to this approach? Is this approach correct? How easy it is, in your opinion?
I use static methods for setting the current tested fragment. But I use this solution for testing fragments in isolation.
Please, suggest solution that is better than this.
Question: How does one create a callback from a DialogFragment to another Fragment. In my case, the Activity involved should be completely unaware of the DialogFragment.
Consider I have
public class MyFragment extends Fragment implements OnClickListener
Then at some point I could do
DialogFragment dialogFrag = MyDialogFragment.newInstance(this);
dialogFrag.show(getFragmentManager, null);
Where MyDialogFragment looks like
protected OnClickListener listener;
public static DialogFragment newInstance(OnClickListener listener) {
DialogFragment fragment = new DialogFragment();
fragment.listener = listener;
return fragment;
}
But there is no guarantee that the listener will be around if the DialogFragment pauses and resumes through its lifecycle. The only guarantees in a Fragment are those passed in through a Bundle via setArguments and getArguments.
There is a way to reference the activity if it should be the listener:
public Dialog onCreateDialog(Bundle bundle) {
OnClickListener listener = (OnClickListener) getActivity();
....
return new AlertDialog.Builder(getActivity())
........
.setAdapter(adapter, listener)
.create();
}
But I don't want the Activity to listen for events, I need a Fragment. Really, it could be any Java object that implements OnClickListener.
Consider the concrete example of a Fragment that presents an AlertDialog via DialogFragment. It has Yes/No buttons. How can I send these button presses back to the Fragment that created it?
Activity involved is completely unaware of the DialogFragment.
Fragment class:
public class MyFragment extends Fragment {
int mStackLevel = 0;
public static final int DIALOG_FRAGMENT = 1;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mStackLevel = savedInstanceState.getInt("level");
}
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("level", mStackLevel);
}
void showDialog(int type) {
mStackLevel++;
FragmentTransaction ft = getActivity().getFragmentManager().beginTransaction();
Fragment prev = getActivity().getFragmentManager().findFragmentByTag("dialog");
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack(null);
switch (type) {
case DIALOG_FRAGMENT:
DialogFragment dialogFrag = MyDialogFragment.newInstance(123);
dialogFrag.setTargetFragment(this, DIALOG_FRAGMENT);
dialogFrag.show(getFragmentManager().beginTransaction(), "dialog");
break;
}
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode) {
case DIALOG_FRAGMENT:
if (resultCode == Activity.RESULT_OK) {
// After Ok code.
} else if (resultCode == Activity.RESULT_CANCELED){
// After Cancel code.
}
break;
}
}
}
}
DialogFragment class:
public class MyDialogFragment extends DialogFragment {
public static MyDialogFragment newInstance(int num){
MyDialogFragment dialogFragment = new MyDialogFragment();
Bundle bundle = new Bundle();
bundle.putInt("num", num);
dialogFragment.setArguments(bundle);
return dialogFragment;
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.ERROR)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(R.string.ok_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getActivity().getIntent());
}
}
)
.setNegativeButton(R.string.cancel_button, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_CANCELED, getActivity().getIntent());
}
})
.create();
}
}
TargetFragment solution doesn't seem the best option for dialog fragments because it may create IllegalStateException after application get destroyed and recreated. In this case FragmentManager couldn't find the target fragment and you will get an IllegalStateException with a message like this:
"Fragment no longer exists for key android:target_state: index 1"
It seems like Fragment#setTargetFragment() is not meant for communication between a child and parent Fragment, but rather for communication between sibling-Fragments.
So alternative way is to create dialog fragments like this by using the ChildFragmentManager of the parent fragment, rather then using the activities FragmentManager:
dialogFragment.show(ParentFragment.this.getChildFragmentManager(), "dialog_fragment");
And by using an Interface, in onCreate method of the DialogFragment you can get the parent fragment:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
callback = (Callback) getParentFragment();
} catch (ClassCastException e) {
throw new ClassCastException("Calling fragment must implement Callback interface");
}
}
Only thing left is to call your callback method after these steps.
For more information about the issue, you can check out the link:
https://code.google.com/p/android/issues/detail?id=54520
I followed this simple steps to do this stuff.
Create interface like DialogFragmentCallbackInterface with some method like callBackMethod(Object data). Which you would calling to pass data.
Now you can implement DialogFragmentCallbackInterface interface in your fragment like MyFragment implements DialogFragmentCallbackInterface
At time of DialogFragment creation set your invoking fragment MyFragment as target fragment who created DialogFragment use myDialogFragment.setTargetFragment(this, 0) check setTargetFragment (Fragment fragment, int requestCode)
MyDialogFragment dialogFrag = new MyDialogFragment();
dialogFrag.setTargetFragment(this, 1);
Get your target fragment object into your DialogFragment by calling getTargetFragment() and cast it to DialogFragmentCallbackInterface.Now you can use this interface to send data to your fragment.
DialogFragmentCallbackInterface callback =
(DialogFragmentCallbackInterface) getTargetFragment();
callback.callBackMethod(Object data);
That's it all done! just make sure you have implemented this interface in your fragment.
Maybe a bit late, but may help other people with the same question like I did.
You can use setTargetFragment on Dialog before showing, and in dialog you can call getTargetFragment to get the reference.
A recommended approach is to use the new Fragment Result API.
By using it, you do not need to override onAttach(context) nor setTargetFragment(), which is now deprecated.
1 - Add a result listener on parent Fragment's onCreate:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
childFragmentManager.setFragmentResultListener("requestKey", this) { key, bundle ->
val result = bundle.getString("bundleKey")
}
}
2- On child Fragment, set the result (on a button click listener, for instance):
button.setOnClickListener {
val result = "resultSample"
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
More info on the docs: https://developer.android.com/guide/fragments/communicate#fragment-result
Hope it helps!
The Communicating with Other Fragments guide says the Fragments should communicate through the associated Activity.
Often you will want one Fragment to communicate with another, for
example to change the content based on a user event. All
Fragment-to-Fragment communication is done through the associated
Activity. Two Fragments should never communicate directly.
You should define an interface in your fragment class and implement that interface in its parent activity. The details are outlined here http://developer.android.com/guide/components/fragments.html#EventCallbacks . The code would look similar to:
Fragment:
public static class FragmentA extends DialogFragment {
OnArticleSelectedListener mListener;
// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
}
Activity:
public class MyActivity extends Activity implements OnArticleSelectedListener{
...
#Override
public void onArticleSelected(Uri articleUri){
}
...
}
According to the official documentation:
Fragment#setTargetFragment
Optional target for this fragment. This may be used, for example, if this fragment is being started by another, and when done wants to give a result back to the first. The target set here is retained across instances via FragmentManager#putFragment.
Fragment#getTargetFragment
Return the target fragment set by setTargetFragment(Fragment, int).
So you can do this:
// In your fragment
public class MyFragment extends Fragment implements OnClickListener {
private void showDialog() {
DialogFragment dialogFrag = MyDialogFragment.newInstance(this);
// Add this
dialogFrag.setTargetFragment(this, 0);
dialogFrag.show(getFragmentManager, null);
}
...
}
// then
public class MyialogFragment extends DialogFragment {
#Override
public void onAttach(Context context) {
super.onAttach(context);
// Then get it
Fragment fragment = getTargetFragment();
if (fragment instanceof OnClickListener) {
listener = (OnClickListener) fragment;
} else {
throw new RuntimeException("you must implement OnClickListener");
}
}
...
}
Update: Please note that there are easier ways of doing this using view models which I can share if someone is interested.
Kotlin guys here we go!
So the problem we have is that we created an activity, MainActivity, on that activity we created a fragment, FragmentA and now we want to create a dialog fragment on top of FragmentA call it FragmentB. How do we get the results from FragmentB back to FragmentA without going through MainActivity?
Note:
FragmentA is a child fragment of MainActivity. To manage fragments created in FragmentA we will use childFragmentManager which does that!
FragmentA is a parent fragment of FragmentB, to access FragmentA from inside FragmentB we will use parenFragment.
Having said that, inside FragmentA,
class FragmentA : Fragment(), UpdateNameListener {
override fun onSave(name: String) {
toast("Running save with $name")
}
// call this function somewhere in a clickListener perhaps
private fun startUpdateNameDialog() {
FragmentB().show(childFragmentManager, "started name dialog")
}
}
Here is the dialog fragment FragmentB.
class FragmentB : DialogFragment() {
private lateinit var listener: UpdateNameListener
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = parentFragment as UpdateNameListener
} catch (e: ClassCastException) {
throw ClassCastException("$context must implement UpdateNameListener")
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
val binding = UpdateNameDialogFragmentBinding.inflate(LayoutInflater.from(context))
binding.btnSave.setOnClickListener {
val name = binding.name.text.toString()
listener.onSave(name)
dismiss()
}
builder.setView(binding.root)
return builder.create()
} ?: throw IllegalStateException("Activity can not be null")
}
}
Here is the interface linking the two.
interface UpdateNameListener {
fun onSave(name: String)
}
That's it.
The correct way of setting a listener to a fragment is by setting it when it is attached. The problem I had was that onAttachFragment() was never called. After some investigation I realised that I had been using getFragmentManager instead of getChildFragmentManager
Here is how I do it:
MyDialogFragment dialogFragment = MyDialogFragment.newInstance("title", "body");
dialogFragment.show(getChildFragmentManager(), "SOME_DIALOG");
Attach it in onAttachFragment:
#Override
public void onAttachFragment(Fragment childFragment) {
super.onAttachFragment(childFragment);
if (childFragment instanceof MyDialogFragment) {
MyDialogFragment dialog = (MyDialogFragment) childFragment;
dialog.setListener(new MyDialogFragment.Listener() {
#Override
public void buttonClicked() {
}
});
}
}
I was facing a similar problem. The solution that I found out was :
Declare an interface in your DialogFragment just like James McCracken has explained above.
Implement the interface in your activity (not fragment! That is not a good practice).
From the callback method in your activity, call a required public function in your fragment which does the job that you want to do.
Thus, it becomes a two-step process : DialogFragment -> Activity and then Activity -> Fragment
Full example how to use setFragmentResultListener:
Parent fragment MainFragment.kt:
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
class MainFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val showDialogButton = view.findViewById<Button>(R.id.dialog_button)
showDialogButton.setOnClickListener {
showMyDialog()
}
}
private fun showMyDialog() {
MyDialogFragment.showOn(this) { bundle ->
/*here handle bundle result*/
}
}
}
your dialog:
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
class MyDialogFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val submitButton = view.findViewById<Button>(R.id.submitButton)
submitButton.setOnClickListener {
parentFragment?.setFragmentResult(KEY_CALLBACK_BUNDLE, buildResultBundle())
}
}
private fun buildResultBundle(): Bundle {
val bundle = Bundle()
/*here build your result bundle for parent fragment*/
return bundle
}
companion object {
const val TAG: String = "MyDialogFragment"
private const val KEY_CALLBACK_BUNDLE: String = "KEY_CALLBACK_BUNDLE"
fun showOn(fragment: Fragment, callback: (Bundle) -> Unit) {
val dialog = MyDialogFragment()
fragment.setFragmentResultListener(KEY_CALLBACK_BUNDLE) { requestKey: String, bundle: Bundle ->
if (requestKey == KEY_CALLBACK_BUNDLE) {
callback(bundle)
}
}
dialog.show(fragment.childFragmentManager, TAG)
}
}
}
I am getting result to Fragment DashboardLiveWall(calling fragment) from Fragment LiveWallFilterFragment(receiving fragment) Like this...
LiveWallFilterFragment filterFragment = LiveWallFilterFragment.newInstance(DashboardLiveWall.this ,"");
getActivity().getSupportFragmentManager().beginTransaction().
add(R.id.frame_container, filterFragment).addToBackStack("").commit();
where
public static LiveWallFilterFragment newInstance(Fragment targetFragment,String anyDummyData) {
LiveWallFilterFragment fragment = new LiveWallFilterFragment();
Bundle args = new Bundle();
args.putString("dummyKey",anyDummyData);
fragment.setArguments(args);
if(targetFragment != null)
fragment.setTargetFragment(targetFragment, KeyConst.LIVE_WALL_FILTER_RESULT);
return fragment;
}
setResult back to calling fragment like
private void setResult(boolean flag) {
if (getTargetFragment() != null) {
Bundle bundle = new Bundle();
bundle.putBoolean("isWorkDone", flag);
Intent mIntent = new Intent();
mIntent.putExtras(bundle);
getTargetFragment().onActivityResult(getTargetRequestCode(),
Activity.RESULT_OK, mIntent);
}
}
onActivityResult
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
if (requestCode == KeyConst.LIVE_WALL_FILTER_RESULT) {
Bundle bundle = data.getExtras();
if (bundle != null) {
boolean isReset = bundle.getBoolean("isWorkDone");
if (isReset) {
} else {
}
}
}
}
}
Updated:
I made a library based on my gist code that generates those casting for you by using #CallbackFragment and #Callback.
https://github.com/zeroarst/callbackfragment.
And the example give you the example that send a callback from a fragment to another fragment.
Old answer:
I made a BaseCallbackFragment and annotation #FragmentCallback. It currently extends Fragment, you can change it to DialogFragment and will work. It checks the implementations with the following order: getTargetFragment() > getParentFragment() > context (activity).
Then you just need to extend it and declare your interfaces in your fragment and give it the annotation, and the base fragment will do the rest. The annotation also has a parameter mandatory for you to determine whether you want to force the fragment to implement the callback.
public class EchoFragment extends BaseCallbackFragment {
private FragmentInteractionListener mListener;
#FragmentCallback
public interface FragmentInteractionListener {
void onEcho(EchoFragment fragment, String echo);
}
}
https://gist.github.com/zeroarst/3b3f32092d58698a4568cdb0919c9a93
this is work for me
i think you can set callback in display method in your fragment,
**in my fragment**
val myDialogFragment=MyDialogFragment()
myDialogFragment.display(fragmentManager!!,this)
//my fragment implement CallbackDialogFragment so set this for display method
**in dialog fragment**
lateinit var callBackResult: CallbackDialogFragment
fun display(fragmentManager: FragmentManager, callback: CallbackDialogFragment) {
callBackResult = callback
show(fragmentManager,"dialogTag")
}
I solved this in an elegant way with RxAndroid. Receive an observer in the constructor of the DialogFragment and suscribe to observable and push the value when the callback being called. Then, in your Fragment create an inner class of the Observer, create an instance and pass it in the constructor of the DialogFragment. I used WeakReference in the observer to avoid memory leaks. Here is the code:
BaseDialogFragment.java
import java.lang.ref.WeakReference;
import io.reactivex.Observer;
public class BaseDialogFragment<O> extends DialogFragment {
protected WeakReference<Observer<O>> observerRef;
protected BaseDialogFragment(Observer<O> observer) {
this.observerRef = new WeakReference<>(observer);
}
protected Observer<O> getObserver() {
return observerRef.get();
}
}
DatePickerFragment.java
public class DatePickerFragment extends BaseDialogFragment<Integer>
implements DatePickerDialog.OnDateSetListener {
public DatePickerFragment(Observer<Integer> observer) {
super(observer);
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the current date as the default date in the picker
final Calendar c = Calendar.getInstance();
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int day = c.get(Calendar.DAY_OF_MONTH);
// Create a new instance of DatePickerDialog and return it
return new DatePickerDialog(getActivity(), this, year, month, day);
}
#Override
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
if (getObserver() != null) {
Observable.just(month).subscribe(getObserver());
}
}
}
MyFragment.java
//Show the dialog fragment when the button is clicked
#OnClick(R.id.btn_date)
void onDateClick() {
DialogFragment newFragment = new DatePickerFragment(new OnDateSelectedObserver());
newFragment.show(getFragmentManager(), "datePicker");
}
//Observer inner class
private class OnDateSelectedObserver implements Observer<Integer> {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(Integer integer) {
//Here you invoke the logic
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
}
You can see the source code here: https://github.com/andresuarezz26/carpoolingapp
More improved way is to just use newInstanceand interface.
Here is a Fragment that needs DailogFragment
public class Fragment extends Fragment implements returnPinInterface {
....
....
public View onCreateView(#NotNull LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
// A simple call to show DialogFragment
btnProceed.setOnClickListener(v -> {
fragment = DailogFragment.newInstance(this);
fragment.show(getChildFragmentManager(),null );
fragment.setCancelable(false);
});
//Grab whatever user clicked/selected/ or typed
#Override
public void onPinReturn(String s) {
Log.d("ReturnedPin", s);
}
}
Here comes your DialogFragment
public class PinDialogFragment extends DialogFragment {
//Create a static variable to help you receive instance of fragment you
//passed
public static Fragment fragm;
// Create new Instance and grab the object passed in Fragment up
//there
public static PinDialogFragment newInstance(Fragment frag) {
PinDialogFragment fragment = new PinDialogFragment();
fragm = frag;
return fragment;
}
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_pin, container, false);
//
btn.setOnClickListener(btn ->
listener.onReturnPin("1234"));
return v;
}
//Use the Fragm to instantiate your Interface
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (fragm instanceof ReturnPinInterface) {
listener = (ReturnPinInterface) fragm;
} else {
throw new RuntimeException("you must implement ReturnPinInterface");
}
}
}
Enjoy!
Using onAttach is advised in official Android documentation. So, we can make use of that method.
Make sure that your parent fragment implements a listener e.g. OnPopupButtonClickListener:
public interface OnPopupButtonClickListener {
void onPositiveButtonClicked();
void onNegativeButtonClicked();
}
In your parent fragment show your DialogFragment instance using getChildFragmentManager():
PopupDialogFragment dialogFragment = new PopupDialogFragment();
dialogFragment.show(getChildFragmentManager(), "PopupDialogFragment");
In your dialog class which extends DialogFragment instance add this method:
(Notice that we are retrieving our parent fragment via getParentFragment() which implements our custom listener interface OnPopupButtonClickListener
#Override
public void onAttach(#NonNull #NotNull Context context) {
super.onAttach(context);
// Verify that the host activity implements the callback interface
try {
listener = (OnPopupButtonClickListener) getParentFragment();
} catch (ClassCastException e) {
// The activity doesn't implement the interface, throw exception
throw new ClassCastException(getActivity().toString()
+ " must implement OnPopupButtonClickListener");
}
}
In your dialog you can then use your listener whenever it is needed in your DialogFragment, for example:
Button positiveButton = view.findViewById(R.id.positiveButton);
positiveButton.setOnClickListener(v -> {
if (listener != null) {
listener.onPositiveButtonClicked();
getDialog().dismiss();
}
});