2012年4月17日火曜日

Android レイアウトから生成した Fragment は FragmentTransaction の対象にしてはいけない

■ レイアウトから作成した Fragment には setArguments できない

前回のエントリで Fragment の Arguments の利点をいろいろ紹介しましたが、レイアウト内に <fragment> タグで定義して生成した Fragment には setArguments() をすることができません。

まず、Fragment.java のコードをみると Arguments を保持するフィールドである mArguments のコメントとして“生成時の引数である”と書いてあります。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#370 370 // Construction arguments; 371 Bundle mArguments; つまり、生成したあとの任意のタイミングでセットするようなものではない、ということです。

さらに、setArguments() の実装をみると

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#652 659 public void setArguments(Bundle args) { 660 if (mIndex >= 0) { 661 throw new IllegalStateException("Fragment already active"); 662 } 663 mArguments = args; 664 } 665 Fragment がアクティブになっている( = mIndex が 0 より大きい)ときに呼ぶと IllegalStateException が投げられることがわかります。

では、mIndex (初期値は -1)はいつセットされるのかというと、FragmentManager の makeActive() メソッドで行われます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1009 1009 void makeActive(Fragment f) { 1010 if (f.mIndex >= 0) { 1011 return; 1012 } 1013 1014 if (mAvailIndices == null || mAvailIndices.size() <= 0) { 1015 if (mActive == null) { 1016 mActive = new ArrayList(); 1017 } 1018 f.setIndex(mActive.size()); 1019 mActive.add(f); 1020 1021 } else { 1022 f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1)); 1023 mActive.set(f.mIndex, f); 1024 } 1025 }

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#593 593 final void setIndex(int index) { 594 mIndex = index; 595 mWho = "android:fragment:" + mIndex; 596 } この makeActive() メソッドは FragmentManager の addFragment() から呼ばれています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#1042 1042 public void addFragment(Fragment fragment, boolean moveToStateNow) { 1043 if (mAdded == null) { 1044 mAdded = new ArrayList(); 1045 } 1046 if (DEBUG) Log.v(TAG, "add: " + fragment); 1047 makeActive(fragment); 1048 if (!fragment.mDetached) { 1049 mAdded.add(fragment); 1050 fragment.mAdded = true; 1051 fragment.mRemoving = false; 1052 if (fragment.mHasMenu && fragment.mMenuVisible) { 1053 mNeedMenuInvalidate = true; 1054 } 1055 if (moveToStateNow) { 1056 moveToState(fragment); 1057 } 1058 } 1059 } レイアウトで定義された <fragment> は、 Activity の onCreateView() メソッドからこの addFragment() を呼ぶことで生成されます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4189 4199 public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { 4200 if (!"fragment".equals(name)) { 4201 return onCreateView(name, context, attrs); 4202 } ... 4223 Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null; ... 4234 if (fragment == null) { 4235 fragment = Fragment.instantiate(this, fname); 4236 fragment.mFromLayout = true; 4237 fragment.mFragmentId = id != 0 ? id : containerId; 4238 fragment.mContainerId = containerId; 4239 fragment.mTag = tag; 4240 fragment.mInLayout = true; 4241 fragment.mFragmentManager = mFragments; 4242 fragment.onInflate(this, attrs, fragment.mSavedFragmentState); 4243 mFragments.addFragment(fragment, true); ... 4263 } 4265 if (fragment.mView == null) { 4266 throw new IllegalStateException("Fragment " + fname 4267 + " did not create a view."); 4268 } 4269 if (id != 0) { 4270 fragment.mView.setId(id); 4271 } 4272 if (fragment.mView.getTag() == null) { 4273 fragment.mView.setTag(tag); 4274 } 4275 return fragment.mView; 4276 } このとき、引数が2つの Fragment.instantiate(Context context, String fname) で Fragment のインスタンスを生成していることに注目してください。このメソッドは第3引数の Bundle を null として instantiate(Context context, String fname, Bundle args) を呼びます。そのため、レイアウトに定義された Fragment は Argument なし(つまり null)で生成されることがわかります。

この onCreateView() は Activity 内での setContentView() をトリガーとして呼ばれます。

レイアウトから生成される Fragment に Argument をセットしないようになっているのは、必要がないからだと思います。そもそも単体で破棄される Fragment はバックスタックにある場合で、 Fragment のもっている View が Activity のレイアウトの一部になっている場合は Activity と一緒に破棄、再生成されます。 それならば setContentView() の後に FragmentManager#getFragmentById() で Fragment を取得して setter なりで値を渡せばいいわけです。

■ レイアウトから生成される Fragment はバックスタックに移動させないのが普通

実は、レイアウトに定義している Fragment に対し、単に FragmentTransaction の remove() を呼んだ場合、Fragment の保持している View のフィールドは null にセットされますが、レイアウトから View は削除されません。

FragmentManager の moveToState() メソッドを見てみましょう。

