I have an Activity that calls setContentView with this XML:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal"
>
<fragment android:name="org.vt.indiatab.GroupFragment"
android:id="#+id/home_groups"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1" />
<..some other fragments ...>
</LinearLayout>
The GroupFragment extends Fragment, and all is well there. However, I show a DialogFragment from within GroupFragment. This shows correctly, HOWEVER when the screen rotates, I get a Force Close.
What's the proper way to display a DialogFragment from within another Fragment other than DialogFragment.show(FragmentManager, String)?
There's a bug in the compatibility library that can cause this. Try putting this in you dialogfragment:
#Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance())
getDialog().setOnDismissListener(null);
super.onDestroyView();
}
I also suggest setting your dialogfragment as retained, so it won't get dismissed after the rotation. Put "setRetainInstance(true);" e.g. in the onCreate() method.
OK, while Zsombor's method works, this is due to me being inexperienced with Fragments and his solution causes issues with the saveInstanceState Bundle.
Apparently (at least for a DialogFragment), it should be a public static class. You also MUST write your own static DialogFragment newInstance() method. This is because the Fragment class calls the newInstance method in its instantiate() method.
So in conclusion, you MUST write your DialogFragments like so:
public static class MyDialogFragment extends DialogFragment {
static MyDialogFragment newInstance() {
MyDialogFragment d = new MyDialogFragment();
return d;
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
...
}
}
And show them with:
private void showMyDialog() {
MyDialogFragment d = MyDialogFragment.newInstance();
d.show(getFragmentManager(), "dialog");
}
This may be unique to the ActionBarSherlock Library, but the official samples in the SDK documentation use this paradigm also.
To overcome the Bundle always being null, I save it to a static field in onSaveInstanceState. It's a code smell, but the only solution I found for both restoring the dialog and saving the state.
The Bundle reference should be nulled in onDestroy.
#Override
public void onCreate(Bundle savedInstanceState)
{
if (savedInstanceState == null)
savedInstanceState = HackishSavedState.savedInstanceState;
setRetainInstance(true);
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
if (savedInstanceState == null)
savedInstanceState = HackishSavedState.savedInstanceState;
...
}
#Override
public void onDestroyView() // necessary for restoring the dialog
{
if (getDialog() != null && getRetainInstance())
getDialog().setOnDismissListener(null);
super.onDestroyView();
}
#Override
public void onSaveInstanceState(Bundle outState)
{
...
HackishSavedState.savedInstanceState = outState;
super.onSaveInstanceState(outState);
}
#Override
public void onDestroy()
{
HackishSavedState.savedInstanceState = null;
super.onDestroy();
}
private static class HackishSavedState
{
static Bundle savedInstanceState;
}
I used a mix of the presented solutions and added one more thing.
This is my final solution:
I used setRetainInstance(true) in the onCreateDialog;
I used this:
public void onDestroyView() {
if (getDialog() != null && getRetainInstance())
getDialog().setDismissMessage(null);
super.onDestroyView();
}
And as a workaround of the savedInstanceState not working, I created a private class called StateHolder (the same way a holder is create for a listView):
private class StateHolder {
String name;
String quantity;
}
I save the state this way:
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
stateHolder = new StateHolder();
stateHolder.name = actvProductName.getText().toString();
stateHolder.quantity = etProductQuantity.getText().toString();
}
In the onDismiss method I set the stateHolder back to null. When the dialog is created, it verifies if the stateHolder isn't null to recover the state or just initialize everything normally.
I solved this issue with answers of #ZsomborErdődy-Nagy and #AndyDennie . You must subclass this class and in you parent fragment call setRetainInstance(true), and dialogFragment.show(getFragmentManager(), "Dialog");
public class AbstractDialogFragment extends DialogFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance())
getDialog().setDismissMessage(null);
super.onDestroyView();
}
}
I had a similar issue, however none of the above worked for me. In the end I needed to create the fragment in code instead of in the XML layout.
See: Replacing fragments and orientation change
I ran into this on my project and none of the above solutions helped.
If the exception looks something like
java.lang.RuntimeException: Unable to start activity ComponentInfo{
...
Caused by: java.lang.IllegalStateException: Fragment....
did not create a view.
It's caused by an issue with a fallback container Id that gets used after rotation. See this ticket for more details:
https://code.google.com/p/android/issues/detail?id=18529
Basically you can prevent the crash by making sure all of your xml fragments have a tag defined in the layout. This prevents the fallback condition from occurring if you rotate when a fragment is visible.
In my case I was able to apply this fix without having to override onDestroyView() or setRetainInstance(true), which is the common recommendation for this situation.
I encountered this problem and the onDestroyView() trick wasn't working. It turned out that it was because I was doing some rather intensive dialog creation in onCreate(). This included saving a reference to the AlertDialog, which I would then return in onCreateDialog().
When I moved all of this code to onCreateDialog() and stopped retaining a reference to the dialog, it started working again. I expect I was violating one of the invariants DialogFragment has about managing its dialog.
In onCreate() call setRetainInstance(true) and then include this:
#Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance()) {
getDialog().setOnDismissMessage(null);
}
super.onDestroyView();
}
When you call setRetainInstance(true) in onCreate(), onCreate() will no longer be called across orientation changes, but onCreateView() will still be called.
So you can still save the state to your bundle in onSaveInstanceState() and then retrieve it in onCreateView():
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("myInt", myInt);
}
#Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.my_layout, container);
if (savedInstanceState != null) {
myInt = savedInstanceState.getInt("myInt");
}
...
return view;
}
Related
So I was following Clean Architecture to design my application. I have an Activity with a view pager which has two Fragment in it. Im injecting the PagerAdapter for this through Dagger.
I understand that calling setRetainInstance(true) on a fragment prevents it from getting destroyed, and that getActivity() on such fragment may return a problem if the Activity is destroyed. I'm getting a NullPointException when trying to resume my activity after it has been on background and the activity has been (presumably) destroyed.
So my question is
Is there a better way to accomplish what I'm trying to do?
Any resource someone can point me to?
Also uncertain why the Fragment and the Adapter is still active if the fragment has been destroyed. I get no memory leaks with LeakCanary.
My activity has an Dagger Component MainActivityComponent which is injected as follows. And also extends HasComponent. For more info on this refer to HasComponent
MainActivity.java
DaggerMainActivityComponent.builder()
.applicationComponent(getApplicationComponent())
.activityModule(getActivityModule())
// Module for each fragment
.conversationListModule(new ConversationListModule(this))
.friendsListModule(new FriendsListModule(this))
.build()
.inject(this);
Getting the Activity's component
// Cause of the NullPointException getActivity()
protected <C> C getComponent(Class<C> componentType) {
return componentType.cast(((HasComponent<C>) getActivity()).getComponent());
}
Let me know if you guys have any confusion. I know my explanation is a mess. Thanks
Update
Seems even if I remove setRetainInstance(true) this error isn't prevented.
Problem
The problem occured when the application stays in the background for a while and a recently displayed activity is recycled by Android. Bringing back the application causes NullPointerException (NPE) as the activity’s component is called by the fragment before it is initialized (both fragment and activity are recreated at the same time).
Solution
Introducing two lifecycle methods in BaseFragment class.
onInjectView() — called to do an optional injection on onCreate(Bundle) and if an exception is thrown or false returned, it is called on onActivityCreated(Bundle) again. Within this method you can get the injection component and inject the view. Retrun true if the injection was succesfull, then it will not be called again.
Based on returned value, the second method is called. The method is named onViewInjected(Bundle), as it is called only when the fragment has been injected and injected fields can be initialized.
Base Fragment
public abstract class BaseFragment extends Fragment {
private boolean mIsInjected = false;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
try {
mIsInjected = onInjectView();
} catch (IllegalStateException e) {
Log.e(e.getClass().getSimpleName(), e.getMessage());
mIsInjected = false;
}
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (mIsInjected) onViewInjected(savedInstanceState);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (!mIsInjected) {
mIsInjected = onInjectView();
if (mIsInjected) onViewInjected(savedInstanceState);
}
}
#SuppressWarnings("unchecked")
protected <C> C getComponent(Class<C> componentType) throws IllegalStateException {
C component = componentType.cast(((HasComponent<C>) getActivity()).getComponent());
if (component == null) {
throw new IllegalStateException(componentType.getSimpleName() + " has not been initialized yet.");
}
return component;
}
protected boolean onInjectView() throws IllegalStateException {
// Return false by default.
return false;
}
#CallSuper
protected void onViewInjected(Bundle savedInstanceState) {
// Intentionally left empty.
}
}
Usage
public class SampleFragment extends BaseFragment implements SampleView {
#Inject
SamplePresenter mSamplePresenter;
#Override
protected boolean onInjectView() throws IllegalStateException {
getComponent(SampleComponent.class).inject(this);
return true;
}
#Override
protected void onViewInjected(Bundle savedInstanceState) {
super.onViewInjected(savedInstanceState);
this.mSamplePresenter.setView(this);
}
}
More info can be found on Efficient and bug-free fragment injection in Android MVP applications
Here is a re-occurring problem that I haven't found a good solution for in the past.
My application is based on a single activity that has multiple child fragments.
What i want to do:
In some of my fragments, I want to take a picture with the phones own camera-app and both show the image for the user and then upload it to my server.
What i do now
Now, i am calling StartActivityForResult with my camera intent which works fine. Then i receive what i need from onActivityResult and are able to show the taken image in an image view and also send it to my server.
The problem
Some times when my onActivityResult is called. My fragment has been uninitiated or just flushed from memory by the OS (As i understand it).
This means that variables now has null-references.
What i have read from similar issues is that OnCreateView() is supposedly to be called before OnActivityResult().
So what I am trying to do here is to save the fragments state to its Arguments in my onDestroyView() and onSaveInstanceState() and then try to restore variables such as the temporary Camera Image FilePath. Here however, the fragment seems to initiate the Fragment with a new Bundle and not the one i've created for it, and causes my app to crash due to my camera file is null.
This is also hard to test as this just happens some times at random.
Code
saveState() is called from onDestroyView() and onSaveInstanceState()
#Override
protected Bundle saveState() {
Bundle state = new Bundle();
state.putSerializable("tempCameraFile", tempCameraFile);
return state;
}
restoreStates() is called in the end by onCreateView()
private void restoreStates(){
tempCameraFile = (File)savedState.getSerializable("tempCameraFile");
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 7777 && resultCode == Activity.RESULT_OK) {
setPendingImage(tempCameraFile);
}
}
private void setPendingImage(File imageFile){
try {
Bitmap bitmap = PhotoUtils.decodeFile(imageFile.getPath(), Utils.convertDpToPixel(40, mActivity));
if(bitmap != null) {
buttonImageChooser.setImageBitmap(bitmap);
}
} catch(NullPointerException npe){
npe.printStackTrace();
Log.d(getClass().getSimpleName(), "imageFile NULLPOINTER!!!! WHYYYY!?");
}
}
#Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
saveStateToArguments();
}
#Override
public void onDestroyView() {
super.onDestroyView();
saveStateToArguments();
}
private void saveStateToArguments() {
if (getView() != null)
savedState = saveState();
if (savedState != null) {
Bundle b = getArguments();
b.putBundle("savedState", savedState);
}
}
I really hope there is an obvious thing I am doing wrong when using fragments and that someone are able to help me out.
This has been a reoccurring problem that I have solved with a really ugly implementation of destroying and re-creating fragments from my Activity, but I now want to do this the right way.
The problem is you are not saving your state at all.
#Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
saveStateToArguments();
}
The bundle outState contains all values which will be saved, but your method
saveStateToArguments(); saves the values in another bundle.
The outState and your bundle are not related, so nothing will be saved.
Besides there is no need to call the saveStateToArguments(); in the onDestroyView 'cause the onSaveInstanceState will be called.
So simply change your code to the following:
#Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putSerializable("tempCameraFile", tempCameraFile);
}
And restore the state in the method onRestoreInstanceState
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null) {
tempCameraFile = (File) savedInstanceState.getSerializable("tempCameraFile");
}
}
Because the lifecycle is the following:
onCreate
onStart
onRestoreInstanceState
onActivityResult
onResume
See State of Activity while in onActivityResult question
Use activity to start the camera app and use activity's onActivityResult to make sure if the fragment exists. If it doesn't reinitialise the fragment.
getActivity().startActivityForResult(intent);
The problem is the system kill background activities on low memory.
Solution :
Save state of Activity or Fragment
/**
* Handled take camera photo on low memory maybe activity on background killed, need to save state.
*/
private Uri cameraMediaOutputUri;
#Override
public Uri getCameraMediaOutputUri() {
return cameraMediaOutputUri;
}
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
outState.putParcelable("cameraMediaOutputUri", cameraMediaOutputUri);
super.onSaveInstanceState(outState);
}
Restore the state and here is the trick as restoring state on Activity is different from Fragment.
Restoring Activity State
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState !=null && savedInstanceState.containsKey("cameraMediaOutputUri"))
cameraMediaOutputUri = savedInstanceState.getParcelable("cameraMediaOutputUri");
}
Restoring Fragment State Fragment Life Cycle
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState !=null && savedInstanceState.containsKey("cameraMediaOutputUri"))
cameraMediaOutputUri = savedInstanceState.getParcelable("cameraMediaOutputUri");
}
In my app I have an AppCompatActivity which has a support fragment. From this fragment I am showing a DialogFragment as follows
final MyDialogFragment completeDialogFragment = MyDialogFragment.newInstance(titleString,
messageString, DialogType.Ok);
completeDialogFragment.setDialogCallBack(new MyDialogFragment.DialogCallBacks() {
#Override
public void onPositive() {
// some code to execute when Ok is pressed
completeDialogFragment.dismiss();
}
#Override
public void onNegative() {
// not relevant
}
});
completeDialogFragment.setCancelable(false);
FragmentManager mgr = getChildFragmentManager();
completeDialogFragment.show(mgr, MY_TAG);
As you can see I am attaching a listener interface to listen to positive / negative button clicks from the dialog fragment. This listener works as expected but when the device gets rotated, it is not. So I wanted to retain or reset this listener whenever the device is rotated. As many people suggested on stackoverflow, I tried to do it the following way in my fragment
#Override
public void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
FragmentManager mgr = getChildFragmentManager();
final MyDialogFragment completeDialogFragment =
(MyDialogFragment) mgr.findFragmentByTag(MY_TAG);
if (completeDialogFragment != null) {
completeDialogFragment
.setDialogCallBack(new MyDialogFragment.DialogCallBacks() {
#Override
public void onPositive() {
// some code to execute when Ok is pressed
completeDialogFragment.dismiss();
}
#Override
public void onNegative() {
// not relevant
}
});
}
}
}
In the above code segment I am trying to find the dialog fragment by its tag and reset the listener but the variable completeDialogFragment is always null. I tried using getFragmentManager() and getActivity().getSupportFragmentManager() instead and it identifies the fragment but the dialog disappears from screen after rotation. Why the ChildFragmentManager is unable to identify the DialogFragment? Have anyone faced similar issue? Any help would be greatly appreciated.
If anyone is interested, the way I solved it was to use ChildFragmentManager and use it in onCreateView() instead of onCreate(). Strange...
I have 2 serializable objects that I want to retain after my fragment is rebuilt. Therefore I saved them during onSaveInstanceState (the objects are not null here):
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(RENDERER, renderer);
outState.putSerializable(SERIES, series);
}
In the onCreate method of the fragment I try to get them out of the Bundle:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
renderer = (DefaultRenderer) savedInstanceState.getSerializable(RENDERER);
series = (CategorySeries) savedInstanceState.getSerializable(SERIES);
}
}
The problem is that "renderer" and "series" are always null after calling getSerializable. Any ideas why?
Did you override onSaveInstanceState method in FragmentActivity that hosts this fragment? If you're, then make sure that it should call super.onSaveInstanceState(Bundle).
I have a fairly simple DialogFragment. It looks something like:
import android.support.v4.app.DialogFragment;
public class MyDialogFragment extends DialogFragment {
private String mData = "empty";
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(LOG_TAG, "onCreate");
setStyle(DialogFragment.STYLE_NO_TITLE, 0);
// setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.mydialog, container);
....
return view;
}
public setData(String _data) {
mData = _data;
}
}
I load this fragment like so from my FragmentActivity:
FragmentManager lFM = getSupportFragmentManager();
MyDialogFragment lDialog = new MyDialogFragment();
lDialog.setData("not empty");
lDialog.show(lFM, "MyDialog");
The code as above works fine. However I would like to retain the fragment on an orientation switch so that the mData field is preserved. If I add setRetainInstance(true); (and after sticking in some debug) I can see that the fragment is indeed retained on an orientation switch - onCreate() is not being called this time. I can see onCreateView() is being called and I return a correct View object, but the dialog is not shown on the screen. What am I missing?
After reading the answer that baboo gave me I implemented the solution as follows .. I hope this is correct (at least it works ok ...)
#Override
public void onCreate(Bundle savedInstanceState) {
// ....
FragmentManager lFM = getSupportFragmentManager();
if(lFM.findFragmentByTag("MyDialog")!=null)
((MyDialogFragment)lFM.findFragmentByTag("MyDialog")).show(lFM, "MyDialog");
// ....
}
Try the following logic in your fragment activity:
Use the put methods to store values in onSaveInstanceState():
protected void onSaveInstanceState(Bundle icicle) {
super.onSaveInstanceState(icicle);
icicle.putBoolean("dialogDisplayed", value); // set value = true when displayin dialog...
}
And restore the values in onCreate():
public void onCreate(Bundle icicle) {
if (icicle != null){
value = icicle.getBoolean("dialogDisplayed");
}
if(value)
//Display Dialog here....
}
The dialog fragment should be preserved automatically as long as you do the following:
If you call an Activity onSaveInstanceState(), make sure you call the super function!!!!. In my case, that was the key. Also make sure you do the same thing in the Fragment.
If you use setRetainInstance, you will need to manually store off the values and re-apply them. Otherwise, you should be able to not worry about it, in most cases. If you're doing something a bit more complicated, you might need to setRetainInstance(true), but otherwise ignore it. In my case, I needed to use it to store a random seed for one of my classes, but otherwise I was okay.
Some people have complained about a bug in the support library, where a dismiss message is sent when it shouldn't be. The latest support library seems to have fixed that, so you shouldn't need to worry about that.
You shouldn't need to do anything fancy like manually store off the fragment, it should be done automatically if you follow these steps.