Real approach for Avoiding Re-creation of Fragment

2020-03-08 06:36发布

i'm finding a Real approach for Avoiding Re-creation of Fragment after Screen Rotate

if (container == null) { return null; } is not really avoiding the Fragment to be re-created at all. (illustrated below)


Where is the Official Fragment Developer Guide?

The official guide we are concerning is at http://developer.android.com/guide/components/fragments.html. The partial example code is at the bottom of the guide. As far as i know, the full sample code is available in the "Samples for SDK" under Android 3.0 (API 11). Also, i have done minimal modification to the sample code for it to run in API 10- and added some debug messages, which is included at the bottom of this question.

Where is R.id.a_item?

You may find in the Developer Guide Example the follow code stub:

if (index == 0) {
    ft.replace(R.id.details, details);
} else {
    ft.replace(R.id.a_item, details);
}

i did some searches on the Internet and find some others are also concerning where is the R.id.a_item. After inspecting the sample in API 11, i am quite sure it is just a meaningless typo. There are no such lines in the sample at all.


Real approach for avoiding re-creation of Fragment after screen rotate?

There are many existing discussions over the web. But it seems that there is not a "real" solution yet.

I have added lots of debug messages into the code below to keep track of the lifecycle of the DetailsFragment class. Try to (1) initiate the program in portrait mode, then (2) turn the device into landscape mode, then (3) turn it back to portrait, (4) to landscape again, (5) back to portrait again, and finally (6) quit it. We will have the following debug messages:

(1) Initiate in portrait mode

TitlesFragment.onCreate() Bundle=null

Only TitlesFragment is created. DetailsFragment is not shown yet.

(2) Turn into landscape mode

TitlesFragment.onCreate() Bundle=Bundle[{shownChoice=-1, android:view_state=android.util.SparseArray@4051d3a8, curChoice=0}]
DetailsFragment.onAttach() Activity=com.example.android.apis.app.FragmentLayout@4051d640
DetailsFragment.onCreate() Bundle=null
DetailsFragment.onCreateView() Activity=android.widget.FrameLayout@4050df68
DetailsFragment.onActivityCreated() Bundle=null
DetailsFragment.onStart()
DetailsFragment.onResume()

First, TitlesFragment is re-created (with the savedInstanceState Bundle). Then DetailsFragment is created dynamically (by TitlesFragment.onActivityCreated(), calling showDetails(), using FragmentTransaction).

(3) Back to portrait mode

DetailsFragment.onPause()
DetailsFragment.onStop()
DetailsFragment.onDestroyView()
DetailsFragment.onDestroy()
DetailsFragment.onDetach()
DetailsFragment.onAttach() Activity=com.example.android.apis.app.FragmentLayout@40527f70
DetailsFragment.onCreate() Bundle=null
TitlesFragment.onCreate() Bundle=Bundle[{shownChoice=0, android:view_state=android.util.SparseArray@405144b0, curChoice=0}]
DetailsFragment.onCreateView() Activity=null
DetailsFragment.onActivityCreated() Bundle=null
DetailsFragment.onStart()
DetailsFragment.onResume()

Here is the first place that we are concerning about the real re-creation avoiding approach.

It is because the DetailsFragment was previously attached to the layout-land/fragment_layout.xml <FrameLayout> ViewGroup in landscape mode. And it is having an ID (R.id.details). When the screen rotates, the ViewGroup, which is an instance of the DetailsFragment, is saved into the Activity FragmentLayout's Bundle, in the FragmentLayout's onSaveInstanceState(). After entering portrait mode, the DetailsFragment is re-created. But it is not needed in portrait mode.

In the sample (and so as many others suggested), the DetailsFragment class uses if (container == null) { return null; } in onCreateView() to avoid the DetailsFragment showing up in portrait mode. However, as shown in the above debug messages, the DetailsFragment is still alive in the background, as an orphan, having all the lifecycle method calls.

(4) To landscape mode again

