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>
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()
:and the
isDevicePortrait()
would be like:*Notice that we cannot use
getResources().getConfiguration().orientation
to determine if the device is currently literally Portrait. It is because theResources
object is changed RIGHT AFTER the screen rotates - EVEN BEFOREonSaveInstanceState()
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 variableprivate int current_orientation;
and initialise it bycurrent_orientation = getResources().getConfiguration().orientation;
inonCreate()
. This seems neater. But we should be aware not to change it anywhere during the Activity lifecycle.*Be sure we
remove_fragments()
beforesuper.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!
I've had some success with the following method.
This does the right thing based on the fragment's state.
This handles the specific memoized fragment.
And then in the lifecycle methods: