Saving states of multiple fragments in activity - android

I am working on an application and there is one specific thing that is bothering me. Let's just say I have one activity and 2 fragments.FragmentA and FragmentB and FragmentA gets attached when activity starts.
I want to save the fragment data and fragment state when orientation changes occur.I have successfully saved fragment data using OnSavedInstanceState method. Now I want to save fragment state in the activity so that if orientation change occurs I want to be on the fragment I was (in my case either FragmentA or FragmentB depends on which was showing before config changes occur).
This is how I am saving the fragment state in the Activity:
#Override
protected void onSaveInstanceState(Bundle outState) {
// Save the values you need into "outState"
super.onSaveInstanceState(outState);
outState.putLong(SS_DATE, userDate.getTime());
android.support.v4.app.FragmentManager manager = getSupportFragmentManager();
Fragment currentFragment = this.getSupportFragmentManager().findFragmentById(R.id.content_container);
manager.putFragment(outState, "currentFragment", currentFragment);
}
And this is how I am retrieving on which fragment I was when the orientation change occurred:
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
FragmentManager manager = getSupportFragmentManager();
#SuppressLint("CommitTransaction")
FragmentTransaction transaction = manager.beginTransaction();
if (savedInstanceState != null) {
Fragment MyFragment = (Fragment) manager.getFragment(savedInstanceState, "currentFragment");
if (MyFragment instanceof FragListStudentsAttendance) {
Log.v("onRestore", FragListStudentsAttendance.TAG);
}else if (MyFragment instanceof FragGetClassesForAttendance){
Log.v("onRestore", FragGetClassesForAttendance.TAG);
if(MyFragment!=null) {
mFragGetClassesForAttendance = (FragGetClassesForAttendance) MyFragment;
}else{
mFragGetClassesForAttendance = new FragGetClassesForAttendance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// mFragGetClassesForAttendanceNew.setRetainInstance(true);
// transaction.replace(R.id.content_ssadmin_container, mFragGetClassesForAttendanceNew, "FragGetClassesForAttendance").addToBackStack(null);
transaction.add(R.id.content_ssadmin_container, mFragGetClassesForAttendance, FragGetClassesForAttendance.TAG);
//transaction.replace(R.id.newEnrollmentMainContainer, mFragNewEnrollmentResults).addToBackStack("FragNewEnrollments");
transaction.commit();
mFragGetClassesForAttendance.setDate(userDate);
}
}
}
}
Now
Scenario 1:
If I am on fragment A and I rotate the device every thing works fine as it should. Like fragment have web services which loads the data into listview so I check if data exist then there is no need to run the web service and that working for now
Scenario 2:
If I am on fragment B and orientation change occurs everything works fine as it is supposed to be on fragment B. Now When I press back button Fragment A gets called again and all the data also comes from service. I think this shouldn't happen because it was supposed to be in BackStack and it's data was saved. So what Should I do now here?
Scenario 3: On FragmentB I have noticed that when I rotates the device the saveInstanceState function of FragmentA also gets called. Why it is so? where as I was replacing the FragmentB with FragmentA ?
Some Confusions:
Let me talk about some of the confusions also , maybe someone clear it to me although I have searched and read a lot about fragment and activity life cycle,
Actually I want to save the data per activity and fragment on device rotation. I know how to do it with activity(how to save states) so I also know how to do it in the fragment (save state of fragment views) now I am confused how to tell activity which fragment was showing and which to go after config changes(rotation) ? also what happens to FragmentA if I am on FragmentB Does its get attach and detach again and again in background?

I got your problems and confusions. I think the life cycle of fragment is confusing you. and indeed it will confuse you.
You need to learn different situations.
1. Fragment Life cycle when it is in foreground (attaching and detaching with activity) . Please keenly observe all the methods that will call i.e OnSaveInstance,onCreateView,OnDestroyView,onDestroy
2. Fragment life cycle when it is in background (observe the methods stated above)
3. Fragment life cycle when it is added to backstack (and not in foreground)
I am quite sure you are confused with the point number 3. As when the fragment is added to backstack it never gets destroy. So rotating device twice will set the ffragment data to null. I think you are restoring data on ActivityCreated or on onViewCreated ,
Ill suggest you to restore the fragment data in the oncreate. this will work for you when your fragment is coming back to foreground from the backstack .
Example
private List<String> mCountries;</pre>
#Override
public void onCreate(Bundle savedInstanceState)
{
if (savedInstanceState != null)
{
// Populate countries from bundle
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_countries, container, false);
if (mCountries == null)
{
// Populate countries by calling AsyncTask
}
return view;
}
public void onSaveInstanceState(Bundle outState)
{
// Save countries into bundle
}
Hope this will clear your confusions.

