Handle Fragment duplication on Screen Rotate (with sample code)
A

4

6

There are some similar answers, but not to this situation.


My situation is simple.

I have an Activity with two different layouts, one in Portrait, another in Landscape.

In Portrait, i use <FrameLayout> and add Fragment into it dynamically.

In Landscape, i use <fragment> so the Fragment is static. (in fact this doesn't matter)

It first starts in Portrait, then i added the Fragment by simply:

ListFrag listFrag = new ListFrag();
getSupportFragmentManager().beginTransaction()
        .replace(R.id.FragmentContainer, listFrag).commit();

where ListFrag extends ListFragment.

Then i do a screen rotate. I found the listFrag is re-creating in the Landscape mode. (In which i noticed the onCreate() method is called again with a non-null Bundle)

i tried to use setRetainInstance(false) like @NPike said in this post. But the getRetainInstance() is already false by default. It does not do what i expected as the docs said. Could anyone please explain?


The fragment i am dealing with, is a ListFragment, which does setListAdapter() in onCreate(). So the if (container == null) return null; method cannot be used here. (or i dont know how to apply).

I got some hints from this post. Should i use if (bundle != null) setListAdapter(null); else setListAdapter(new ...); in my ListFragment? But is there a nicer way to indeed removing/deleting the fragment when it is destroyed/detached, rather than doing it in its creation time? (so as the if (container == null) return null; method)


Edit:

The only neat way i found is doing getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentById(R.id.FragmentContainer)).commit(); in onSaveInstanceState(). But it will raise another problems.

  1. When screen is partially obscured, like WhatsApp or TXT dialogue boxes pop up, the fragment will be disappeared also. (this is relatively minor, just visual issue)

  2. When screen rotate, the Activity is completely destroyed and re-created. So i can re-add the fragment in onCreate(Bundle) or onRestoreInstanceState(Bundle). But in the case of (1), as well as switching Activities, neither onCreate(Bundle) nor onRestoreInstanceState(Bundle) will be called when user get back to my Activity. I have nowhere to recreate the Activity (and retrieve data from Bundle).

Sorry I didn't say it clearly that, i already have a decision making, which the getSupportFragmentManager()...replace(...).commit(); line only run in Portrait mode.


Example code

I have extracted the simple code, for better illustration of the situration :)

MainActivity.java

package com.example.fragremovetrial;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

public class MainActivity extends FragmentActivity {

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

        if (findViewById(R.id.FragmentContainer) != null) {
            System.out.println("-- Portrait --");       // Portrait
            ListFrag listFrag = new ListFrag();
            getSupportFragmentManager().beginTransaction()
                    .replace(R.id.FragmentContainer, listFrag).commit();
        } else {
            System.out.println("-- Landscape --");      // Landscape
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (findViewById(R.id.FragmentContainer) != null) {
            System.out.println("getRetainInstance = " +
                    getSupportFragmentManager().findFragmentById(R.id.FragmentContainer).getRetainInstance());
        }
    }
}

layout/activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/FragmentContainer"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" />

layout-land/activity_main.xml (doesn't matter)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
</LinearLayout>

ListFrag.java

package com.example.fragremovetrial;

import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.widget.ArrayAdapter;

public class ListFrag extends ListFragment {
    private String[] MenuItems = { "Content A", "Contnet B" };

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

        System.out.println("ListFrag.onCreate(): " + (savedInstanceState == null ? null : savedInstanceState));

        setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, MenuItems));
    }
}

Notice i got the following

Debug messages

-- Portrait --
ListFrag.onCreate(): null
getRetainInstance = false

(rotate Port -> Land)

ListFrag.onCreate(): Bundle[{android:view_state=android.util.SparseArray@4052dd28}]
-- Landscape --
Previously focused view reported id 16908298 during save, but can't be found during restore.

(rotate Land -> Port)

ListFrag.onCreate(): Bundle[{android:view_state=android.util.SparseArray@405166c8}]
-- Portrait --
ListFrag.onCreate(): null
getRetainInstance = false

(rotate Port -> Land)

ListFrag.onCreate(): Bundle[{android:view_state=android.util.SparseArray@4050fb40}]
-- Landscape --
Previously focused view reported id 16908298 during save, but can't be found during restore.

(rotate Land -> Port)

ListFrag.onCreate(): Bundle[{android:view_state=android.util.SparseArray@40528c60}]
-- Portrait --
ListFrag.onCreate(): null
getRetainInstance = false

where the number of Fragment created will not increase infinitely as screen keep rotating, which i cannot explain. (please help)

Arand answered 19/3, 2013 at 10:1 Comment(2)
After further reading on the docs, i guess i understand why setRetainInstance(false) isnt working in my case. (1) When setRetainInstance(true), as docs says, it calls onDetach() then onAttach(), and skip onDestroy() & onCreate(). (2) Thus it means when setRetainInstance(false), the Fragment will be onDestroy() then onCreate(Bundle). This means setRetainInstance() is not something dealing with "keeping/removing" the Fragment. It controls only "retain/recreate" it instead.Arand
A more structured inspection on #15537179Arand
A
2

