Saving State of Fragments in a FragmentActivity with FragmentTabHost - android

I have a Fragment Activity with a FragmentTabHost. I add the fragments to the tab using the following code:
mTabHost.addTab(mTabHost.newTabSpec(tab1Name).setIndicator(tabIndicator1),
EventSettingsStep1Fragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec(tab2Name).setIndicator(tabIndicator2),
EventSettingsStep2Fragment.class, null);
When I switch to different tabs, I'd like to retain all the values (view state, etc) so that I have the same data when I switch back to the tab.
I overrode the onSaveInstanceState method & in there, I added values that I want retained to the bundle.
I ran through the methods being called and I have the following:
Switching from Tab1 to Tab2: Tab1:onPause then Tab2:onCreateView, Tab2:onResume
Switching from Tab2 to Tab1: Tab2:onPause then Tab1:onCreateView, Tab1:onResume
onSaveInstanceState is not being called.
Here is the code for one of my fragments:
public class EventSettingsStep1Fragment extends Fragment implements View.OnClickListener {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
if (savedInstanceState != null) {
Log.d(TAG, "restoring onSavedInstanceState");
Gson gson = new Gson();
event = gson.fromJson(savedInstanceState.getString("event"), Event.class);
}
if (event != null) {
//set views
}
return v;
}
#Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
#Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
#Override
public void onSaveInstanceState(Bundle outState) {
Log.d(TAG, "onSaveInstanceState");
super.onSaveInstanceState(outState);
Gson gson = new Gson();
outState.putString("event", gson.toJson(event));
}
}
Why is onSaveInstanceState not being called? Is it only triggered through the FragmentActivity?

onSaveInstanceState is not being called because the framework simply reuses the already-existing instance of the fragment. onSaveInstanceState only gets called when the instance is about to be destroyed and then recreated. This happens for example when you rotate the display and force the hosting activity to be recreated.
onSaveInstanceState is also not called when you push a fragment on the backstack of a FragmentManager. You will have to restore the state from the already existing instance, which can be very annoying. See SO questions How can I maintain fragment state when added to the back stack? and Once for all, how to correctly save instance state of Fragments in back stack? for example.
Basically you will have to do what the answers to these questions suggest: continue using the values of your instance variables and do not rely on a saved instance state.

Related

Save custom object on screen rotate in Fragment or Activity

I know this question is very common and I have read so many different answers but none fits in my problem. In my application, I have an activity and in rhye activity I load a fragment. I also send some data(in the form of Bundle) to the fragment. So my Problem is when the screen is rotated, I save the fragment in onSaveInstanceState Activity method and check in onCreate Method weather savedInstance is null or not and on that basis I load the fragment.
Activity code :
#Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
outState.putParcelable(Const.TAG_REQ_CUSTOM,DetailsItems);
outState.putString(Const.TAG_FLOW, Const.TAG_MAIN_FLOW);
getSupportFragmentManager().putFragment(outState,"current_fragment",fragment);
}
onCreate Method :
if (findViewById(R.id.fragment_frame) != null) {
if (savedInstanceState != null) {
// this invoke when screen rotate but the app crash
DetailsItems = savedInstanceState.getParcelable(Const.TAG_REQ_CUSTOM);
String flow = savedInstanceState.getString(Const.TAG_FLOW);
ft = getSupportFragmentManager().getFragment(savedInstanceState,"current_fragment");
mFragmentManager=getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
bundle= new Bundle();
bundle.putString(Const.TAG_FLOW, flow);
bundle.putParcelable(Const.TAG_REQ_BOOKING_DETAILS, bookingDetailsItems);
ft.setArguments(bundle);
mFragmentTransaction.replace(R.id.fragment_frame, ft).commit();
}else{
// load fragment on first time
}
}
So my Question is: Where do I have to save the custom Object(in parent Activity or in fragment) ?
When my saved Instance is not null than app crashesh and logs is :
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Class java.lang.Object.getClass()' on a null object reference
You should use ViewModel. ViewModel is specifically made for this purpose.
From the docs:
ViewModel is a class that is responsible for preparing and managing the data for an Activity or a Fragment. It also handles the communication of the Activity / Fragment with the rest of the application (e.g. calling the business logic classes).
use this code in Activity :
if (findViewById(R.id.fragment_frame) != null) {
if (savedInstanceState != null) {
fragment =getSupportFragmentManager().getFragment(savedInstanceState,"current_fragment");
}else{
// load fragment on first time
}
}
and in fragment :
//save custom object
#Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putParcelable("key",customObject);
}
//now retrieve here
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
customObject= savedInstanceState.getParcelable("key");
}
Take a look at onRetainNonConfigurationInstance() and getLastNonConfigurationInstance()
From docs:
Called by the system, as part of destroying an activity due to a configuration change, when it is known that a new instance will immediately be created for the new configuration. You can return any object you like here, including the activity instance itself, which can later be retrieved by calling getLastNonConfigurationInstance() in the new activity instance.

Which Fragment lifecycle methods we should commit FragmentTrasaction to avoid famous java.lang.IllegalStateException

I was wondering, what is the Fragment lifecycle methods, I should commit FragmentTransaction to avoid famous
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
According to http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html, it gives great tip, on how to avoid such exception, by commit FragmentTransaction
FragmentActivity
onCreate()
onResumeFragments()
onPostResume()
Fragment
???
However, how about Fragment? What is the suitable Fragment lifecycle we should commit our fragment? For instance, under very rare situation, I will get exception from Google Play Console crash report, while trying to commit Fragment in another Fragment's onCreate.
public class BuyPortfolioFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final FragmentManager fm = this.getFragmentManager();
// Check to see if we have retained the worker fragment.
this.statusBarUpdaterFragment = (StatusBarUpdaterFragment)fm.findFragmentByTag(STATUS_BAR_UPDATER_FRAGMENT);
if (this.statusBarUpdaterFragment == null) {
this.statusBarUpdaterFragment = StatusBarUpdaterFragment.newInstance();
this.statusBarUpdaterFragment.setTargetFragment(this, 0);
// java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
fm.beginTransaction().add(statusBarUpdaterFragment, STATUS_BAR_UPDATER_FRAGMENT).commit();
} else {
statusBarUpdaterFragment.setTargetFragment(this, 0);
}
p/s I know I can avoid such exception by using commitAllowingStateLoss. I want to use it as last resource.
Fragment's lifecycle state not always matches Activity's. Fragment's method getFragmentManager() returns the FragmentManager of it's hosting Activity (unless it's a child Fragment, if so this method returns the child fragment manager of a hosting Fragment). You may never know in which state is Fragment's hosting Activity unless you make tracking code. So it's really possible that the transaction eventually may be committed after Activity onSaveInstanceState() was called.
I suggest using getChildFragmentManager() and deal with child fragments from fragments.
Or if your intention was really to control Activity Fragments, make accessors for controlling it's state, like
// Activity method
public void showSomeFragment() {
if (mFragmentTransactionsAllowed) {
// do transaction
}
}
// And track the boolean
#Override
protected void onCreate(Bundle b) {
super.onCreate(b);
// override on onCreate() in case if Activity object is reused and state was true
mFragmentTransactionsAllowed = true;
}
#Override
protected void onStart() {
super.onStart();
// override here so that if activity goes foreground but not yet destroyed
mFragmentTransactionsAllowed = true;
}
#Override
protected void onResume() {
super.onResume();
mFragmentTransactionsAllowed = true;
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mFragmentTransactionsAllowed = false;
}

How can we retain a simple State Array across Configuration changes using Headless Fragments?

I have gone through so many other similar questions but none solve my problem.
One of the uses of Fragments (apparently) is to persist a state.
I have a State array called arrState that I have wrapped in a headless fragment called StateFragment.
public class StateFragment extends Fragment {
public static ArrayList<Character> arrState;
protected ActMainGame mActivity = null;
private Character crtX;
public static final String TAG = "StateFragment";
#Override
public void onAttach(Activity activity) {
Log.d(StateFragment.TAG, "StateFragment: onAttach");
super.onAttach(activity);
mActivity = (ActMainGame) activity;
}
#Override
public void onCreate(Bundle b) {
Log.d(StateFragment.TAG, "StateFragment: onCreate");
super.onCreate(b);
setRetainInstance(true);
}
public void setToX() {
arrState = new ArrayList<Character>();
for (int i = 0; i < 9; i++) {
arrState.add(crtX);
}
}
I have an Activity called ActMainGame that fills the array arrState with Xs.
public class ActMainGame extends Activity {
// Fragments
private StateFragment mStateFragment = null;
private static final String TAG_FRAGMENT = "state_fragment";
#Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(StateFragment.TAG, "ActMainGame: onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.main_game);
// Add Headless Fragment (if not already retained)
FragmentManager FM = getFragmentManager();
mStateFragment = (StateFragment) FM.findFragmentByTag(TAG_FRAGMENT);
if (mStateFragment == null) {
Log.d(StateFragment.TAG, "++ Existing fragment not found. ++");
mStateFragment = new StateFragment();
FM.beginTransaction().add(mStateFragment, TAG_FRAGMENT).commit();
} else {
Log.d(StateFragment.TAG, "++ Existing fragment found. ++");
}
}
#Override
public void onStart() {
super.onStart();
if (D) Log.d(StateFragment.TAG, "ActMainGame: ON START");
mStateFragment.setToX();
}
I have set up logging on the major lifecycle events of both classes.
What I expect is that on doing an orientation change that the main activity gets rebuilt but is able to find the fragment (withs it state array containing 9 Xs). Sure enough the fragment is not destroyed but the activity cannot find the persisting fragment via the line:
mStateFragment = (StateFragment) FM.findFragmentByTag(TAG_FRAGMENT);
And therefore creates a new fragment.
Here is the result of the debugging:
++ Existing fragment not found. ++
StateFragment: onAttach
StateFragment: onCreate
ActMainGame: ON START
ActMainGame: onResume
<Orientation change done here>
ActMainGame: onPause
ActMainGame: onStop
StateFragment: onDetach
ActMainGame: onDestroy
ActMainGame: onCreate
++ Existing fragment not found. ++
StateFragment: onAttach
StateFragment: onCreate
ActMainGame: ON START
ActMainGame: onResume
I am well aware that there are other ways to persist a state variable but I want to do it the "fragment way".
I finally resolved this! It is a MAJOR bug in Eclipse!!
When you create a new Android Application Project in Eclipse, the wizard allows you to start with an actionbar. If you do this and also select Navigation Type "Action Bar Spinner" the wizard creates among other things the following code by default:
#Override
public void onSaveInstanceState(Bundle outState) {
// Serialize the current dropdown position.
outState.putInt(STATE_SELECTED_NAVIGATION_ITEM, getActionBar()
.getSelectedNavigationIndex());
}
This is very kind of said wizard but there is a huge problem! There is no call to super!! The code works perfectly well in most circumstances BUT if you want to persist a fragment for your activity using SetRetainInstance(true) you need to add the "super" in as follows or it will not detect the fragment when your activity recreates.
Should be:
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Serialize the current dropdown position.
outState.putInt(STATE_SELECTED_NAVIGATION_ITEM, getActionBar()
.getSelectedNavigationIndex());
}
Hope this saves someone else the days it has cost me!

Save state of TextView and Spinner inside of CustomView

I have 3 custom views.
The first one works great. It contains an EditText when I launch an intent and come back whatever the user entered is restored.
The 2nd contains a TextView and the 3rd a Spinner. They do not save when I launch my intent and return.
I think know how to preserve the data using onSaveInstanceState and onRestoreInstanceState in my custom views, However when the activity containing the custom views is not killed (meaning it is only paused), and I return onRestoreInstanceState is not called.
This is what I'm calling in my custom views when I need to save them.
#Override
public Parcelable onSaveInstanceState() {
textValue = editText.getText().toString();
Bundle bundle = new Bundle();
bundle.putParcelable("instanceState", super.onSaveInstanceState());
bundle.putString(TEXT_VALUE_KEY, this.textValue);
return bundle;
}
#Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
textValue = bundle.getString(TEXT_VALUE_KEY);
editText.setText(textValue);
super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
return;
}
super.onRestoreInstanceState(state);
}
I'm unsure I what I should do since onRestoreInstanceState is not called. I think the EditText customView works because default android behavior saved them temporarily, but it doesn't save spinners or TextViews.
You should change your onCreate() method in order to check if the Activity has already called onSavedInstanceState like this:
#Override
public void onCreate(Bundle savedInstanceState) {
if(savedInstanceState == null) {
// The activity haven't called onSavedInstanceState(), which means you should initialize your objects and UI here
}
else {
// Whatever states you saved onSavedInstanceState() are stored in savedInstaceState, so use them to reconstruct your customViews
}
}

