The Android Developer Challenge is back! Submit your idea before December 2.

フラグメント   Part of Android Jetpack.

Fragment は、FragmentActivity でのユーザー インターフェースの挙動や部位を表すものです。1 つのアクティビティに複数のフラグメントを組み合わせてマルチペイン UI をビルドしたり、複数のアクティビティでフラグメントを再利用したりできます。フラグメントとは、アクティビティのモジュラー セクションのようなもので、独自のライフサイクルを持ち、独自の入力イベントを受信します。フラグメントはアクティビティの実行中に追加したり削除したりできます(別のアクティビティで再利用できる「サブ アクティビティ」のようなものです)。

フラグメントは常にアクティビティでホストされている必要があり、フラグメントのライフサイクルはホストのアクティビティのライフサイクルの影響を直接受けます。たとえば、アクティビティが一時停止しているとき、その中のすべてのフラグメントも一時停止し、アクティビティが破棄されると、すべてのフラグメントも破棄されます。ただし、アクティビティの実行中(ライフサイクルで再開された状態)は、追加や削除といった操作を各フラグメントで独立して行うことができます。このようなフラグメントのトランザクションを実行するとき、アクティビティで管理されているバックスタックにそれを追加することもできます。アクティビティの各バックスタック エントリは、発生したフラグメントのトランザクションの記録です。バックスタックでは、[戻る] ボタンを押すとフラグメントのトランザクションを戻す(前に戻る)ことができます。

アクティビティ レイアウトの一部としてフラグメントを追加すると、それはアクティビティのビュー階層内の ViewGroup に配置され、そのフラグメントによって独自のビュー レイアウトが定義されます。フラグメントをアクティビティ レイアウトに挿入するには、アクティビティのレイアウト ファイルで <fragment> 要素としてフラグメントを宣言するか、またはアプリケーション コードから既存の ViewGroup にフラグメントを追加します。

このドキュメントでは、アクティビティのバックスタックに追加したときにフラグメントの状態を維持する方法、アクティビティ内でアクティビティや他のフラグメントとイベントを共有する方法、アクティビティのアプリバーへの影響など、フラグメントを使用してアプリケーションをビルドする方法について説明します。

ライフサイクルの処理に関する情報は、ベスト プラクティスに関するガイダンスも含めて、以下のリソースをご覧ください。

デザインの指針

Android では、タブレットなどの大画面でのよりダイナミックで柔軟な UI デザインに対応するため、Android 3.0(API レベル 11)でフラグメントが採用されました。タブレットの画面はハンドセットよりもかなり大きいため、UI コンポーネントを組み合わせたり入れ替えたりできる領域が広くなります。フラグメントでは、ビュー階層に複雑な変更を加えることなく、そのようなデザインを実現できます。アクティビティのレイアウトをフラグメントに分割することで、アクティビティの外観を実行時に変更でき、それらの変更をアクティビティが管理するバックスタックに保持できます。それらのフラグメントは、フラグメント サポート ライブラリを使用して広く利用できるようになりました。

たとえばニュース アプリケーションでは、1 つのフラグメントを使って左側に記事の一覧を表示し、別のフラグメントを使って右側に記事の内容を表示できます。両方のフラグメントが 1 つのアクティビティに横並びに表示され、それぞれのフラグメントには自身のライフサイクル コールバック メソッドがあり、それぞれのユーザー入力イベントを処理します。つまり、1 つのアクティビティで記事を選択して、別のアクティビティで記事を閲覧するのではなく、図 1 のタブレットのレイアウトのように 1 つのアクティビティ内で記事を選択して閲覧できるようになります。

各フラグメントはモジュール型の、再利用可能なアクティビティ コンポーネントとしてデザインする必要があります。各フラグメントは独自のライフサイクル コールバックを使用して自身のレイアウトと挙動とを定義するため、1 つのフラグメントを複数のアクティビティに含めることができます。このことから、再利用可能なデザインを用いることに加えて、1 つのフラグメントを他のフラグメントから直接操作しないようにする必要があります。これは、モジュール型のフラグメントでは画面のサイズごとにフラグメントの組み合わせを変更できる点からも、特に重要です。タブレットとハンドセットの両方に対応したアプリケーションをデザインする場合、異なるレイアウト構成でフラグメントを再利用することで、利用可能な画面の領域に応じて最適な使い心地を実現できます。たとえばハンドセットの場合、同一のアクティビティ内に 2 つ以上のフラグメントが収まりきらないときは、フラグメントを分割してシングルペインの UI を提示する必要があることもあります。

図 1 フラグメントで定義された 2 つの UI モジュールを 1 つのアクティビティに組み合わせたタブレット用デザインと、それぞれを分けたハンドセット用デザインの例。

引き続きニュース アプリケーションの例を使うと、アプリケーションがタブレット サイズの端末で実行中は、2 つのフラグメントをアクティビティ A に埋め込むことができます。しかし、ハンドセット サイズの画面では両方のフラグメントを表示する領域が足りないため、アクティビティ A には記事の一覧のフラグメントだけが含まれます。ユーザーが記事を選択すると、記事を閲覧するための 2 つ目のフラグメントが含まれるアクティビティ B が開始されます。そのため、アプリケーションは図 1 のようにフラグメントを異なる組み合わせで再利用することで、タブレットとハンドセットの両方に対応できるようになります。

