33

I'm switching between fragments by hiding the last fragment and adding a new one (See code below) - adding it to the back-stack as well. This way, users can quickly switch between the fragments without reloading the fragment data.

This works well until the app is killed (Scenario: users uses several other apps and my app is getting persisted and killed).

When a user opens the app, it is being restored and all the fragments are shown - overlapping one another.

Question: How can the restored fragments be restored with their hidden state? Perhaps I'm missing some flag? somewhere? Perhaps there is a better solution for fast switching between fragments (without reloading the data)?

Sample code of adding fragments - invoked several times with different fragments upon clicking somewhere:

FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.hide(lastFragment);
fragmentTransaction.add(newFragment);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
lastFragment = newFragment;
AlikElzin-kilaka
  • 34,335
  • 35
  • 194
  • 277
  • 1
    When using `replace` (instead of `hide`), there is no overlapping of course. But when switching back to the previous fragment, it is reloaded - which is what I want to prevent. – AlikElzin-kilaka Apr 24 '13 at 10:04
  • 1
    [Side note] Another, very important, advantage of `hide` vs `replace` is when using the back button (Regardless of app being killed). When hiding instead of replacing and pushing the back button, the previous fragment pops back up without reloading its data, because it's already there. – AlikElzin-kilaka Apr 24 '13 at 17:30
  • 2
    This seems to me as something needed in almost any application, I would expect this to be easy and well documented :( – RonK Apr 25 '13 at 18:45
  • 1
    i think this's bug of android and don't know why it still going on in android 5. – VAdaihiep Apr 06 '15 at 14:47

7 Answers7

16

Hope somebody finds a better solution. I'll wait for one before I accept my solution:

In general, I use generated tags to find the unhidden fragments and hide them.

In details, I generate a unique tag for each fragment (StackEntry) and stack the tags as the fragments themselves get stacked. I persist the stack in the bundel and load it when the app gets restored in order to continure using it. Then I use the list of tags to find all of the unhidden fragments and hide them - except for the last one.

Heres sample code:

public class FragmentActivity extends Activity {

    private static final String FRAGMENT_STACK_KEY = "FRAGMENT_STACK_KEY";

    private Stack<StackEntry> fragmentsStack = new Stack<StackEntry>();

    public FragmentActivity() {
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.content_frame);

        if (savedInstanceState == null) {
            // Init for the first time - not restore
            // ...
        } else {
            Serializable serializable = savedInstanceState.getSerializable(FRAGMENT_STACK_KEY);
            if (serializable != null) {
                // Workaround Android bug.
                // See: http://stackoverflow.com/questions/13982192/when-using-an-android-bundle-why-does-a-serialised-stack-deserialise-as-an-arra
                // And: https://code.google.com/p/android/issues/detail?id=3847
                @SuppressWarnings("unchecked")
                List<StackEntry> arrayList = (List<StackEntry>) serializable;
                fragmentsStack = new Stack<StackEntry>();
                fragmentsStack.addAll(arrayList);
            }

            // Hide all the restored fragments instead of the last one
            if (fragmentsStack.size() > 1) {
                FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
                for (int i = 0; i < fragmentsStack.size()-1; i++) {
                    String fragTag = fragmentsStack.get(i).getFragTag();
                    Fragment fragment = getFragmentManager().findFragmentByTag(fragTag);
                    fragmentTransaction.hide(fragment);
                }
                fragmentTransaction.commit();
            }
        }
        getFragmentManager().addOnBackStackChangedListener(new OnBackStackChangedListener() {
            @Override
            public void onBackStackChanged() {
                Fragment lastFragment = getLastFragment();
                if (lastFragment.isHidden()) {
                    FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
                    fragmentTransaction.show(lastFragment);
                    fragmentTransaction.commit();
                }
            }
        });
    }

    private Fragment getLastFragment() {
        if (fragmentsStack.isEmpty()) return null;
        String fragTag = fragmentsStack.peek().getFragTag();
        Fragment fragment = getFragmentManager().findFragmentByTag(fragTag);
        return fragment;
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSerializable(FRAGMENT_STACK_KEY, fragmentsStack);
    }

    @Override
    public void onBackPressed() {
        if (!fragmentsStack.isEmpty()) {
            fragmentsStack.pop();
        }
    }

    public void switchContent(Fragment fragment) {
        FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
        Fragment lastFragment = getLastFragment();
        if (lastFragment != null) {
            fragmentTransaction.hide(lastFragment);
        }
        String fragTag;
        if (fragment.isAdded()) {
            fragmentTransaction.show(fragment);
            fragTag = fragment.getTag();
        } else {
            fragTag = Long.toString(System.currentTimeMillis());
            fragmentTransaction.add(R.id.content_frame, fragment, fragTag);
        }
        if (!isFirstFragment()) {
            // Add to backstack only the first content fragment and not the state before (that has nothing)
            fragmentTransaction.addToBackStack(null);
        }
        fragmentTransaction.commit();

        fragmentsStack.push(new StackEntry(fragTag));
    }

    public boolean isFirstFragment() {
        return fragmentsStack.size() == 0;
    }

    private static class StackEntry implements Serializable {
        private static final long serialVersionUID = -6162805540320628024L;

        private String fragTag = null;
        public StackEntry(String fragTag) {
            super();
            this.fragTag = fragTag;
        }
        public String getFragTag() {
            return fragTag;
        }
    }


    public static class Intent extends android.content.Intent {
        public Intent(Context packageContext) {
            super(packageContext, FragmentActivity.class);
        }
    }
}
AlikElzin-kilaka
  • 34,335
  • 35
  • 194
  • 277
  • 1
    Nice work, thanks for sharing. Obviously far from ideal -- to me, the OS should be keeping track of this state -- but provides something workable in the absence of anything else. – Brian Dupuis Aug 15 '13 at 13:45
  • Be careful with this approach, you might get into out of memory error. http://stackoverflow.com/a/17981491/1112882 BTW I really don't understand why someone need this type of functionality. There are so many nice, complex and big applications out there, they work fine, no lag no reloading. Can you tell me a scenario where you must use this functionality? – M-Wajeeh Nov 12 '13 at 05:29
  • To my understanding, if cache is properly implemented in your app then pressing back button will just take time to recreate GUI and that's it. And GUI creation time is so less that it is not noticeable to user. – M-Wajeeh Nov 12 '13 at 05:38
  • 2
    @M-WaJeEh - I want to hide old fragments so that when going right back to them, they won't be refreshed and I won't need to save any state. The problem was that when the app is killed by the OS, the fragments get resurrected without their hidden state - overlapping each other. Side note: I don't mind having the fragments reload themselves from scratch. – AlikElzin-kilaka Nov 12 '13 at 09:34
  • @AlikElzin-kilaka it's interesting, I was having the same issue and an answer below of changing the layout to a `LinearLayout` seems to have done the trick for me. Though I have no idea how it fixes it. – Alan Mar 09 '16 at 19:47
