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
  1. 370     // Construction arguments;  
  2. 371     Bundle mArguments;  
つまり、生成したあとの任意のタイミングでセットするようなものではない、ということです。

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

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Fragment.java#652
  1. 659     public void setArguments(Bundle args) {  
  2. 660         if (mIndex >= 0) {  
  3. 661             throw new IllegalStateException("Fragment already active");  
  4. 662         }  
  5. 663         mArguments = args;  
  6. 664     }  
  7. 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
  1. 1009     void makeActive(Fragment f) {  
  2. 1010         if (f.mIndex >= 0) {  
  3. 1011             return;  
  4. 1012         }  
  5. 1013   
  6. 1014         if (mAvailIndices == null || mAvailIndices.size() <= 0) {  
  7. 1015             if (mActive == null) {  
  8. 1016                 mActive = new ArrayList<fragment>();  
  9. 1017             }  
  10. 1018             f.setIndex(mActive.size());  
  11. 1019             mActive.add(f);  
  12. 1020   
  13. 1021         } else {  
  14. 1022             f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1));  
  15. 1023             mActive.set(f.mIndex, f);  
  16. 1024         }  
  17. 1025     }  
  18. ragment>  


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

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

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/Activity.java#4189
  1.    4199     public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {  
  2.    4200         if (!"fragment".equals(name)) {  
  3.    4201             return onCreateView(name, context, attrs);  
  4.    4202         }  
  5. ...  
  6.    4223         Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null;  
  7. ...  
  8.    4234         if (fragment == null) {  
  9.    4235             fragment = Fragment.instantiate(this, fname);  
  10.    4236             fragment.mFromLayout = true;  
  11.    4237             fragment.mFragmentId = id != 0 ? id : containerId;  
  12.    4238             fragment.mContainerId = containerId;  
  13.    4239             fragment.mTag = tag;  
  14.    4240             fragment.mInLayout = true;  
  15.    4241             fragment.mFragmentManager = mFragments;  
  16.    4242             fragment.onInflate(this, attrs, fragment.mSavedFragmentState);  
  17.    4243             mFragments.addFragment(fragment, true);  
  18. ...  
  19.    4263         }  
  20.    4265         if (fragment.mView == null) {  
  21.    4266             throw new IllegalStateException("Fragment " + fname  
  22.    4267                     + " did not create a view.");  
  23.    4268         }  
  24.    4269         if (id != 0) {  
  25.    4270             fragment.mView.setId(id);  
  26.    4271         }  
  27.    4272         if (fragment.mView.getTag() == null) {  
  28.    4273             fragment.mView.setTag(tag);  
  29.    4274         }  
  30.    4275         return fragment.mView;  
  31.    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
  1.     712     void moveToState(Fragment f, int newState, int transit, int transitionStyle) {  
  2. ...  
  3.     722         if (f.mState < newState) {  
  4. ...  
  5.     737             switch (f.mState) {  
  6.     738                 case Fragment.INITIALIZING:  
  7. ...  
  8.     769                     if (f.mFromLayout) {  
  9.     770                         // For fragments that are part of the content view  
  10.     771                         // layout, we need to instantiate the view immediately  
  11.     772                         // and the inflater will take care of adding it.  
  12.     773                         f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState),  
  13.     774                                 null, f.mSavedFragmentState);  
  14.     775                         if (f.mView != null) {  
  15.     776                             f.mView.setSaveFromParentEnabled(false);  
  16.     777                             if (f.mHidden) f.mView.setVisibility(View.GONE);  
  17.     778                             f.onViewCreated(f.mView, f.mSavedFragmentState);  
  18.     779                         }  
  19.     780                     }  
  20.     781                 case Fragment.CREATED:  
  21.     782                     if (newState > Fragment.CREATED) {  
  22. ...  
  23.     784                         if (!f.mFromLayout) {  
  24.     785                             ViewGroup container = null;  
  25.     786                             if (f.mContainerId != 0) {  
  26.     787                                 container = (ViewGroup)mActivity.findViewById(f.mContainerId);  
  27. ...  
  28.     793                             }  
  29.     794                             f.mContainer = container;  
  30.     795                             f.mView = f.onCreateView(f.getLayoutInflater(f.mSavedFragmentState),  
  31.     796                                     container, f.mSavedFragmentState);  
  32.     797                             if (f.mView != null) {  
  33.     798                                 f.mView.setSaveFromParentEnabled(false);  
  34.     799                                 if (container != null) {  
  35. ...  
  36.     806                                     container.addView(f.mView);  
  37.     807                                 }  
  38.     808                                 if (f.mHidden) f.mView.setVisibility(View.GONE);  
  39.     809                                 f.onViewCreated(f.mView, f.mSavedFragmentState);  
  40.     810                             }  
  41. ...  
  42.     811                         }  
  43.     812   
  44. ...  
  45.     849             }  
  46.     850         } else if (f.mState > newState) {  
  47.     851             switch (f.mState) {  
  48. ...  
  49.     873                 case Fragment.STOPPED:  
  50.     874                 case Fragment.ACTIVITY_CREATED:  
  51.     875                     if (newState < Fragment.ACTIVITY_CREATED) {  
  52. ...  
  53.     890                         if (f.mView != null && f.mContainer != null) {  
  54. ...  
  55.     918                             f.mContainer.removeView(f.mView);  
  56.     919                         }  
  57.     920                         f.mContainer = null;  
  58.     921                         f.mView = null;  
  59.     922                     }  
  60. ...  
  61.     970             }  
  62.     971         }  
  63.     972   
  64.     973         f.mState = newState;  
  65.     974     }  
  66.     975   
remove() ではなく replace() で別の Fragment に置き換えた場合、Activity を再生成する(画面回転など)と IllegalStateException で落ちます。

例えば
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="fill_parent"  
  4.     android:layout_height="fill_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <Button  
  8.         android:id="@+id/button"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:text="show dialog" />  
  12.   
  13.     <FrameLayout  
  14.         android:id="@+id/container"  
  15.         android:layout_width="match_parent"  
  16.         android:layout_height="match_parent" >  
  17.   
  18.         <fragment  
  19.             android:id="@+id/fragment"  
  20.             android:layout_width="match_parent"  
  21.             android:layout_height="match_parent"  
  22.             class="yanzm.example.dialogfragmentsample.MainActivity$MyFragment" />  
  23.     </FrameLayout>  
  24.   
  25. </LinearLayout>  
に対してボタンが押されたら R.id.container 内の Fragment を入れ替えるようにします。
  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.main);  
  7.           
  8.         findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {  
  9.               
  10.             @Override  
  11.             public void onClick(View v) {  
  12.                 switchFragment();  
  13.             }  
  14.         });  
  15.     }  
  16.       
  17.     private void switchFragment() {  
  18.         Fragment fragment = new MyFragment2();  
  19.         getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();  
  20.     }  
  21.           
  22.     public static class MyFragment extends Fragment {  
  23.   
  24.         @Override  
  25.         public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  26.             return inflater.inflate(R.layout.main2, container, false);  
  27.         }  
  28.     }  
  29.       
  30.     public static class MyFragment2 extends Fragment {  
  31.   
  32.         @Override  
  33.         public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  34.             return inflater.inflate(R.layout.main4, container, false);  
  35.         }  
  36.     }      
  37. }  
ボタンを押して MyFragment を MyFragment2 に入れ替えた状態で画面を回転させると落ちます。

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

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

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

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

上記のコードでは
  1. 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 件のコメント:

コメントを投稿