フラグメントとナビゲーション コンポーネント

アクティビティとインテントの Codelab では、Words アプリにインテントを追加して、2 つのアクティビティ間を移動しました。これは便利なナビゲーション パターンですが、アプリの動的ユーザー インターフェースを作る話の一部にすぎません。多くの Android アプリは、画面ごとに個別のアクティビティを必要としません。実際、タブなど共通の UI パターンの多くはフラグメントというものを使用して、1 つのアクティビティ内に存在します。

586ff7b88b0d2455.png

フラグメントは UI の再利用可能な部分です。フラグメントは 1 つ以上のアクティビティで再利用と埋め込みができます。上のスクリーンショットでは、タブをタップしても、次の画面を表示するためのインテントはトリガーされません。タブを切り替えると、前のフラグメントが別のフラグメントに置き換えられます。こうした処理はすべて、別のアクティビティを起動せずに行われます。

タブレット デバイスのマスター ディテール レイアウトなど、複数のフラグメントを 1 つの画面に同時に表示することもできます。次の例では、左側のナビゲーション UI と右側のコンテンツの両方を、それぞれ別々のフラグメントに含めることができます。両方のフラグメントが同じアクティビティに同時に存在します。

92f1ecb9aadb7797.png

ご覧のように、フラグメントは高品質なアプリを作成するうえで欠かせない要素です。この Codelab では、フラグメントの基本を学び、フラグメントを使用するように Words アプリを変換します。また、Jetpack Navigation コンポーネントの使用方法と、ナビゲーション グラフという新しいリソース ファイルを使用して、同じホスト アクティビティのフラグメント間を移動する方法についても学びます。この Codelab を修了すると、次のアプリでフラグメントを実装するための基本スキルを習得できます。

前提条件

この Codelab を開始する前に知っておくべきことは次のとおりです。

  • Android Studio プロジェクトにリソース XML ファイルと Kotlin ファイルを追加する方法。
  • アクティビティのライフサイクルの仕組みに関する概要。
  • 既存のクラスのメソッドをオーバーライドして実装する方法。
  • Kotlin クラスのインスタンスを作成し、クラス プロパティにアクセスして、メソッドを呼び出す方法。
  • null 許容値と非 null 許容値に関する基本的な知識があり、null 値を安全に処理する方法を理解していること。

学習内容

  • フラグメントのライフサイクルとアクティビティのライフサイクルの違い。
  • 既存のアクティビティをフラグメントに変換する方法。
  • Safe Args プラグインを使用して、ナビゲーション グラフにデスティネーションを追加し、フラグメント間でデータを渡す方法。

作成するアプリの概要

  • 1 つのアクティビティと複数のフラグメントを使用するように Words アプリを変更し、Navigation コンポーネントを使用してフラグメント間を移動します。

必要なもの

  • Android Studio がインストールされているパソコン
  • アクティビティとインテントの Codelab で作成した Words アプリのソリューション コード

この Codelab では、アクティビティとインテントの Codelab の最後に Words アプリで中断したところから再開します。アクティビティとインテントの Codelab をすでに完了している場合は、そのコードを出発点として自由にご使用ください。ここまでのコードを GitHub からダウンロードすることもできます。

この Codelab のスターター コードをダウンロードする

この Codelab では、ここで学んだ機能を使って拡張するためのスターター コードが提供されます。スターター コードには、以前の Codelab で学んだコードが含まれている場合があります。学んでいないコードが含まれている可能性もありますが、これについては今後の Codelab で学習します。

GitHub のスターター コードを使用する場合、フォルダ名は android-basics-kotlin-words-app-activities です。Android Studio でプロジェクトを開くときは、このフォルダを選択します。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。

