Fragment Maintain View State on detach/destroy/orientation change - android

I have an application that has two fragments as actionbar tabs. The fragments are attached/detached when switching between the tabs. Any time I switch a tab, change the orientation, or press back to exit the application, the view is destroyed. I need it to be restored to its previous state when it is reopened. I know, at least on the orientation change, to use onSaveInstanceState and save the data there so I can restore it when the view is recreated. However, for some reason even though the data gets saved properly to the outState bundle and is read properly from the savedInstanceState bundle, the view doesn't update to what it should update to. For example, I start a service and while that service is running I need to hide two buttons and show two other buttons in their place. I use a boolean to check if the service is running, then put it in the outState so I can see which buttons to show or hide. My code for that is:
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isRunning", isRunning);
}
In onCreateView:
if (savedInstanceState != null) {
isRunning = savedInstanceState.getBoolean("isRunning", false);
if (isRunning) {
showStopButton();
}
}
And the showStopButton code is:
private void showStopButton() {
btnStart.setVisibility(View.GONE);
btnReset.setVisibility(View.GONE);
btnStop.setVisibility(View.VISIBLE);
btnLoop.setVisibility(View.VISIBLE);
}
So all this works, the boolean is found as true while the service is running, and showStopButton() is called. However, it doesn't appear to actually do anything. The view state just resets itself to as if the first two buttons (which I want to be hidden) are shown instead of the ones I actually want to be shown. Any idea why this is happening/how to fix it?
I also have a listview that I need to stay populated with the same values as before that I can't get to work either.
Also, onSaveInstanceState isn't called when switching tabs (and I think not when pressing the back button either?). How should I go about retaining the view state in these cases?

Related

Fragment refreshing on backpress

I have an activity MainActivity there are three fragments associated with this activity.
Now one of my fragment Timeline has a listview. Which I populate from a Database in the backend. I use an AsyncTask to fetch values from the DB and process them to the List. I trigger this AsyncTask in the onCreate of the Fragment Timeline.
Now from Timeline on click of any list item I navigate to a different Activity called as DetailActivity
The problem is whenever I press back from the DetailActivity the onCreate of my MainActivity is called and my list refreshes again - the whole DB operation is called again and my list does not retain its state.
I am calculating the visible items of my List before I navigate away from the Fragment but I am forced to use static values for these variables so that I retain the position. How to avoid this?
Below are the snippets of my onPause and onResume as laid down in the fragment Timeline
static int index;
static int top;
#Override
public void onPause(){
System.out.println("onPause");
index = lv.getFirstVisiblePosition();
View v = lv.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
super.onPause();
uiHelper.onPause();
}
#Override
public void onResume(){
super.onResume();
//dbHelper.open();
System.out.println("onResumr");
lv.setSelectionFromTop(index, top);
ActionBar actionBar = getActivity().getActionBar();
actionBar.setTitle("Timeline");
uiHelper.onResume();
AppEventsLogger.activateApp(getActivity());
updateUI();
}
This also forces my AsyncTask to run again and again, which is an overhead.
Edit:
The root of this problem - After struggling for so many days I borrowed a friends phone to test and all was sorted on this new phone. I found out that I had turned on the Do not keep Activities option in my Developer Settings. The Dumb me!!
This is, unfortunately, the default behavior of the Fragment class. A Fragment is destroyed whenever the containing Activity is paused, and recreated whenever the containing Activity is resumed. If you use an Activity instead of a Fragment for the list, you would not experience the same behavior. With an Activity:
AsyncTasks and/or web services would not be called again.
The list would show the previously scrolled position.
If you want the same behavior with a Fragment, you need to override the onSaveInstanceState() method. And while interesting, it is not a small amount of work.
EDIT:
Make sure the Do not keep Activities option is unselected in your phone's Developer Settings. This, though, does not change the essential behavior of the Fragment class that I have outlined above.
You can call setRetainInstance(true) on your fragment. The lifecycle will be slightly different though.
A nice view of a fragment's lifecycle is available here
http://corner.squareup.com/2014/10/advocating-against-android-fragments.html

How to save/restore fragment state when device screen turned off and on again

I have a fragment with an EditText in it, where users can enter a longer text. Now when users type something, turn the screen off for whatever reason, and then turn the screen on again the EditText is empty.
I thought that onSaveInstance should be the right place to save the state, and any of the create methods (that actually have the saveInstance parameter) should be enough to retrieve the previously saved state - but it's not working. onSaveInstance is called, but the create methods do not retrieve this object.
So the question:
How can I save my fragment's state when screens turn off, and how can I restore this state when the screen is turned on again?
Edit:
I have implemented blackbelt's approach, but it's still not working. (savedInstanceState is null)
#Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
review.finalStatement = savedInstanceState.getString
(BUNDLE_KEY_STATEMENT);
}
}
#Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(BUNDLE_KEY_STATEMENT, review.finalStatement);
}
I have a fragment with an EditText in it, where users can enter a
longer text. Now when users type something, turn the screen off for
whatever reason, and then turn the screen on again the EditText is
empty.
It is not the default behaviour. EditText states should be kept as it was.
I thought that onSaveInstance should be the right place to save the
state, and any of the create methods (that actually have the
saveInstance parameter) should be enough to retrieve the previously
saved state - but it's not working. onSaveInstance is called, but the
create methods do not retrieve this object.
It is. You should use the pair onSavedInstanceState/onActivityCreated and not onCreate

