hidden backstack fragments re-shown on configuration change - android

I am building a one activity-multiple fragments application. I add to the backstack after every transaction. After a couple of hiding and showing fragments and then I rotate the phone, all the fragments added on the container were restored and every fragment is on top of the other.
What can be the problem? Why is my activity showing the fragments I have previously hidden?
I am thinking of hiding all the previously-hidden-now-shown fragments but is there a more 'graceful' way of doing this?

Use setRetainInstance(true) on each fragment and your problem will disappear.
Warning: setting this to true will change the Fragments life-cycle.
While setRetainInstance(true) resolves the issue, there may be cases where you don't want to use it.
To fix that, setup a boolean attribute on the Fragment and restore the visibility:
private boolean mVisible = true;
#Override
public void onCreate(Bundle _savedInstanceState) {
super.onCreate(_savedInstanceState);
if (_savedInstanceState!=null) {
mVisible = _savedInstanceState.getBoolean("mVisible");
}
if (!mVisible) {
getFragmentManager().beginTransaction().hide(this).commit();
}
// Hey! no setRetainInstance(true) used here.
}
#Override
public void onHiddenChanged(boolean _hidden) {
super.onHiddenChanged(_hidden);
mVisible = !_hidden;
}
#Override
public void onSaveInstanceState(Bundle _outState) {
super.onSaveInstanceState(_outState);
if (_outState!=null) {
_outState.putBoolean("mVisible", mVisible);
}
}
Once the configuration changes (e.g. screen orientation), the instance will be destroyed, but the Bundle will be stored and injected to the new Fragment instance.

I had the same problem. you should check source code in the function onCreateView() of your activity.
public class MainActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
if(savedInstanceState == null){//for the first time
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
FragmentExample fragment = new FragmentExample();
fragmentTransaction.add(R.id.layout_main, fragment);
fragmentTransaction.commit();
}else{//savedInstanceState != null
//for configuration change or Activity UI is destroyed by OS to get memory
//no need to add Fragment to container view R.id.layout_main again
//because FragmentManager supported add the existed Fragment to R.id.layout_main if R.id.layout_main is existed.
//here is one different between Fragment and View
}
}
activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/layout_main">

You might want to try to use the replace() function rather than hide and show. I had the same problem when I started using Fragments and using the replace function really helped manage the Fragments better. Here is a quick example:
fragmentManager.replace(R.id.fragmentContainer, desiredFragment, DESIRED_FRAGMENT_TAG)
.addToBackStack(null)
.commit();

Related

Android Navigation Drawer wrong toolbar title onResume

I have a Navigation Drawer in the main activity of my app. On the onCreate method of the activity I initialize one of the fragments like this :
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
MenuItem menuItem = navigationView.getMenu().findItem(R.id.menu_history);
openFragment(menuItem);
}
public void openFragment(MenuItem menuItem){
Fragment newFragment = null;
switch (menuItem.getItemId()){
case R.id.menu_history :
newFragment = new HistoryFragment();
break;
//.....
}
if (newFragment != null){
//Replace content frame in activity_main.xml with newFragment
getSupportFragmentManager().beginTransaction()
.replace(R.id.content_frame, newFragment)
.commit();
menuItem.setChecked(true);
getSupportActionBar().setTitle(menuItem.getTitle());
}
drawerLayout.closeDrawers();
}
This all works well, the fragment appears on startup with the title on the toolbar being "History". But when the app goes into onPause and then onResume the toolbar title switches from "History" to the app name. I suspect this is an issue with onResume not opening the fragment / returning to its previous state correctly, because when I added the following lines to onResume the issue stopped:
#Override
protected void onResume() {
super.onResume();
MenuItem menuItem = navigationView.getMenu().findItem(R.id.menu_history);
openFragment(menuItem);
}
That solution seems to fix it but that means it has to reload the fragment with the animations every time the app is resumed, which isn't optimal. Any ideas on how to fix this?
If it helps, to recreate the issue I found useful to make the app split screen, because it always calls onResume. Thanks.
Inside your openFragment() method, you write:
getSupportActionBar().setTitle(menuItem.getTitle());
This is the only thing that changes your toolbar's title.
Note that this code has nothing to do with your Fragments, per se. When your activity is destroyed and recreated, the active Fragment will be successfully (automatically) destroyed and recreated by the FragmentManager... but your openFragment() method won't run again and so nothing will update your toolbar's title.
There are numerous ways you could solve this. Probably the right thing to do is update your toolbar's title from within one of your Fragment's lifecycle methods.
Edit: A reasonable place to update your toolbar's title would be in your fragment's onActivityCreated() method. This will run both when the fragment is first added and during recreation. Something like:
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
((AppCompatActivity) getActivity()).getSupportActionBar().setTitle("hello world");
}
If you do not want to re-create the fragment each time, you should use saveInstanceState like mentioned here: https://stackoverflow.com/a/17135346/1505074