コードを取得する

  1. 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
  2. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
  2. パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
  3. ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開かれるまで待ちます。
  4. 実行ボタン j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
  5. [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように実装されているかを確認します。

フラグメントは、アプリのユーザー インターフェースの再利用可能な部分です。アクティビティと同様に、フラグメントにはライフサイクルがあり、ユーザー入力に応答できます。フラグメントは画面に表示されるとき、常にアクティビティのビュー階層内に含まれます。再利用性とモジュール性を重視しているため、1 つのアクティビティで複数のフラグメントを同時にホストすることも可能です。フラグメントはそれぞれ独自のライフサイクルを管理します。

フラグメント ライフサイクル

アクティビティと同様に、フラグメントは初期化してメモリから削除できます。また、フラグメントが存在している間は、画面上で表示、非表示、再表示されます。また、アクティビティと同様に、フラグメントには複数の状態を持つライフサイクルがあり、フラグメント間の遷移に応じてオーバーライドできるメソッドがいくつか用意されています。フラグメントのライフサイクルには 5 つの状態があり、Lifecycle.State 列挙型で表されます。

  • INITIALIZED: フラグメントの新しいインスタンスがインスタンス化されました。
  • CREATED: 最初のフラグメントのライフサイクル メソッドが呼び出されます。この状態では、フラグメントに関連付けられたビューも作成されます。
  • STARTED: フラグメントは画面上に表示されますが、「フォーカス」がありません。つまり、ユーザー入力に応答できません。
  • RESUMED: フラグメントが表示され、フォーカスがあります。
  • DESTROYED: フラグメント オブジェクトのインスタンス化が解除されました。

また、アクティビティと同様に、Fragment クラスには、ライフサイクル イベントに応答するためにオーバーライドできる多くのメソッドが用意されています。

  • onCreate(): フラグメントがインスタンス化され、CREATED 状態になっています。ただし、対応するビューはまだ作成されていません。
  • onCreateView(): このメソッドでは、レイアウトをインフレートします。フラグメントが CREATED 状態になりました。
  • onViewCreated(): ビューの作成後に呼び出されます。このメソッドでは通常、findViewById() を呼び出して特定のビューをプロパティにバインドします。
  • onStart(): フラグメントが STARTED 状態になりました。
  • onResume(): フラグメントが RESUMED 状態になり、フォーカスされました(ユーザー入力に応答できます)。
  • onPause(): フラグメントが再び STARTED 状態になりました。UI がユーザーに表示されます。
  • onStop(): フラグメントが再び CREATED 状態になりました。オブジェクトはインスタンス化されますが、画面に表示されなくなります。
  • onDestroyView(): フラグメントが DESTROYED 状態になる直前に呼び出されます。ビューはすでにメモリから削除されていますが、フラグメント オブジェクトはまだ存在します。
  • onDestroy(): フラグメントが DESTROYED 状態になります。

次の表は、フラグメントのライフサイクルと状態間の遷移をまとめたものです。

74470aacefa170bd.png

ライフサイクルの状態とコールバック メソッドは、アクティビティに使用するものとよく似ています。ただし、onCreate() メソッドの違いに注意してください。アクティビティでは、このメソッドを使用してレイアウトをインフレートし、ビューをバインドします。ただし、フラグメントのライフサイクルでは、ビューの作成前に onCreate() が呼び出されるため、ここでレイアウトをインフレートすることはできません。代わりに、onCreateView() で行います。その後、ビューが作成されると onViewCreated() メソッドが呼び出され、プロパティを特定のビューにバインドできます。

理論が多いように思われるかもしれませんが、フラグメントの仕組みやアクティビティとの類似点および相違点について、基本はすでに学んでいます。この Codelab の残りの部分では、その知識を活用します。まず、前に扱った Words アプリを、フラグメント ベースのレイアウトを使用するように移行します。次に、1 つのアクティビティ内でフラグメント間のナビゲーションを実装します。

アクティビティと同様に、追加する各フラグメントは 2 つのファイル(レイアウト用の XML ファイルと、データを表示してユーザー操作を処理するための Kotlin クラス)で構成されます。文字リストと単語リストの両方について、フラグメントを追加します。

  1. Project Navigator で「app」を選択した状態で、次のフラグメントを追加します([File] > [New] > [Fragment] > [Fragment (Blank)])。それぞれにクラスファイルとレイアウト ファイルが生成されます。
  • 最初のフラグメントでは、[Fragment Name] を「LetterListFragment」に設定します。[Fragment Layout Name] に「fragment_letter_list」と入力されます。

898650e4cd0b2486.png

  • 2 番目のフラグメントでは、[Fragment Name] を「WordListFragment」に設定します。[Fragment Layout Name] に「fragment_word_list.xml」と入力されます。

4f04fca641487da1.png

  1. 両方のフラグメントに対して生成される Kotlin クラスには、フラグメントの実装時によく使用されるボイラープレート コードが多数含まれています。ただし、初めてフラグメントについて学習している場合は、LetterListFragmentWordListFragment のクラス宣言を除くすべてを両方のファイルから削除します。すべてのコードがどのように機能するかを理解できるように、フラグメントを最初から実装する手順を説明します。ボイラープレート コードを削除すると、Kotlin ファイルは次のようになります。

LetterListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class LetterListFragment : Fragment() {

}

WordListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class WordListFragment : Fragment() {

}
  1. activity_main.xml の内容を fragment_letter_list.xml にコピーし、activity_detail.xml の内容を fragment_word_list.xml にコピーします。fragment_letter_list.xmltools:context.LetterListFragment に更新し、fragment_word_list.xmltools:context.WordListFragment に更新します。

変更すると、フラグメント レイアウト ファイルは次のようになります。

fragment_letter_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
   tools:context=".WordListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp" />

</FrameLayout>

fragment_word_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
   tools:context=".WordListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp"
       tools:listitem="@layout/item_view" />

</FrameLayout>

アクティビティと同様に、レイアウトをインフレートし、個々のビューをバインドする必要があります。フラグメントのライフサイクルを扱う場合、若干の違いがあります。LetterListFragment の設定手順について説明します。その後は、WordListFragment にも同じことができます。

LetterListFragment にビュー バインディングを実装するには、まず FragmentLetterListBinding への null 許容参照を取得する必要があります。このようなバインディング クラスは、build.gradle ファイルの buildFeatures セクションで viewBinding プロパティを有効にすると、Android Studio によってレイアウト ファイルごとに生成されます。FragmentLetterListBinding のビューごとに、フラグメント クラスのプロパティを割り当てるだけで済みます。

型は FragmentLetterListBinding? にする必要があり、初期値は null にする必要があります。null 許容にするのはなぜでしょうか。これは、onCreateView() が呼び出されるまでレイアウトをインフレートできないためです。LetterListFragment のインスタンスが作成されてから(ライフサイクルが onCreate() で始まる)、このプロパティが実際に使用可能になるまで、一定の期間があります。また、フラグメントのビューは、フラグメントのライフサイクルを通じて何度も作成、破棄される可能性があることにもご注意ください。このため、別のライフサイクル メソッド onDestroyView() の値をリセットする必要もあります。

  1. LetterListFragment.kt で、まず FragmentLetterListBinding への参照を取得し、参照に _binding という名前を付けます。
private var _binding: FragmentLetterListBinding? = null

null 許容であるため、_binding のプロパティ(_binding?.someView など)にアクセスするたび、null 安全のために「?」を含める必要があります。ただし、null 値 1 つのためだけに、コードに疑問符を付ける必要はありません。ある値にアクセスするとき、その値が null でないことが確かであれば、型名に「!!」を付加できます。すると、? 演算子を使用せずに、他のプロパティと同じようにアクセスできます。

  1. binding(アンダースコアなし)という新しいプロパティを作成し、_binding!! に設定します。
private val binding get() = _binding!!

ここで、get() はこのプロパティが「get-only」であることを意味します。つまり、値を取得できますが、一度割り当てると(ここでのように)、他に割り当てることはできません。

  1. onCreate() を実装するには、単に setHasOptionsMenu() を呼び出します。
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setHasOptionsMenu(true)
}
  1. フラグメントでは、onCreateView() でレイアウトがインフレートされます。ビューをインフレートし、_binding の値を設定して、ルートビューを返すことで、onCreateView() を実装します。