異なる画面構成ごとに異なるフラグメントの組み合わせを用いたアプリケーションのデザインの詳細については、画面互換性の概要をご覧ください。

フラグメントを作成する

図 2 フラグメントのライフサイクル(アクティビティの実行中)

フラグメントを作成するには、Fragment(またはその既存のサブクラス)のサブクラスを作成する必要があります。Fragment クラスには、Activity に非常に似ているコードがあります。これには、onCreate()onStart()onPause()onStop() のようなアクティビティと同様のコールバック メソッドが含まれています。実際に、既存の Android アプリケーションをフラグメントを使用するように変換するには、アクティビティのコールバック メソッドから、フラグメントの各コールバック メソッドにコードを移動させるだけで済みます。

通常は、少なくとも次のライフサイクル メソッドを実装する必要があります。

onCreate()
フラグメントの作成時にシステムが呼び出します。実装内で、フラグメントが一時停止、停止して、再開されたときに保持するフラグメントの必須コンポーネントを初期化する必要があります。
onCreateView()
フラグメントが初めてユーザー インターフェースを描画するタイミングでシステムがこれを呼び出します。フラグメントの UI を描画するには、このメソッドからフラグメントのレイアウトのルートとなっている View を返す必要があります。null を返せば、フラグメントが UI を提示しないようにできます。
onPause()
システムは、ユーザーがフラグメントから離れたことを初めて示すものとして、このメソッドを呼び出します(フラグメントが破棄されようとしていない場合も含む)。通常はここで、現在のユーザー セッション後も維持する必要のある変更点を保存しておきます(ユーザーが戻ってこない可能性があるため)。

ほとんどのアプリケーションでは、各フラグメントで少なくともこれら 3 つのメソッドを実装する必要がありますが、フラグメントのライフサイクルのさまざまなステージを処理する際に使用する他のコールバック メソッドもいくつかあります。すべてのライフサイクル コールバック メソッドについては、フラグメントのライフサイクルを処理するのセクションで説明します。

なお、従属コンポーネントのライフサイクル アクションを実装するコードは、フラグメントのコールバック実装の中に直接配置するのではなく、コンポーネントそのものの中に置く必要があることにご注意ください。従属コンポーネントをライフサイクル対応にする方法については、ライフサイクル対応コンポーネントによるライフサイクルの処理をご覧ください。

基本の Fragment クラスの代わりに拡張できるサブクラスもいくつかあります。

DialogFragment
フローティング ダイアログを表示します。Activity でダイアログ ヘルパー メソッドの代わりにこのクラスを使用してダイアログを作成すると、アクティビティで管理されるフラグメントのバックスタックにフラグメント ダイアログを組み込むことができるため、ユーザーは終了したフラグメントに戻ることが可能になります。
ListFragment
ListActivity と同様に、アダプタ(SimpleCursorAdapter など)で管理されるアイテム リストを表示します。クリック イベントを処理するための onListItemClick() コールバックなど、リストビューを管理するメソッドがいくつか提供されます。(リストを表示する方法としては、ListView ではなく RecyclerView を使用することをお勧めします。この場合は、レイアウトに RecyclerView を含めたフラグメントを作成する必要があります。その方法については、RecyclerView によるリストの作成をご覧ください。)
PreferenceFragmentCompat
Preference オブジェクトの階層をリストとして表示します。これは、アプリケーションの設定画面を作成するために使用します。

ユーザー インターフェースを追加する

通常、フラグメントはアクティビティのユーザー インターフェースの一部であり、独自のレイアウトをアクティビティで使用できるようにします。

フラグメントのレイアウトを提供するには、onCreateView() コールバック メソッドを実装する必要があります。これは、フラグメントがレイアウトを描画するタイミングで Android システムが呼び出します。このメソッドの実装では、フラグメントのレイアウトのルートである View を返す必要があります。

注:フラグメントが ListFragment のサブクラスの場合、デフォルトの実装で ListViewonCreateView() から返されるため、これを実装する必要はありません。

onCreateView() からレイアウトを返すには、XML で定義したレイアウト リソースからインフレートできます。これを行うため、onCreateView() から LayoutInflater オブジェクトが提供されます。

たとえば、次の例では Fragment のサブクラスが example_fragment.xml ファイルからレイアウトを読み込みます。

Kotlin

class ExampleFragment : Fragment() {

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.example_fragment, container, false)
    }
}

Java

public static class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.example_fragment, container, false);
    }
}

注:前の例では、R.layout.example_fragment はアプリリソースに保存されている example_fragment.xml という名前のレイアウト リソースへの参照です。XML でレイアウトを作成する方法の詳細については、ユーザー インターフェースのドキュメントをご覧ください。

onCreateView() に渡される container パラメータは、フラグメントのレイアウトが(アクティビティのレイアウトから)挿入される親 ViewGroup です。savedInstanceState パラメータは、フラグメントが再開された場合にフラグメントの前のインスタンスに関する情報を提供する Bundle です(状態の復元の詳細については、フラグメントのライフサイクルを処理するで説明します)。