DetailsFragment.onPause()
DetailsFragment.onStop()
DetailsFragment.onDestroyView()
DetailsFragment.onDestroy()
DetailsFragment.onDetach()
DetailsFragment.onAttach() Activity=com.example.android.apis.app.FragmentLayout@4052c7d8
DetailsFragment.onCreate() Bundle=null
TitlesFragment.onCreate() Bundle=Bundle[{shownChoice=0, android:view_state=android.util.SparseArray@40521b80, curChoice=0}]
DetailsFragment.onCreateView() Activity=android.widget.FrameLayout@40525270
DetailsFragment.onActivityCreated() Bundle=null
DetailsFragment.onStart()
DetailsFragment.onResume()

Notice in the first 5 lines, the DetailsFragment completes its lifecycle states then destroy and detached.

This further proves the if (container == null) { return null; } method is not a real approach to get rid of the DetailsFragment instance. (i thought the Garbage Collector would destroy this dangling child but it didn't. It's because Android does allow a dangling Fragment. Ref: Adding a fragment without a UI.)

As far as i understand, starting from the 6th line, it should be a new DetailsFragment instance creating by the TitlesFragment, as it did in (2). But i cannot explain why the DetailsFragment's onAttach() and onCreate() methods are called before the TitlesFragment's onCreate().

But the null Bundle in DetailsFragment's onCreate() would prove it is a new instance.

To my understanding, the previous dangling DetailsFragment instance is not re-creating this time because it does not have an ID. So it did not auto-save with the view hierarchy into the savedInstanceState Bundle.

(5) Back to portrait mode again

DetailsFragment.onPause()
DetailsFragment.onStop()
DetailsFragment.onDestroyView()
DetailsFragment.onDestroy()
DetailsFragment.onDetach()
DetailsFragment.onAttach() Activity=com.example.android.apis.app.FragmentLayout@4052d7d8
DetailsFragment.onCreate() Bundle=null
TitlesFragment.onCreate() Bundle=Bundle[{shownChoice=0, android:view_state=android.util.SparseArray@40534e30, curChoice=0}]
DetailsFragment.onCreateView() Activity=null
DetailsFragment.onActivityCreated() Bundle=null
DetailsFragment.onStart()
DetailsFragment.onResume()

Notice here that all the lifecycle callbacks are identical to the first time back to portrait in (3), except with the different Activity ID (40527f70 vs 4052d7d8) and view_state Bundle (405144b0 vs 40534e30). This is reasonable. Both the FragmentLayout Activity and the Instance State Bundle are re-created.

(6) Quit (by BACK button)

I/System.out(29845): DetailsFragment.onPause() I/System.out(29845): DetailsFragment.onStop() I/System.out(29845): DetailsFragment.onDestroyView() I/System.out(29845): DetailsFragment.onDestroy() I/System.out(29845): DetailsFragment.onDetach()

It would be perfect if we can remove the DetailsFragment in FragmentLayout's onDestroy(). But the FragmentTransaction's remove() method needs to be called before the onSaveInstanceState(). However there is no way to determine if it is a screen rotate or not in onSaveInstanceState().

It is also not possible to remove the DetailsFragment in FragmentLayout's onSaveInstanceState() anyway. First, if the DetailsFragment is just partially obscured by a dialogue box, it will be disappeared in the background. In addition, in cases of obscured by dialogue box, or switching activities, neither onCreate(Bundle) nor onRestoreInstanceState(Bundle) will be called again. Thus we have no where to restore the Fragment (and retrieve data from Bundle).


Source codes & files

FragmentLayout.java

package com.example.android.apis.app;

import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.ListFragment;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ScrollView;
import android.widget.TextView;

public class FragmentLayout extends FragmentActivity {

    private final static class Shakespeare {
        public static final String[] TITLES = { "Love", "Hate", "One", "Day" };
        public static final String[] DIALOGUE = {
            "Love Love Love Love Love",
            "Hate Hate Hate Hate Hate",
            "One One One One One",
            "Day Day Day Day Day" };
    }

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

    public static class DetailsActivity extends FragmentActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            if (getResources().getConfiguration().orientation
                    == Configuration.ORIENTATION_LANDSCAPE) {
                // If the screen is now in landscape mode, we can show the
                // dialog in-line with the list so we don't need this activity.
                finish();
                return;
            }

