Android: using savedInstanceState with fragments - android

Have a question regarding the savedInstanceState bundle that you get on the fragment's callbacks when the fragment is first created and then reattached to the activity.
So I use the setRetainInstance(true), as a result the fragment shouldn't be destroyed but just unattached from the activity when the activity is destroyed and then reattached back when the activity is recreated on a configuration change for example.
So, because using this setRetainInstance(true) this will cause the savedInstanceState bundle to ALWAYS be null in the fragment's callbacks such as: onActivityCreated(), onCreate() etc.
So far so good, now I'm getting some crashes from some users (a really really small number) which is caused by this savedInstanceState not being null on the fragment.
So, the onActivityCreated() callback is doing something like this:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
adapter = new CustomAdapter(getActivity(), getListView(), data, savedInstanceState);
setListAdapter(adapter);
setupEmptyListView();
getListView().setOnScrollListener(this);
}
The CustomAdapter is calling at some point from its constructor a loadInstanceState() method which does its job, code below:
#Override
public void loadInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
//do some stuff which is causing a crash
}
}
Now my question is how is it possible to get into that if statement when the savedInstanceState that I'm passing should always null.
And now the QUESTION:
Is Android guaranteeing THAT WHEN USING setRetainInstance(true) on a fragment the savedInstanceState bundle which is passed to the callbacks will ALWAYS be null?
Thanks for answers guys!