inflate() メソッドは、次の 3 つの引数を受け取ります。

  • インフレートするレイアウトのリソース ID。
  • インフレートされたレイアウトの親となる ViewGroup。システムがインフレートされたレイアウトのルートビュー(親ビューが指定)にレイアウト パラメータ適用するには、container を渡すことが重要です。
  • インフレート中に、インフレートされたレイアウトを ViewGroup(2 つ目のパラメータ)にアタッチすべきかどうかを示すブール値(この場合、システムがインフレートされたレイアウトを container に既に挿入しているため、false にします。true を渡すと、最終レイアウトに余分なビューグループが作成されます)。

ここまで、レイアウトを提供するフラグメントの作成方法について説明しました。次は、フラグメントをアクティビティに追加する必要があります。

フラグメントをアクティビティに追加する

通常、フラグメントはホスト アクティビティに UI の一部を提供し、アクティビティの全体的なビュー階層の一部として埋め込まれます。アクティビティのレイアウトにフラグメントを追加する方法は 2 つあります。

  • アクティビティのレイアウト ファイル内でフラグメントを宣言する

    この場合、フラグメントがビューであるかのようにレイアウト プロパティを指定できます。以下は、2 つのフラグメントを持つアクティビティのレイアウト ファイルです。

    <?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>
    

    <fragment>android:name 属性では、レイアウト内でインスタンス化する Fragment クラスを指定します。

    システムはこのアクティビティ レイアウトを作成するとき、レイアウトで指定された各フラグメントをインスタンス化し、それぞれの onCreateView() メソッドを呼び出して、各フラグメントのレイアウトを取得します。システムはフラグメントから返された View<fragment> 要素の代わりに直接挿入します。

    注:各フラグメントには、アクティビティの再開時にフラグメントを復元するためにシステムが使用できる(そしてフラグメントをキャプチャして削除などのトランザクションを実行する際に使用できる)一意の識別子が必要です。フラグメントの ID を提供するには、次の 2 つの方法があります。

    • android:id 属性に一意の ID を提供する。
    • android:tag 属性に一意の文字列を提供する。
  • または、既存の ViewGroup にプログラムを使用してフラグメントを追加します

    アクティビティの実行中は、いつでもフラグメントをアクティビティ レイアウトに追加できます。必要なのは、フラグメントを配置する場所に ViewGroup を指定することだけです。

    アクティビティ内でフラグメントのトランザクション(フラグメントの追加、削除、置換など)を実行するには、FragmentTransaction から API を使用する必要があります。FragmentTransaction のインスタンスは、FragmentActivity から次のようにして取得できます。

    Kotlin

    val fragmentManager = supportFragmentManager
    val fragmentTransaction = fragmentManager.beginTransaction()
    

    Java

    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    

    次に、add() メソッドを使用して、追加するフラグメントと挿入するビューを指定してフラグメントを追加できます。次に例を示します。

    Kotlin

    val fragment = ExampleFragment()
    fragmentTransaction.add(R.id.fragment_container, fragment)
    fragmentTransaction.commit()
    

    Java

    ExampleFragment fragment = new ExampleFragment();
    fragmentTransaction.add(R.id.fragment_container, fragment);
    fragmentTransaction.commit();
    

    add() に渡される最初の引数はフラグメントを配置する ViewGroup で、リソース ID で指定されており、2 つ目のパラメータは追加するフラグメントです。

    FragmentTransaction で変更を加えたら、commit() を呼び出して変更を適用する必要があります。

フラグメントを管理する

アクティビティのフラグメントを管理するには、FragmentManager を使用する必要があります。それを取得するには、アクティビティから getSupportFragmentManager() を呼び出します。

FragmentManager では、次の操作が可能です。

  • findFragmentById()(アクティビティ レイアウトで UI を提供するフラグメントの場合)や findFragmentByTag()(UI を提供する、または提供しないフラグメントの場合)を使用して、アクティビティにあるフラグメントを取得する。
  • popBackStack() を使用してフラグメントをバックスタックから取り出す(ユーザーによる「戻る」コマンドをシミュレートする)。
  • addOnBackStackChangedListener() を使用して、バックスタックの変更に対するリスナーを登録する。

これらのメソッドや他のメソッドの詳細については、FragmentManager クラスのドキュメントをご覧ください。

前のセクションで説明したように、FragmentManager を使用して FragmentTransaction を開くことで、フラグメントの追加や削除といったトランザクションを実行することもできます。

フラグメントのトランザクションを実行する

アクティビティでフラグメントを使用すると、ユーザー操作への応答として、フラグメントを追加、削除、置換したり、フラグメントに対して他のアクションを実行したりできます。アクティビティに加える変更のことをそれぞれトランザクションといいます。トランザクションは、FragmentTransaction の API を使用して実行できます。各トランザクションはアクティビティが管理するバックスタックに保存でき、ユーザーはフラグメントの変更を元に戻すことができます(アクティビティ間で元に戻す動作と似ています)。