9

I also had this problem, here's one possible solution: have each fragment save its own state about whether or not it was hidden, and then hide itself in its onCreate.

@Override
public void onSaveInstanceState(Bundle bundle) {
    super.onSaveInstanceState(bundle);
    if (this.isHidden()) {
        bundle.putBoolean("hidden",  true);
    }
}

@Override
public void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    if (bundle != null) {
        if (bundle.getBoolean("hidden",  false)) {
            getFragmentManager()
                .beginTransaction()
                .hide(this)
                .commit();
        }
    }
}
Walt
  • 111
  • 1
  • 2
  • Nice. The only downfall I see here is that each fragment needs to handle it's own state inside a container. I would expect the container to handle the state of it's siblings - transparent to the fragment. – AlikElzin-kilaka Apr 06 '15 at 15:32
3

Finally i found simplest way to fix this issue: Change content_frame from FramLayout to LinearLayout.

VAdaihiep
  • 521
  • 9
  • 19
  • this seems to fix my issue when I changed it to a LinearLayout. I don't understand why this fixes it though. – Alan Mar 09 '16 at 19:46
  • this fixed my overlapping issue but fragment still not click able app ui is stuck on that fragment only it not updating with fragment. – Nishant Pardamwar Nov 11 '16 at 14:48
  • Same problem with me, Were you able to figure out a solution for this @NishantPardamwar? – Anuj Kumar Dec 05 '21 at 06:36
