DialogFragment with setRetainInstanceState(true) is not displayed after the device is rotated - android

I have a question regarding DialogFragment. I am trying to make a dialog that keeps it's state after the device is rotated. This dialog has a bunch of references to things such as adapters and other heavier objects and I need this to be kept upon rotation, if possible without having to make every reference Parcelable or Serializable in order for me to use onSaveInstanceState to save and restore them when the original activity is re-created.
I've noticed there's a method called setRetainInstance(boolean) on the DialogFragment which allows you to keep the dialog fragment instance when the activity is re-created. However, when I rotate the device now, the dialog is not showing anymore. I know I can get it from the activity's FragmentManager, but I cannot find a way to make it visible again. Any suggestions on this?
Thanks,
Mihai

There are few things you need to do :
use instance factory method to initiate a DialogFragment instance like this :
public static MyDialogFragment newInstance(MyModel model) {
MyDialogFragment myDialogFragment = new MyDialogFragment();
Bundle bundle = new Bundle();
bundle.putSerializable("MODEL", model);
myDialogFragment .setArguments(bundle);
return myDialogFragment;
}
by putting setRetainInstance(true) in onCreate, all of your references declared in the fragment will be kept after the original activity is re-created
#Override
public void onCreate(Bundle icicle) {
this.setCancelable(true);
setRetainInstance(true);
super.onCreate(icicle);
}
avoiding disappear on rotation by doing this
#Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance())
getDialog().setDismissMessage(null);
super.onDestroyView();
}
get your object by using
(MyModel) getArguments().getSerializable("MODEL")

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 need to manually store off the values. 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.
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.

Related

Memory leak with writeToParcel() called multiple times after onSaveInstanceState()

In some activity, I have to save MVC model as a parcelable. The parcelable is built according to the doc, which I've read more than enough (but who knows, I obviously could have missed something). There is a leak in this activity, but I'm struggling to understand its cause. The SO question Communication objects between multiple fragments in ViewPager was interesting but my code was already following guidelines from the answer.
The activity own a viewpager with around 60 fragments inside (but up to 350). The model is passed from the activity to all fragments, and user actions in the fragments are saved into the model.
Whenever I pause my activity, onSaveInstanceState is triggered once, and immediately after multiple triggers of my parcelable's writeToParcel method. The number of triggers depends on the number of Fragments ever loaded in the viewpager + 1. So at activity startup, if I turn the emulator off and back on, writeToParcel is called 3 times (only 1st and 2nd fragment are loaded), if I swipe once right and do it again, it is called 4 times (the 2nd is showing and the 3rd is loaded), if I setExtPosition() on the adapter and go to 10th fragment, writeToParcel is called 7 times (9th, 10th and 11h are loaded).
Of course if my user swipe every fragment, it will eventually get an ugly TransactionTooLargeException, which brings me here.
Here is some code. There may be a ton of code/concept improvements here, and any tips is very welcome, but my main problem is this dirty little leak I've found.
In my activity:
#Override
public void onSaveInstanceState (Bundle outState) {
outState.putParcelable("model", myParcelable);
super.onSaveInstanceState(outState);
}
In my fragment:
public static MyFragment newInstance(Model model) {
MyFragment fragment = new MyFragment();
Bundle args = new Bundle();
args.putParcelable(KEY_MODEL, model);
fragment.setArguments(args);
return fragment;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
mModel = args.getParcelable(KEY_MODEL);
}
In my parcelable model:
#Override
public void writeToParcel(Parcel dest, int flags) {
int startSize = dest.dataSize();
dest.writeString(foo); //one of these string is supposed to be null
dest.writeString(bar);
dest.writeString(foobar);
dest.writeByte((byte) (isMyObjectTrue ? 1 : 0));
dest.writeList(someStringList);
dest.writeList(someIntegerList);
dest.writeBundle(someBundle); //I use a Bundle to save a Map<String,String>
int endSize = dest.dataSize();
}
I ran the debugger inside the writeToParcel() method, and I was surprised to see that startSize is never 0. Is it normal ?
I searched throughout my code, and putParcelable() or any writing method with parcelable in its name is only called in this activity and in the fragment newInstance().
How can I find the cause of this weird exponential behaviour ?
PS: of course feel free to ask for more code.
EDIT
I've implemented the solution advised by #Ben P., and the problem have improved a lot, but is not totally solved. My activity implements an interface which now has a getModel() method called in onAttach(), and a setUserInput(char userInput) I use to update the model from the fragment. The fragment's newInstance() method don't save the model anymore.
MyFragment
#Override
public void onAttach(Context context) {
super.onAttach(context);
try {
callBack = (MyInterface) context; //callBack is a class field
mModel = callBack.getModel(); //mModel too
} catch (ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement MyInterface");
}
}
This turned the exponential problem into a linear problem which is obviously better but still a problem.
Now, writeToParcel() is only called once, but the total parcel size is still growing with the number of item loaded. My model takes around 3kb inside the parcel (+/-10% depending on the number of inputs), as measured by endSize-startSize.
How can I know where the growth comes from ?
Before I get into your problem specifically, I want to point out that the Bundle passed to setArguments() is part of the fragment's instance state. Every time a fragment is destroyed and recreated, these arguments need to be persisted. So anything you put into that Bundle has the potential to be parceled and unparceled during configuration changes.
The activity own a viewpager with around 60 fragments inside (but up to 350). The model is passed from the activity to all fragments, and user actions in the fragments are saved into the model.
This sounds like you have one single Model object that all fragments share. If this is the case, I recommend not passing the model object to each fragment as part of its arguments Bundle. Doing this will cause tremendous duplication when instance state is saved and restored. Instead, I'd expose a method in your Activity (something like getModel()) and then call that from your fragments to retrieve the model instance.
On the other hand, it also sounds like maybe you're only starting with the same Model object, and that each fragment can mutate it in some way. This would mean that you do have to save something to instance state for each fragment... but it's possible you can optimize here. Rather than saving and restoring the entire Model object, perhaps you could just store the diffs. That is, if fragment #1 changes the model's name, and fragment #2 changes the model's value, then you could have fragment #1 only save the new name and have fragment #2 only save the new value. Doing this instead of saving two extra copies of the model object could potentially amount to huge savings.