Related

When do Fragments have non-empty savedInstanceState

I have a Fragment inside a ViewPager. The Fragment contains a RecyclerView (think ListView). When the user pages away and then returns to the fragment, no matter where the user left the list, the list always restarts at the top. So I need to retain the currPosition somehow. Would savedInstanceState be a good place to store it? If yes, in which callback do I read the data? Again this is inside a ViewPager and I am using a FragmentPagerAdapter and so not many callbacks are called. Still my broader question stands: What causes a Fragment's savedInstanceState to be non-empty?
To give further breadth to the question. Imagine I have an activity with two fragments. I detach and attach the fragments when needed. Say I navigate away from the activity. Would the savedInstanceState of the fragment be empty in this case if the activity's savedInstanceState is not empty? To be exact here is some code for this second case
private void addMainFragment() {
FragmentManager fm=getSupportFragmentManager();

 FragmentTransaction transaction = fm.beginTransaction();

 Fragment removeFragment = fm.findFragmentByTag(getString(R.string.fragment_a));

 if(null != removeFragment){
transaction.detach(removeFragment);

 }
Fragment fragment = fm.findFragmentByTag(getString(R.string.fragment_b));

 if(null != fragment){

 transaction.attach(fragment);

 }else{
fragment=MainFragment.newInstance(null,null);

 transaction.
add(R.id.fragment_container,fragment,getString(R.string.fragment_b));

 }
transaction.commit();
 }
savedInstanceState is the best place to save information like current postion, selected items etc.
View pagers have an offscreen page limit that handles how many pages (in this case fragments) are kept idle either side of the current visible page. If the page is outside this limit the pages are destroyed. This is to keep memory usage lower. This setting defaults to 1. So when a fragment goes out side this limit and then comes back its savedInstanceState will be non-null.
To answer your second question. Im not quite sure what you mean by navigate away from the activity. If you navigate away from the activity (e.g press the back / home button) then come back. The activities savedInstanceState will be null.
If you start another activity on top of the current then onSavedInstance will be called.
Here is a snippet of code I use in one of my projects for restoring a fragments state in a viewpager.
You can override onSaveInstanceState to save some information about the fragment.
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// restore state
mListState = mLayoutManager.onSaveInstanceState();
state.putParcelable("list_state", mListState);
}
Then in the fragments onCreateView you can restore the state of the recycelerview
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
...
if (savedInstanceState != null) {
// save list state
mListState = state.getParcelable("list_state");
mLayoutManager.onRestoreInstanceState(mListState);
}
return root;
}

Fragment calling fragments loosing state on screen rotation

Hi i created a project with a default "Navigation Drawer Activity".
So i have a MainActivity with a fragment with is replaced for each item on menu.
One of the menus is "Customers" with shows a list of customers.
From customers fragment i can see the Interests of this customers, with is a Fragment(CustomerListFragment) calling the interests(InterestsListFragment).
There is even more levels, but to be short that's enough.
This is the code on MainActivity that i use to call fragment from fragment and pass data between
public void passData(Object[] data, Fragment f) {
Bundle args = new Bundle();
args.putSerializable("PASSED_DATA", data);
f.setArguments(args);
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.container, f)
.addToBackStack("")
.commit();
}
And i use like :
mCallbacks.passData(new Object[]{c}, new OpportunityListFragment());
The problem is that when i rotate the phone does not matter from wich level of activity i have, it comes back to the first fragment called(CustomerListFragment), and if i click "Back" on cellphone it gets back to where i was when i rotate the phone.
What do i have to do, to avoid this kind of problem? why it gets back to the first activity evoked if i am replacing fragments?
The answer from ste-fu is correct but let's explore programmatically. There is a good working code in Google documentation # Handling Runtime Changes. There are 2 code snippets that you have to do.
1) Code snippet:
public class MyActivity extends Activity {
private RetainedFragment dataFragment;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
Note: Code uses FragmentManager to find the current Fragment. If fragment is null, then the UI or app has not been executed. if not null, then you can get data from RetainedFragment object.
2) Need to retain the Fragment state.
public class RetainedFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
Note: setRetainInstance is used in OnCreate. And subclassing the Fragment is recommended, naming it RetainedFragment, used on snippet 1.
When you change screen orientation your parent Activity is destroyed and recreated. Unless you persist the level structure in some fashion, it will always appear as when you first started the activity. You can either use the bundle object, or for more complicated objects you need to persist it to a database.
Either way, onSaveInstanceState is your friend. Then in your onCreate method you need to check the bundle or database, and the set the fragment accordingly.

