360andev david hope share fb

ビギナーのためのフラグメント活用術

フラグメントはパワフルですが、Android開発の中でも未だに誤解されている機能のひとつです。フラグメントはとりわけアクティビティやViewPager、Navigation drawerなどでコンポーネントを再利用するのに役立ちます。一方で、独自のライフサイクルやユースケース、アクティビティとのやりとりがあるため、複雑になり、混乱を招きがちです。

360|AnDevで発表されたこの講演ではDavid Hopeと一緒にフラグメントの基礎を旅することで、フラグメントを活用する方法を学びましょう


イントロダクション (00:00)

フラグメントはパワフルですが、Android開発の中でも未だに議論を呼ぶ機能のひとつです。問題にあがることも多いですが、それと同じぐらい多くの利点をもたらしてくれます。この講演ではフラグメントを使う理由フラグメントの様々な種類、構造とのつながり、そして悪名高いライフサイクルについてお話しします。

その後、基本的なアクティビティ内でのフラグメントの使い方と、3つの代表的な用途を通して一般的な使い方を確認します。そして、最後にベストプラクティスを確認します。

フラグメントが導入されたのはAndroid Honeycombまで遡ります。タブレットの導入により新しい画面サイズが追加され、それに対応するために導入されました。共通機能を提供するだけでなく、他にもいくつかのことを良くしてくれるので、複数のアクティビティでコンポーネントを再利用するのに役立ちます。

フラグメントの種類 (01:53)

フラグメントには大きく2つの種類があります。

  • 静的フラグメントはアクティビティのレイアウトファイル内に配置され、変更されることはありません。例えば、このような形で配置されます。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    <fragment
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/fragment"
        android:name="xxx" />

</LinearLayout>

  • 動的フラグメントは再構成が可能なパズルのピースみたいなものです。独自のレイアウトを持つだけでなく、独自のライフサイクル、構造、クラス、種類を持ちます(多くの点でアクティビティと似ています)。動的フラグメントを使うためには、ビュー階層上にフラグメントを配置するための入れ物が必要です。

動的フラグメントにも大きく2つの種類があります。

フレームワーク版フラグメントは5年前にフラグメントが導入された当初からあります。こちらはほとんどアップデートされず、最新バージョンのアクティビティであるAppCompatActivity上では動作しません。

サポートライブラリ版フラグメントはv4 supportライブラリの一部として提供され、AppCompatActivity上で動作します。こちらはサポートライブラリと共に継続的にアップデートされます。通常、1年に3、4回の頻度でアップデートされています。Androidは継続的にアップデートされているので、フラグメントについてもchild fragmentのような新たな機能が追加されることがあります。

サポートライブラリを使うことで、古いバージョンのAndroidでもそのような新しい機能を使えるようになります。このため通常はサポートライブラリ版のフラグメントを使ったほうが良いでしょう。

フラグメントのライフサイクル (05:08)

各フラグメントにはアクティビティのライフサイクルと連動した独自のライフサイクルがあります。スライド17の図を見てください。経験豊富なAndroidディベロッパーでもこれを見たら複雑だと思うでしょう。これらはそのフラグメントを管理するアクティビティによって制御されます。いくつかの例外を除いて、これらのうちのほとんどのライフサイクルメソッドはアクティビティにも同様のものがあります。このプレゼンテーションの残り時間の内でも、このライフサイクルの説明には多くの時間を割くことになります。

記事の更新情報を受け取る

onAttach (05:42)

onAttach()はフラグメントがアクティビティと最初に関連付けられたときに呼ばれます。まだフラグメントは操作可能な状態ではありません。ここからContextを使うことができます。


@Override
public void onAttach(Context context) {
    super.onAttach(context);
}

しかしながら、nullの場合があるのでここでContextを参照する処理を記述することはオススメしません。

onCreateView (06:40)

次にonCreate()が呼ばれ、続いてonCreateView()が呼び出されます。onCreateView()はユーザインタフェースを設定するところです。


@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  return super.onCreateView(inflater, container, savedInstanceState);
}

フラグメントのUIを描画するには、このメソッドからフラグメントのレイアウトのルートとなっているViewを返す必要があります。Viewは引数として渡されるinflaterのinflate()メソッドにレイアウトリソースを渡すことで生成できます。また、ビューコレクションまたはビューコンテナコレクションとともに引数として渡されるbundleを用いて、フラグメントを再生成することができます(この場合、inflate()メソッドの第3引数にfalseを設定する必要があります)。

onActivityCreated (07:34)

onCreateView()の次に呼ばれる、onActivityCreated()では重労働があります。このメソッドはアクティビティのonCreateメソッドが完了した後に呼び出されるので、初期化の最後の方で行いたい処理があれば、ここで行います。


