アクティビティとインテントの Codelab では、Words アプリにインテントを追加して、2 つのアクティビティ間を移動しました。これは便利なナビゲーション パターンですが、アプリの動的ユーザー インターフェースを作る話の一部にすぎません。多くの Android アプリは、画面ごとに個別のアクティビティを必要としません。実際、タブなど共通の UI パターンの多くはフラグメントというものを使用して、1 つのアクティビティ内に存在します。
フラグメントは UI の再利用可能な部分です。フラグメントは 1 つ以上のアクティビティで再利用と埋め込みができます。上のスクリーンショットでは、タブをタップしても、次の画面を表示するためのインテントはトリガーされません。タブを切り替えると、前のフラグメントが別のフラグメントに置き換えられます。こうした処理はすべて、別のアクティビティを起動せずに行われます。
タブレット デバイスのマスター ディテール レイアウトなど、複数のフラグメントを 1 つの画面に同時に表示することもできます。次の例では、左側のナビゲーション UI と右側のコンテンツの両方を、それぞれ別々のフラグメントに含めることができます。両方のフラグメントが同じアクティビティに同時に存在します。
ご覧のように、フラグメントは高品質なアプリを作成するうえで欠かせない要素です。この 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 で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。
- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。
- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開かれるまで待ちます。
- 実行ボタン をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
- [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
状態になります。
次の表は、フラグメントのライフサイクルと状態間の遷移をまとめたものです。
ライフサイクルの状態とコールバック メソッドは、アクティビティに使用するものとよく似ています。ただし、onCreate()
メソッドの違いに注意してください。アクティビティでは、このメソッドを使用してレイアウトをインフレートし、ビューをバインドします。ただし、フラグメントのライフサイクルでは、ビューの作成前に onCreate()
が呼び出されるため、ここでレイアウトをインフレートすることはできません。代わりに、onCreateView()
で行います。その後、ビューが作成されると onViewCreated()
メソッドが呼び出され、プロパティを特定のビューにバインドできます。
理論が多いように思われるかもしれませんが、フラグメントの仕組みやアクティビティとの類似点および相違点について、基本はすでに学んでいます。この Codelab の残りの部分では、その知識を活用します。まず、前に扱った Words アプリを、フラグメント ベースのレイアウトを使用するように移行します。次に、1 つのアクティビティ内でフラグメント間のナビゲーションを実装します。
アクティビティと同様に、追加する各フラグメントは 2 つのファイル(レイアウト用の XML ファイルと、データを表示してユーザー操作を処理するための Kotlin クラス)で構成されます。文字リストと単語リストの両方について、フラグメントを追加します。
- Project Navigator で「app」を選択した状態で、次のフラグメントを追加します([File] > [New] > [Fragment] > [Fragment (Blank)])。それぞれにクラスファイルとレイアウト ファイルが生成されます。
- 最初のフラグメントでは、[Fragment Name] を「
LetterListFragment
」に設定します。[Fragment Layout Name] に「fragment_letter_list
」と入力されます。
- 2 番目のフラグメントでは、[Fragment Name] を「
WordListFragment
」に設定します。[Fragment Layout Name] に「fragment_word_list.xml
」と入力されます。
- 両方のフラグメントに対して生成される Kotlin クラスには、フラグメントの実装時によく使用されるボイラープレート コードが多数含まれています。ただし、初めてフラグメントについて学習している場合は、
LetterListFragment
とWordListFragment
のクラス宣言を除くすべてを両方のファイルから削除します。すべてのコードがどのように機能するかを理解できるように、フラグメントを最初から実装する手順を説明します。ボイラープレート コードを削除すると、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() {
}
activity_main.xml
の内容をfragment_letter_list.xml
にコピーし、activity_detail.xml
の内容をfragment_word_list.xml
にコピーします。fragment_letter_list.xml
のtools:context
を.LetterListFragment
に更新し、fragment_word_list.xml
のtools: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()
の値をリセットする必要もあります。
LetterListFragment.kt
で、まずFragmentLetterListBinding
への参照を取得し、参照に_binding
という名前を付けます。
private var _binding: FragmentLetterListBinding? = null
null 許容であるため、_binding
のプロパティ(_binding?.someView
など)にアクセスするたび、null 安全のために「?
」を含める必要があります。ただし、null 値 1 つのためだけに、コードに疑問符を付ける必要はありません。ある値にアクセスするとき、その値が null でないことが確かであれば、型名に「!!
」を付加できます。すると、?
演算子を使用せずに、他のプロパティと同じようにアクセスできます。
- binding(アンダースコアなし)という新しいプロパティを作成し、
_binding!!
に設定します。
private val binding get() = _binding!!
ここで、get()
はこのプロパティが「get-only」であることを意味します。つまり、値を取得できますが、一度割り当てると(ここでのように)、他に割り当てることはできません。
onCreate()
を実装するには、単にsetHasOptionsMenu()
を呼び出します。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
- フラグメントでは、
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
}
binding
プロパティの下に、リサイクラー ビューのプロパティを作成します。
private lateinit var recyclerView: RecyclerView
- 次に、
onViewCreated()
でrecyclerView
プロパティの値を設定し、MainActivity
で行ったようにchooseLayout()
を呼び出します。まもなくchooseLayout()
メソッドをLetterListFragment
に移動するため、エラーは気にしないでください。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.recyclerView
chooseLayout()
}
バインディング クラスによって recyclerView
のプロパティがすでに作成されています。ビューごとに findViewById()
を呼び出す必要はありません。
- 最後に、ビューがすでに存在しなくなったため、
onDestroyView()
で_binding
プロパティをnull
にリセットします。
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- 他に注意すべき点は、フラグメントを扱う際、
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)
}
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)
}
}
- 最後に、
MainActivity
からisLinearLayoutManager
プロパティをコピーします。これを、recyclerView
プロパティの宣言のすぐ下に配置します。
private var isLinearLayoutManager = true
- すべての機能が
LetterListFragment
に移動されたので、MainActivity
クラスはすべて、フラグメントがビューに表示されるようにレイアウトをインフレートするだけで済みます。onCreate()
を除くすべてをMainActivity
から削除します。変更後、MainActivity
には次の内容だけが含まれます。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
実践
MainActivity
を LettersListFragment
に移行する作業は以上です。DetailActivity
の移行はほぼ同じです。コードを WordListFragment
に移行する手順は次のとおりです。
- コンパニオン オブジェクトを
DetailActivity
からWordListFragment
にコピーします。WordAdapter
のSEARCH_PREFIX
への参照が、WordListFragment
を参照するように更新されていることを確認します。 _binding
変数を追加します。この変数は null 許容で、null
を初期値にする必要があります。_binding
変数と等しい binding という get-only 変数を追加します。onCreateView()
でレイアウトをインフレートし、_binding
の値を設定してルートビューを返します。onViewCreated()
の残りの設定を行います。リサイクラー ビューへの参照を取得し、レイアウト マネージャーとアダプターを設定して、アイテム デコレーションを追加します。インテントから文字を取得する必要があります。フラグメントにはintent
プロパティがなく、通常は親アクティビティのインテントにアクセスしません。現時点では、activity.intent
(DetailActivity
のintent
ではなく)を参照してエクストラを取得します。onDestroyView
で_binding
を null にリセットします。DetailActivity
から残りのコードを削除します(onCreate()
メソッドのみ残します)。
先のステップに進む前に、ご自身で試してください。詳細なチュートリアルは次のステップでご利用いただけます。
DetailActivity
を WordListFragment
に移行してみて、いかがでしたか。これは、MainActivity
を LetterListFragment
に移行することとほぼ同じです。行き詰まってしまった場合のために、手順のまとめを次に示します。
- まず、コンパニオン オブジェクトを
WordListFragment
にコピーします。
companion object {
val LETTER = "letter"
val SEARCH_PREFIX = "https://www.google.com/search?q="
}
- 次に、インテントを実行する
onClickListener()
のLetterAdapter
で、putExtra()
の呼び出しを更新し、DetailActivity.LETTER
をWordListFragment.LETTER
に置き換える必要があります。
intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())
- 同様に、
WordAdapter
では、単語の検索結果に移動したonClickListener()
を更新し、DetailActivity.SEARCH_PREFIX
をWordListFragment.SEARCH_PREFIX
に置き換える必要があります。
val queryUrl: Uri = Uri.parse("${WordListFragment.SEARCH_PREFIX}${item}")
WordListFragment
に戻り、FragmentWordListBinding?
型のバインディング変数を追加します。
private var _binding: FragmentWordListBinding? = null
- 次に、
?
を使用せずにビューを参照できるように、get-only 変数を作成します。
private val binding get() = _binding!!
- その後、レイアウトをインフレートし、
_binding
変数を割り当て、ルートビューを返します。フラグメントの場合、onCreate()
ではなくonCreateView()
で行います。
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWordListBinding.inflate(inflater, container, false)
return binding.root
}
- 次に、
onViewCreated()
を実装します。これは、DetailActivity
のonCreateView()
で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)
)
}
- 最後に、
onDestroyView()
で_binding
変数をリセットできます。
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- この機能はすべて WordListFragment に移動され、DetailActivity からコードを削除できるようになりました。残るのは onCreate() メソッドだけです。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
}
DetailActivity を削除する
DetailActivity
の機能が WordListFragment
に正常に移行されたため、DetailActivity
は不要になりました。先に進み、DetailActivity.kt
と activity_detail.xml
の両方を削除して、マニフェストを少し変更できます。
- まず、
DetailActivity.kt
を削除します。
- [Safe Delete] チェックボックスがオフになっていることを確認し、[OK] をクリックします。
- 次に、
activity_detail.xml
を削除します。再度、[Safe Delete] チェックボックスがオフになっていることを確認します。
- 最後に、
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 コンポーネントでは、NavController
のnavigate()
メソッドを呼び出して、表示されるフラグメントを入れ替えることができます。またNavController
では、システムの「上へ」ボタンに応答して前に表示したフラグメントに戻るなどの一般的なタスクも処理できます。
ナビゲーションの依存関係
- プロジェクト レベルの
build.gradle
ファイルの [buildscript] > [ext] で、material_version
のnav_version
を2.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"
}
...
}
- アプリレベルの
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 をプロジェクトに統合する手順は次のとおりです。
- 最上位の
build.gradle
ファイルの [buildscript] > [dependencies] で、次のクラスパスを追加します。
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
- アプリレベルの
build.gradle
ファイルの上部にあるplugins
内で、androidx.navigation.safeargs.kotlin
を追加します。
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'androidx.navigation.safeargs.kotlin'
}
- Gradle ファイルを編集すると、プロジェクトを同期するよう求める黄色のバナーが上部に表示されることがあります。[Sync Now] をクリックし、Gradle がプロジェクトの依存関係を更新して変更内容を反映するまで、1~2 分待ちます。
同期が完了したら、ナビゲーション グラフを追加する次のステップに進みます。
フラグメントとライフサイクルの基本事項について理解したので、ここからはもう少し面白いことをしてみましょう。次のステップは、Navigation コンポーネントの組み込みです。ナビゲーション コンポーネントとは単に、(特にフラグメント間で)ナビゲーションを実装するためのツールのコレクションを指します。ここでは新しいビジュアル エディタを使用して、フラグメント間のナビゲーションを実装します(ナビゲーション グラフ、略称 NavGraph)。
ナビゲーション グラフとは
ナビゲーション グラフ(略称 NavGraph)は、アプリのナビゲーションの仮想マッピングです。各画面(今回はフラグメント)は、移動できる「デスティネーション」になります。NavGraph
は、各デスティネーションが互いにどのように関連しているかを示す XML ファイルで表すことができます。
背後で、これが実際に NavGraph
クラスの新しいインスタンスを作成します。ただし、ナビゲーション グラフのデスティネーションは、FragmentContainerView
によってユーザーに表示されます。必要な作業は、XML ファイルを作成し、可能性のあるデスティネーションを定義することだけです。その後、生成されたコードを使用してフラグメント間を移動できます。
MainActivity で FragmentContainerView を使用する
レイアウトが fragment_letter_list.xml
と fragment_word_list.xml
に含まれるようになったため、activity_main.xml
ファイルにアプリの最初の画面のレイアウトを含める必要がなくなりました。代わりに、FragmentContainerView
を含むように MainActivity
を再利用し、フラグメントの NavHost として機能させます。これ以降、アプリのナビゲーションはすべて、FragmentContainerView
内で行われます。
- activity_main.xml の
FrameLayout
の内容(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" />
- id 属性の下に
name
属性を追加し、androidx.navigation.fragment.NavHostFragment
に設定します。この属性に特定のフラグメントを指定できますが、NavHostFragment
に設定すると、FragmentContainerView
でフラグメント間を移動できるようになります。
android:name="androidx.navigation.fragment.NavHostFragment"
- layout_height 属性と layout_width 属性の下に app:navHost という属性を追加し、
"true"
に設定します。これにより、フラグメント コンテナでナビゲーション階層を操作できるようになります。たとえば、システムの戻るボタンを押すと、新しいアクティビティが表示されたときと同様に、コンテナは前に表示されたフラグメントに戻ります。
app:defaultNavHost="true"
app:navGraph
という属性を追加し、"@navigation/nav_graph"
に設定します。これは、アプリのフラグメントが相互に移動する方法を定義する XML ファイルを指します。現在のところ、Android Studio では未解決シンボルエラーが表示されます。これについては次のタスクで対処します。
app:navGraph="@navigation/nav_graph"
- 最後に、アプリの名前空間で 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」という新しいリソース フォルダが作成されます。
XML ファイルを作成すると、新しいビジュアル エディタが表示されます。FragmentContainerView
の navGraph
プロパティで nav_graph
をすでに参照しているため、新しいデスティネーションを追加するには、画面左上の新規作成ボタンをクリックして、フラグメントごとにデスティネーションを作成します(fragment_letter_list
用に 1 つ、fragment_word_list
用に 1 つ)。
追加したフラグメントは、画面中央のナビゲーション グラフに表示されます。左側に表示されるコンポーネント ツリーを使用して特定のデスティネーションを選択することもできます。
ナビゲーション アクションを作成する
letterListFragment
デスティネーションと wordListFragment
デスティネーションの間のナビゲーション アクションを作成するには、letterListFragment
デスティネーションにカーソルを合わせて、右側に表示される円から wordListFragment
デスティネーションにドラッグします。
これで、2 つのデスティネーション間のアクションを表す矢印が作成されました。矢印をクリックすると、このアクションがコードで参照できる名前 action_letterListFragment_to_wordListFragment
を持っていることが [Attributes] ペインに表示されます。
WordListFragment の引数を指定する
インテントを使用してアクティビティ間を移動するとき、選択した文字が wordListFragment
に渡されるように「エクストラ」を指定しました。ナビゲーションは、デスティネーション間のパラメータの受け渡しもサポートしており、型安全な方法でこれを行います。
wordListFragment
デスティネーションを選択し、[Attributes] ペインの [Arguments] で、プラスボタンをクリックして新しい引数を作成します。
引数の名前は letter
とし、型は String
とします。ここで、先ほど追加した Safe Args プラグインの出番です。この引数を文字列として指定すると、ナビゲーション アクションがコード内で実行されたときに String
が想定されます。
開始デスティネーションの設定
NavGraph は必要なデスティネーションをすべて認識しますが、FragmentContainerView
は、最初に表示するフラグメントをどのようにして把握するでしょうか。NavGraph では、文字リストを開始デスティネーションとして設定する必要があります。
「letterListFragment
」を選択して [Assign start destination] ボタンをクリックし、開始デスティネーションを設定します。
NavGraph エディタで行う必要がある作業は以上です。この時点で、プロジェクトを作成します。ナビゲーション グラフに基づいてコードが生成されるため、作成したナビゲーション アクションを使用できます。
ナビゲーション アクションを実行する
LetterAdapter
.kt
を開いて、ナビゲーション アクションを実行します。必要なステップは 2 つだけです。
- ボタンの
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
に少しだけ変更が必要です。
navController
プロパティを作成します。これはonCreate
で設定されるため、lateinit
とマークされます。
private lateinit var navController: NavController
- 次に、
onCreate()
でsetContentView()
を呼び出した後に、nav_host_fragment
への参照(FragmentContainerView
の ID)を取得してnavController
プロパティに割り当てます。
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
- 次に
onCreate()
で、navController
を渡してsetupActionBarWithNavController()
を呼び出します。これにより、LetterListFragment
のメニュー オプションなど、アクションバー(アプリバー)ボタンが表示されるようになります。
setupActionBarWithNavController(navController)
- 最後に、
onSupportNavigateUp()
を実装します。XML でdefaultNavHost
をtrue
に設定するとともに、このメソッドでは、「上へ」ボタンを処理できます。ただし、アクティビティは実装を提供する必要があります。
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
この時点で、すべてのコンポーネントが所定の位置に配置され、ナビゲーションでフラグメントを扱えるようになります。ただし、インテントではなくフラグメントを使用してナビゲーションを実行するようになったため、WordListFragment
で使用する文字のインテント エクストラは機能しなくなります。次のステップでは、WordListFragment
を更新して letter
引数を取得します。
前に、WordListFragment
で activity?.intent
を参照して、letter
エクストラにアクセスしました。これは機能しますが、フラグメントは他のレイアウトに埋め込まれることがあるため、おすすめしません。また大規模なアプリでは、フラグメントが属するアクティビティの判断がはるかに困難になります。さらに、nav_graph
を使用してナビゲーションが実行され、安全な引数が使用されている場合、インテントはないため、インテント エクストラにアクセスしようとしてもうまくいきません。
幸い、安全な引数へのアクセスは非常に簡単であり、onViewCreated()
が呼び出されるまで待機する必要もありません。
WordListFragment
でletterId
プロパティを作成します。これを lateinit としてマークすれば、null 許容にする必要はありません。
private lateinit var letterId: String
- 次に、(
onCreateView()
やonViewCreated()
ではなく)onCreate()
をオーバーライドし、以下を追加します。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
letterId = it.getString(LETTER).toString()
}
}
arguments
は省略可能な場合があるため、let()
を呼び出してラムダを渡しています。このコードは、arguments
が null ではないと仮定して実行され、it
パラメータに null でない引数を渡します。ただし、arguments
が null
の場合、ラムダは実行されません。
実際のコードの一部ではないものの、Android Studio では、it
パラメータを認識するための便利なヒントが提供されます。
Bundle
とは正確には何でしょうか。これは、アクティビティやフラグメントなどのクラス間でデータを渡すために使用される Key-Value ペアと考えてください。実は、このアプリの最初のバージョンでインテントを実行する際に intent?.extras?.getString()
を呼び出したとき、すでにバンドルを使用していました。フラグメントを操作するときに引数から文字列を取得する方法も、まったく同じです。
- 最後に、リサイクラー ビューのアダプターを設定すると、
letterId
にアクセスできます。onViewCreated()
のactivity?.intent?.extras?.getString(LETTER).toString()
をletterId
に置き換えます。
recyclerView.adapter = WordAdapter(letterId, requireContext())
それでは、アプリを実行してみましょう。インテントなしで、すべて 1 つのアクティビティで、2 つの画面間を移動できるようになりました。
両方の画面がフラグメントを使用するように正常に変換されました。変更が行われる前、各フラグメントのアプリバーには、アプリバー内のアクティビティごとにわかりやすいタイトルが付けられていました。しかし、フラグメントを使用するように変換した後、このタイトルは詳細アクティビティからなくなりました。
フラグメントには "label"
というプロパティがあり、親アクティビティがアプリバーで使用するタイトルを設定できます。
strings.xml
で、アプリ名の後に次の定数を追加します。
<string name="word_list_fragment_label">Words That Start With {letter}</string>
- ナビゲーション グラフの各フラグメントのラベルを設定できます。
nav_graph.xml
に戻り、コンポーネント ツリーでletterListFragment
を選択します。[Attributes] ペインで、ラベルをapp_name
文字列に設定します。
wordListFragment
を選択し、ラベルをword_list_fragment_label
に設定します。
おつかれさまでした。アプリをもう一度実行すると、この Codelab を始めたときと同様にすべてが表示されるはずです。ただし今回は、画面ごとに別々のフラグメントで、すべてのナビゲーションが 1 つのアクティビティでホストされます。
この Codelab のソリューション コードは、以下のプロジェクトにあります。
この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。
- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。
- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開かれるまで待ちます。
- 実行ボタン をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
- [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように実装されているかを確認します。
- フラグメントは、アクティビティに埋め込むことができる、UI の再利用可能な部分です。
- フラグメントのライフサイクルは、アクティビティのライフサイクルとは異なり、ビューの設定は
onCreateView()
ではなくonViewCreated()
で行われます。 FragmentContainerView
は、フラグメントを他のアクティビティに埋め込むために使用します。フラグメント間のナビゲーションを管理できます。
Navigation コンポーネントの使用
FragmentContainerView
のnavGraph
属性を設定すると、アクティビティ内のフラグメント間を移動できます。NavGraph
エディタを使用すると、ナビゲーション アクションを追加でき、異なるデスティネーション間で引数を指定できます。- インテントを使用したナビゲーションではエクストラを渡す必要がありますが、Navigation コンポーネントでは SafeArgs を使用して、ナビゲーション アクションのクラスとメソッドを自動生成し、引数で型安全性を確保します。
フラグメントのユースケース
- Navigation コンポーネントを使用することで、多くのアプリは 1 つのアクティビティ内でレイアウト全体を管理でき、すべてのナビゲーションがフラグメント間で行われます。
- フラグメントを使用すると、タブレットでのマスター ディテール レイアウトや、同じアクティビティ内での複数のタブなど、一般的なレイアウト パターンが可能となります。