Saving and Restoring Custom Object in fragment

I have two fragments FragmentA and FragmentB.
From Fragment A i update Views in Fragment B by passing custom object as a parameter to a method in Fragment B.
For example below is the Fragment B:
public class FragmentB extends Fragment {
private ArrayList<String> customObj = new ArrayList<String>();
public void updateViews(ArrayList<String> obj) {
customObj = obj;
}
#Override
public void onSaveInstanceState(Bundle outState) {
System.out.println("Custom Object : "+customObj);//custom object is always empty
}
Now, whenever i rotate the screen, the custom object is al;ways empty.
Note
Here I am just updating the views of Fragement B. The object is passed as a parameter from Fragment A to the method updateViews of Fragment B.
Also, i am not looking forward to define static for custom object.
You can do two things
1. Store that custom object somewhere which is not created every time orientation is changed i.e. Application class, Service etc.
2. Store that object inside the Bundle which you get as a parameter inside OnSaveInstanceState() and use that Bundle to set the Object in OnRestoreInstanceState() or OnCreate()...
For Example,
#Override
protected void onCreate(Bundle savedInstanceState) {
Logger.d("Dash onCreate");
super.onCreate(savedInstanceState);
...
// create fragments to use
if (savedInstanceState != null) {
//get your Object
}
if (yourObject != null)
//restore the View using yourObject
...
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
Now in your Custom Class you will have to either implement Serializable or Parcelable for storing that inside a Bundle. Serialization is simple but if you implement Parcellable you have more control on it.
Some other similar threads on SO related to this topic are
Similar Example
Another good example
You can get a similar Example in Android Documentations for Fragments.

Categories

Resources