@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  super.onActivityCreated(savedInstanceState);
}

標準的なライフサイクルメソッド (08:03)

onActivityCreated()の後には、アクティビティのメソッドと同じようにonStart()onResume()onPause()といった標準的なライフサイクルメソッドがいくつか続きます。

onDestroyView (08:20)

フラグメントが破棄される過程で、onDestroyView()が呼び出されます。ここでは、システム自身のリソースを確実に保存できるように、viewに関わるすべてのものを削除します。ボタンクリック、テキストリスナー、チェックボックスリスナーなどの登録をここで解除することができます。


@Override
public void onDestroyView() {
    super.onDestroyView();
}

onDestroyView()の後に、onDestroy()が呼び出されます。そして最後にonDetach()が呼び出されます。

onDetach (08:56)

onDetach()は、フラグメントがアクティビティから完全に切り離される直前に呼ばれるライフサイクルメソッドです。この時点で、フラグメントはもはや利用できなくなり、 getActivity()を通して、親アクティビティのコンテキストを参照するあらゆるタイプの呼び出しは nullを返し、NullPointerExceptionを引き起こします。


@Override
public void onDetach() {
    super.onDetach();
}

newInstance (09:34)

フラグメントの設定にもう一つ必要なメソッドとして、newInstance()メソッドがあります。このメソッドによりフラグメントの生成が可能になります。ここでは、インスタンス生成時に使用できるように常に設定しておきたいデフォルトのデータを設定します。例えば、タイトルに表示する文字列やその他さまざまなプリミティブなデータです。

newInstance()を使うには、常に空のコンストラクタを作成しなければなりません。それはフラグメントを生成する過程の一部で、ある意味、アクティビティに似ています。newInstance()でバンドルにkey-value形式で値をセットすることで、後からonCreateView()メソッド内でそれらさまざまなデータを取得することができます。


public static FragmentOne newInstance() {
    Bundle args = new Bundle();
    FragmentOne fragment = new FragmentOne();
    fragment.setArguments(args);
    return fragment;
}

これらのさまざまなメソッドをすべて設定できたら、今度はフラグメントの使い方を学んでいきましょう。フラグメントはActivityfragment managerを通して操作します。

Fragment Manager (11:19)

フラグメントマネージャは、フラグメントを管理するための包括的なメソッドを持ちます。フラグメントの出し入れ、移動、追加、削除、置換、復元を行うことができます。

フラグメントマネージャはリストを保持するアクティビティによってフラグメントのリストを管理し、トランザクションを実行するのに役立ちます。fluentなインターフェイスによって呼び出しが変更されるため、不必要に複数のフラグメントマネージャが作成されることはありません。


FragmentManager fm = getSupportFragmentManager();

FragmentManagerには、使用するフラグメントの種類に対応したクラスがあります。フレームワークフラグメントを使用する場合、getFragmentManager()を使用します。サポートフラグメントを使用する場合はgetSupportFragmentManager()を使用します。

Fragment ID, Fragment TAG (12:12)

フラグメントマネージャは、どのフラグメントを使用しているかを識別する必要があり、フラグメントIDまたはタグのいずれかを使用してそれを行います。これらはfindViewById()と同じように動作し、これによって、トランザクションにどのフラグメントを使うのかを決めることができます。

findFragmentById()は、おそらく皆さんが予想するものとは違い、フラグメントのIDではなくコンテナのレイアウト上でのIDを引き数に取ります。一方、 findFragmentByTag()は、(デバッグで使用するタグのように)どのフラグメントが使われているかを決めるために文字列定数を使います。

Fragment Transaction (12:59)

トランザクションとは、フラグメントを出し入れする、または置き換える一連のアクションのことです。それらは常に FragmentManagerによって呼び出される beginTransaction() メソッドから始まります。そして、通常 add replaceなどの一連のアクションが続きます。ひとつのトランザクションで複数のアクションを実行することもできます。すべてのアクションを適用するために、最後に必ずcommit()メソッドを実行します。

これがフラグメントマネージャによる基本的なaddアクションの実行例です。


fm.beginTransaction()
        .add(R.id.fragment_list_container, fragmentOne)
        .commit();

一般的な使われ方 (15:02)

これまで見てきた基本的な使い方とともに、3つの代表的な用途を確認することでフラグメントを効果的に使うことができるようになるでしょう。

  • タブレット
  • タブレイアウト
  • ダイアログレイアウト

一般的な使われ方 - タブレット (15:04)

タブレットには、このように使いたくなるような余分な広いスペースがあります。そのため、一度に複数のフラグメントを画面に収めることができます。これを”Master Detail Flow“というよく使われるデザインパターンで確認することができます。