override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentLetterListBinding.inflate(inflater, container, false)
   val view = binding.root
   return view
}
  1. binding プロパティの下に、リサイクラー ビューのプロパティを作成します。
private lateinit var recyclerView: RecyclerView
  1. 次に、onViewCreated()recyclerView プロパティの値を設定し、MainActivity で行ったように chooseLayout() を呼び出します。まもなく chooseLayout() メソッドを LetterListFragment に移動するため、エラーは気にしないでください。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   chooseLayout()
}

バインディング クラスによって recyclerView のプロパティがすでに作成されています。ビューごとに findViewById() を呼び出す必要はありません。

  1. 最後に、ビューがすでに存在しなくなったため、onDestroyView()_binding プロパティを null にリセットします。
override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}
  1. 他に注意すべき点は、フラグメントを扱う際、onCreateOptionsMenu() メソッドと微妙な違いがあることです。Activity クラスには menuInflater というグローバル プロパティがありますが、フラグメントにはこのプロパティはありません。代わりに、メニュー インフレータが onCreateOptionsMenu() に渡されます。また、フラグメントで使用される onCreateOptionsMenu() メソッドに return 文は必要ありません。次のようにメソッドを実装します。
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
   inflater.inflate(R.menu.layout_menu, menu)

   val layoutButton = menu.findItem(R.id.action_switch_layout)
   setIcon(layoutButton)
}
  1. chooseLayout()setIcon()onOptionsItemSelected() の残りのコードを MainActivity からそのまま移動します。注意すべき唯一の違いは、アクティビティとは異なり、フラグメントは Context ではないことです。this(フラグメント オブジェクトを指す)をレイアウト マネージャーのコンテキストとして渡すことはできません。ただし、フラグメントは代わりに使用できる context プロパティを提供します。コードの残りの部分は MainActivity と同じです。