FragmentTransaction のインスタンスは、次のように FragmentManager から取得できます。

Kotlin

val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()

Java

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

各トランザクションは、同時に実行する一連の変更点です。add()remove()replace() などのメソッドを使って、特定のトランザクションで実行するすべての変更点をセットアップできます。次に、トランザクションをアクティビティに適用するために、commit() を呼び出す必要があります。

ただし、commit() を呼び出す前に、addToBackStack() を呼び出して、フラグメントのトランザクションのバックスタックにトランザクションを追加することもできます。このバックスタックはアクティビティによって管理され、ユーザーが [戻る] ボタンを選択して前のフラグメントの状態に戻れるようにします。

例として、フラグメントを別のフラグメントに置き換えて、バックスタックに前の状態を保持する方法を次に示します。

Kotlin

val newFragment = ExampleFragment()
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, newFragment)
transaction.addToBackStack(null)
transaction.commit()

Java

// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);

// Commit the transaction
transaction.commit();

この例では、R.id.fragment_container ID で識別されるレイアウト コンテナに現在あるフラグメント(存在する場合)を newFragment に置き換えます。addToBackStack() を呼び出すと、置き換えのトランザクションがバックスタックに保存されるため、ユーザーは [戻る] ボタンを押すことで、トランザクションを元に戻して前のフラグメントに戻れるようになります。

すると、FragmentActivityonBackPressed() を使用してバックスタックからフラグメントを自動的に取得します。

トランザクションに複数の変更を加えて(別の add()remove() など)から addToBackStack() を呼び出した場合、commit() を呼び出す前に適用されたすべての変更点が 1 つのトランザクションとしてバックスタックに追加され、[戻る] ボタンを押すとすべてが同時に元に戻るようになります。

次の点を除き、FragmentTransaction に加える変更の順序は影響しません。

  • commit() は最後に呼び出す必要があります。
  • 複数のフラグメントを同じコンテナに追加する場合、追加する順序がビュー階層に表示される順序になります。

フラグメントを削除するトランザクションの実行時に addToBackStack() を呼び出さない場合、トランザクションの実行時にそのフラグメントは破棄され、ユーザーはそのフラグメントに戻ることができなくなります。それに対して、フラグメントの削除時に addToBackStack() を呼び出した場合、フラグメントは停止状態になり、ユーザーが元に戻したときに再開します。

ヒント: フラグメントの各トランザクションでは、コミットする前に setTransition() を呼び出して、遷移のアニメーションを適用できます。

commit() を呼び出しても、トランザクションはすぐには実行されません。具体的には、アクティビティの UI スレッド(「メイン」スレッド)で実行可能になったときにすぐに実行されるようスケジュールされます。ただし、必要であれば UI スレッドから executePendingTransactions() を呼び出して、commit() から送信されたトランザクションをすぐに実行することもできます。通常、トランザクションが他のスレッドのジョブに依存していない限り、この操作は必要ありません。

注意:commit() を使用してトランザクションをコミットできるのは、(ユーザーがアクティビティを離れるときに)アクティビティが状態を保存する前だけです。それより後にコミットしようとすると、例外がスローされます。これは、アクティビティの復元が必要な場合に、コミット後のステータスが失われてしまう可能性があるためです。コミットが失われてもよい場合は、commitAllowingStateLoss() を使用します。

アクティビティと通信する

FragmentFragmentActivity から独立したオブジェクトとして実装され、複数のアクティビティ内で使用可能ですが、フラグメントの特定のインスタンスは、ホストされているアクティビティに直接結び付いています。

具体的には、フラグメントは getActivity() を使用して FragmentActivity インスタンスにアクセスでき、アクティビティのレイアウトでビューを見つけるなどのタスクを簡単に実行できます。

Kotlin

val listView: View? = activity?.findViewById(R.id.list)

Java

View listView = getActivity().findViewById(R.id.list);

同様に、アクティビティが findFragmentById()findFragmentByTag() を使って FragmentManager からFragment への参照を取得することで、フラグメント内のメソッドを呼び出すこともできます。次に例を示します。

Kotlin

val fragment = supportFragmentManager.findFragmentById(R.id.example_fragment) as ExampleFragment

Java

ExampleFragment fragment = (ExampleFragment) getSupportFragmentManager().findFragmentById(R.id.example_fragment);

アクティビティへのイベント コールバックを作成する

場合によっては、フラグメントで、アクティビティやアクティビティによってホストされた他のフラグメントとイベントまたはデータを共有する必要が生じることがあります。データを共有するには、ViewModel ガイドにあるフラグメント間でのデータ共有のセクションで概説されているように、共有された ViewModel を作成します。ViewModel では処理できないイベントを伝播する必要がある場合は、代わりにフラグメント内にコールバック インターフェースを定義することができますが、それを実装するホスト アクティビティが必要になります。アクティビティがインターフェースを介してコールバックを受け取ると、必要に応じてレイアウト内の他のフラグメントと情報を共有します。