I didn't find any mention that savedInstanceState bundle must be null in this case.
The official documentation says:
public void setRetainInstance (boolean retain)
Control whether a fragment instance is retained
across Activity re-creation (such as from a configuration change).
This can only be used with fragments not in the back stack. If set,
the fragment lifecycle will be slightly different when an activity is
recreated:
onDestroy() will not be called (but onDetach() still will be, because
the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being
re-created.
onAttach(Activity) and onActivityCreated(Bundle) will
still be called.
IMHO that's main difference - changed lifecycle.

Related

Difference between creating a new fragment and getting an existing one from the manager on rotation

It seems whether I set retainInstance to true or not, when I rotate the device, I get an existing fragment. The difference is that if I set it to true, I get "test = yes!", otherwise I get "test = no!" after rotating the device after clicking the test button to change test. That is, the member variable is kept, if I retain the instance.
But as I have said, even if I do not retain it, I get an existing fragment from the manager, anyway (always get "Reusing existing" on rotation). In that case, if all member variables are lost and the views of the fragment are recreated, what are kept? What is the point of getting an existing instance of the fragment?
In the activity's onCreate,
var frag = supportFragmentManager.findFragmentById(R.id.frame)
if(frag == null)
{
frag = Fragment1.newInstance("", "");
}
else
{
Log.d("sss", "Reusing exsiting");
}
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.frame, frag)
transaction.commit()
In the fragment,
var test = "no!";
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
{
super.onViewCreated(view, savedInstanceState)
Log.d("sss", "test = " + test);
testButton.setOnClickListener {
test = "yes!";
}
}
I have spent some hours trying to recreate your situation with different scenarios. First of all, I should point out that the life cycle of Fragments are in fact so complicated that during Google I/O 2018, one of the lead developers asked the audiences if onCreate() method of the Activity is invoked first or that of Fragment's. And the answer was that, it depends on the SDK version. But they are focusing more and more on Compatibility Libraries and advice developers to use these to have a universal experience across different devices, as well as enjoying the new APIs that will do the job of dealing with Fragments, so much easier for us.
While looking at the documentation of AppCompatActivity class, I realized that this behavior is their way of dealing with fragments.
Protected methods
void onCreate(Bundle savedInstanceState)
Perform initialization of all fragments.
void onDestroy()
Destroy all fragments.
void onPostCreate(Bundle savedInstanceState)
void onPostResume()
Dispatch onResume() to fragments.
void onSaveInstanceState(Bundle outState)
Save all appropriate fragment state.
void onStart()
Dispatch onStart() to all fragments.
void onStop()
Dispatch onStop() to all fragments.
As you can see they "save all appropriate fragment state" in onSaveInstanceState(); meaning that the states will be restored later on, after the Activity gets destroyed and recreated. So in onDestroy() all fragments get destroyed and when the Activity is created again, they get recreated as well. To make sure, you could override these methods inside both Fragment and Activity and check the result. If you do not check FragmentManager for already attached fragments, onCreate() method of the Fragment will be called twice, once directly by you -adding as a new Fragment- and once by the AppCompatActivity itself.
About the retainInstance = true, the documentation says that it keeps the member variables during configuration changes and will cause a slight difference in the life cycle of the Fragment.
setRetainInstance(true)
Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. If set, the fragment lifecycle will be slightly different when an activity is recreated:
onDestroy() will not be called (but onDetach() still will be, because the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being re-created.
onAttach(Activity) and onActivityCreated(Bundle) will still be called.

Can onViewCreated be called before Activity onCreate finishes?

I am trying to redesign some fragments to remove dependencies from the onAttach and onActivityCreated overrides and instead look up the Activity later on in the onViewCreated override.
Are there any cases in the Android application lifecycle where onViewCreated for the fragment is called before Activity onCreate finishes. For example I know that:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
will usually not result in Fragments onAttach being called (assuming the fragment is added to the fragment manager programmatically later on), however in rare cases with configuration updates the fragment manager can recreate the fragments in the super.onCreate which causes the onAttach to be called before onCreate for the activity has finished.
onCreate()
The system calls this when creating the fragment. Within your implementation, you should initialize essential components of the fragment that you want to retain when the fragment is paused or stopped, then resumed.
onCreateView()
Called to create the view hierarchy associated with the fragment.
The system calls this when it's time for the fragment to draw its user interface for the first time. To draw a UI for your fragment, you must return a View from this method that is the root of your fragment's layout. You can return null if the fragment does not provide a UI.
Refer Fragments Life Cycle

ViewPager and Fragment lifecycle with Activity

I'm using an Activity which has a ViewPager holding 2 fragments, the pager handler is some implementation of FragmentPagerAdapter.
As I understand, pager adapter handles the lifecycle of the fragments inside it.
I found out that my Activity onResume() method already gets called but the fragment onStart() method didn't even started.
how in the world can I fix that? it destroy the whole point of lifecycle interactions between activity an fragments...
Since pager adapter handles the lifecycle of the Fragment, does this means I can no longer depend on interaction with the Activity? I mean, if I want the Activity to do something in the onResume() but after the Fragment onStart() is called, I just can't do it...
Edited:
To make things clear:
Google says lifecycle of activity and fragment are going together, once one gets called, the other also gets called, e.g
Activiy -> onCreate() , and then, Fragment -> onCreate()
Activiy -> onResume() , and then, Fragment -> onResume()
BUT! in my case I get:
Activity -> onCreate() -> onStart() -> onResume() -> onPostResume()
And then:
Fragment -> onAttach() -> onCreateView() -> ... ->onResume().
and to be clear, I am using a pager adapter (not "state" pager) and I have an abstract base activiy in my app which all activities should extend.
public abstract class AbsLoginAppCompatActivity extends AppCompatActivity {
.............
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "*******************onCreate");
//do some general stuff like check for updates on server
}
And in my extend activity:
public class A extends AbsLoginAppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "*******************onCreate");
setContentView(R.layout.activity_a);
//also set pager + adapter + give it getSupportFragmentManager()
}
I am using:
android.support.v4.view.ViewPager
android.support.v4.app.FragmentPagerAdapter
android.support.v4.app.Fragment
android.support.v7.app.AppCompatActivity (for abs activity)
The Fragment[State]PagerAdapter uses the activities FragmentManager - or in case of a nested ViewPager in a parent fragment - that fragment's child FragmentManager to manage the fragments, just like normal fragments would do. Really, the only thing that these adapter implementations do is that they hide the nasty FragmentTransaction stuff for you.
I had never problems that particular lifecycle callbacks weren't called for me in my fragments, so I cannot say anything about that. One thing however that is important to understand and that many people get wrong is that the adapter's getItem() method is called only when a fragment is freshly created; if it is restored from a saved state this method is not called again and people tend to do all fancy things there to initialize their just "created" fragment, while they should really look into instantiateItem(), which either returns the instance you give the adapter via getItem() or returns the reference of the fragment that was automatically re-created for you.
Another thing that is good to know about fragments in pager is the method setUserVisibleHint(boolean). Since fragments are usually recreated and resumed all at once (non-state adapter) or on demand (state adapter), its usually important to know when one instance is actually visible to the user. This can be achieved by overriding the aforementioned method in a custom fragment.

FragmentActivity onSaveInstanceState not getting called