I finally come up with a solution to prevent unwanted Fragment re-creating upon the Activity re-creates. It is like what i mentioned in the question:

The only neat way i found is doing getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentById(R.id.FragmentContainer)).commit(); in onSaveInstanceState(). But it will raise another problems...

... with some improvements.

In onSaveInstanceState():

@Override
protected void onSaveInstanceState(Bundle outState) {
    if (isPortrait2Landscape()) {
        remove_fragments();
    }
    super.onSaveInstanceState(outState);
}

private boolean isPortrait2Landscape() {
    return isDevicePortrait() && (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
}

and the isDevicePortrait() would be like:

private boolean isDevicePortrait() {
    return (findViewById(R.id.A_View_Only_In_Portrait) != null);
}

*Notice that we cannot use getResources().getConfiguration().orientation to determine if the device is currently literally Portrait. It is because the Resources object is changed RIGHT AFTER the screen rotates - EVEN BEFORE onSaveInstanceState() is called!!

If we do not want to use findViewById() to test orientation (for any reasons, and it's not so neat afterall), keep a global variable private int current_orientation; and initialise it by current_orientation = getResources().getConfiguration().orientation; in onCreate(). This seems neater. But we should be aware not to change it anywhere during the Activity lifecycle.

*Be sure we remove_fragments() before super.onSaveInstanceState().

(Because in my case, i remove the Fragments from the Layout, and from the Activity. If it is after super.onSaveInstanceState(), the Layout will already be saved into the Bundle. Then the Fragments will also be re-created after the Activity re-creates. ###)

### I have proved this phenomenon. But the reason of What to determine a Fragment restore upon Activity re-create? is just by my guess. If you have any ideas about it, please answer my another question.

Arand answered 1/4, 2013 at 19:4 Comment(0)
E
3

The correct way to handle this is to put the following in your onCreate()

if (savedInstanceState == null) {
     // Do fragment transaction.
}
Ellison answered 21/7, 2014 at 17:46 Comment(5)
Fragment will be recreated anyway if it has been already created in other orientation, no matter if you will add(replace) it in OnCreate with (savedInstanceState == null) . Simly, see log - Fragments onCreateView and onAttach are called even before your activity will be called onCreate!!Vaporize
Yes the fragment will be recreated automatically on rotation, though it doesn't matter when or where. You will get a duplicate fragment if you always add your fragment in the Activity's onCreate. savedInstanceState will only be null the first time your activity is create and will not be null after a rotation. Therefore you only manually add your fragment when savedInstanceState is null. Maybe I'm not understanding your comment.Ellison
Yep, I totaly understand this. But I think in terms of performance - is it smart to keep in memory all those fragments, even if they are not visible? Is there any way to tell fragment manager not to recreate them? Or it is ok to allow fragment live , even if it not needed on the screen (for example in portrait mode)Vaporize
setRetainInstance(true) will keep that actually fragment in memory through rotation and reuse it. Your activity will still be destroyed and recreated, there is no way to prevent that. After rotation the system will then attach your fragment to the new activity. Another way to think of this is "refreshing the activity" that your fragment is using. Just like Activities cannot survive rotation neither can views, so your fragment will get onDestroyView and onCreateViewcalled plus all the lifecycle inbetween. Note:"The fragment's onCreate will only be called once and not during rotation.Ellison
If setRetainInstance(flase) then during rotation your old fragment will be detach and garbage collected. Therefore it will not be kept in memory. The system will create a brand new instance of your previous fragment after rotation. You can log all of your pointers to observe this. As far as I know there is no way to tell the system to not recreate your fragments on rotation. I cannot think of a good use case for that though.Ellison
A
2

I finally come up with a solution to prevent unwanted Fragment re-creating upon the Activity re-creates. It is like what i mentioned in the question:

The only neat way i found is doing getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentById(R.id.FragmentContainer)).commit(); in onSaveInstanceState(). But it will raise another problems...

... with some improvements.

In onSaveInstanceState():

@Override
protected void onSaveInstanceState(Bundle outState) {
    if (isPortrait2Landscape()) {
        remove_fragments();
    }
    super.onSaveInstanceState(outState);
}

private boolean isPortrait2Landscape() {
    return isDevicePortrait() && (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
}

and the isDevicePortrait() would be like:

private boolean isDevicePortrait() {
    return (findViewById(R.id.A_View_Only_In_Portrait) != null);
}

*Notice that we cannot use getResources().getConfiguration().orientation to determine if the device is currently literally Portrait. It is because the Resources object is changed RIGHT AFTER the screen rotates - EVEN BEFORE onSaveInstanceState() is called!!

If we do not want to use findViewById() to test orientation (for any reasons, and it's not so neat afterall), keep a global variable private int current_orientation; and initialise it by current_orientation = getResources().getConfiguration().orientation; in onCreate(). This seems neater. But we should be aware not to change it anywhere during the Activity lifecycle.

*Be sure we remove_fragments() before super.onSaveInstanceState().

(Because in my case, i remove the Fragments from the Layout, and from the Activity. If it is after super.onSaveInstanceState(), the Layout will already be saved into the Bundle. Then the Fragments will also be re-created after the Activity re-creates. ###)

### I have proved this phenomenon. But the reason of What to determine a Fragment restore upon Activity re-create? is just by my guess. If you have any ideas about it, please answer my another question.

Arand answered 1/4, 2013 at 19:4 Comment(0)
B
0

In your onCreate() method you should only create your ListFrag if you are in portrait mode. You can do that by checking if the FrameLayout view that you only have in the portrait layout is not null.

if (findViewById(R.id.yourFrameLayout) != null) {
    // you are in portrait mode
    ListFrag listFrag = new ListFrag();
    getSupportFragmentManager().beginTransaction()
            .replace(R.id.FragmentContainer, listFrag).commit();
} else {
    // you are in landscape mode
    // get your Fragment from the xml
}

In the end if you switch from portrait to landscape layout, you don't want to have another ListFrag created, but use the Fragment you have specified in your xml.

Bakki answered 19/3, 2013 at 10:37 Comment(1)
Thanks @Bakki for your reply. But i have already made this decision choosing. Details in the revision of my question. Sorry i didnt make myself clear in the first time.Arand
V
0

@midnite ,first of all thanks for your question, I had the same question, on wich I worked for days. Now I wound some tricky resolution to this problem - and want to share this to community. But If someone has better solution, please write in comments.

So, I have same situation - different layouts for two device orientation. In portrait there is no need for DetailsFragment.

I simply in OnCreate trying to find already created fragments:

    fragment = (ToDoListFragment) getFragmentManager().findFragmentByTag(LISTFRAGMENT);
    frameDetailsFragment = (FrameLayout) findViewById(R.id.detailsFragment);
    detailsFragment = (DetailsFragment) getFragmentManager().findFragmentByTag(DETAILS_FRAGMENT);
    settingsFragment = (SettingsFragment) getFragmentManager().findFragmentByTag(SETTINGS_FRAGMENT);

Right after that I am adding my main fragment -

    if (fragment == null){
        fragment = new ToDoListFragment();
        getFragmentManager().beginTransaction()
                .add(R.id.container, fragment, LISTFRAGMENT)
                .commit();
    }

And clear back stask of my fragments:

  if (getFragmentManager().getBackStackEntryCount() > 0 ) {
        getFragmentManager().popBackStackImmediate();
    }

And the MOST interesting is here -

  destroyTmpFragments();

Here is the method itself:

  private void destroyTmpFragments(){
    if (detailsFragment != null && !detailsFragment.isVisible()) {
        Log.d("ANT", "detailsFragment != null, Destroying");
        getFragmentManager().beginTransaction()
                .remove(detailsFragment)
                .commit();

        detailsFragment = null;
    }

    if (settingsFragment != null && !settingsFragment.isVisible()) {
        Log.d("ANT", "settingsFragment != null, Destroying");
        getFragmentManager().beginTransaction()
                .remove(settingsFragment)
                .commit();

        settingsFragment = null;
    }
}

As you can see, I clean manually all fragments, that FragmentManager gentfully saved for me (great thanks to him). In log I have next lifecycle calls:

 04-24 23:03:27.164 3204    3204    D   ANT MainActivity onCreate()
 04-24 23:03:27.184 3204    3204    I   ANT DetailsFragment :: onCreateView
 04-24 23:03:27.204 3204    3204    I   ANT DetailsFragment :: onActivityCreated
 04-24 23:03:27.204 3204    3204    I   ANT DetailsFragment :: onDestroy
 04-24 23:03:27.208 3204    3204    I   ANT DetailsFragment :: onDetach

So in onActivityCreated make your views check for null - in case fragment is not visible. It will be deleted little bit later (It is so maybe cuz fragment manager's remove is async) After, in activity's code we can create brand new Fragment instance, with proper layout (fargmnet's placeholder) and fragment will be able to find it's views for example, and will not produce NullPointerException

But, fragments that are in back stack are deleted pretty fast, without call to onActivityCreated (with th help of above code:

 if (getFragmentManager().getBackStackEntryCount() > 0 ) {
        getFragmentManager().popBackStackImmediate();
    }

At last, at the end i add fragment if I am in land orientation -

 if (frameDetailsFragment != null){
        Log.i("ANT", "frameDetailsFragment != null");

        if (EntryPool.getPool().getEntries().size() > 0) {
            if (detailsFragment == null) {
                detailsFragment = DetailsFragment.newInstance(EntryPool.getPool().getEntries().get(0), 0);
            }

            getFragmentManager().beginTransaction()
                    .replace(R.id.detailsFragment, detailsFragment, DETAILS_FRAGMENT)
                    .commit();
        }
    }
Vaporize answered 24/4, 2015 at 23:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.