Android: How to make sure in any case getActivity in Fragment should not return null

I am not much experienced in Android Programming. And Currently I am working on an Application which makes use of Fragments. Sometimes i see that getActivity() returns null randomly. It happens once in thousand times or so. For now it's fine because there are no much users using it. But when app will go live may create many issues.
So my question is what is the best programming practice to make sure getActivity() never returns null in any case?
I searched online but everybody talks about solving issue in their existing code. If anyone has a generalised way of explaining it, then please let me know. Thanks you so much in advance
It's not random, it means that your Fragment is not currently attached to your Activity.
There is a ton of answer on that topic on SO, that I don't consider to be a good practice, is to override onAttach() and keep a reference to your Activity, like that :
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = activity;
}
I think you should not do that and just check if getActivity() doesn't return null.
If you just need a Context reference you could however to that :
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity.getApplicationContext();
}
And use you mContext which is the application Context.
PS:
If you run your application on a device with API 23, onAttach(Context) will be called. On previous APIs onAttach(Activity) will be called.
EDIT :
In that case you should use Loaders :
https://www.youtube.com/watch?v=s4eAtMHU5gI
http://developer.android.com/intl/es/guide/components/loaders.html
You should override onActivityCreated and call getActivity() from there, in this way you can be sure getActitity() never returns null, because called after parent activity onCreate has done.
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// call getActivity() here
}
Here is supporting quote from android doc(link):
public void onActivityCreated (Bundle savedInstanceState)
Called when the fragment's activity has been created and this fragment's view hierarchy instantiated. It can be used to do final initialization once these pieces are in place, such as retrieving views or restoring state. It is also useful for fragments that use setRetainInstance(boolean) to retain their instance, as this callback tells the fragment when it is fully associated with the new activity instance. This is called after onCreateView(LayoutInflater, ViewGroup, Bundle) and before onViewStateRestored(Bundle).

ViewPager Fragments getting destroyed while app is in background?

I can't for the life of me figure out why sometimes my Fragments become uncontrollable after my app has been launched, paused, and then resumed.
I hold references to my Fragments like this:
public class MainActivity ... {
public static AccountFragment accountFragment;
....
#Override
protected void onCreate( ... {
accountFragment = new AccountFragment(this);
...
}
I have a custom toolbar with a Spinner which theoretically should allow the user to cause an AsyncTask in the AccountFragment to run. This is implemented like so:
if (accountFragment.getView() != null) {
accountFragment.load()
}
I then create a ViewPager, a FragmentPagerAdapter, etc.
This works completely fine most of the time. The user can select a new item in the Spinner and the AccountFragment will update accordingly. However, sometimes after the app has been stopped and then later resumed, the AccountFragment will not respond at all to anything. Even other views in the app which should affect the AccountFragment are useless. It makes no sense to me because the AccountFragment's view cannot be null, and the AsyncTask which is executed by accountFragment.load() makes some very simple changes to the UI in its onPreExecute which should be very apparent (e.g. Everything disappears and a ProgressBar appears) which is simply not happening.
Any ideas?
There's not enough code to know exactly what's going wrong here, but there are also a bunch of stuff you're doing wrong.
1) Don't store fragment reference in a public static field. Make it just private, or protected at most. Read basic Java manuals for explanation.
2) Don't overload Fragment's constructor, it's discouraged by design. If you need to pass some values to it, do it by using arguments.
3) I guess you're not performing a check during onCreate() method in your Activity if it's being recreated or freshly created. You could end up with two instances of AccountFragment where one of them is in some weird detached state.
...
You should probably spend more time researching basics about Fragment/Activity relationship and how to avoid situations like this.
What you need is override these methods:
onSaveInstanceState(){}
onCreate (Bundle savedInstanceState){}
or onCreateView()
See http://developer.android.com/reference/android/app/Fragment.html#onSaveInstanceState(android.os.Bundle) . You put all the necessary parameters to bundle in onSaveInstanceState() and restore them in onCreate() or onCreateView().