How to save an Activity's Fragment's transactional states?

Related Question.
I put together a simple app that goes like this:
Activity -> FirstFragment
Activity: onCreate() -> createFirstFragment()
FirstFragment firstFragment = (FirstFragment) getSupportFragmentManager().findFragmentByTag(FirstFragment.TAG);
if (firstFragment == null)
{
firstFragment = FirstFragment.newInstance();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.firstFragmentContainer, firstFragment, FirstFragment.TAG)
.hide(firstFragment)
//.addToBackStack(null)
.commit();
}
Plain and simple, during onCreate() add and hide a fragment so that I can do show/hide animations later.
Now, my question is this: why does the Activity/FragmentManager not remember this transaction (regardless of whether I .addToBackStack() or setRetainInstance(true) on the fragment) when the activity is killed and recreated? You can test this by checking the Do not keep activities developer option. Start the app, firstFragment is hidden as expected, minimize and come back, and viola! firstFragment is there for all the world to see!
I would expect that this sort of thing would be managed by Android, or do I need to specifically record all my transactions and repeat them when the app is recreated?
Any help/advice would be greatly appreciated!
Edit: Also see related logged bug
Use FragmentStatePagerAdapter like below in your main activity. This internally calls 'onSaveInstanceState' of the fragments and hence keeps the track of the changes you made and retains the transactional states
class MyAdapter extends FragmentStatePagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
// your code here
}
#Override
public int getCount() {
// returns no. of fragments count. in my case it is 4
return 4;
}
onCreate() in mainactivity generally looks like this:
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView (R.layout.scrollabletabs_main);
viewPager = (ViewPager) findViewById (R.id.pager);
FragmentManager fragManager = getSupportFragmentManager();
viewPager.setAdapter(new MyAdapter(fragManager));
}
From
http://developer.android.com/training/basics/fragments/fragment-ui.html it is mentioned that,
Note: When you remove or replace a fragment and add the transaction to the back stack, the fragment that is removed is stopped (not destroyed). If the user navigates back to restore the fragment, it restarts. If you do not add the transaction to the back stack, then the fragment is destroyed when removed or replaced.

Restoring Fragment state after configuration change if Activity layout changes

My Activity has a one-pane layout in portrait and a two-pane layout in landscape, using Fragments. I'd like the system to restore all the views after an orientation change, but I don't know where to start.
Is this even possible considering the layout is different (but similar) in the two orientations?
If anyone has implemented this (or at least tried), please share you experience.
Maybe I know what the problem is. Hope my answer helps someone want to solve such problem.
In case there are a Fragment in your Activity, you config that Activity in AndroidManifest.xml with
android:configChanges="keyboardHidden|orientation|screenSize"
then, you want to change the layout of the Activity when onConfigurationChanged triggered. Once the Activity inflate a new layout or using setContentView(R.layout.new_layout);, the fragment will disappear, however, that fragment still running and just disappear from the view.
So the problem is that fragment is attached to the previous layout, what you need to do:
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
if (fm.findFragmentById(R.id.pane_list) == null) {
// instantiate fragment and add to view
mFragment = new ItemFragment();
transaction.add(R.id.pane_list, mFragment );
} else {
// fragment already exists, we re-attahced it
mFragment = fm.findFragmentById(R.id.pane_list);
transaction.detach(mFragment);
transaction.attach(mFragment);
}
transaction.commit();
After detached from old layout and attached to new layout, problem solved.
Correct me if there any wrong :)
I urge you to use a Fragment with setRetainInstance(true) in its onCreate method. This makes the fragment retain its member variables across Activity configuration changes. Please read this blog post which explains the information you need:
http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("curChoice", mCurCheckPosition);
}
Which you can use later like this:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
// Restore last state for checked position.
mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
}
}

Getting back to a fragment keeping its previous state

