前回のエントリで 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 件のコメント:
コメントを投稿