private fun chooseLayout() {
   when (isLinearLayoutManager) {
       true -> {
           recyclerView.layoutManager = LinearLayoutManager(context)
           recyclerView.adapter = LetterAdapter()
       }
       false -> {
           recyclerView.layoutManager = GridLayoutManager(context, 4)
           recyclerView.adapter = LetterAdapter()
       }
   }
}

private fun setIcon(menuItem: MenuItem?) {
   if (menuItem == null)
       return

   menuItem.icon =
       if (isLinearLayoutManager)
           ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
       else ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_linear_layout)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           isLinearLayoutManager = !isLinearLayoutManager
           chooseLayout()
           setIcon(item)

           return true
       }

       else -> super.onOptionsItemSelected(item)
   }
}
  1. 最後に、MainActivity から isLinearLayoutManager プロパティをコピーします。これを、recyclerView プロパティの宣言のすぐ下に配置します。
private var isLinearLayoutManager = true
  1. すべての機能が LetterListFragment に移動されたので、MainActivity クラスはすべて、フラグメントがビューに表示されるようにレイアウトをインフレートするだけで済みます。onCreate() を除くすべてを MainActivity から削除します。変更後、MainActivity には次の内容だけが含まれます。
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

実践

MainActivityLettersListFragment に移行する作業は以上です。DetailActivity の移行はほぼ同じです。コードを WordListFragment に移行する手順は次のとおりです。

  1. コンパニオン オブジェクトを DetailActivity から WordListFragment にコピーします。WordAdapterSEARCH_PREFIX への参照が、WordListFragment を参照するように更新されていることを確認します。
  2. _binding 変数を追加します。この変数は null 許容で、null を初期値にする必要があります。
  3. _binding 変数と等しい binding という get-only 変数を追加します。
  4. onCreateView() でレイアウトをインフレートし、_binding の値を設定してルートビューを返します。
  5. onViewCreated() の残りの設定を行います。リサイクラー ビューへの参照を取得し、レイアウト マネージャーとアダプターを設定して、アイテム デコレーションを追加します。インテントから文字を取得する必要があります。フラグメントには intent プロパティがなく、通常は親アクティビティのインテントにアクセスしません。現時点では、activity.intentDetailActivityintent ではなく)を参照してエクストラを取得します。
  6. onDestroyView_binding を null にリセットします。
  7. DetailActivity から残りのコードを削除します(onCreate() メソッドのみ残します)。

先のステップに進む前に、ご自身で試してください。詳細なチュートリアルは次のステップでご利用いただけます。

DetailActivityWordListFragment に移行してみて、いかがでしたか。これは、MainActivityLetterListFragment に移行することとほぼ同じです。行き詰まってしまった場合のために、手順のまとめを次に示します。

  1. まず、コンパニオン オブジェクトを WordListFragment にコピーします。
companion object {
   val LETTER = "letter"
   val SEARCH_PREFIX = "https://www.google.com/search?q="
}
  1. 次に、インテントを実行する onClickListener()LetterAdapter で、putExtra() の呼び出しを更新し、DetailActivity.LETTERWordListFragment.LETTER に置き換える必要があります。
intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())
  1. 同様に、WordAdapter では、単語の検索結果に移動した onClickListener() を更新し、DetailActivity.SEARCH_PREFIXWordListFragment.SEARCH_PREFIX に置き換える必要があります。
val queryUrl: Uri = Uri.parse("${WordListFragment.SEARCH_PREFIX}${item}")
  1. WordListFragment に戻り、FragmentWordListBinding? 型のバインディング変数を追加します。
private var _binding: FragmentWordListBinding? = null
  1. 次に、? を使用せずにビューを参照できるように、get-only 変数を作成します。
private val binding get() = _binding!!
  1. その後、レイアウトをインフレートし、_binding 変数を割り当て、ルートビューを返します。フラグメントの場合、onCreate() ではなく onCreateView() で行います。
override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentWordListBinding.inflate(inflater, container, false)
   return binding.root
}
  1. 次に、onViewCreated() を実装します。これは、DetailActivityonCreateView()recyclerView を構成する場合とほぼ同じです。ただし、フラグメントはインテントに直接アクセスできないため、activity.intent で参照する必要があります。これは onCreateView() 内で行う必要があります。ライフサイクルの早い段階でアクティビティが存在する保証がないためです。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   val recyclerView = binding.recyclerView
   recyclerView.layoutManager = LinearLayoutManager(requireContext())
   recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())

   recyclerView.addItemDecoration(
       DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
   )
}
  1. 最後に、onDestroyView()_binding 変数をリセットできます。