1

I had the same issue, and solved it by setting setRetainInstance(true); in the onCreate() method of each fragment.

manelizzard
  • 1,060
  • 8
  • 19
  • 1
    Quoting: *This can only be used with fragments not in the back stack*. This means a nono. See source: http://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean) – AlikElzin-kilaka Oct 18 '13 at 20:38
  • Hmm, it's true. Howere, it seems to work. Maybe because it is not "destroyed and re-created" – manelizzard Oct 21 '13 at 16:03
1

I had the exact same problem. Unfortunately the only good solution was to switch over to using fragmentTransaction.replace instead of fragmentTransaction.hide and add.

It sucked at the time but I'm actually glad I did it. It forced me to think about savedInstanceState and deal with it properly. You mentioned that on navigating back the fragment is reloaded. I had the exact same problem which forced me to handle savedInstanceState properly. There are 2 cases there.

  1. If the activity was not destroyed the only thing that needed recreating was the view (via onCreateView) everything else is still in memory so it was just a question of hooking the adapter up the view and you're done.

  2. If the activity was destroyed I needed to recreate the view and adapter. In order to minimize load time I stored the data needed to recreate the adapter in savedInstanceState

Your gripe is however valid, I don't know why Android doesn't support coming back with correct hidden state from a destroyed Activity with fragments that were using add and hide.

Abu Saleh Musa
  • 101
  • 1
  • 12
atlithorn
  • 401
  • 4
  • 8
1

Since you mentioned that you don't mind having the fragments reload themselves from scratch, why not use intent and start the main fragment activity all over again?

I faced the same issue like the one you have mentioned, where the fragments were overlapping with one another. I looked all over stackoverflow and found only this thread where this particular issue was discussed. I tried the solution provided by Walt, but it didn't work as expected.

The below workaround works at least for me, so sharing it in case someone ends up with this scenario

At onSaveInstanceState in the parent fragment, i set a marker to make sure there is something saved in the bundle.

public void onSaveInstanceState(Bundle outState) 
{
    // TODO Auto-generated method stub
    super.onSaveInstanceState(outState);

    outState.putString(TAG, "Get ready to be terminated");
};

And inside onCreate you can specify your class to be loaded using Intent when the saved instance state is not null,

@Override
protected void onCreate(Bundle savedInstanceState)
{       
    super.onCreate(savedInstanceState);
    this.setContentView(R.layout.layout);

    if(savedInstanceState != null)
    {   
        Intent myIntent = new Intent(this, your.class);

        // Closing the parent fragment
        finish();

        this.startActivity(myIntent);
    }
    else
    {
        // your main code
        ...........
        ...........
    };
};

This will make sure that your fragments are re-created from scratch.

In my case when a user opened my app i had a login screen and if they were already "logged in" before they will be redirected to the fragment screens.

During the kill process, I re-direct the users to the login page once the app comes to the foreground and from there onwards my already existing code takes care of re-directing the user again back to the newly created fragment screens.

Note: You need to make sure that your child fragment has no loose ends at onCreateView.

Vikram Ezhil
  • 995
  • 1
  • 9
  • 15
  • I still want to keep the backstack of the fragments. – AlikElzin-kilaka Nov 07 '14 at 20:03
  • If i am not mistaken, you want to show the particular fragment screen where the kill occurred to be shown again during re-creation? If that is the requirement, you can save the fragment position in a bundle and pass it via the intent and use it to show the selected fragment screen again. This worked for me, since i had to show the exact fragment screen which was killed before to the user. – Vikram Ezhil Nov 08 '14 at 10:19
  • Right. That's what I did in one of the answers. I don't see it in your answer's code. – AlikElzin-kilaka Nov 08 '14 at 14:18
0

I met the same problem,I think this is Android framework bug.Here is the issue.

However my way will work for you, we should override the onSaveInstanceState(Bundle outState) method, save our custom data to outState, but never to call super.onSaveInstanceState(outState);.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ......

    if (savedInstanceState != null) {
        mCustomVariable = savedInstanceState.getInt("variable", 0);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    //super.onSaveInstanceState(outState);
    outState.putInt("variable", mCustomVariable);
}
Folyd
  • 1,148
  • 1
  • 16
  • 20