社会人1年目エンジニアのブログ

if you can't explain it simply, you don't understand it well enough.

[Android]フラグメントについてまとめ

フラグメントってなに?

一言で言えばActivityの中で使われるロジックを含んだUIです。
(UIをもたないフラグメントの使い方というのもあるようですが、いまいちイメージ湧かないのでここではスキップします。)
Activityの中で画面を分割したい時とか、タブ機能を実装したいというときにフラグメントは使われるようです。
自分のandroid端末にはいってるアプリでこれだ!って言えるものあるかなーってさがしてみました。
agodaのtop画面は背景の写真がスワイプで切り替わる一方でフォームは固定なのでこれもフラグメントっぽいです。 タブUIはほとんどどのアプリでも使われてました(FB,Twitter,Lineえとせとら)
f:id:chonesu:20160924224006p:plain:w300  

そもそもなんでフラグメントが必要なの?

ダイアログ:ActivityのDialog関連メソッドでダイアログを生成すると画面回転にダイアログが消える (アクティビティの再生成)
タブ:タブコンテンツ分のActivityとそれをラップするActivityが必要になる
レイアウトの使い回し:<include~>ではロジックの使い回しはできない
などActivityだけでやろうとすると色々不便があるようです。
参考: 今さら聞けない Activity と Fragment の使い分け - Qiita

フラグメントは独自のライフサイクルを持つ

フラグメントのライフサイクルはアクティビティのライフサイクルに影響を受けます。
アクティビティの中に表示されるUIなので当然といえば当然です。

http://techbooster.org/wp-content/uploads/2014/12/lifecycle.png
参考: Activity&Fragmentライフサイクルポスター | TechBooster

図だけみてもまったく意味不明なので、後で実際にコードを書きながらまとめます。

フラグメントを作成する

フラグメントにUI(レイアウト)をセットするにはフラグメントのライフサイクルイベントである onCreateView()内でレイアウトファイル(xml)をインフレートする必要があります。

public static class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // レイアウトをインフレート
        return inflater.inflate(R.layout.example_fragment, container, false);
    }
}

公式のガイドには

onCreateView() に渡される container パラメータは、フラグメントのレイアウトが挿入される ViewGroup の親になります(アクティビティのレイアウトから)。

とあって日本語謎なんですが(私の読解力がないだけかも)、英語のガイドみると