override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}
  1. この機能はすべて WordListFragment に移動され、DetailActivity からコードを削除できるようになりました。残るのは onCreate() メソッドだけです。
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityDetailBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

DetailActivity を削除する

DetailActivity の機能が WordListFragment に正常に移行されたため、DetailActivity は不要になりました。先に進み、DetailActivity.ktactivity_detail.xml の両方を削除して、マニフェストを少し変更できます。

  1. まず、DetailActivity.kt を削除します。

dd3b0bcf3ec81c9.png

  1. [Safe Delete] チェックボックスがオフになっていることを確認し、[OK] をクリックします。

f2f1ff137b0057a7.png

  1. 次に、activity_detail.xml を削除します。再度、[Safe Delete] チェックボックスがオフになっていることを確認します。

6090c1d640433e07.png

  1. 最後に、DetailActivity は存在しなくなったため、以下を AndroidManifest.xml から削除します。
<activity
   android:name=".DetailActivity"
   android:parentActivityName=".MainActivity" />

詳細アクティビティを削除すると、2 つのフラグメント(LetterListFragment、WordListFragment)と 1 つのアクティビティ(MainActivity)が残ります。次のセクションでは、Jetpack Navigation コンポーネントについて学び、静的レイアウトをホストするのではなく、フラグメントの表示と移動ができるように activity_main.xml を編集します。

Android Jetpack には Navigation コンポーネントが用意されており、アプリ内でのナビゲーションの実装を(単純か複雑かにかかわらず)処理できます。Navigation コンポーネントには、Words アプリでナビゲーションを実装するために使用する主要部分が 3 つあります。

  • ナビゲーション グラフ: ナビゲーション グラフは、アプリ内のナビゲーションを視覚的に表現する XML ファイルです。このファイルは、個々のアクティビティとフラグメントに対応するデスティネーションと、それらの間のアクション(あるデスティネーションから別のデスティネーションに移動するためのコードで使用)で構成されます。レイアウト ファイルと同様に、Android Studio には、ナビゲーション グラフにデスティネーションとアクションを追加するビジュアル エディタが用意されています。
  • NavHost: NavHost は、アクティビティ内のナビゲーション グラフからデスティネーションを表示するために使用します。フラグメント間を移動すると、NavHost に表示されるデスティネーションが更新されます。MainActivity では、NavHostFragment という組み込み実装を使用します。
  • NavController: NavController オブジェクトを使用すると、NavHost に表示されるデスティネーション間のナビゲーションを制御できます。インテントを扱うときは、startActivity を呼び出して新しい画面に移動する必要がありました。Navigation コンポーネントでは、NavControllernavigate() メソッドを呼び出して、表示されるフラグメントを入れ替えることができます。また NavController では、システムの「上へ」ボタンに応答して前に表示したフラグメントに戻るなどの一般的なタスクも処理できます。
  1. プロジェクト レベルの build.gradle ファイルの [buildscript] > [ext] で、material_versionnav_version2.3.1 に設定します。
buildscript {
    ext {
        appcompat_version = "1.2.0"
        constraintlayout_version = "2.0.2"
        core_ktx_version = "1.3.2"
        kotlin_version = "1.3.72"
        material_version = "1.2.1"
        nav_version = "2.3.1"
    }

    ...

}

  1. アプリレベルの build.gradle ファイルで、以下を依存関係グループに追加します。
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

Safe Args プラグイン

Words アプリで最初にナビゲーションを実装したとき、2 つのアクティビティの間に明示的なインテントを使用しました。2 つのアクティビティ間でデータを渡すために、putExtra() メソッドを呼び出し、選択した文字を渡しました。

Navigation コンポーネントを Words アプリに実装する前に、Safe Args というものも追加します。これは、フラグメント間でデータを渡すときの型安全性を支援する Gradle プラグインです。

SafeArgs をプロジェクトに統合する手順は次のとおりです。

  1. 最上位の build.gradle ファイルの [buildscript] > [dependencies] で、次のクラスパスを追加します。
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
  1. アプリレベルの build.gradle ファイルの上部にある plugins 内で、androidx.navigation.safeargs.kotlin を追加します。
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'androidx.navigation.safeargs.kotlin'
}
  1. Gradle ファイルを編集すると、プロジェクトを同期するよう求める黄色のバナーが上部に表示されることがあります。[Sync Now] をクリックし、Gradle がプロジェクトの依存関係を更新して変更内容を反映するまで、1~2 分待ちます。