例えば、アイテムのリストを持つEメールアプリを考えてみます。スマートフォン向けアプリでは、アイテム一覧に各アイテムの詳細情報がいくつか表示され、その中から1つを選択するとさらに詳細な情報を表示する画面が現れます。しかし、タブレットでは、広いスペースを最大限に活用するため、これらを横並びで表示したいと思うでしょう。

フラグメントを使用するためにアプリケーションを拡張し、この余分なスペースをすべて使用するには、余分な作業が必要になります。一般的には、特定の「リソース修飾子」を持つレイアウトと、使用しているデバイスのタイプを判定する条件式が必要です。

リソース修飾子 (16:22)

リソース修飾子を使用すると、特定の条件(言語、昼夜モード、画面の幅や高さなど)に基づいてリソースファイルに異なる設定を適用することができます。タブレットの場合、画面幅のリソース修飾子が使われます。この修飾子は条件として画面の最小幅を設定し、特定の最小幅を持つ画面に対してリソースを適用することができます。

一般的に、リソース修飾子の値を直接参照せず、isTablet = falseのような名前でデフォルト値とともにboolean型の変数を定義したリソースファイルを作成し、それに対して画面幅600などの値でリソース修飾子を追加する、という方法がベストプラクティスと考えられています。これによりアプリの最小幅が設定されることになるので、アプリに適したレイアウトリソースを使用していることを確認してください。

それをアクティビティ内で使用するのは簡単です。同じような名前でプライベートなboolean型の変数を作成することができます。


boolean isTablet = getResources().getBoolean(R.bool.is_tablet);

そしてアプリケーション内でそれをチェックすることでレイアウトを変更することができます。


if (isTablet) {

    fm.beginTransaction()
            .add(R.id.tablet_list_container, fragmentOne)
            .commit();

    fm.beginTransaction()
            .add(R.id.tablet_item_detail_container, fragmentTwo)
            .commit();
}

一般的な使われ方 - タブレイアウト (18:13)

タブレイアウトはよく使われるパワフルなレイアウトです。ひとつにまとめられた異なる画面を簡単に切り替えることができます。各画面がひとつのフラグメントになっています。タブレイアウトはDesign Support libraryの一部として提供されています。このライブラリはアプリケーションに手動でインポートする必要があります。

タブレイアウトにはいくつかの異なる形式があります。固定タブスライドタブ です。個人的には、固定タブがユーザーエクスペリエンスのパターンとしてより優れていると思います。なぜなら、スライドタブはスマートフォンでは見失う可能性があるからです。タブレイアウトは、ViewPagerを使う際のパターンの一つです。

View Pager (19:28)

ViewPagerを使用すると、タイトルテキストやアイコンに触れることなく、異なるタブを簡単に、かつすばやくスワイプできます。ViewPagerはv4 supportライブラリから導入されていて、タブレイアウトを実装するアクティビティのレイアウトファイルで設定します。ViewPagerはある意味でRecyclerViewのような感じに使うことができます。

しかし、まだこのレイアウトを使用するのに必要なことがあります。それはPagerAdapterの設定です。

Pager Adapter (20:10)

PagerAdapterは、タブレイアウトで使う各ページ(フラグメントなどでできている)を操作します。一方、ViewPagerとTabLayoutはユーザがどのフラグメントを表示しているかについて知っています。PagerAdapterは独自のクラス内でも、匿名クラス(ボタンのon-clickリスナーなど)内でも設定することができます。

PagerAdapterにはFragmentPagerAdapterとFragmentStatePagerAdapterの2種類があります。どちらもフラグメントの数を返す getCount()メソッドを持っています。また、引き数にpositionをとってその位置に対応するフラグメントを返すgetItem()メソッドもあります。返されるフラグメントはそれぞれのPagerAdapterのコンストラクタに渡されたfragment managerによって管理されます。共通点はこれで終わりです。

Fragment Pager Adapterはすべてのページを一度にメモリに保持し、完全に削除することはありません。これは、メモリを大量に消費するアプリケーションでは問題になります。そこで、Fragment State Pager Adapter が登場します。これは表示に必要なページだけを保持し、必要なくなったフラグメントを開放するので、不要なメモリを占有しません。後でnewInstancesavedInstanceStateを通して再生成することができます。

メモリ消費がわずかである場合でも、FragmentStatePagerAdapterを使うべきです。

FragmentStatePagerAdapterを設定するには、2つのことが必要です。まず、ViewPagerやTabLayoutをビューやアクティビティと同じように設定し、 setAdapterの引数にstatePagerAdapterを渡します。


ViewPager.setAdapter(statePagerAdapter);

次に以下のように呼び出し、


tabLayout.setupWithViewPager(ViewPager)

tabLayoutViewPagerと接続します。

一般的な使われ方 - ダイアログフラグメント (22:42)

