FragmentとActivityの連携方法
最初に、エアコードなので動作するかは分かりません。ちゃんと動くコードをまとめて公開するようにした方がいい気がするけど時間があるときに。
Activity→Activity→Activity
超基本。呼び出し元はstartActivityForResult()
を使う。
Intent intent = new Intent(this, FugaActivity.class);
startActivityForResult(intent, HOGE_QUEST_CODE);
結果はsetResult()
で返す。
Intent data = new Intent();
data.putExtra("key", "value"); // 任意のデータを呼び出し元に返せる
setResult(RESULT_OK, data);
finish(); // finishのタイミングは任意だが、速やかに書かないと可読性が著しく落ちる
すると呼び出し元ActivityのonActivityResult()
が呼び出されるのでオーバーライドして処理を記述する。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode == HOGE_QUEST_CODE && resultCode == RESULT_OK){
data.getStringExtra("key"); // "value"を取得できる
}
}
Fragment→Activity
コールバック用のインターフェース(以下、リスナ)を作成し、Activityに実装する。あとは普通にActivityからFragmentを表示する。
ActivityにFragmentを表示するのは以下のお馴染みのコード。
HogeFragment hogeFragment = new HogeFragment();
getSupportFragmentManager().beginTransaction().add(R.id.placeholder, hogeFragment ).commit();
するとFragment側でonAttach()
が呼び出されるので、両者の紐付けをここで行う。
@Override
public void void onAttach (Activity activity){
super.void onAttach (activity);
try {
mListener = (HogeFragmentListener) activity;
} catch (ClassCastException e) {
// リスナが実装されていない場合、キャスト例外が発生する
// バグなので、ClassCastExceptionを再スローするかIllegalStateException辺りを投げるのが一般的
e.printStackTrace();
}
}
getActivity()
を使えば任意のタイミングでリスナを登録できるように思えるが、なぜonAttach()
で行うのか。
おそらく画面回転(および画面サイズ変更)時には以下の4ケースが考えられるからと考えられる。
- Activity/Fragment共に再作成されるケース(デフォルト)
- Activityの再作成を抑制するケース
-
android:configChanges="orientation|screenSize"
を指定している場合、Activityは破棄されないが、 Fragmentは再作成される 。その際、再びonAttach()
が呼び出される。
-
- Fragmentの再作成を抑制するケース
-
setRetainInstance(true)
でFragmentの再作成を抑制した場合は、 Activityのみ破棄される 。新しいActivityに対してonAttach()
が呼び出され、正しく紐付けられる。
-
- Activity/Fragmentの再作成を抑制するケース
- 両者の複合パターン。やったことないけどやる人もいるんだろう、きっと。この場合は最初の紐付けが有効のままなので特に問題ないはず。
いずれのパターンでも必ず一度だけ紐付けられるonAttach()
が最も都合が良いわけですね。
FragmentのonAttach()
はonCreate()
より先に呼び出されるので、このタイミングはまだFragmentの状態は不完全であることに注意。リスナの登録のみ行うという認識であること。
Fragment→Fragment→Fragment
Fragmentの場合、それが兄弟関係にあるFragmentか、親子関係にあるFragmentかによってコールバックの方法を変えなければならない 。
Fragmentが兄弟関係にある場合は、呼び出し元はsetTargetFragment()
を利用して、自身を渡す。
HogeFragment hogeFragment = (HogeFragment)getFragmentManager().findFragmentByTag(HOGE_FRAGMENT)
hogeFragment.setTargetFragment(this, HOGE_QUEST_CODE);
呼ばれた側はgetTargetFragment()
を用いて取得できる。画面回転などでいずれかのFragmentが再作成された場合でも両者は紐付けられたままとなる。
FugaFragment callerFragment = (FugaFragment)getTargetFragment();
callerFragment.doSomething();
直接メソッドを呼び出すのではなく、Activity→Fragmentのときと同様に、何らかのリスナを実装して汎化するのが一般的。キャスト例外を捕まえてエンバグを防ぐ手法も同じように使える。
DialogFragment
をchildFragmentManager()
で表示する場合など、Fragment in FramentでsetTargetFragment()
をすると、Activity
が再生成されたときに正しく復元されない。
この場合はコールバック先は、getParentFragment()
で取得する。
FugaDialogFragment.newInstance();
.show(getChildFragmentManager());
呼び出し側のコードは以下の通り。
Fragment callerFragment = getParentFragment();
callerFragment.doSomething();
FragmentManagerの処理は闇が深い。
Fragment→Activity→Activity→Activity
そのFragmentが属している親Activityが、別のActivityに結果を求めて、親Activityで結果を処理するケース。
処理そのものは最初の「Activity→Activity→Activity」と同じになる。
Intent intent = new Intent(this, FugaActivity.class);
getActivity().startActivityForResult(intent, HOGE_QUEST_CODE);
が、この書き方は混乱を招くので、一旦リスナなどでActivityへ処理を移譲して、ActivityからstartActivityForResult()
を呼び出したほうが良い。
Fragment→Activity→Fragment
API Lv11以上以上では、Fragmentから直接別のActivityに結果を求めて、Fragmentで結果を処理することができる。
Intent intent = new Intent(this, FugaActivity.class);
// getActivity()がないことに注意
startActivityForResult(intent, HOGE_QUEST_CODE);
上と比較すると、getActivity()
がないだけでほぼ同じコードに見えるが、Activityからのコールバックは Fragment内のonActivityResult()
に記述する という点が異なる。
// Activityではなく、Fragmentに書く
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent){
if(requestCode == HOGE_QUEST_CODE && resultCode == Activity.RESULT_OK){
//RESULT_OKはActivityクラスの定数なので、Activity.RESULT_OKと書く
}
}
Fragment→Activity→Activity→Fragment
サポートパッケージのFragmentが利用されている場合でも「Fragment→Activity→Fragment」と同じ処理は行える。ただし、 仕組みが異なるため、直接ActivityからFragmentへコールバックできない 。
コードそのものは「Fragment→Activity→Fragment」パターンと同じなので割愛するが、親ActivityであるFragmentActivityが、そのonActivityResult()
内で、呼び出し元FragmentのonActivityResult()
をコールしているという点を認識しておけばOK。
この場合、もしActivityとFragmentとでリクエストコードが重複したら、親ActivityのonActivityResult()
で処理がブロックされ、正しくコールバックされないのではないか?と疑問に思うところだけど、そうならないためにリクエストコードに対して16bitのシフト演算を行っている。
リクエストコードのintは32bitなので、下位16bitをFragmentの、上位16bitをActivityのリクエストコードとして引き渡して識別するしている模様。
このシフト演算は自動的に行ってくれるため通常意識する必要はないが、演算の結果オーバーフローする場合(つまりFragmentから指定したリクエストコードが16bitを超える場合)には例外が発生することは、頭の片隅に入れる必要がある。
ちなみにこの方法はgetChildFragmentManager()
が絡むと以下略。
アンチパターン
- Fragmentの
getActivity()
からstartActivityForResult()
をコールするのは実際ヤバイ。 - 基本だけど、Fragmentのコンストラクタ引数を使わないこと。ここでコールバックリスナを渡しても、画面を回転させると引数なしのデフォルトコンストラクタで再生成されるため。(lintを走らせるとコンパイルエラーになる)
setArgments()
でコールバックリスナを渡す。リスナが再作成された場合に紐付けが更新されないため不適切。- Fragment in Fragment(Fragmentが親子関係にある)ときに、
setTargetFragment()
を使う。Activity
が再生成された場合に紐付けがおかしくなるため不適切。最悪クラッシュする。 - FragmentからFragmentに結果を返すとき、
getTargetFragment()
で取得したFragmentのonActivityResult()
を直接呼び出せばリスナは不要だけどたぶんバッドノウハウ。
うーーん…。