I am an old Android programmer, but I used to use only Activities and I'm quite new with Fragments.
I've created an application with a menu bar (4 buttons) that, when clicked, are supposed to load each one a different Fragment above the menu bar.
The problem is that each time I click on one of the menu bar buttons, it reloads entirely the fragment and I would like it to show the Fragment on its previous state (just like the device Back button does).
How can I do such a thing?
Call from the main activity:
Fragment my_fragment = new MyFragment();
FragmentManager fragment_manager = getSupportFragmentManager();
FragmentTransaction fragment_transaction = fragment_manager.beginTransaction();
fragment_transaction.replace(R.id.fragment_container, my_fragment);
fragment_transaction.addToBackStack(null);
fragment_transaction.commit();
and most my MyFragment process code is in the function
onActivityCreated
You also can use onSaveInstanceState method as in activities,to restore data use Bundle savedInstanceState object, for example
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("test", 1);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
int testValue = savedInstanceState.getInt("test", 0);
}
}
http://developer.android.com/guide/components/fragments.html

Fragment gets initialized twice when reloading activity with tabs when orientation changes

I have a problem reloading an activity with tabs and fragments when I change the orientation of my device.
Here's the situation:
I have an activity which has 3 tabs in the action bar. Each tab loads a different fragment in a FrameLayout in main view. Everything works fine if I don't change the orientation of the device. But when I do that Android tries to initialize the currently selected fragment twice which produce the following error:
E/AndroidRuntime(2022): Caused by: android.view.InflateException: Binary XML file line #39: Error inflating class fragment
Here's the sequence of steps that produce the error:
I load the activity, select tab nr 2. and change the orientation of the device.
Android destroys the activity and the instance of the fragment loaded by tab nr 2 (from now on, 'Fragment 2'). Then it proceeds to create new instances of the activity and the fragment.
Inside Activity.onCreate() I add the first tab to the action bar. When I do that, this tab gets automatically selected. It may represent a problem in the future, but I don't mind about that now. onTabSelected gets called and a new instance of the first fragment is created and loaded (see code below).
I add all the other tabs without any event being triggered, which is fine.
I call ActionBar.selectTab(myTab) to select Tab nr 2.
onTabUnselected() gets called for the first tab, and then onTabSelected() for the second tab. This sequence replaces the current fragment for an instance of Fragment 2 (see code below).
Next, Fragment.onCreateView() is called on Fragment 2 instance and the fragment layout gets inflated.
Here is the problem. Android Calls onCreate() and then onCreateView() on the fragment instance ONCE AGAIN, which produces the exception when I try to inflate (a second time) the layout.
Obviously the problem is Android is initializing the fragment twice, but I don't know why.
I tried NOT selecting the second tab when I reaload the activity but the second fragment gets initialized anyway and it is not shown (since I didn't select its tab).
I found this question: Android Fragments recreated on orientation change
The user asks basically the same I do, but I don't like the chosen answer (it's only a workaroud). There must be some way to get this working without the android:configChanges trick.
In case it's not clear, what I want to know how whether to prevent the recreation of the fragment or to avoid the double initialization of it. It would be nice to know why is this happening also. :P
Here is the relevant code:
public class MyActivity extends Activity implements ActionBar.TabListener {
private static final String TAG_FRAGMENT_1 = "frag1";
private static final String TAG_FRAGMENT_2 = "frag2";
private static final String TAG_FRAGMENT_3 = "frag3";
Fragment frag1;
Fragment frag2;
Fragment frag3;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// my_layout contains a FragmentLayout inside
setContentView(R.layout.my_layout);
// Get a reference to the fragments created automatically by Android
// when reloading the activity
FragmentManager fm = getFragmentManager();
this.frag1 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_1);
this.frag2 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_2);
this.frag3 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_3)
ActionBar actionBar = getActionBar();
// snip...
// This triggers onTabSelected for the first tab
actionBar.addTab(actionBar.newTab()
.setText("Tab1").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_1));
actionBar.addTab(actionBar.newTab()
.setText("Tab2").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_2));
actionBar.addTab(actionBar.newTab()
.setText("Tab3").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_3));
Tab t = null;
// here I get a reference to the tab that must be selected
// snip...
// This triggers onTabUnselected/onTabSelected
ab.selectTab(t);
}
#Override
protected void onDestroy() {
// Not sure if this is necessary
this.frag1 = null;
this.frag2 = null;
this.frag3 = null;
super.onDestroy();
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
if (curFrag == null) {
curFrag = createFragmentInstanceForTag(tab.getTag().toString());
if(curFrag == null) {
// snip...
return;
}
}
ft.replace(R.id.fragment_container, curFrag, tab.getTag().toString());
}
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft)
{
Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
if (curFrag == null) {
// snip...
return;
}
ft.remove(curFrag);
}
private Fragment getFragmentInstanceForTag(String tag)
{
// Returns this.frag1, this.frag2 or this.frag3
// depending on which tag was passed as parameter
}
private Fragment createFragmentInstanceForTag(String tag)
{
// Returns a new instance of the fragment requested by tag
// and assigns it to this.frag1, this.frag2 or this.frag3
}
}
The code for the Fragment is irrelevant, it just returns an inflated view on onCreateView() method override.
I got a simple answer for that:
Just add setRetainInstance(true); to the Fragment's onAttach(Activity activity) or onActivityCreated(Bundle savedInstanceState).
These two are call-backs in the Fragment Class.
So basically, what setRetainInstance(true) does is:
It maintains the state of your fragment as it is, when it goes through:
onPause();
onStop();
It maintains the instance of the Fragment no matter what the Activity goes through.
The problem with it could be, if there are too many Fragments, it may put a strain on the System.
Hope it helps.
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
setRetainInstance(true);
}
Open for Correction as always. Regards, Edward Quixote.
It seems that, when the screen is rotated and the app restarted, it is recreating each Fragment by calling the default constructor for the Fragment's class.
I have encountered the same issue and used the following workaround:
in the fragment's onCreateView begining of:
if (mView != null) {
// Log.w(TAG, "Fragment initialized again");
((ViewGroup) mView.getParent()).removeView(mView);
return mView;
}
// normal onCreateView
mView = inflater.inflate(R.layout...)
I think this is a fool proof way to avoid re-inflating of the root view of the fragment:
private WeakReference<View> mRootView;
private LayoutInflater mInflater;
/**
* inflate the fragment layout , or use a previous one if already stored <br/>
* WARNING: do not use in any function other than onCreateView
* */
private View inflateRootView() {
View rootView = mRootView == null ? null : mRootView.get();
if (rootView != null) {
final ViewParent parent = rootView.getParent();
if (parent != null && parent instanceof ViewGroup)
((ViewGroup) parent).removeView(rootView);
return rootView;
}
rootView = mFadingHelper.createView(mInflater);
mRootView = new WeakReference<View>(rootView);
return rootView;
}
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
mInflater=inflater!=null?inflater:LayoutInflater.from(getActivity());
final View view = inflateRootView();
... //update your data on the views if needed
}
add
android:configChanges="orientation|screenSize"
in the manifest file
To protect activity recreate try to add configChanges in your Activity tag (in manifest), like:
android:configChanges="keyboardHidden|orientation|screenSize"
My code was a little different, but I believe our problem is the same.
In the onTabSelected I didn't use replace, I use add when is the first time creating the fragment and attach if isn't. In the onTabUnselected I use detach.
The problem is that when the view is destroyed, my Fragment was attached to the FragmentManager and never destroyed. To solve that I implemented on the onSaveInstanceBundle to detach the fragment from the FragmentManager.
The code was something like that:
FragmentTransition ft = getSupportFragmentManager().begin();
ft.detach(myFragment);
ft.commit();
In the first try I put that code in the onDestroy, but I get a exception telling me that I couldn't do it after the onSaveInstanceBundle, so I moved the code to the onSaveInstanceBundle and everything worked.
Sorry but the place where I work don't allow me to put the code here on StackOverflow. This is what I remember from the code. Feel free to edit the answer to add the code.
I think you are facing what I faced. I had a thread downloader for json which starts in onCreate() , each time I changed the orientation the thread is called and download is fired. I fixed this using onSaveInstance() and onRestoreInstance() to pass the json response in a list, in combination of checking if the list is not empty, so the extra download is not needed.
I hope this gives you a hint.
I solved this problem by using below code.
private void loadFragment(){
LogUtil.l(TAG,"loadFragment",true);
fm = getSupportFragmentManager();
Fragment hf = fm.findFragmentByTag("HOME");
Fragment sf = fm.findFragmentByTag("SETTING");
if(hf==null) {
homeFragment = getHomeFragment();// new HomeFragment();
settingsFragment = getSettingsFragment();// new Fragment();
fm.beginTransaction().add(R.id.fm_place, settingsFragment, "SETTING").hide(settingsFragment).commit();
fm.beginTransaction().add(R.id.fm_place, homeFragment, "HOME").commit();
activeFragment = homeFragment;
}else{
homeFragment = hf;
settingsFragment = sf;
activeFragment = sf;
}
}
Initiate this method in OnCreate();

Categories

Resources