たとえば、新しいアプリケーションでアクティビティ内に、記事のリストを表示するフラグメント(フラグメント A)と、記事を表示するフラグメント(フラグメント B)の 2 つのフラグメントがある場合、リストのアイテムが選択されたときに、フラグメント B に記事を表示するよう伝えるため、選択されたことをフラグメント A からアクティビティに伝える必要があります。ここでは、OnArticleSelectedListener インターフェースがフラグメント A で宣言されています。

Kotlin

public class FragmentA : ListFragment() {
    ...
    // Container Activity must implement this interface
    interface OnArticleSelectedListener {
        fun onArticleSelected(articleUri: Uri)
    }
    ...
}

Java

public static class FragmentA extends ListFragment {
    ...
    // Container Activity must implement this interface
    public interface OnArticleSelectedListener {
        public void onArticleSelected(Uri articleUri);
    }
    ...
}

次に、フラグメントをホストするアクティビティが OnArticleSelectedListener インターフェースを実装して onArticleSelected() をオーバーライドし、フラグメント B にフラグメント A からのイベントを通知します。ホスト アクティビティがこのインターフェースを確実に実装するようにするため、フラグメント A の onAttach() コールバック メソッド(フラグメントをアクティビティに追加するときにシステムが呼び出すメソッド)が、onAttach() に渡される Activity をキャストして OnArticleSelectedListener をインスタンス化します。

Kotlin

public class FragmentA : ListFragment() {

    var listener: OnArticleSelectedListener? = null
    ...
    override fun onAttach(context: Context) {
        super.onAttach(context)
        listener = context as? OnArticleSelectedListener
        if (listener == null) {
            throw ClassCastException("$context must implement OnArticleSelectedListener")
        }

    }
    ...
}

Java

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener listener;
    ...
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try {
            listener = (OnArticleSelectedListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement OnArticleSelectedListener");
        }
    }
    ...
}

アクティビティがインターフェースを実装していない場合、フラグメントは ClassCastException をスローします。成功すると、mListener メンバーがアクティビティの OnArticleSelectedListener の実装への参照を保持し、フラグメント A が OnArticleSelectedListener インターフェースで定義されたコールバック メソッドを呼び出すことでイベントをアクティビティと共有できるようになります。たとえば、フラグメント A が ListFragment を拡張したものである場合、ユーザーがリストアイテムをクリックするたびにシステムはフラグメントの onListItemClick() を呼び出し、それが onArticleSelected() を呼び出してイベントをアクティビティと共有します。

Kotlin

public class FragmentA : ListFragment() {

    var listener: OnArticleSelectedListener? = null
    ...
    override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
        // Append the clicked item's row ID with the content provider Uri
        val noteUri: Uri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id)
        // Send the event and Uri to the host activity
        listener?.onArticleSelected(noteUri)
    }
    ...
}

Java

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener listener;
    ...
    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        // Append the clicked item's row ID with the content provider Uri
        Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
        // Send the event and Uri to the host activity
        listener.onArticleSelected(noteUri);
    }
    ...
}

onListItemClick() に渡される id パラメータはクリックされたアイテムの行 ID で、アクティビティ(または他のフラグメント)がアプリの ContentProvider から記事を取得する際に使用します。

コンテンツ プロバイダの使用に関する詳細については、コンテンツ プロバイダのドキュメントをご覧ください。

アイテムをアプリバーに追加する

フラグメントは、onCreateOptionsMenu() を実装することでアクティビティのオプション メニュー(とそのアプリバー)にメニュー アイテムを提供することができます。ただし、このメソッドが呼び出しを受け取るには、onCreate() の間に setHasOptionsMenu() を呼び出して、フラグメントがオプション メニューにアイテムを追加することを示す必要があります。これを行わない場合、フラグメントは onCreateOptionsMenu() への呼び出しを受け取りません。

フラグメントからオプション メニューに追加する項目はすべて、既存のメニュー アイテムに追加されます。また、メニュー アイテムが選択されたとき、フラグメントは onOptionsItemSelected() へのコールバックも受け取ります。

また、registerForContextMenu() を呼び出して、フラグメントのレイアウトにビューを登録してコンテキスト メニューを提供することもできます。ユーザーがコンテキスト メニューを開くと、フラグメントは onCreateContextMenu() への呼び出しを受け取ります。ユーザーが項目を選択すると、フラグメントは onContextItemSelected() への呼び出しを受け取ります。

注:追加するメニュー アイテムごとにフラグメントは on-item-selected コールバックを受け取りますが、アクティビティが最初にそれぞれのコールバックを受け取るのはユーザーがメニュー アイテムを選択したときになります。アクティビティの on-item-selected コールバックの実装で、選択されたアイテムが処理されなかった場合、イベントはフラグメントのコールバックに渡されます。この挙動は、オプション メニューとコンテキスト メニューの両方に適用されます。

メニューの詳細については、デベロッパー ガイドのメニューとトレーニング クラスのアプリバーをご覧ください。

フラグメントのライフサイクルを処理する

図 3 フラグメントのライフサイクルに対するアクティビティのライフサイクルの影響

フラグメントのライフサイクルの管理は、アクティビティのライフサイクルの管理によく似ています。アクティビティと同様に、フラグメントには次の 3 つの状態があります。

