Home » Android » android – Overlapping hidden fragments after application gets killed and restored

android – Overlapping hidden fragments after application gets killed and restored

Posted by: admin April 23, 2020 Leave a comment

Questions:

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;
How to&Answers:

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);
        }
    }
}

Answer:

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();
        }
    }
}

Answer:

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

Answer:

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

Answer:

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.

Answer:

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.

Answer:

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);
}