ダイアログフラグメントはアラートダイアログとともに動作します。ダイアログフラグメントを使うことにより、ダイアログの状態を簡単に復元することができます。例えば、デバイスが回転しても消えることはありません。ダイアログをカスタマイズするには、OnCreateView()メソッドを実装するか 、onCreateDialog()メソッドを実装します。


@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {

    AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity());
    dialog.setTitle(R.string.dialog_fragment_title)
          .setMessage(R.string.dialog_fragment_message)
          .setPositiveButton(android.R.string.yes, null)
          .setNegativeButton(androld.R.string.no, null);

    AlertDialog builder = dialog. create();
    builder.show();

    return dialog.create();
}

ダイアログフラグメントを設定するのは比較的簡単ですが、フラグメントのみを扱う場合と比べて、いくつかの大きな違いがあります。


FragmentManager fm = getSupportFragmentManager();
DialogFragmentEx dialogFragmentEx = new DialogFragmentEx();
dialogFragmentEx.show(fm, FRAGMENT_TAG);

FragmentManagerを使用し、 DialogFragmentのインスタンスを生成するだけでなく、ダイアログフラグメントの show()メソッドを使って AlertDialogを表示する必要があります。show()メソッドはフラグメントマネージャとフラグメントタグを引数として受け取ります。

雑感 (24:21)

言及しておいた方がいいと思われるトピックがいくつかあります(これらについては、今日は詳しく説明しませんでした)。

  • navigation drawerは悪名高いハンバーガーメニューの一部です。これはアクティビティやフラグメントの一部を隠し、他の画面へと素早く遷移するためのナビゲーションを表示するものです。
  • Child fragmentsは、他のフラグメント内にネストされたフラグメントです。通常とは異なる独自のフラグメントマネージャを持っており、初心者が使うには少し難しいかもしれません。
  • Headless fragmentsは、ユーザーインターフェイスのないフラグメントです。それらは、同様の機能(例えば、サービスまたは他のバックグラウンドスレッドのように)を用いて、バックグラウンドで進行中のオペレーションを実行するために使われます。ヘッドレスフラグメントは使用しないことをお勧めします。

ベストプラクティス (25:45)

以下にフラグメントのベストプラクテイスをいくつかあげます。

  • アクティビティひとつに対して、使用するフラグメントは最大3つにしましょう。これにより、トランザクションの管理が楽になります(それは後々悩まされる頭痛を軽減してくれるでしょう)。
  • タブレイアウトのフラグメントは、最大5つにするのが良いでしょう。それ以上になると、ユーザーインターフェイスが乱雑になります。
  • アクティビティとフラグメントの間のあらゆる種類のインタラクションは、フラグメント側にインタフェースを定義し、そのインターフェースのメソッドをアクティビティが実装するようにしてください。特にボタンをクリックして画面上に別のフラグメントを表示するような場合に役立ちます。フラグメント自体は、組み合わせ可能なコンポーネントであり、任意のアクティビティに組み込めるようにするため、直接操作しないようにするべきです。
  • フラグメントを使用する場合は、早めに作成しておきましょう。後から複数のアクティビティをフラグメントに分割しようとするのは拷問です。

まとめると、フラグメントは多くの理由で優れています。また、独自のライフサイクル、さまざまな種類、複数のユースケースがあります。まだフラグメントを使用していないのでしたら、それらについてよく学んだ後に考え直すことを願っています。

Q & A (27:50)

Q: なぜ、ヘッドレスフラグメントは使用しない方が良いのでしょうか?

David: 私はそれがあまり多くの利点をもたらしてくれるとは思いません。それよりは、それに特化した機能であるserviceを使うか、別の選択肢を選んだ方が良いと思います。それにフラグメントの場合、メモリに保持されてしまいます。それはフラグメント自体の問題でもあるのですが。

Q: フラグメントはカスタムビューで代替できると思いますか?

David: カスタムビューはまだこの世界に独自の場所を持っています。画面全体をカスタムビューに置き換えようとしているなら、フラグメントを使ったほうが良いでしょう。ただし、RecyclerViewのように画面の特定の部分を作成するなら、カスタムビューを使った方が良いでしょう。

Q: フラグメント間のナビゲーションについては何がベストプラクティスとなるでしょうか?

David: addToBackStackメソッドがあります。それが役立つでしょう。このメソッドの引数にはnullを渡すことができます。特定のフラグメントを戻して再表示するには、個別のタグを引数に渡す必要があります。そうすれば、後からでもそれがどこにあるのかが分かります。その場合はnullを渡すのは問題になります。

About the content

This talk was delivered live in July 2016 at 360 AnDev. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

David Hope

David is the lead Android Engineer for Atlanta startup Streetstop App. He is an active member of the local Android community and Toastmasters International.

4 design patterns for a RESTless mobile integration »

close