初期化の段階(#738)では、レイアウトから生成した Fragment(= mFromLayout が true)の場合この段階で onCreateView() から View を生成し、その処理については LayoutInflater にまかせています。 どういうことかというと、この段階で生成された View (mView として保持される)が Activity の onCreateView() での戻り値になるのです。

生成の段階(#781)では、レイアウトから生成していない Fragment であればコンテナ(Fragment の View の追加先の ViewGroup)の ID からコンテナのインスタンスを取得して mContainer として保持し、onCreateView() から生成した View をこのコンテナに追加しています。

破棄の段階(#874)では、Fragment の持つ View とそのコンテナが両方とも null ではない場合にコンテナから View を削除しています。レイアウトから生成した Fragment はコンテナが null のままなので、この if 文のなかには入らず View は削除されません。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/FragmentManager.java#712 712 void moveToState(Fragment f, int newState, int transit, int transitionStyle) { ... 722 if (f.mState < newState) { ... 737 switch (f.mState) { 738 case Fragment.INITIALIZING: ... 769 if (f.mFromLayout) { 770 // For fragments that are part of the content view 771 // layout, we need to instantiate the view immediately 772 // and the inflater will take care of adding it. 773 f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState), 774 null, f.mSavedFragmentState); 775 if (f.mView != null) { 776 f.mView.setSaveFromParentEnabled(false); 777 if (f.mHidden) f.mView.setVisibility(View.GONE); 778 f.onViewCreated(f.mView, f.mSavedFragmentState); 779 } 780 } 781 case Fragment.CREATED: 782 if (newState > Fragment.CREATED) { ... 784 if (!f.mFromLayout) { 785 ViewGroup container = null; 786 if (f.mContainerId != 0) { 787 container = (ViewGroup)mActivity.findViewById(f.mContainerId); ... 793 } 794 f.mContainer = container; 795 f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState), 796 container, f.mSavedFragmentState); 797 if (f.mView != null) { 798 f.mView.setSaveFromParentEnabled(false); 799 if (container != null) { ... 806 container.addView(f.mView); 807 } 808 if (f.mHidden) f.mView.setVisibility(View.GONE); 809 f.onViewCreated(f.mView, f.mSavedFragmentState); 810 } ... 811 } 812 ... 849 } 850 } else if (f.mState > newState) { 851 switch (f.mState) { ... 873 case Fragment.STOPPED: 874 case Fragment.ACTIVITY_CREATED: 875 if (newState < Fragment.ACTIVITY_CREATED) { ... 890 if (f.mView != null && f.mContainer != null) { ... 918 f.mContainer.removeView(f.mView); 919 } 920 f.mContainer = null; 921 f.mView = null; 922 } ... 970 } 971 } 972 973 f.mState = newState; 974 } 975 remove() ではなく replace() で別の Fragment に置き換えた場合、Activity を再生成する(画面回転など)と IllegalStateException で落ちます。

例えば <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <Button android:id="@+id/button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="show dialog" /> <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" > <fragment android:id="@+id/fragment" android:layout_width="match_parent" android:layout_height="match_parent" class="yanzm.example.dialogfragmentsample.MainActivity$MyFragment" /> </FrameLayout> </LinearLayout> に対してボタンが押されたら R.id.container 内の Fragment を入れ替えるようにします。 public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { switchFragment(); } }); } private void switchFragment() { Fragment fragment = new MyFragment2(); getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit(); } public static class MyFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.main2, container, false); } } public static class MyFragment2 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.main4, container, false); } } } ボタンを押して MyFragment を MyFragment2 に入れ替えた状態で画面を回転させると落ちます。

Activity の onCreateView() をもう一度みてみましょう。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4223 4223 Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null; 4224 if (fragment == null && tag != null) { 4225 fragment = mFragments.findFragmentByTag(tag); 4226 } 4227 if (fragment == null && containerId != View.NO_ID) { 4228 fragment = mFragments.findFragmentById(containerId); 4229 } ... 4234 if (fragment == null) { 4235 fragment = Fragment.instantiate(this, fname); ... 4263 } 4264 4265 if (fragment.mView == null) { 4266 throw new IllegalStateException("Fragment " + fname 4267 + " did not create a view."); 4268 } fragment のインスタンスを見つける順番として次の段階を踏みます。

1. android:id で指定されているID
2. 1. で見つからなかったら android:tag で指定されているタグ名
3. 2. でも見つからなかったら親 View の ID

ここで思いだして欲しいのが Fragment は id が明示的に指定されていない場合、親の id を自分の id として持つ、ということです。

上記のコードでは Fragment fragment = new MyFragment2(); getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit(); によって、MyFragment2 の id には R.id.container が入ることになります。そのため、画面回転時には

3. 2. でも見つからなかったら親 View の ID

の段階で MyFragment2 のインスタンスが見つかってしまうということです。そのため、#4234 の if 文には入らず、Fragment はクラス名から生成されません。そのまま #4265 に行くのですが、MyFragment2 はレイアウトから生成されたわけではないので、この段階ではまだ View は生成されていません。そのため、IllegalStateException が投げられてしまうのです。

この流れについては、上記の

初期化の段階(#738)では、レイアウトから生成した Fragment(= mFromLayout が true)の場合この段階で onCreateView() から View を生成し、その処理については LayoutInflater にまかせています。 どういうことかというと、この段階で生成された View (mView として保持される)が Activity の onCreateView() での戻り値になるのです。


の部分を思い出してください。



結論としては

レイアウトから生成する Fragment は FragmentTransaction に対象にしない。FragmentTransaction で入れ替える Fragment はコードから生成する。

ということですね。







0 件のコメント:

コメントを投稿