            if (savedInstanceState == null) {
                // During initial setup, plug in the details fragment.
                DetailsFragment details = new DetailsFragment();
                details.setArguments(getIntent().getExtras());
                getSupportFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
            }
        }
    }

    public static class TitlesFragment extends ListFragment {
        boolean mDualPane;
        int mCurCheckPosition = 0;
        int mShownCheckPosition = -1;

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            System.out.println(getClass().getSimpleName() + ".onCreate() Bundle=" + 
                    (savedInstanceState == null ? null : savedInstanceState));
        }

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

            // Populate list with our static array of titles.
            setListAdapter(new ArrayAdapter<String>(getActivity(),
                    android.R.layout.simple_list_item_1, Shakespeare.TITLES));
            // API 11:android.R.layout.simple_list_item_activated_1

            // Check to see if we have a frame in which to embed the details
            // fragment directly in the containing UI.
            View detailsFrame = getActivity().findViewById(R.id.details);
            mDualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;

            if (savedInstanceState != null) {
                // Restore last state for checked position.
                mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
                mShownCheckPosition = savedInstanceState.getInt("shownChoice", -1);
            }

            if (mDualPane) {
                // In dual-pane mode, the list view highlights the selected item.
                getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
                // Make sure our UI is in the correct state.
                showDetails(mCurCheckPosition);
            }
        }

        @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putInt("curChoice", mCurCheckPosition);
            outState.putInt("shownChoice", mShownCheckPosition);
        }

        @Override
        public void onListItemClick(ListView l, View v, int position, long id) {
            showDetails(position);
        }

        /**
         * Helper function to show the details of a selected item, either by
         * displaying a fragment in-place in the current UI, or starting a
         * whole new activity in which it is displayed.
         */
        void showDetails(int index) {
            mCurCheckPosition = index;

            if (mDualPane) {
                // We can display everything in-place with fragments, so update
                // the list to highlight the selected item and show the data.
                getListView().setItemChecked(index, true);

                if (mShownCheckPosition != mCurCheckPosition) {
                    // If we are not currently showing a fragment for the new
                    // position, we need to create and install a new one.
                    DetailsFragment df = DetailsFragment.newInstance(index);

                    // Execute a transaction, replacing any existing fragment
                    // with this one inside the frame.
                    FragmentTransaction ft = getFragmentManager().beginTransaction();
                    ft.replace(R.id.details, df);
                    ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                    ft.commit();
                    mShownCheckPosition = index;
                }

            } else {
                // Otherwise we need to launch a new activity to display
                // the dialog fragment with selected text.
                Intent intent = new Intent();
                intent.setClass(getActivity(), DetailsActivity.class);
                intent.putExtra("index", index);
                startActivity(intent);
            }
        }
    }

    public static class DetailsFragment extends Fragment {
        /**
         * Create a new instance of DetailsFragment, initialized to
         * show the text at 'index'.
         */
        public static DetailsFragment newInstance(int index) {
            DetailsFragment f = new DetailsFragment();

            // Supply index input as an argument.
            Bundle args = new Bundle();
            args.putInt("index", index);
            f.setArguments(args);

            return f;
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            System.out.println(getClass().getSimpleName() + ".onCreate() Bundle=" + 
                    (savedInstanceState == null ? null : savedInstanceState));
        }
        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            System.out.println(getClass().getSimpleName() + ".onAttach() Activity=" + 
                    (activity == null ? null : activity));
        }
        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            System.out.println(getClass().getSimpleName() + ".onActivityCreated() Bundle=" + 
                    (savedInstanceState == null ? null : savedInstanceState));
        }
        @Override
        public void onStart() { super.onStart(); System.out.println(getClass().getSimpleName() + ".onStart()"); }
        @Override
        public void onResume() { super.onResume(); System.out.println(getClass().getSimpleName() + ".onResume()"); }
        @Override
        public void onPause() { super.onPause(); System.out.println(getClass().getSimpleName() + ".onPause()"); }
        @Override
        public void onStop() { super.onStop(); System.out.println(getClass().getSimpleName() + ".onStop()"); }
        @Override
        public void onDestroyView() { super.onDestroyView(); System.out.println(getClass().getSimpleName() + ".onDestroyView()"); }
        @Override
        public void onDestroy() { super.onDestroy(); System.out.println(getClass().getSimpleName() + ".onDestroy()"); }
        @Override
        public void onDetach() { super.onDetach(); System.out.println(getClass().getSimpleName() + ".onDetach()"); }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            System.out.println(getClass().getSimpleName() + ".onCreateView() Activity=" + 
                    (container == null ? null : container));

            if (container == null) {
                // We have different layouts, and in one of them this
                // fragment's containing frame doesn't exist.  The fragment
                // may still be created from its saved state, but there is
                // no reason to try to create its view hierarchy because it
                // won't be displayed.  Note this is not needed -- we could
                // just run the code below, where we would create and return
                // the view hierarchy; it would just never be used.
                return null;
            }

            ScrollView scroller = new ScrollView(getActivity());
            TextView text = new TextView(getActivity());
            int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    4, getActivity().getResources().getDisplayMetrics());
            text.setPadding(padding, padding, padding, padding);
            scroller.addView(text);
            text.setText(Shakespeare.DIALOGUE[getArguments().getInt("index", 0)]);
            return scroller;
        }
    }

}

layout/fragment_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles"
            android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

layout-land/fragment_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:baselineAligned="false"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent" />

    <FrameLayout android:id="@+id/details" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent" />
    <!-- API 11:android:background="?android:attr/detailsElementBackground" -->

</LinearLayout>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.apis.app"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="4"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.android.apis.app.FragmentLayout"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name="com.example.android.apis.app.FragmentLayout$DetailsActivity"
            android:label="@string/app_name" >
        </activity>
    </application>

</manifest>

2条回答
SAY GOODBYE
2楼-- · 2020-03-08 07:10

Thanks @RogerGarzonNieto very much for spotting the method that disable auto Activity re-creation upon orientation changes. It is very useful. I am sure i will have to use it in some situations in the further.

For just avoiding Fragments re-creation upon screen rotates, i have found a simpler method that we can still allow the Activity re-creates as usual.

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 you 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. Thanks!

查看更多
smile是对你的礼貌
3楼-- · 2020-03-08 07:24

I've had some success with the following method.

This does the right thing based on the fragment's state.

public void activate(FragmentTransaction ft, Fragment f, String tag, int resId) {
    boolean changed =   resId != f.getId();

    if (changed && (f.isAdded() || f.isDetached())) {
        ft.remove(f);
        ft.add(resId, f, tag);
        return;
    }

    // Currently in a detached mode
    if (f.isDetached()) {
        ft.attach(f);
        return;
    }

    // Not in fragment manager add
    if (!f.isAdded() && ! f.isDetached()) {
        ft.add(resId, f, tag);
        return;
    }
}

This handles the specific memoized fragment.

private enum FragmentOp {
    ADD
    ,DETACH
    ,REMOVE
    ;
}

private void ensureFragmentState(FragmentOp op) {
    if (null == iChild) {
        iChild = (FragmentChild) iFM.findFragmentById(R.id.fragment_phonenumber_details);
    }

    FragmentTransaction ft = iFM.beginTransaction();
    switch(op) {
    case ADD:
        if (null == iChild) {
            iChild = new FragmentChild();
        }
        activate(ft, iChild, null, R.id.fragment_phonenumber_details);
        break;
    case DETACH:
        if (null != iChild) {
            iChild.deactivate(ft);
        }
        break;
    case REMOVE:
        if (null != iChild) {
            iChild.remove(ft);
        }
        break;
    }
    // Only if something shows up did we do anything!
    if (null != iChild) {
        ft.commit();
    }
}

And then in the lifecycle methods:

@Override public void onResume() {
    super.onResume();
    if (iDualPane) {
        ensureFragmentState(FragmentOp.ADD);
    }
}

@Override public void onPause() {
    super.onPause();

    recordState();   // Grab what I need from the child fragment

    ensureFragmentState(FragmentOp.DETACH);
}
查看更多
登录 后发表回答