Switching between two fixed fragments

I have this activity where I run an asynctask that gathers a series of data. This data can be represented in several ways, for the sake of simplicity let's say there's two ways. I want the user to be able to switch between these two representations (listview and graphical view).
So I created a FragmentActivity with my ListFragment and my GraphicFragment.
How do you switch between these two so that user can go back and forth? I tried using replace but then the previous fragment gets destroyed which is not of my interest since its reference has the data passed by my Activity. (i.e. I don't want to recreate the fragment each times the user switches between views).
I also tried hiding and showing but I can't get it to work, one of the fragments is not showing.
Also, when I rotate I guess the fragments get destroyed because I get a nullpointerexception when trying to access them.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
pBar = (ProgressBar) findViewById(R.id.analyzingProgress);
// However, if we're being restored from a previous state, then don't
// create the fragment again (please)
if (savedInstanceState == null) {
firstFragment = new DirListFragment();
secondFragment = new GraphViewFragment();
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, secondFragment).commit();
fragments= new ArrayList<IFragment>();
fragments.add(firstFragment);
fragments.add(secondFragment);
}
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId() == R.id.listView){
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
/*transaction.hide(secondFragment);
transaction.show(firstFragment);*/
transaction.replace(R.id.fragment_container, firstFragment);
transaction.addToBackStack(null);
transaction.commit();
}
else if(item.getItemId() == R.id.graph){
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
/*transaction.hide(firstFragment);
transaction.show(secondFragment);*/
transaction.replace(R.id.fragment_container, secondFragment);
transaction.addToBackStack(null);
transaction.commit();
}
return true;
}
The problem comes when I try to access the fragment fields to update their data:
for(IMyFragment frag: fragments){
frag.updateData(data);
}
I get a NullPointerException after rotating the screen.
Also when switching fragmenents, their data is not updated (i.e. I start with list fragment, it's showing the proper data, then switch to graphic, no data is shown, switch back to list, no data again.)
If the fields are null when using setRetainInstance(true);, then you are recreating them (the fragments) in your activity. Make sure you are setting something in onSaveInstanceState() in your activity so in onCreate(), savedInstanceState will not be null, it can even be an empty bundle or useless variable, ex:
#Override
public void onSaveInstanceState(Bundle outstate){
super.onSaveInstanceState(outstate);
outstate.putBoolean("stateChanged", true);
}
Just make sure you call super before adding your arguments.
On another note, I personally have never stored my fragments in an array. I use a FrameLayout as a fragment container in my activities, just like you, and if I need to update something I just call: getSupportFragmentManager().findFragmentById(R.id.fragment_container).setWhatINeed(); This way I always get the visible fragment.
Let me know how the update goes and I'll update this post with any information I can.

Detaching a Fragment not triggering onSaveInstanceState()

My Android application has an ActionBar that changes which Fragment occupies a certain FrameLayout. I am trying to use onSaveInstanceState to save the state of a Fragment when the tab is changed, so that it can be recovered in onCreateView.
The problem is, onSaveInstanceState is never called. The Fragment's onDestroyView and onCreateView methods are called, but the Bundle supplied to onCreateView remains null.
Can someone please explain to me when onSaveInstanceState is actually called, how I can make sure it gets called when switching tabs, or the best practice for saving and restoring the state of a Fragment when it is detached and re-attached?
Fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.event_log, container, false);
// Retrieve saved state
if (savedInstanceState != null){
System.out.println("log retrieved");
} else {
System.out.println("log null");
}
return view;
}
#Override
public void onSaveInstanceState(Bundle outState) {
System.out.println("log saved");
super.onSaveInstanceState(outState);
// more code
}
Activity:
/**
* Detach the current Fragment, because another one is being attached.
*/
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (tab.getText().equals(getString(R.string.tab_events))){
if (frEventLog != null) {
ft.detach(frEventLog);
}
}
Fragment#onSaveInstanceState is only called when the Activity hosting the Fragment is destroyed AND there is a chance that you can come back to the same activity AND the fragment is still added to the FragmentManager. The most common case would be screen rotation.
I think your Fragment will also need to do setRetainInstance(true) in onCreate for example. Not exactly sure about that point though.
You should also see this method being called when you press the home button for example. That will destroy the activity but you can go back to it by using the task list for example.
If you just detach() the fragment all you need to do to get it back is to ask the FragmentManager for it.
There are two examples you should have a look at:
ActionBar FragmentTabs and TabHost FragmentTabs
The TabHost example uses
ft.add(containerId, fragment, tag);
// later
fragment = mActivity.getSupportFragmentManager().findFragmentByTag(tag);
to find the instances of previously added Fragments, works until you remove() a Fragment
Regarding onCreateView / onDestroyView: That is called once a fragment gets detached because the next time you attach it needs to create a new View. Note that Fragment#onDetached() is not called when you detach() the fragment because it is still attached to the Activity. It is only detached from the view-hierarchy.
There is another nice example on how to retain fragment state / how to use fragments to retain state in Android Training - Caching Bitmaps.
That example is missing a critical line though:
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit(); // << add this
}
return fragment;
}