How can getArguments() be called in onCreate() of a Fragment..?

Can someone explain how setArguments() works? I don't get it. I get how to use it, but I don't really understand how the code below can actually work:
public class MyDialogFragment extends DialogFragment{
public static MyDialogFragment newInstance(String test) {
MyDialogFragment f = new MyDialogFragment();
Bundle args = new Bundle();
args.putString("test", test);
f.setArguments(args);
return f;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("DEBUG", getArguments().getString("test")); // This actually works!
}
}
I would have thought that onCreate() got called before the call to setArguments(), but clearly it does not. Is this the way it is supposed to work, or is it just pure luck that the call to setArguments() gets executed before onCreate() on my particular device? Could the opposite happen under different circumstances (faster/slower/different device, etc.)?
Maybe someone can point me to an article that describes how the flow of events for this works? I don't see where onCreate() would get called in the code above, unless it is called asynchronously, which to me sounds like it would be risky to rely on getArguments() inside onCreate()...
Your newInstance() is a factory method you call yourself. It creates the Fragment object and sets the arguments on it.
The created fragment object is then passed to a fragment transaction which eventually makes the fragment lifecycle callbacks such as onCreate() to be invoked at appropriate times.
In case the framework needs to recreate your fragment e.g. due to an orientation change, it will use the no-arg constructor of the fragment and retain the arguments you have set on the object. newInstance() and such are useful when the fragment is created for the first time.
For documentation, the Fragment class documentation is a good starting point.
onCreate() is a method that is only called later by the framework, after the Fragment has been attached to the Activity, as you can see in the Fragment Lifecycle.

Fragment state lost when device left idle

Fragment losing state and shows an empty UI if left idle for 20 minutes. I'm using FragmentStatePagerAdapter and I have tried calling the notifyDataSetChanged in onStart() of my FragmentActivity.
Kindly help me how to save the object and state of my fragment and reuse it on reload of the app.
Android can kill your app if needed, you need to use onSaveInstanceState to keep your state in this cases. (Remember: Save important data in onPause!)
onSaveInstanceState exists in Activity and Fragments and is used in the same way like an activity
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("integer", anInteger);
}
Now in onCreate, onCreateView or onActivityCreated you have this argument BundlesavedInstanceState which corrisponds to the bundle saved. (Check if it's null too.)
If not enought maybe Android killed your FragmentManager too, so you need to override onSaveInstanceState and onRetoreInstanceState in your Activity and restore the fragment.
Maybe this answer could help you about the last thing i said: Using onSaveInstanceState with fragments in backstack?
A Fragment's life-cycle is closely tied to the Activity's lifecycle. This means, when your Activity goes idle; it will kill off any contained Fragments. To store Fragments you could always retain them in concordance with the Fragment API. This means you will generally be using the Fragment in a background. However the best way to keep a from being destroyed or lost from an Activity's end would be to store relevant information in a custom object and then to recreate the Fragment when the Activity is resumed.
For instance; I could have a custom object that would store relevent UI values for my Fragment and when my Activity either idles or changes I would save those relevant values to my custom object that I created. Then, when either a new Activity is created; or my old Activity is resumed; I would retrieve those values and put them back into my Fragment's UI. Hoped this helped :)
In case android needs memory, it kills the running apps. So you must save the objects using
public void onSaveInstanceState(Bundle savedState) {}
Note that savedState must be serializable.
You must call notifyDataSetChanged() in onResume(), because it ensures that it is called when the activity resumes.
For a detailed answer, please post your code.
Hard to answer without your code.
However I can say that the state is usually saved by the savedInstanceState
Usually in the onActivityCreated you have something like the following. In the example I give I save a boolean of either text was entered or not.
#Override
public void onActivityCreated(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mHasText = savedInstanceState.getBoolean(HAS_TEXT_TAG);
} else {
// do something
}
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(HAS_TEXT_TAG, mHasText);
}
it's just an example as without your code it's difficult to anwer

Categories

Resources