854d44a6f7c4c080.png

同期が完了したら、ナビゲーション グラフを追加する次のステップに進みます。

フラグメントとライフサイクルの基本事項について理解したので、ここからはもう少し面白いことをしてみましょう。次のステップは、Navigation コンポーネントの組み込みです。ナビゲーション コンポーネントとは単に、(特にフラグメント間で)ナビゲーションを実装するためのツールのコレクションを指します。ここでは新しいビジュアル エディタを使用して、フラグメント間のナビゲーションを実装します(ナビゲーション グラフ、略称 NavGraph)。

ナビゲーション グラフとは

ナビゲーション グラフ(略称 NavGraph)は、アプリのナビゲーションの仮想マッピングです。各画面(今回はフラグメント)は、移動できる「デスティネーション」になります。NavGraph は、各デスティネーションが互いにどのように関連しているかを示す XML ファイルで表すことができます。

背後で、これが実際に NavGraph クラスの新しいインスタンスを作成します。ただし、ナビゲーション グラフのデスティネーションは、FragmentContainerView によってユーザーに表示されます。必要な作業は、XML ファイルを作成し、可能性のあるデスティネーションを定義することだけです。その後、生成されたコードを使用してフラグメント間を移動できます。

MainActivity で FragmentContainerView を使用する

レイアウトが fragment_letter_list.xmlfragment_word_list.xml に含まれるようになったため、activity_main.xml ファイルにアプリの最初の画面のレイアウトを含める必要がなくなりました。代わりに、FragmentContainerView を含むように MainActivity を再利用し、フラグメントの NavHost として機能させます。これ以降、アプリのナビゲーションはすべて、FragmentContainerView 内で行われます。

  1. activity_main.xmlFrameLayout の内容(androidx.recyclerview.widget.RecyclerView)を、FragmentContainerView に置き換えます。ID を nav_host_fragment とし、高さと幅を match_parent に設定して、フレーム レイアウト全体を埋めます。

これを

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        ...
        android:padding="16dp" />

次のように置き換えます。

<androidx.fragment.app.FragmentContainerView
   android:id="@+id/nav_host_fragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. id 属性の下に name 属性を追加し、androidx.navigation.fragment.NavHostFragment に設定します。この属性に特定のフラグメントを指定できますが、NavHostFragment に設定すると、FragmentContainerView でフラグメント間を移動できるようになります。
android:name="androidx.navigation.fragment.NavHostFragment"
  1. layout_height 属性と layout_width 属性の下に app:navHost という属性を追加し、"true" に設定します。これにより、フラグメント コンテナでナビゲーション階層を操作できるようになります。たとえば、システムの戻るボタンを押すと、新しいアクティビティが表示されたときと同様に、コンテナは前に表示されたフラグメントに戻ります。
app:defaultNavHost="true"
  1. app:navGraph という属性を追加し、"@navigation/nav_graph" に設定します。これは、アプリのフラグメントが相互に移動する方法を定義する XML ファイルを指します。現在のところ、Android Studio では未解決シンボルエラーが表示されます。これについては次のタスクで対処します。
app:navGraph="@navigation/nav_graph"
  1. 最後に、アプリの名前空間で 2 つの属性を追加したため、必ず xmlns:app 属性を FrameLayout に追加してください。
<xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

activity_main.xml の変更は以上です。次に、nav_graph ファイルを作成します。

ナビゲーション グラフをセットアップする

ナビゲーション グラフファイルを追加し([File] > [New] > [Android Resource File])、次のとおりフィールドに入力します。

  • File Name: nav_graph.xml.。これは、app:navGraph 属性に設定した名前と同じです。
  • Resource Type: Navigation。[Directory Name] が自動的に「navigation」に変更されます。「navigation」という新しいリソース フォルダが作成されます。

e26ed91764a5616e.png

XML ファイルを作成すると、新しいビジュアル エディタが表示されます。FragmentContainerViewnavGraph プロパティで nav_graph をすでに参照しているため、新しいデスティネーションを追加するには、画面左上の新規作成ボタンをクリックして、フラグメントごとにデスティネーションを作成します(fragment_letter_list 用に 1 つ、fragment_word_list 用に 1 つ)。

307d036fce790feb.gif

追加したフラグメントは、画面中央のナビゲーション グラフに表示されます。左側に表示されるコンポーネント ツリーを使用して特定のデスティネーションを選択することもできます。

ナビゲーション アクションを作成する