再開状態
フラグメントが実行中のアクティビティで表示されている。
一時停止状態
他のアクティビティがフォアグラウンドにあってフォーカスがあるが、このアクティビティも表示されている(フォアグラウンドにある別のアクティビティは半透明になっているか、全画面をカバーしていない)。
停止状態
フラグメントは表示されていない。ホスト アクティビティが停止状態か、フラグメントがアクティビティから削除されたが、バックスタックに追加されている。停止状態のフラグメントはまだ存続しています(すべての状態とメンバー情報がシステムで保持されています)。ただし、ユーザーには表示されなくなっており、アクティビティが破棄されるとフラグメントも破棄されます。

また、アクティビティと同様に、onSaveInstanceState(Bundle)ViewModel、および永続的なローカル ストレージの組み合わせを使用して、構成の変更とプロセスの終了をまたいでフラグメントの UI の状態を保持することができます。UI の状態に保持に関する詳細は、UI の状態を保存するをご覧ください。

アクティビティとフラグメントのライフサイクルの最も重要な違いは、バックスタックでのそれぞれの保存方法です。デフォルトではアクティビティは、停止した時点で、システムによって管理されているアクティビティのバックスタックに置かれます(これにより、タスクとバックスタックで説明されているように、ユーザーは [戻る] ボタンで元に戻ることができます)。それに対して、フラグメントは、フラグメントを削除するトランザクション中に addToBackStack() を呼び出してインスタンスを保存することを明示的にリクエストした場合にのみ、ホスト アクティビティによって管理されているバックスタックに置かれます。

これ以外については、フラグメントのライフサイクルの管理は、アクティビティのライフサイクルの管理とほとんど同じで、同じ習慣が適用されます。アクティビティのライフサイクルとそれを管理する方法の詳細については、アクティビティのライフサイクルガイドとライフサイクル対応コンポーネントによるライフサイクルの処理をご覧ください。

注意:Fragment 内の Context オブジェクトが必要な場合は、getContext() を呼び出すことができます。ただし、getContext() は、フラグメントがアクティビティにアタッチされている場合にのみ呼び出すようにしてください。フラグメントがまだアタッチされていない場合、またはライフサイクルの終わりにデタッチされた場合は、getContext() は null を返します。

アクティビティのライフサイクルと連携する

フラグメントが含まれるアクティビティのライフサイクルは、フラグメントのライフサイクルに直接影響し、アクティビティに対する各ライフサイクル コールバックは、各フラグメントに対して同様のコールバックをもたらします。たとえば、アクティビティが onPause() を受け取ると、アクティビティ内の各フラグメントが onPause() を受け取ります。

ただし、フラグメントには、フラグメントの UI をビルドしたり破棄したりするアクションを実行する際のアクティビティとの独自のやり取りを処理する追加のライフサイクル コールバックがいくつかあります。これらの追加のコールバック メソッドは次のとおりです。

onAttach()
フラグメントがアクティビティと関連付けられたときに呼び出されます(ここで Activity が渡されます)。
onCreateView()
フラグメントに関連付けられたビュー階層を作成する際に呼び出されます。
onActivityCreated()
アクティビティの onCreate() メソッドから戻ったときに呼び出されます。
onDestroyView()
フラグメントに関連付けられたビュー階層が削除されたときに呼び出されます。
onDetach()
フラグメントとアクティビティとの関連付けが解除されたときに呼び出されます。

図 3 は、ホスト アクティビティに対応するフラグメントのライフサイクルのフローを表しています。この図から、アクティビティの一連の状態によって、フラグメントが受け取るコールバック メソッドがどのように決まるかがわかります。たとえば、アクティビティが onCreate() コールバックを受け取ると、アクティビティ内のフラグメントは onActivityCreated() コールバックまでを受け取ります。

アクティビティが再開状態になると、アクティビティでのフラグメントの追加や削除を自由に行えます。つまり、アクティビティが再開された状態は、フラグメントのライフサイクルを独立して変更できる唯一の期間です。

ただし、アクティビティが再開状態でなくなると、フラグメントはアクティビティによって再度ライフサイクル間を通過することになります。

このドキュメントで解説した内容をすべて網羅するため、2 つのフラグメントを使用して 2 つのペインのレイアウトを作成するアクティビティの例を紹介します。次のアクティビティには、シェイクスピア劇のタイトル リストを表示するフラグメントと、リストから劇が選択されたときに劇のあらすじを表示するフラグメントがあります。また、ここでは画面構成によって異なるフラグメント構成を提供する方法も示しています。

注:このアクティビティの完全なソースコードは、FragmentLayout クラスの使用例を示したサンプルアプリで入手可能です。

メイン アクティビティでは、通常の方法で onCreate() の間にレイアウトを適用します。

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.fragment_layout)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.fragment_layout);
}

適用されるレイアウトは fragment_layout.xml です。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent" />

    <FrameLayout android:id="@+id/details" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent"
            android:background="?android:attr/detailsElementBackground" />

</LinearLayout>