savedInstanceState when restoring fragment from back stack

Can I use savedInstanceState() to save the state when removing a fragment, then restore the state when I pop the fragment off the back stack? When I restore the fragment from the back stack, savedInstanceState bundle is always null.
Right now, the app flow is: fragment created -> fragment removed (added to back stack) -> fragment restored from back stack (savedInstanceState bundle is null).
Here is the relevant code:
public void onActivityCreated(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getArguments();
Long playlistId = bundle.getLong(Constants.PLAYLIST_ID);
int playlistItemId = bundle.getInt(Constants.PLAYLISTITEM_ID);
if (savedInstanceState == null) {
selectedVideoNumber = playlistItemId;
} else {
selectedVideoNumber = savedInstanceState.getInt("SELECTED_VIDEO");
}
}
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(Constants.SELECTED_VIDEO, selectedVideoNumber);
}
I think the problem is that onSavedInstanceState() is never called when being removed and being added to back stack. If I cant use onsavedInstanceState(), is there another way to fix this?
onSaveInstanceState is (unfortunately) not called in normal back-stack re-creation of a fragment. Check out http://developer.android.com/guide/components/fragments.html#Creating and the answer on How can I maintain fragment state when added to the back stack?
I like to store the View I return in onCreateView as a global variable and then when I return I simply check this:
if(mBaseView != null) {
// Remove the view from the parent
((ViewGroup)mBaseView.getParent()).removeView(mBaseView);
// Return it
return mBaseView;
}
The problem is that the fragment needs to have an Id or Tag associated with it in order for the FragmentManager to keep track of it.
There are at least 3 ways to do this:
In xml layout declare an Id for your fragment:
android:id=#+id/<Id>
If your fragments container View has an Id, use FragmentTransaction:
FragmentTransaction add (int containerViewId, Fragment fragment)
If your fragment is not associated with any View (e.g. headless fragment), give it a Tag:
FragmentTransaction add (Fragment fragment, String tag)
Also, see this SO answer.
FWIW, I hit this as well, but in my case onSaveInstanceState was called properly and I pushed in my state data when a new activity fragment was brought up on the smartphone. Same as you, the onActivityCreated was called w/ savedInstanceState always null. IMHO, I think it's a bug.
I worked around it by creating a static MyApplication state and putting the data there for the equivalent of "global variables"...

Categories

Resources