how to force a fragment to save it's state (parent activity being only *paused*)

I am developing a (custom) bottom action bar, managed through a fragment. The bottom bar is made of 5 buttons, and is shared by 3 activities (I want each individual activity to present this bar at the bottom of the screen).
I would like to save/restore the state of the fragment in order to save buttons' state across activity changes (through startActivity(Intent) and so on).
However, when starting a child activity from a parent one, the first one is not destroyed and the state of the activity (and it's inner fragments) is not saved. In consequence, when my child activity starts, the state of it's bottom bar fragment is not restored.
The code
The link between my activities :
AndroidManifest.xml
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:theme="#style/Theme.TicketPromo" />
<activity
android:name=".SavingsActivity"
android:parentActivityName=".MainActivity"
android:screenOrientation="portrait"
android:theme="#style/Theme.TicketPromo" />
My save / restore code :
public class BottomBarFragment extends Fragment {
...
// Indicates whether the buttons are enabled or not
protected boolean isPromoFlashEnabled;
protected boolean isCouponsModeEnabled;
protected boolean isSavingsModeEnabled;
protected boolean isSearchModeEnabled;
protected boolean isAccountModeEnabled;
...
/*
* Save the state of buttons so they can be restored on the next screen.
*/
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
// Save buttons' state
state.putBoolean("isPromoFlashEnabled", isPromoFlashEnabled);
state.putBoolean("isCouponsModeEnabled", isCouponsModeEnabled);
state.putBoolean("isSavingsModeEnabled", isSavingsModeEnabled);
state.putBoolean("isSearchModeEnabled", isSearchModeEnabled);
state.putBoolean("isAccountModeEnabled", isAccountModeEnabled);
}
/*
* Restore the state of buttons and call their trigger on the screen opener.
*/
public void onViewStateRestored(Bundle state) {
super.onViewStateRestored(state);
if (state == null) { return; }
// Promo flash
isPromoFlashEnabled = state.getBoolean("isPromoFlashEnabled", false);
toggleActionButton(ActionButton.PromoFlash, isPromoFlashEnabled);
screenOpenener.togglePromoFlashButton(isPromoFlashEnabled);
// My coupons
isCouponsModeEnabled = state.getBoolean("isCouponsModeEnabled", false);
toggleActionButton(ActionButton.Coupons, isCouponsModeEnabled);
screenOpenener.toggleMyCouponsButton(isCouponsModeEnabled);
// My savings
isSavingsModeEnabled = state.getBoolean("isSavingsModeEnabled", false);
toggleActionButton(ActionButton.Savings, isSavingsModeEnabled);
screenOpenener.toggleMySavingsButton(isSavingsModeEnabled);
// Search mode
isSearchModeEnabled = state.getBoolean("isSearchModeEnabled", false);
toggleActionButton(ActionButton.Search, isSearchModeEnabled);
screenOpenener.toggleSearchButton(isSearchModeEnabled);
// My account
isAccountModeEnabled = state.getBoolean("isAccountModeEnabled", false);
toggleActionButton(ActionButton.Account, isAccountModeEnabled);
screenOpenener.toggleMyAccountButton(isAccountModeEnabled);
}
...
}
The code to obtain a reference on the fragment when creating the activities (the fragment is directly inserted in the activity's layout with <fragment .../> ) :
FragmentManager fm = getSupportFragmentManager();
bottomBar = (BottomBarFragment) fm.findFragmentById(R.id.bottom_bar);
THE question
How to save the state of a fragment that is still attached to a paused activity
How to restore this state
Is there a link with setRetainInstance(true) or a hack regarding this?
Edit, reworded question :
How to share a single fragment between two activities so the fragment is not destroyed, or at least so that it can be saved/restored across activities' lifecyle ?
1- If your main activity gets in pause state your fragment should get in pause state to.
This corresponds to Activity.onSaveInstanceState(Bundle) and most of
the discussion there applies here as well. Note however: this method
may be called at any time before onDestroy(). There are many
situations where a fragment may be mostly torn down (such as when
placed on the back stack with no UI showing), but its state will not
be saved until its owning activity actually needs to save its state.
http://developer.android.com/reference/android/app/Fragment.html#onSaveInstanceState(android.os.Bundle)
2- You could get the saved bundle at onViewStateRestored.
Called when all saved state has been restored into the view hierarchy
of the fragment. This can be used to do initialization based on saved
state that you are letting the view hierarchy track itself, such as
whether check box widgets are currently checked. This is called after
onActivityCreated(Bundle) and before onStart().
http://developer.android.com/reference/android/app/Fragment.html#onViewStateRestored(android.os.Bundle)
3- If you set retain instance to TRUE the save and restore methods will only be called when the fragment was realy create/destroyed because when the flag is TRUE the first instance is "never" lost.
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.
http://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean)
I solved the problem using SharedPreferences when the first activity comes to be paused.
Not a great deal, just sad I couldn't use the native life cycle to handle this.

How does `onViewStateRestored` from Fragments work?

I am really confused with the internal state of a Fragment.
I have an Activity holding only one Fragment at once and replaces it, if another Fragment should get shown. From the docs onSaveInstanceState is called ONLY if the Activitys onSaveInstanceState is getting called (which isn't called in my case).
If I stop my Fragment, I'll store its state myself inside a Singleton (yeah, I know I hate Singletons, too, but wasn't my idea to do so).
So I have to recreate the whole ViewHirarchy, create new Views (by using the keyword new), restore its state and return them in onCreateView.
I also have a Checkbox inside this View from which I explicitly do NOT want to store its state.
However the FragmentManager wants to be "intelligent" and calls onViewStateRestored with a Bundle I never created myself, and "restores" the state of the old CheckBox and applies it to my NEW CheckBox. This throws up so many questions:
Can I control the bundle from onViewStateRestored?
How does the FragmentManager take the state of a (probably garbage-collected) CheckBox and applies it to the new one?
Why does it only save the state of the Checkbox (Not of TextViews??)
So to sum it up: How does onViewStateRestored work?
Note I'm using Fragmentv4, so no API > 17 required for onViewStateRestored
Well, sometimes fragments can get a little confusing, but after a while you will get used to them, and learn that they are your friends after all.
If on the onCreate() method of your fragment, you do: setRetainInstance(true); The visible state of your views will be kept, otherwise it won't.
Suppose a fragment called "f" of class F, its lifecycle would go like this:
- When instantiating/attaching/showing it, those are the f's methods that are called, in this order:
F.newInstance();
F();
F.onCreate();
F.onCreateView();
F.onViewStateRestored;
F.onResume();
At this point, your fragment will be visible on the screen.
Assume, that the device is rotated, therefore, the fragment information must be preserved, this is the flow of events triggered by the rotation:
F.onSaveInstanceState(); //save your info, before the fragment is destroyed, HERE YOU CAN CONTROL THE SAVED BUNDLE, CHECK EXAMPLE BELLOW.
F.onDestroyView(); //destroy any extra allocations your have made
//here starts f's restore process
F.onCreateView(); //f's view will be recreated
F.onViewStateRestored(); //load your info and restore the state of f's view
F.onResume(); //this method is called when your fragment is restoring its focus, sometimes you will need to insert some code here.
//store the information using the correct types, according to your variables.
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("foo", this.foo);
outState.putBoolean("bar", true);
}
#Override
public void onViewStateRestored(Bundle inState) {
super.onViewStateRestored(inState);
if(inState!=null) {
if (inState.getBoolean("bar", false)) {
this.foo = (ArrayList<HashMap<String, Double>>) inState.getSerializable("foo");
}
}
}