I have seen a few similar questions about onSaveInstanceState not getting called for Fragments, but in my case Fragments work fine, it's the main FragmentActivity that's having trouble.
The relevant code looks fairly simple:
public class MyFActivity extends FragmentActivity implements ActionBar.TabListener {
String[] allValues; // data to save
#Override
protected void onSaveInstanceState (Bundle outState) {
Log.d("putting it!", allValues.toString());
outState.putStringArray("allValues", allValues);
super.onSaveInstanceState(outState);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
allValues = savedInstanceState.getStringArray("allValues");
Log.d("getting it!", allValues.toString());
}
}
}
When pausing the activity (using the back button), the onSaveInstanceState is never called, and consequently, savedInstanceState is always null within the onCreate method upon resuming the app. I tried adding a block like this:
#Override
public void onPause() {
super.onPause();
onSaveInstanceState(new Bundle());
}
which was suggested in https://stackoverflow.com/a/14195202/362657 but while onSaveInstanceState then gets called, savedInstanceState remains null within onCreate method. What am I missing?
The issue here is that you are misunderstanding how onSaveInstanceState works. It is designed to save the state of the Activity/Fragment in the case that the OS needs to destroy it for memory reasons or configuration changes. This state is then passed back in onCreate when the Activity/Fragment is returned to / restarted.
In a Fragment, all of their lifecycle callbacks are directly tied to their parent Activity. So onSaveInstanceState gets called on the Fragment when its parent Activity has onSaveInstanceState called.
When pausing the activity (using the back button), the onSaveInstanceState is never called, and consequently, savedInstanceState is always null within the onCreate method upon resuming the app.
When pressing back, the user is destroying the Activity, and therefore its children Fragments, so there is no reason to call onSaveInstanceState, since the instance is being destroyed. When you reopen the Activity, it's a brand new instance, with no saved state, so the Bundle passed in onCreate is null. This is behaving exactly as designed. However, try rotating the device or hitting the home button, then you will see the Activity and its children Fragments have onSaveInstanceState called, and passed back in onCreate when returned to.
The hack you added, directly calling onSaveInstanceState(new Bundle()); inside of onPause, is a very bad practice, as you should never call the lifecycle callbacks directly. Doing so can put your app into illegal states.
If what you really want is for your data to persist beyond an instance of your app, I suggest you look into using SharedPreferences or databases for more advanced data. You can then save your persistent data in onPause() or whenever it changes.
In an update to the accepted answer:
A fragment's onSaveInstanceState may be called if you are using a ViewPager with a FragmentStatePagerAdapter (rather than FragmentPagerAdapter)
FragmentStatePagerAdapter
This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages.
And don't forget:
When using FragmentPagerAdapter the host ViewPager must have a valid ID set.
Not an accurate answer to the question, but may help someone's day.
In my case, I called
#Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
}
I replaced the above code as below and things worked
#Override
protected void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
}

Can you detach a fragment in onDestroy()?

Here's the issue I'm having. I've got an activity A that has a fragment F, which is contained in FragmentPagerAdapter FPA, which is in view V. (A->V->FPA->F)
When A gets destroyed (or in this case, swapped out), F is attached, and is in FPA, which is in V. However, when A gets recreated (someone hits the back button back into the activity, for instance), V and FPA don't exist, so F is recreated (in the attached state!), but to something that doesn't exist, so it's not in the view hierarchy at all. Then, when FPA tries to instantiateState on this fragment, it'll try to attach it, which does nothing because it's already attached to thin air.
There are obviously a few ways to fix this (have V and FPA exist in onCreate of the activity, so that the fragment has somewhere to go when it gets created, for instance), but I'd like to continue to lazily create FPA and V only when needed.
Thusly, it seems like updating the state of F to detached in onDestroy() would be desirable. However, state is saved in onPause(), which means I'm kinda out of luck here.
Is there a way to update the saved state of F in A's onDestroy()? Is there a way to say "don't rehydrate this fragment if the activity gets destroyed"? Is there some other obvious way of thinking about this that I'm not considering? It feels like I'm going about things the wrong way here.
I've had my trouble with FragmentPagers. What I do is passing a null bundle in the Activity onCreate() and then create everything from scratch every time it is created. Like so:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
// do my stuff
}
This way the fragment wont be passed on when the activity is recreated.
You wrote:
Is there some other obvious way of thinking about this that I'm not considering?
This doesn't directly answer your title question but provides a convenient solution to how to preserve Fragment state in a ViewPager.
You can save the Fragment states when the Activity is destroyed by tagging the Fragment in the Activity that initializes the Fragment:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.photos_pager_activity);
MyImageFragment fragment;
if (savedInstanceState != null) {
fragment = (MyImageFragment) getFragmentManager().findFragmentByTag("my_image_fragment_tag");
} else {
fragment = new MyImageFragment();
getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "my_image_fragment_tag").commit();
}
See also:
Uses of fragment tags
ViewPager and fragments — what's the right way to store fragment's state?

Categories

Resources