letterListFragment デスティネーションと wordListFragment デスティネーションの間のナビゲーション アクションを作成するには、letterListFragment デスティネーションにカーソルを合わせて、右側に表示される円から wordListFragment デスティネーションにドラッグします。

c9477af5828a83f4.gif

これで、2 つのデスティネーション間のアクションを表す矢印が作成されました。矢印をクリックすると、このアクションがコードで参照できる名前 action_letterListFragment_to_wordListFragment を持っていることが [Attributes] ペインに表示されます。

WordListFragment の引数を指定する

インテントを使用してアクティビティ間を移動するとき、選択した文字が wordListFragment に渡されるように「エクストラ」を指定しました。ナビゲーションは、デスティネーション間のパラメータの受け渡しもサポートしており、型安全な方法でこれを行います。

wordListFragment デスティネーションを選択し、[Attributes] ペインの [Arguments] で、プラスボタンをクリックして新しい引数を作成します。

引数の名前は letter とし、型は String とします。ここで、先ほど追加した Safe Args プラグインの出番です。この引数を文字列として指定すると、ナビゲーション アクションがコード内で実行されたときに String が想定されます。

b6bc3eaacd14bf50.png

開始デスティネーションの設定

NavGraph は必要なデスティネーションをすべて認識しますが、FragmentContainerView は、最初に表示するフラグメントをどのようにして把握するでしょうか。NavGraph では、文字リストを開始デスティネーションとして設定する必要があります。

letterListFragment」を選択して [Assign start destination] ボタンをクリックし、開始デスティネーションを設定します。

99bb085e39dd7b4a.png

NavGraph エディタで行う必要がある作業は以上です。この時点で、プロジェクトを作成します。ナビゲーション グラフに基づいてコードが生成されるため、作成したナビゲーション アクションを使用できます。

ナビゲーション アクションを実行する

LetterAdapter.kt を開いて、ナビゲーション アクションを実行します。必要なステップは 2 つだけです。

  1. ボタンの onClickListener() の内容を削除します。代わりに、先ほど作成したナビゲーション アクションを取得する必要があります。onClickListener() に以下を追加します。
val action = LetterListFragmentDirections.actionLetterListFragmentToWordListFragment(letter = holder.button.text.toString())

クラス名と関数名の中には、見覚えのないものもあるはずです。これは、プロジェクトの作成後に自動的に生成されたためです。そこで、最初のステップで追加した Safe Args プラグインを使用します。NavGraph で作成されたアクションは、使用可能なコードに変換されます。ただし、名前はかなり直感的です。LetterListFragmentDirections を使用すると、letterListFragment から始まるナビゲーション パスをすべて参照できます。関数 actionLetterListFragmentToWordListFragment()

は、wordListFragment. に移動する具体的なアクションです。

ナビゲーション アクションへの参照を取得したら、NavController(ナビゲーション アクションを実行できるオブジェクト)への参照を取得し、アクションを渡して navigate() を呼び出します。

holder.view.findNavController().navigate(action)

MainActivity を設定する

最後の設定は MainActivity です。すべてを機能させるには、MainActivity に少しだけ変更が必要です。

  1. navController プロパティを作成します。これは onCreate で設定されるため、lateinit とマークされます。
private lateinit var navController: NavController
  1. 次に、onCreate()setContentView() を呼び出した後に、nav_host_fragment への参照(FragmentContainerView の ID)を取得して navController プロパティに割り当てます。
val navHostFragment = supportFragmentManager
    .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
  1. 次に onCreate() で、navController を渡して setupActionBarWithNavController() を呼び出します。これにより、LetterListFragment のメニュー オプションなど、アクションバー(アプリバー)ボタンが表示されるようになります。
setupActionBarWithNavController(navController)
  1. 最後に、onSupportNavigateUp() を実装します。XML で defaultNavHosttrue に設定するとともに、このメソッドでは、「上へ」ボタンを処理できます。ただし、アクティビティは実装を提供する必要があります。
override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}

この時点で、すべてのコンポーネントが所定の位置に配置され、ナビゲーションでフラグメントを扱えるようになります。ただし、インテントではなくフラグメントを使用してナビゲーションを実行するようになったため、WordListFragment で使用する文字のインテント エクストラは機能しなくなります。次のステップでは、WordListFragment を更新して letter 引数を取得します。

前に、WordListFragmentactivity?.intent を参照して、letter エクストラにアクセスしました。これは機能しますが、フラグメントは他のレイアウトに埋め込まれることがあるため、おすすめしません。また大規模なアプリでは、フラグメントが属するアクティビティの判断がはるかに困難になります。さらに、nav_graph を使用してナビゲーションが実行され、安全な引数が使用されている場合、インテントはないため、インテント エクストラにアクセスしようとしてもうまくいきません。