Save instance of dynamically generated views when we switch back and forth between activities

I am inflating a view on button click and the user can add as many views as he likes, all is fine I made it work, but now the problem is when I go back one activity and come again to my dynamically generated activity every single view that was generated is gone. Similar is the case if I go to next activity and come back to the inflated activity. I know about onSaveInstance and onRestoreSaveInstance. But how do I put view information in a bundle in onSaveInstanceState? Please note that my view was generated Dynamically i.e. on button Click and I want to know as of how to preserve the state of my activity.
How do you go about it?
I am thinking that you should implement some kind of logic that helps you restore the state of your Views. So you should be designing a class, let say ViewDetail that somehow keeps details about the Views that you are adding.... type, dimension, etc. This class should implement Parcelable so you are able to add it to the bundle.
So you will keep an ArrayList<ViewDetail>, myViews where everytime the user adds a new View you create a new ViewDetail object that you add to your myViews array.
And then save your Views and restore them using those objects:
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//save your view states
outState.putParcelableArrayList("MY_VIEWS",myViews);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
//get the views back...
myViews=savedInstanceState.getParcelableArrayList("MY_VIEWS");
//TODO: add the views back to your Activity
}
As your application may be killed completely at any moment without noticem you have to provide long term storage off heap memory
You only have to restore all the views, if your activity was terminated (and it can be at any time). When it is activated again after termination, it goes through onCreate() method
- this would be proper place to restore activity state.
Only callback which is guaranted to be called before your application / activity is destroyed is onPause() - this is a proper place to save views states into long term off-heap storage.

Categories

Resources