このレイアウトを使って、アクティビティがレイアウトを読み込むとすぐにシステムが TitlesFragment(これが劇のタイトルをリストします)をインスタンス化します。このとき、FrameLayout(劇のあらすじを表示するフラグメントの配置場所)が画面の右側を使用しますが、最初は空白の状態です。下に示したとおり、フラグメントが FrameLayout に置かれるのは、ユーザーがリストからアイテムを選択してからになります。

ただし、画面構成によっては、劇の一覧とあらすじを横並びに表示するほどの幅がない場合もあります。そのため、上記のレイアウトを res/layout-land/fragment_layout.xml に保存して、横向きの画面構成の場合のみ使用されるようにします。

画面が縦向きの場合、システムは res/layout/fragment_layout.xml に保存された次のレイアウトを適用します。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles"
            android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

このレイアウトには、TitlesFragment のみが含まれます。つまり、端末が縦方向の場合、劇のタイトルのみが表示されます。そのため、この構成時にユーザーがリストのアイテムをクリックしたとき、アプリケーションは 2 つ目のフラグメントを読み込む代わりに新しいアクティビティを開始してあらすじを表示します。

次に、フラグメントのクラスでこれをどう実現するのかを見ていきます。まず、TitlesFragment はシェイクスピア劇のタイトル リストを表示します。このフラグメントは ListFragment を拡張し、リストビューの動作の処理のほとんどを委ねます。

このコードをよく見ると、ユーザーがリストのアイテムをクリックしたときの挙動が 2 つ考えられます。2 つのレイアウトのどちらがアクティブになっているかによって、新しいフラグメントを作成して表示することで同じアクティビティで詳細を表示するか(FrameLayout にフラグメントを追加する)、新しいアクティビティ(フラグメントを表示する場所)を開始するかのいずれかになります。

Kotlin

class TitlesFragment : ListFragment() {

    private var dualPane: Boolean = false
    private var curCheckPosition = 0

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        // Populate list with our static array of titles.
        listAdapter = ArrayAdapter<String>(
                activity,
                android.R.layout.simple_list_item_activated_1,
                Shakespeare.TITLES
        )

        // Check to see if we have a frame in which to embed the details
        // fragment directly in the containing UI.
        val detailsFrame: View? = activity?.findViewById(R.id.details)
        dualPane = detailsFrame?.visibility == View.VISIBLE

        curCheckPosition = savedInstanceState?.getInt("curChoice", 0) ?: 0

        if (dualPane) {
            // In dual-pane mode, the list view highlights the selected item.
            listView.choiceMode = ListView.CHOICE_MODE_SINGLE
            // Make sure our UI is in the correct state.
            showDetails(curCheckPosition)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("curChoice", curCheckPosition)
    }

    override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
        showDetails(position)
    }

    /**
     * Helper function to show the details of a selected item, either by
     * displaying a fragment in-place in the current UI, or starting a
     * whole new activity in which it is displayed.
     */
    private fun showDetails(index: Int) {
        curCheckPosition = index

        if (dualPane) {
            // We can display everything in-place with fragments, so update
            // the list to highlight the selected item and show the data.
            listView.setItemChecked(index, true)

            // Check what fragment is currently shown, replace if needed.
            var details = fragmentManager?.findFragmentById(R.id.details) as? DetailsFragment
            if (details?.shownIndex != index) {
                // Make new fragment to show this selection.
                details = DetailsFragment.newInstance(index)

                // Execute a transaction, replacing any existing fragment
                // with this one inside the frame.
                fragmentManager?.beginTransaction()?.apply {
                    if (index == 0) {
                        replace(R.id.details, details)
                    } else {
                        replace(R.id.a_item, details)
                    }
                    setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
                    commit()
                }
            }

        } else {
            // Otherwise we need to launch a new activity to display
            // the dialog fragment with selected text.
            val intent = Intent().apply {
                setClass(activity, DetailsActivity::class.java)
                putExtra("index", index)
            }
            startActivity(intent)
        }
    }
}

Java

public static class TitlesFragment extends ListFragment {
    boolean dualPane;
    int curCheckPosition = 0;

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

        // Populate list with our static array of titles.
        setListAdapter(new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));

        // Check to see if we have a frame in which to embed the details
        // fragment directly in the containing UI.
        View detailsFrame = getActivity().findViewById(R.id.details);
        dualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;

        if (savedInstanceState != null) {
            // Restore last state for checked position.
            curCheckPosition = savedInstanceState.getInt("curChoice", 0);
        }

        if (dualPane) {
            // In dual-pane mode, the list view highlights the selected item.
            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            // Make sure our UI is in the correct state.
            showDetails(curCheckPosition);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("curChoice", curCheckPosition);
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        showDetails(position);
    }

    /**
     * Helper function to show the details of a selected item, either by
     * displaying a fragment in-place in the current UI, or starting a
     * whole new activity in which it is displayed.
     */
    void showDetails(int index) {
        curCheckPosition = index;

        if (dualPane) {
            // We can display everything in-place with fragments, so update
            // the list to highlight the selected item and show the data.
            getListView().setItemChecked(index, true);

            // Check what fragment is currently shown, replace if needed.
            DetailsFragment details = (DetailsFragment)
                    getSupportFragmentManager().findFragmentById(R.id.details);
            if (details == null || details.getShownIndex() != index) {
                // Make new fragment to show this selection.
                details = DetailsFragment.newInstance(index);

                // Execute a transaction, replacing any existing fragment
                // with this one inside the frame.
                FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
                if (index == 0) {
                    ft.replace(R.id.details, details);
                } else {
                    ft.replace(R.id.a_item, details);
                }
                ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                ft.commit();
            }

        } else {
            // Otherwise we need to launch a new activity to display
            // the dialog fragment with selected text.
            Intent intent = new Intent();
            intent.setClass(getActivity(), DetailsActivity.class);
            intent.putExtra("index", index);
            startActivity(intent);
        }
    }
}