幸い、安全な引数へのアクセスは非常に簡単であり、onViewCreated() が呼び出されるまで待機する必要もありません。

  1. WordListFragmentletterId プロパティを作成します。これを lateinit としてマークすれば、null 許容にする必要はありません。
private lateinit var letterId: String
  1. 次に、(onCreateView()onViewCreated() ではなく)onCreate() をオーバーライドし、以下を追加します。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {
        letterId = it.getString(LETTER).toString()
    }
}

arguments は省略可能な場合があるため、let() を呼び出してラムダを渡しています。このコードは、arguments が null ではないと仮定して実行され、it パラメータに null でない引数を渡します。ただし、argumentsnull の場合、ラムダは実行されません。

96a6a3253cea35b0.png

実際のコードの一部ではないものの、Android Studio では、it パラメータを認識するための便利なヒントが提供されます。

Bundle とは正確には何でしょうか。これは、アクティビティやフラグメントなどのクラス間でデータを渡すために使用される Key-Value ペアと考えてください。実は、このアプリの最初のバージョンでインテントを実行する際に intent?.extras?.getString() を呼び出したとき、すでにバンドルを使用していました。フラグメントを操作するときに引数から文字列を取得する方法も、まったく同じです。

  1. 最後に、リサイクラー ビューのアダプターを設定すると、letterId にアクセスできます。onViewCreated()activity?.intent?.extras?.getString(LETTER).toString()letterId に置き換えます。
recyclerView.adapter = WordAdapter(letterId, requireContext())

それでは、アプリを実行してみましょう。インテントなしで、すべて 1 つのアクティビティで、2 つの画面間を移動できるようになりました。

両方の画面がフラグメントを使用するように正常に変換されました。変更が行われる前、各フラグメントのアプリバーには、アプリバー内のアクティビティごとにわかりやすいタイトルが付けられていました。しかし、フラグメントを使用するように変換した後、このタイトルは詳細アクティビティからなくなりました。

c385595994ba91b5.png

フラグメントには "label" というプロパティがあり、親アクティビティがアプリバーで使用するタイトルを設定できます。

  1. strings.xml で、アプリ名の後に次の定数を追加します。
<string name="word_list_fragment_label">Words That Start With {letter}</string>
  1. ナビゲーション グラフの各フラグメントのラベルを設定できます。nav_graph.xml に戻り、コンポーネント ツリーで letterListFragment を選択します。[Attributes] ペインで、ラベルを app_name 文字列に設定します。

4568d78c606999d.png

  1. wordListFragment を選択し、ラベルを word_list_fragment_label に設定します。

7e7e55ea2dfb65bb.png

おつかれさまでした。アプリをもう一度実行すると、この Codelab を始めたときと同様にすべてが表示されるはずです。ただし今回は、画面ごとに別々のフラグメントで、すべてのナビゲーションが 1 つのアクティビティでホストされます。

この Codelab のソリューション コードは、以下のプロジェクトにあります。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。

コードを取得する

  1. 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
  2. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
  2. パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
  3. ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開かれるまで待ちます。
  4. 実行ボタン j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
  5. [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように実装されているかを確認します。
  • フラグメントは、アクティビティに埋め込むことができる、UI の再利用可能な部分です。
  • フラグメントのライフサイクルは、アクティビティのライフサイクルとは異なり、ビューの設定は onCreateView() ではなく onViewCreated() で行われます。
  • FragmentContainerView は、フラグメントを他のアクティビティに埋め込むために使用します。フラグメント間のナビゲーションを管理できます。

Navigation コンポーネントの使用

  • FragmentContainerViewnavGraph 属性を設定すると、アクティビティ内のフラグメント間を移動できます。
  • NavGraph エディタを使用すると、ナビゲーション アクションを追加でき、異なるデスティネーション間で引数を指定できます。
  • インテントを使用したナビゲーションではエクストラを渡す必要がありますが、Navigation コンポーネントでは SafeArgs を使用して、ナビゲーション アクションのクラスとメソッドを自動生成し、引数で型安全性を確保します。

フラグメントのユースケース

  • Navigation コンポーネントを使用することで、多くのアプリは 1 つのアクティビティ内でレイアウト全体を管理でき、すべてのナビゲーションがフラグメント間で行われます。
  • フラグメントを使用すると、タブレットでのマスター ディテール レイアウトや、同じアクティビティ内での複数のタブなど、一般的なレイアウト パターンが可能となります。