The container parameter passed to onCreateView() is the parent ViewGroup (from the activity's layout) in which your fragment layout will be inserted.

とあり、

あなたのフラグメントのレイアウトが挿入される親のビューグループ

と解釈できるので、このcontainerに渡ってくるのは、結論としてはアクティビティのレイアウトに定義してある、フラグメントをくっつけるviewgroupになると思います。
inflate()の引数はそれぞれ、

inflate(インフレートするレイアウトID, 親ビュー, 親ビューにattachするか否か)

です。 第3引数をfalseにすると第1引数のレイアウトファイルのLayoutparamsが設定され、親ビューにはattachされません。戻り値はインフレートしたビューです。
逆にtrueにすると親ビューのLayoutparamsが設定かつ親にattachされ、戻り値として親ビューが返ります。
ここではfalseにすることで、インフレートしたビューを返しています。 このあたりはinflateの復習。

フラグメントを利用する

レイアウトを設定できたところで、いよいよフラグメントを利用してみます。
フラグメントの利用方法には主に2つあります。
静的利用
直接アクティビティのレイアウトファイルにフラグメントを定義する方法です。
一意になるようにidもしくはtagを指定する必要があります。 しかしながらこの方法だと動的にフラグメントを追加したり削除したり置き換えたりといった操作ができません。
そこで動的利用を検討します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment android:name="com.example.news.ArticleListFragment"
            android:id="@+id/list"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
    <fragment android:name="com.example.news.ArticleReaderFragment"
            android:id="@+id/viewer"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
</LinearLayout>

動的利用
動的にフラグメントを追加したり削除したい場合は、Activityでフラグメントを操作するgetFragmentManagerとFragmentTransactionを使用します。

// フラグメントマネージャーでフラグメント操作をする
FragmentManager fragmentManager = getFragmentManager();
// 処理の開始
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

// 追加したいフラグメントの生成
ExampleFragment fragment = new ExampleFragment();
// フラグメントの追加(第1引数:フラグメントをくっつける親ビュー 第2引数:追加するフラグメント)
fragmentTransaction.add(R.id.fragment_container, fragment);
// 処理の確定
fragmentTransaction.commit();

レイアウトの例

<?xml version="1.0" encoding="utf-8"?>
<Linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
     
    <fragment
            android:name="jp.co.test.fragment.Fragment1"
            android:id="@+id/fragment1"
            android:layout_width="0dip"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    <!-- 以下のフレームレイアウトに動的にフラグメントが追加されたり削除されたりする -->
    <FrameLayout
            android:id="@+id/fragment_container"
            android:layout_width="0dip"
            android:layout_height="match_parent"
            android:layout_weight="1" />
     
</LinearLayout>

フラグメントに値をセットする(Bundleを利用する)

Bundleに詰めてそれをFragment#setArgumentsします。

// フラグメントの生成
HogeFragment fragment =  new HogeFragment();
// 値を格納するBundleを生成
Bundle bundle = new Bundle();
// Bundleに値を詰める
bundle.putString("param", "Param");
// フラグメントにセットする
fragment.setArguments(bundle);

すべてのフラグメントのコンストラクタは常に空じゃなきゃいけないので、

HogeFragment fragment =  new HogeFragment(param);

とかやるのはダメ。理由はフラグメントが再生成されるときは常に空のコンストラクタが呼ばれるため。 再生成が行われた際に空のコンストラクタがないとruntime exceptionが発生します。

setterつかって値を渡すのも、フラグメントが再生成されたときにsetterが呼ばれないので、ダメ。

HogeFragment fragment =  new HogeFragment();
fragment.setParam(param);

bundleをフラグメントにセットできたら、フラグメント内ではgetArguments()で取り出せます。
onViewCreated()の引数で渡ってくるインフレートしたビューからウィジェットを取得して、値をセットすることができます。

参考: Y.A.M の 雑記帳: Android Fragment で setArguments() してるサンプルが多いのはなぜ?

アクティビティにフラグメントのイベントを伝える

例えばダイアログをDialogFragmentで実装した場合、OKボタン/キャンセルボタンが押下されたというダイアログフラグメント側のイベントは、アクティビティに通知してあげなければ、それを受けての処理ができません。
具体的な例としてはダイアログのOKボタンが押下されたら、アクティビティを閉じる。 という実装をしたい時などが考えられます。 フラグメントのイベントを、アクティビティでキャッチしたい場合は フラグメントのコールバックをアクティビティで実装します。

public class HogeFragment extends Fragment {
    OnHogeFragmentListener mListener;
    ...
    // onAttach()は一番最初に呼ばれるフラグメントのライフサイクルメソッド
    @Override
    // ※onAttach(Activity activity)はAPI23以降は非推奨になっています。API23以降はonAttach(Context context)を使用します。
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            // アクティビティをコールバックインターフェースにキャスト
            mListener = ( OnHogeFragmentListener) activity;
        } catch (ClassCastException e) {
            // アクティビティがコールバックを実装してなかったらエラーになる
            throw new ClassCastException(activity.toString() + " must implement OnHogeFragmentListener");
        }
    }
    ...
    // フラグメント側のクリックイベントでアクティビティに処理をさせたい
   @Override
   public void onClick(View v) {
        // Activityにイベント通知
        mListener.onHogeFragmentClicked(getTag());
   }

   コールバックインターフェースの定義
   public interface OnHogeFragmentListener {
     public void onHogeFragmentClicked(String tag);
   }
}

アクティビティ側ではコールバックを実装します。

public static class HogeActivity extends AppCompatActivity implements HogeFragment.OnHogeFragmentListener{

    @Override
    public void onHogeFragmentClicked(String tag){
      if ("HogeFragmentのタグ".equals(tag)) { 
        // 処理
      }
    }
}

基本的な理解は以上です。 次回は実際にダイアログ/ViewPager/タブを実装してみます。