Android開発のFragmentについての注意事項をまめました。いくつかの守るべきルールや既に確立されたコーディングスタイルがあるようです。それを守らないとある程度は動いているんだけど、動作をさせていたらあるタイミングで落ちてしまうという事になるようです(WEBページのコピペでも動作するけで、回転させると落ちてしまった・・・・ような経験は何度もしてきました)。なるべく同じようなミスをしないように今までハマった点をまとめてみました。
静的な方法、動的な方法
FragmentのActivityへの組み込みは、XMLレイアウトの<fragment>を用いる静的な方法と、FragmentTransactionを用いる動的な方法があります。
(静的な方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="fill_parent" > <fragment android:name="com.example.helloandroid.HelloFragment" android:id="@+id/fragment1" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_weight="1" /> </LinearLayout> |
fragmentのandroid:nameは上で作成したFragmentクラスのクラス名です。フラグメントの生成管理をシステム側で行うので、以下の動的なコードを記述する必要がありません。
(動的な方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { FragmentManager manager = getFragmentManager(); FragmentTransaction transaction = manager.beginTransaction(); transaction.replace(R.id.main, TitleFragment.newInstance()); transaction.commit(); } } <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/main"> </RelativeLayout> |
上記はActivityにFragmentを貼付けるケースです。ポイントはsavedInstanceState==nullの時のみ、R.id.mainの部分にFragmentを貼付ける(=replaceする)処理を行う事です。Fragmentの貼付け/張り替えはFragmentManager経由で行います。savedInstanceState==nullのif文がない場合は、回転した都度、新しいフラグメントが生成される事になります。
値の引き渡しについて
(1)呼出し元のActivity(Fragment)から呼出し先のFramentへの値の引き渡し
NewInstanceメソッドの引数で引き渡します。NewInstanceメソッド内では、setArguments()でバンドルに格納するようにします。Fragment のコンストラクタで引数を渡すのはダメです。Fragmentを継承したクラスは空のコンストラクタを用意する必要があるためです。Fragmentの再生成の時に空のコンストラクタがシステムからコールされます。
(2)呼出し先のFramentから呼出し元のActivity(Fragment)への値の引き渡し
(方法1)
コールバックリスナーを使用する方法です。しかし、回転やプロセス管理による再生成処理で、登録したリスナーが初期化されるので(nullになるので)、onAtttachイベントでリスナーを復活する(呼出し元との紐付けを再度行う)必要があります。呼出し元がActivityの場合はonAtttachの引数がActivityなので、そのままリスナーのインターフェースでキャストすれば問題ありませんが、呼出し元がFragmentの場合は、setTargetFragmentで設定したフラグメントをgetTargeFramentで呼び出して、リスナーのインターフェースでキャストする必要があります。setTargetFragment()メソッドで呼び出し元Fragmentをセットしており、ここでセットされた呼び出し元FragmentオブジェクトはgetTargetFragmentで取り出せます。getTargetFragmentで取り出したFragmentは、Activity/Fragment再生成時に関係の整合性を保持した状態のFragmentになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Override public void onAttach(Context context) { super.onAttach(context); //呼出し元によって処理を分岐 if (listenerType == E_LISTENER_TYPE.ACTIVITY) { mListener = (Interface_T) context; } else if (listenerType == E_LISTENER_TYPE.FRAGMENT) { //呼び出し元のFragmentオブジェクトを取得する //setTargetFragment()でセットされた呼び出し元のFragmentオブジェクトはgetTargetFragment()で取得できます mListener = (Interface_T) getTargetFragment(); } |
(方法2)
Intentに結果を詰めて呼び出し元のonActivityResult()を直接呼ぶ方法です。呼び出し元はonActivityResult()で結果を処理します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public static MyDialogFragment newInstance(Fragment target, int requestCode) { MyDialogFragment fragment = new MyDialogFragment(); fragment.setTargetFragment(target, requestCode); Bundle args = new Bundle(); fragment.setArguments(args); return fragment; } void submit() { Fragment target = getTargetFragment(); if (target == null) { dismiss(); return; } Dialog dialog = getDialog(); EditText editText = (EditText) dialog.findViewById(R.id.edit); Intent data = new Intent(); data.putExtra(Intent.EXTRA_TEXT, editText.getText().toString()); target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, data); dismiss(); } |
setTargetFragment()メソッドで呼び出し元Fragmentとリクエストコードをセットしています。ここでセットされた呼び出し元Fragmentオブジェクトは、Activity/Fragment再生成時に関係の整合性を保持したままセットされるというものです。よって、getTargetFragmentで取り出したFragmentは、再生成時に関係の整合性を保持した状態のFragmentになっています。
入れ子(ネストする)のFragment
ネストするフラグメントを使用する時は、getChildFragmentManager()を使用します。
1 2 3 4 5 6 |
FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); Fragment childFragment1 = new ChildFragment1(); Fragment childFragment2 = new ChildFragment2(); transaction.add(R.id.child_fragment_1, childFragment1, "child_1"); transaction.add(R.id.child_fragment_2, childFragment2, "child_2"); transaction.commit(); |
getActivity()でnullが返ってくる場合
getActivity()とは、現在Fragmentと関連付けられているActivityを返すメソッドになります。回転などによるconfigChangeの発生やメモリ圧迫等によるシステムからの破棄によって、Activityの再生成が発生した場合、通常であればシステム側が自動で関連付けを行うためnullが返ってくる事はないが、AsyncTaskの非同期タスク実行中に回転などが発生した場合、非同期タスク完了後のコールバック処理のリスナー側処理でnullになる場合がある。
(例)AsyncTaskの処理完了後のコールバックリスナーの処理でgetActivity()を実行した場合。AsyncTaskに対応するActivityは既に破棄されている。AsyncTaskLoaderを使って対応する。
再生成処理
Activity/Fragmentの再生成には二つのパターンがあります。
1.メモリ圧迫等によるシステムのprocess killによる破棄からの再生成
2.回転などのconfigChangeでの再生成
onAttach
Fragmentで用意されているonAttach(Activity)メソッドは、一度fragmentで呼び出されると、Activityと関連付きます。API 23以前は、以下のように利用していました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class SampleActivity extends AppCompatActivity implements SampleFragment.OnListFragmentInteractionListener { @Override public void onListFragmentInteraction(Main item) { // something } } public class SampleFragment extends Fragment { private OnListFragmentInteractionListener mListener; @Override public void onAttach(Activity activity) { super.onAttach(activity); if (activity instanceof OnListFragmentInteractionListener) { mListener = (OnListFragmentInteractionListener) activity; } } } |
上記のコードは、FragmentからActivityにコールバックする必要がある場合の処理です。Fragmentはメモリが不足すると、システムによって破棄され、必要な時に再生成されます。すると、コールバックが受け取れなってしまう場合があります。API 23でonAttach(Activity)メソッドは、deprecatedになりました。代わりにonAttach(Context) を使います。新たに追加されたonAttach(Context)メソッドは、API 23(Android6.0)からの利用となります。なので、以前のandroidにも対応する場合は、両方のメソッドを呼び出す必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class SampleFragment extends Fragment { public interface OnListFragmentInteractionListener { void onListFragmentInteraction(Main item); } private OnListFragmentInteractionListener mListener; @Override public void onAttach(Activity activity) { super.onAttach(activity); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) return; if (activity instanceof OnListFragmentInteractionListener) { mListener = (OnListFragmentInteractionListener) activity; } } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnListFragmentInteractionListener) { mListener = (OnListFragmentInteractionListener) context; } } } |
よく使うFragment関連のメソッド
getFragmentManager().findFragmentByTag(TagName); Fragmentのインスタンスを取得する
List<Fragment> list = getFragmentManager().getFragments(); 現在保持しているFragmentをListで返す
setArguments()を初期化以外で使用すると問題がある
setArguments() を初期化以外で使用すると、「java.lang.IllegalStateException: Fragment already active and state has been saved」が発生する。
状態保存には onSaveInstanceState を使用する
参考にした資料:【Android 実装の罠 #1】 Fragment#setArguments()
onSaveInstanceStateがコールされない場合がある
- OnSaveInstanceStateリスナでは呼ばれない場合があるため、onPauseを使用して、 getArgumentsで取得したバンドルに保存する。
- 画面の回転においては、OnSaveInstanceStateが正常にコールされるが、フラグメントをreplaceした時に別フラグメントの表示でOnSaveInstanceStateがコールされないため、バックスタックからの復活した再作成で値の保存・復活が出来ない。
- onPauseでBundleリスナーの発生は、OnSaveInstanceStateを包含するため、値の保存処理はonPauseで処理する。setArguments()は実行しないこと。