2 つ目のフラグメントである DetailsFragment は、TitlesFragment のリストで選択された劇のあらすじを表示します。

Kotlin

    class DetailsFragment : Fragment() {

        val shownIndex: Int by lazy {
            arguments?.getInt("index", 0) ?: 0
        }

        override fun onCreateView(
                inflater: LayoutInflater,
                container: ViewGroup?,
                savedInstanceState: Bundle?
        ): View? {
            if (container == null) {
                // We have different layouts, and in one of them this
                // fragment's containing frame doesn't exist. The fragment
                // may still be created from its saved state, but there is
                // no reason to try to create its view hierarchy because it
                // isn't displayed. Note this isn't needed -- we could just
                // run the code below, where we would create and return the
                // view hierarchy; it would just never be used.
                return null
            }

            val text = TextView(activity).apply {
                val padding: Int = TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP,
                        4f,
                        activity?.resources?.displayMetrics
                ).toInt()
                setPadding(padding, padding, padding, padding)
                text = Shakespeare.DIALOGUE[shownIndex]
            }
            return ScrollView(activity).apply {
                addView(text)
            }
        }

        companion object {
            /**
             * Create a new instance of DetailsFragment, initialized to
             * show the text at 'index'.
             */
            fun newInstance(index: Int): DetailsFragment {
                val f = DetailsFragment()

                // Supply index input as an argument.
                val args = Bundle()
                args.putInt("index", index)
                f.arguments = args

                return f
            }
        }
    }
}

Java

public static class DetailsFragment extends Fragment {
    /**
     * Create a new instance of DetailsFragment, initialized to
     * show the text at 'index'.
     */
    public static DetailsFragment newInstance(int index) {
        DetailsFragment f = new DetailsFragment();

        // Supply index input as an argument.
        Bundle args = new Bundle();
        args.putInt("index", index);
        f.setArguments(args);

        return f;
    }

    public int getShownIndex() {
        return getArguments().getInt("index", 0);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (container == null) {
            // We have different layouts, and in one of them this
            // fragment's containing frame doesn't exist. The fragment
            // may still be created from its saved state, but there is
            // no reason to try to create its view hierarchy because it
            // isn't displayed. Note this isn't needed -- we could just
            // run the code below, where we would create and return the
            // view hierarchy; it would just never be used.
            return null;
        }

        ScrollView scroller = new ScrollView(getActivity());
        TextView text = new TextView(getActivity());
        int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                4, getActivity().getResources().getDisplayMetrics());
        text.setPadding(padding, padding, padding, padding);
        scroller.addView(text);
        text.setText(Shakespeare.DIALOGUE[getShownIndex()]);
        return scroller;
    }
}

TitlesFragment クラスで説明したように、ユーザーがリストアイテムをクリックしたときに、現在のレイアウトに R.id.details ビュー(DetailsFragment が属する場所)が含まれていない場合、アプリは DetailsActivity のアクティビティを開始してアイテムのコンテンツを表示します。

次の DetailsActivity では、画面が縦方向のときに単純に DetailsFragment を埋め込んで選択された劇のあらすじを表示します。

Kotlin

class DetailsActivity : FragmentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            // If the screen is now in landscape mode, we can show the
            // dialog in-line with the list so we don't need this activity.
            finish()
            return
        }

        if (savedInstanceState == null) {
            // During initial setup, plug in the details fragment.
            val details = DetailsFragment().apply {
                arguments = intent.extras
            }
            supportFragmentManager.beginTransaction()
                    .add(android.R.id.content, details)
                    .commit()
        }
    }
}

Java

public static class DetailsActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE) {
            // If the screen is now in landscape mode, we can show the
            // dialog in-line with the list so we don't need this activity.
            finish();
            return;
        }

        if (savedInstanceState == null) {
            // During initial setup, plug in the details fragment.
            DetailsFragment details = new DetailsFragment();
            details.setArguments(getIntent().getExtras());
            getSupportFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
        }
    }
}

構成が横向きの場合、このアクティビティそのものは終了し、メイン アクティビティが引き継いで TitlesFragment の横に DetailsFragment を表示します。この動作は、画面が縦向きのときにユーザーが DetailsActivity を開始し、その後で横向きに回転した場合(現在のアクティビティが再始動される)にも発生します。

参考資料

FragmentSunflower デモアプリで使用されています。