1. 始める前に
ここまで取り組んできたアプリは、1 つの画面で構成されていました。しかし、普段使用している多くのアプリは画面が複数あり、画面間を移動できるようになっているはずです。たとえば設定アプリには、複数の画面にまたがるコンテンツ ページが多数あります。
|
|
|
最新の Android 開発では、マルチスクリーン アプリの作成に Jetpack Navigation コンポーネントを使用します。Navigation Compose コンポーネントを使用すると、ユーザー インターフェースを作成する場合と同様に、宣言型のアプローチによって Compose でマルチスクリーン アプリを簡単に作成できます。この Codelab では、複雑化するアプリにおけるおすすめの方法を示しつつ、Navigation Compose コンポーネントの基本、AppBar をレスポンシブにする方法、インテントを使用してアプリ間でデータを送信する方法を紹介します。
前提条件
- 関数型、ラムダ、スコープ関数など、Kotlin 言語に精通していること
- Compose の基本的な
RowレイアウトとColumnレイアウトに精通していること
学習内容
NavHostコンポーザブルを作成して、アプリのルートと画面を定義する。NavHostControllerを使用して画面間を移動する。- バックスタックを操作して前の画面に移動する。
- インテントを使用して別のアプリとデータを共有する。
- AppBar をカスタマイズする(タイトルや「戻る」ボタンなど)。
作成するアプリの概要
- マルチスクリーン アプリのナビゲーションを実装します。
必要なもの
- Android Studio の最新バージョン
- スターター コードをダウンロードするためのインターネット接続
2. スターター コードをダウンロードする
まず、スターター コードをダウンロードします。
または、GitHub リポジトリのクローンを作成してコードを入手することもできます。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git $ cd basic-android-kotlin-compose-training-cupcake $ git checkout starter
この Codelab のスターター コードを確認する場合は、GitHub で表示します。
3. アプリの内容
Cupcake アプリは、これまで取り組んできたアプリとは少し異なります。すべてのコンテンツが 1 つの画面に表示されるのではなく、個別の画面が 4 つあり、ユーザーは各画面を移動しながらカップケーキを注文します。アプリを実行しても何も起こらず、ナビゲーション コンポーネントがまだアプリコードに追加されていないため、これらの画面間を移動できません。ただし、各画面のコンポーザブルのプレビューを確認し、以下に示す最終的なアプリの画面と照らし合わせることはできます。
注文開始画面
最初の画面には、注文するカップケーキの数量に対応した 3 つのボタンが表示されます。
|
|
コードでは、これは StartOrderScreen.kt の StartOrderScreen コンポーザブルで表現します。
画面は、画像とテキストを含む 1 つの列と、異なる量のカップケーキを注文するための 3 つのカスタムボタンで構成されています。カスタムボタンは、StartOrderScreen.kt の SelectQuantityButton コンポーザブルで実装します。
フレーバー選択画面
数量を選択すると、カップケーキのフレーバーを選択するよう促されます。このアプリでは、ラジオボタンと呼ばれるものを使用して、さまざまなオプションを表示します。ユーザーは複数のフレーバーの中から 1 つを選択できます。
|
|
フレーバーのリストは、文字列リソース ID のリストとして data.DataSource.kt に格納されます。
受け取り日選択画面
フレーバーを選択すると、受け取り日を選択する別のラジオボタン群が表示されます。受け取りオプションは、OrderViewModel の pickupOptions() 関数が返すリストに由来します。
|
|
フレーバー選択画面と受け取り日選択画面は、同じ SelectOptionScreen.kt の SelectOptionScreen コンポーザブルで表現します。なぜ同じコンポーザブルを使用するのかというと、これらの画面のレイアウトがまったく同じだからです。唯一、データが異なりますが、同じコンポーザブルを使用して、フレーバー画面と受け取り日画面の両方を表示できます。
注文概要画面
受け取り日を選択すると、アプリに注文概要画面が表示され、ユーザーは注文を確認して完了できます。
|
|
この画面は、SummaryScreen.kt の OrderSummaryScreen コンポーザブルで実装します。
このレイアウトは、注文に関するすべての情報を含む Column、小計のための Text コンポーザブル、注文を別のアプリに送信するボタン、注文をキャンセルして最初の画面に戻るボタンで構成されています。
ユーザーが別のアプリに注文を送信することを選択した場合、Cupcake アプリは、さまざまな共有オプションを示す Android ShareSheet を表示します。

アプリの現在の状態は data.OrderUiState.kt に格納されます。OrderUiState データクラスには、各画面でのユーザーの選択内容を保存するプロパティが含まれています。
アプリの画面は CupcakeApp コンポーザブルで表示されます。しかしスターター プロジェクトでは、アプリは単に最初の画面を表示するだけです。今のところアプリのすべての画面を移動することはできませんが、ご安心ください。今回はこの点に取り組みます。ナビゲーション ルートの定義、画面間を移動するための NavHost コンポーザブルのセットアップ(デスティネーションともいいます)、共有画面などのシステム UI コンポーネントと統合するインテントの実行、ナビゲーションの変更に対する AppBar の反応について学習します。
再利用可能なコンポーザブル
このコースのサンプルアプリは、おすすめの方法を適宜実践できるように設計されています。Cupcake アプリも例外ではありません。ui.components パッケージには CommonUi.kt というファイルがあり、FormattedPriceLabel コンポーザブルが含まれています。アプリ内の複数の画面でこのコンポーザブルを使用して、注文価格の書式を統一しています。同じ書式と修飾子で同じ Text コンポーザブルを複製するのではなく、FormattedPriceLabel を一度定義しておけば、他の画面で必要な回数だけ再利用できます。
フレーバーの画面と受け取り日の画面は SelectOptionScreen コンポーザブルを使用しており、これも再利用できます。このコンポーザブルは、表示するオプションを表す List<String> 型の options というパラメータを受け取ります。オプションは Row で表示され、RadioButton コンポーザブルと、各文字列を含む Text コンポーザブルで構成されます。Column はレイアウト全体を囲みます。また、書式設定された価格を表示する Text コンポーザブル、[Cancel] ボタン、[Next] ボタンが含まれます。
4. ルートを定義して NavHostController を作成する
Navigation コンポーネントの各部
Navigation コンポーネントは主に 3 つの部分から構成されています。
- NavController: デスティネーション(アプリの画面)間の移動を担います。
- NavGraph: コンポーザブルのデスティネーションをマッピングして移動できるようにします。
- NavHost: NavGraph の現在のデスティネーションを表示するコンテナとして機能するコンポーザブル。
この Codelab では、NavController と NavHost に焦点を当てます。NavHost 内で、Cupcake アプリの NavGraph のデスティネーションを定義します。
アプリ内のデスティネーションのルートを定義する
Compose アプリにおけるナビゲーションの基本コンセプトとして、ルートがあります。ルートとは、デスティネーションに対応する文字列のことで、考え方は URL のコンセプトに似ています。異なる URL がウェブサイトの異なるページにマッピングされるように、ルートはデスティネーションにマッピングされる文字列であり、固有識別子として機能します。デスティネーションは通常、表示内容に対応する単一のコンポーザブル、またはコンポーザブルのグループです。Cupcake アプリでは、注文開始画面、フレーバー画面、受け取り日画面、注文概要画面のデスティネーションが必要です。
アプリの画面数には限りがあるため、ルートの数にも限りがあります。アプリのルートは列挙型クラスを使用して定義できます。Kotlin の列挙型クラスには、プロパティ名を含む文字列を返す name プロパティがあります。
まず、Cupcake アプリの 4 つのルートを定義します。
Start: カップケーキの数量を 3 つのボタンのいずれかで選択します。Flavor: フレーバーを選択肢のリストから選択します。Pickup: 受け取り日を選択肢のリストから選択します。Summary: 選択内容を確認し、注文を送信またはキャンセルします。
列挙型クラスを追加してルートを定義します。
CupcakeScreen.ktのCupcakeAppBarコンポーザブルの上に、CupcakeScreenという列挙型クラスを追加します。
enum class CupcakeScreen() {
}
- この列挙型クラスに、
Start、Flavor、Pickup、Summaryという 4 つのケースを追加します。
enum class CupcakeScreen() {
Start,
Flavor,
Pickup,
Summary
}
アプリに NavHost を追加する
NavHost は、所定のルートに基づいて他のコンポーザブルのデスティネーションを表示するコンポーザブルです。たとえばルートが Flavor であれば、NavHost はカップケーキのフレーバーを選択する画面を表示します。ルートが Summary であれば、アプリは概要画面を表示します。
NavHost の構文は、他のコンポーザブルの場合と同じです。

注目すべきパラメータは 2 つあります。
navController:NavHostControllerクラスのインスタンス。このオブジェクトを使用して画面間を移動できます(navigate()メソッドを呼び出して別のデスティネーションに移動するなど)。NavHostControllerは、コンポーズ可能な関数からrememberNavController()を呼び出すことで取得できます。startDestination: アプリが最初にNavHostを表示したときにデフォルトで表示されるデスティネーションを定義する文字列ルート。Cupcake アプリの場合、これはStartルートになります。
他のコンポーザブルと同様に、NavHost も modifier パラメータを受け取ります。
CupcakeScreen.kt の CupcakeApp コンポーザブルに NavHost を追加します。まず、ナビゲーション コントローラへの参照が必要です。ナビゲーション コントローラは、これから追加する NavHost と後のステップで追加する AppBar の両方で使用できます。そのため、変数を CupcakeApp() コンポーザブルで宣言する必要があります。
CupcakeScreen.ktを開きます。ScaffoldのuiState変数の下にNavHostコンポーザブルを追加します。
import androidx.navigation.compose.NavHost
Scaffold(
...
) { innerPadding ->
val uiState by viewModel.uiState.collectAsState()
NavHost()
}
navControllerパラメータにnavController変数を渡し、startDestinationパラメータにCupcakeScreen.Start.nameを渡します。CupcakeApp()に渡された修飾子を修飾子パラメータに渡します。最後のパラメータに空の後置ラムダを渡します。
import androidx.compose.foundation.layout.padding
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
}
NavHost でルートを処理する
他のコンポーザブルと同様に、NavHost のコンテンツは関数型です。

NavHost のコンテンツ関数内で、composable() 関数を呼び出します。composable() 関数には必須のパラメータが 2 つあります。
route: ルートの名前に対応する文字列。一意の文字列を指定できます。CupcakeScreen列挙型の定数の name プロパティを使用します。content: ここで、所定のルートに対して表示するコンポーザブルを呼び出すことができます。
4 つのルートそれぞれについて、composable() 関数を 1 回ずつ呼び出します。
composable()関数を呼び出し、routeにCupcakeScreen.Start.nameを渡します。
import androidx.navigation.compose.composable
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
}
}
- 後置ラムダ内で、
StartOrderScreenコンポーザブルを呼び出し、quantityOptionsプロパティにquantityOptionsを渡します。modifierにはModifier.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium))を渡します。
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.res.dimensionResource
import com.example.cupcake.ui.StartOrderScreen
import com.example.cupcake.data.DataSource
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
- 最初の
composable()の呼び出しの下で、composable()を再度呼び出し、routeにCupcakeScreen.Flavor.nameを渡します。
composable(route = CupcakeScreen.Flavor.name) {
}
- 後置ラムダ内で、
LocalContext.currentへの参照を取得し、contextという変数に格納します。Contextは Android システムによって実装が提供される抽象クラスです。アプリケーション固有のリソースとクラスだけでなく、アプリケーション レベルのオペレーション(例: アクティビティの起動)のアップコールへのアクセスを可能にします。この変数を使用して、ビューモデルのリソース ID のリストから文字列を取得しフレーバーのリストを表示できます。
import androidx.compose.ui.platform.LocalContext
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
}
SelectOptionScreenコンポーザブルを呼び出します。
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
)
}
- ユーザーがフレーバーを選択したとき、フレーバー画面が小計を表示、更新する必要があります。
subtotalパラメータにuiState.priceを渡します。
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price
)
}
- フレーバー画面では、アプリの文字列リソースからフレーバーのリストを取得します。
map()関数を使用し、それぞれのフレーバーに対してcontext.resources.getString(id)を呼び出すことで、リソース ID のリストを文字列のリストに変換します。
import com.example.cupcake.ui.SelectOptionScreen
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
options = DataSource.flavors.map { id -> context.resources.getString(id) }
)
}
onSelectionChangedパラメータには、ビューモデルのsetFlavor()を呼び出すラムダ式を渡し、it(onSelectionChanged()に渡された引数)を渡します。modifierパラメータにはModifier.fillMaxHeight().を渡します。
import androidx.compose.foundation.layout.fillMaxHeight
import com.example.cupcake.data.DataSource.flavors
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
受け取り日画面はフレーバー画面に似ています。唯一の違いは、SelectOptionScreen コンポーザブルに渡されるデータです。
composable()関数を再度呼び出し、routeパラメータにCupcakeScreen.Pickup.nameを渡します。
composable(route = CupcakeScreen.Pickup.name) {
}
- 後置ラムダで、
SelectOptionScreenコンポーザブルを呼び出し、前と同じようにsubtotalにuiState.priceを渡します。optionsパラメータにはuiState.pickupOptionsを渡し、onSelectionChangedパラメータにはviewModelのsetDate()を呼び出すラムダ式を渡します。modifierパラメータにはModifier.fillMaxHeight().を渡します。
SelectOptionScreen(
subtotal = uiState.price,
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
composable()をもう一度呼び出し、routeにCupcakeScreen.Summary.nameを渡します。
composable(route = CupcakeScreen.Summary.name) {
}
- 後置ラムダで、
OrderSummaryScreen()コンポーザブルを呼び出し、orderUiStateパラメータにuiState変数を渡しますmodifierパラメータにはModifier.fillMaxHeight().を渡します。
import com.example.cupcake.ui.OrderSummaryScreen
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
modifier = Modifier.fillMaxHeight()
)
}
NavHost のセットアップは以上です。次のセクションでは、アプリがルートを変更して、ユーザーが各ボタンをタップしたときに画面間を移動するようにします。
5. ルート間を移動する
ルートを定義し、NavHost でコンポーザブルにマッピングしたところで、今度は画面間を移動しましょう。NavHostController(rememberNavController() を呼び出す際の navController プロパティ)は、ルート間の移動を担います。ただし、このプロパティは CupcakeApp コンポーザブルで定義されています。アプリのさまざまな画面からアクセスする方法が必要です。
単に、navController をパラメータとして各コンポーザブルに渡すだけです。
このアプローチは機能しますが、アプリの設計に理想的な方法ではありません。NavHost を使用してアプリのナビゲーションを処理するメリットは、ナビゲーション ロジックが個々の UI とは別に維持されることです。このオプションでは、navController をパラメータとして渡す場合の大きな欠点を回避できます。
- ナビゲーション ロジックが 1 か所にまとまるため、コードをメンテナンスしやすくなり、誤って個々の画面からアプリ内を自由に移動できるようにしないことで、バグを防止できます。
- さまざまなフォーム ファクタ(縦向きのスマートフォン、折りたたみ式スマートフォン、大画面のタブレットなど)で動作する必要があるアプリでは、アプリのレイアウトに応じて、ボタンがナビゲーションをトリガーする場合もあれば、しない場合もあります。個々の画面は自己完結している必要があります。アプリ内の他の画面を認識する必要はありません。
代わりに、ユーザーがボタンをクリックしたときの動作について、関数型を各コンポーザブルに渡します。これにより、コンポーザブルとその子コンポーザブルは、関数を呼び出すタイミングを決定します。ただし、ナビゲーション ロジックはアプリの個々の画面には公開されません。ナビゲーション動作はすべて NavHost で処理されます。
StartOrderScreen にボタンハンドラを追加する
まず、最初の画面で数量ボタンのいずれかが押されたときに呼び出される関数型のパラメータを追加します。この関数は StartOrderScreen コンポーザブルに渡され、ビューモデルの更新と次の画面への移動を担います。
StartOrderScreen.ktを開きます。quantityOptionsパラメータの下、修飾子パラメータの前に、() -> Unit型のonNextButtonClickedというパラメータを追加します。
@Composable
fun StartOrderScreen(
quantityOptions: List<Pair<Int, Int>>,
onNextButtonClicked: () -> Unit,
modifier: Modifier = Modifier
){
...
}
StartOrderScreenコンポーザブルがonNextButtonClickedの値を必要とするため、StartOrderPreviewを見つけて空のラムダ本体をonNextButtonClickedパラメータに渡します。
@Preview
@Composable
fun StartOrderPreview() {
CupcakeTheme {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
各ボタンはそれぞれ異なる数量のカップケーキに対応しています。この情報は、onNextButtonClicked に渡される関数がそれに応じて適切にビューモデルを更新できるようにするために必要です。
Intパラメータを受け取るようにonNextButtonClickedパラメータの型を変更します。
onNextButtonClicked: (Int) -> Unit,
onNextButtonClicked() を呼び出すときに渡す Int を確認するために、quantityOptions パラメータの型を見てみましょう。
型は List<Pair<Int, Int>> か、Pair<Int, Int> のリストです。Pair 型にはなじみがないかもしれませんが、その名のとおり、値のペアです。Pair は汎用型パラメータを 2 つ受け取ります。この場合、どちらも Int 型です。

ペアの各アイテムには、1 つ目のプロパティまたは 2 つ目のプロパティでアクセスします。StartOrderScreen コンポーザブルの quantityOptions パラメータの場合、1 つ目の Int は、各ボタンに表示する文字列のリソース ID です。2 つ目の Int は、カップケーキの実際の数量です。
onNextButtonClicked() 関数を呼び出すときに、選択したペアの 2 つ目のプロパティを渡します。
SelectQuantityButtonのonClickパラメータの空のラムダ式を見つけます。
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = {}
)
}
- そのラムダ式で
onNextButtonClickedを呼び出し、item.second(カップケーキの数)を渡します。
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = { onNextButtonClicked(item.second) }
)
}
SelectOptionScreen にボタンハンドラを追加する
SelectOptionScreen.ktで、SelectOptionScreenコンポーザブルのonSelectionChangedパラメータの下に、() -> Unit型のonCancelButtonClickedというパラメータを追加し、{}のデフォルト値を指定します。
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
)
onCancelButtonClickedパラメータの下に、() -> Unit型のonNextButtonClickedという名前の別のパラメータを追加し、{}のデフォルト値を指定します。
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
onNextButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
)
- Cancel ボタンの
onClickパラメータにonCancelButtonClickedを渡します。
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
- Next ボタンの
onClickパラメータにonNextButtonClickedを渡します。
Button(
modifier = Modifier.weight(1f),
enabled = selectedValue.isNotEmpty(),
onClick = onNextButtonClicked
) {
Text(stringResource(R.string.next))
}
SummaryScreen にボタンハンドラを追加する
最後に、概要画面の [Cancel] ボタンと [Send] ボタンのボタンハンドラ関数を追加します。
SummaryScreen.ktのOrderSummaryScreenコンポーザブルに、() -> Unit型のonCancelButtonClickedというパラメータを追加します。
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
modifier: Modifier = Modifier
){
...
}
(String, String) -> Unit型のパラメータを追加し、名前をonSendButtonClickedにします。
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
onSendButtonClicked: (String, String) -> Unit,
modifier: Modifier = Modifier
){
...
}
OrderSummaryScreenコンポーザブルによってonSendButtonClickedとonCancelButtonClickedの値が要求されます。OrderSummaryPreviewを見つけて 2 つのStringパラメータが設定された空のラムダ本体をonSendButtonClickedに渡し、空のラムダ本体をonCancelButtonClickedパラメータに渡します。
@Preview
@Composable
fun OrderSummaryPreview() {
CupcakeTheme {
OrderSummaryScreen(
orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
onSendButtonClicked = { subject: String, summary: String -> },
onCancelButtonClicked = {},
modifier = Modifier.fillMaxHeight()
)
}
}
- [Send] ボタンの
onClickパラメータにonSendButtonClickedを渡します。newOrderとorderSummary(前にOrderSummaryScreenで定義した 2 つの変数)を渡します。これらの文字列は、ユーザーが別のアプリと共有できる実際のデータで構成されます。
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
Text(stringResource(R.string.send))
}
- [Cancel] ボタンの
onClickパラメータにonCancelButtonClickedを渡します。
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
別のルートに移動する
別のルートに移動するには、単に NavHostController のインスタンスで navigate() メソッドを呼び出します。

navigate メソッドは、1 つのパラメータ(NavHost で定義したルートに対応する String)を受け取ります。ルートが NavHost の composable() の呼び出しのいずれかと一致する場合、アプリはその画面に移動します。
Start、Flavor、Pickup の各画面でユーザーがボタンを押したときに、navigate() を呼び出す関数を渡します。
CupcakeScreen.ktで、開始画面のcomposable()の呼び出しを見つけます。onNextButtonClickedパラメータにラムダ式を渡します。
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {
}
)
カップケーキの数について、この関数に渡した Int プロパティを覚えているでしょうか。次の画面に移動する前に、ビューモデルを更新してアプリに正しい小計が表示されるようにする必要があります。
viewModelに対してsetQuantityを呼び出し、itを渡します。
onNextButtonClicked = {
viewModel.setQuantity(it)
}
navControllerに対してnavigate()を呼び出し、routeにCupcakeScreen.Flavor.nameを渡します。
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
}
- フレーバー画面の
onNextButtonClickedパラメータについては、単にnavigate()を呼び出すラムダを渡し、routeにCupcakeScreen.Pickup.nameを渡します。
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
- 次に実装する
onCancelButtonClickedに空のラムダを渡します。
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
onCancelButtonClicked = {},
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
- 受け取り画面の
onNextButtonClickedパラメータについては、navigate()を呼び出すラムダを渡し、routeにCupcakeScreen.Summary.nameを渡します。
composable(route = CupcakeScreen.Pickup.name) {
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
}
- 再度、
onCancelButtonClicked()に空のラムダを渡します。
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
onCancelButtonClicked = {},
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
OrderSummaryScreenについては、onCancelButtonClickedとonSendButtonClickedに空のラムダを渡します。onSendButtonClickedに渡されるsubjectとsummaryのパラメータを追加します。これらはまもなく実装します。
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = {},
onSendButtonClicked = { subject: String, summary: String ->
},
modifier = Modifier.fillMaxHeight()
)
}
これで、アプリの各画面を移動できるようになりました。navigate() を呼び出すと、画面が変更されるだけでなく、実際にバックスタックの上に配置されます。また、システムの「戻る」ボタンを押すと、前の画面に戻ることができます。
表示された画面は一つひとつ前の画面の上に積み重ねられ、「戻る」ボタン(
)で削除できます。一番下の startDestination から、表示されている一番上の画面までの履歴を、バックスタックといいます。
開始画面までポップする
システムの「戻る」ボタンとは異なり、[Cancel] ボタンを押しても前の画面に戻りません。バックスタックからすべての画面をポップ(削除)して、開始画面に戻る必要があります。
そのためには、popBackStack() メソッドを呼び出します。

popBackStack() メソッドには必須のパラメータが 2 つあります。
route: 戻るデスティネーションのルートを表す文字列。inclusive: ブール値。true の場合、指定したルートもポップ(削除)します。false の場合、popBackStack()は開始デスティネーションより上にあるデスティネーションをすべて削除し、ユーザーが目にする一番上の画面として開始デスティネーションを残します。
ユーザーがいずれかの画面で [Cancel] ボタンを押すと、アプリはビューモデルの状態をリセットし、popBackStack() を呼び出します。まずこれを行うメソッドを実装してから、[Cancel] ボタンを備えた 3 つの画面すべてで、適切なパラメータに渡します。
CupcakeApp()関数の後に、cancelOrderAndNavigateToStart()というプライベート関数を定義します。
private fun cancelOrderAndNavigateToStart() {
}
OrderViewModel型のviewModelとNavHostController型のnavControllerという 2 つのパラメータを追加します。
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
}
- 関数本体で、
viewModelに対してresetOrder()を呼び出します。
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
}
navControllerに対してpopBackStack()を呼び出し、routeにCupcakeScreen.Start.nameを渡して、inclusiveにfalseを渡します。
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
CupcakeApp()コンポーザブルで、2 つのSelectOptionScreenコンポーザブルとOrderSummaryScreenコンポーザブルのonCancelButtonClickedパラメータにcancelOrderAndNavigateToStartを渡します。
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
composable(route = CupcakeScreen.Pickup.name) {
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
}
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
onSendButtonClicked = { subject: String, summary: String ->
},
modifier = Modifier.fillMaxHeight()
)
}
- アプリを実行し、いずれかの画面で [Cancel] ボタンを押すと最初の画面に戻ることを確認します。
6. 別のアプリに移動する
ここまでで、アプリ内の別の画面に移動する方法と、ホーム画面に戻る方法を学習しました。Cupcake アプリにナビゲーションを実装するには、もう 1 つだけステップがあります。注文概要画面で、ユーザーは別のアプリに注文を送信できます。この選択により、ShareSheet(画面の下部を覆うユーザー インターフェース コンポーネント)が表示され、共有オプションが表示されます。
この UI 部分は、Cupcake アプリの一部ではありません。実際は Android オペレーティング システムが提供しています。共有画面などのシステム UI は、navController で呼び出すわけではありません。代わりに、インテントというものを使用します。
インテントはシステムになんらかのアクションを行わせるリクエストです。通常は、新しいアクティビティを提示します。さまざまなインテントが存在します。包括的なリストについては、ドキュメントをご覧になることをおすすめします。ここでは、ACTION_SEND というインテントを活用します。このインテントに文字列などのデータを指定し、そのデータに適した共有アクションを提示できます。
インテントをセットアップする基本的なプロセスは次のとおりです。
- インテント オブジェクトを作成し、インテントを指定します(
ACTION_SENDなど)。 - インテントで送信する追加データの型を指定します。単純なテキストには
"text/plain"を使用できますが、"image/*"や"video/*"など他のタイプも使用できます。 putExtra()メソッドを呼び出して、共有するテキストや画像などの追加データをインテントに渡します。このインテントはEXTRA_SUBJECTとEXTRA_TEXTという 2 つのエクストラを受け取ります。- コンテキストの
startActivity()メソッドを呼び出し、インテントから作成したアクティビティを渡します。
ここでは共有アクション インテントの作成方法を説明しますが、プロセスは他のタイプのインテントでも同じです。今後のプロジェクトでは必要に応じて、特定の種類のデータと必要なエクストラについてドキュメントをご覧になることをおすすめします。
次の手順を実施して、カップケーキの注文を別のアプリに送信するインテントを作成します。
- CupcakeScreen.kt の
CupcakeAppコンポーザブルの下にshareOrder()というプライベート関数を作成します。
private fun shareOrder()
Context型のcontextというパラメータを追加します。
import android.content.Context
private fun shareOrder(context: Context) {
}
subjectとsummaryの 2 つのStringパラメータを追加します。これらの文字列は共有アクション シートに表示されます。
private fun shareOrder(context: Context, subject: String, summary: String) {
}
- 関数本体内に
intentというインテントを作成し、引数としてIntent.ACTION_SENDを渡します。
import android.content.Intent
val intent = Intent(Intent.ACTION_SEND)
この Intent オブジェクトは一度構成するだけで済むため、後続する数行のコードは、前の Codelab で学習した apply() 関数を使用してさらに簡潔にできます。
- 新しく作成したインテントに対して
apply()を呼び出し、ラムダ式を渡します。
val intent = Intent(Intent.ACTION_SEND).apply {
}
- ラムダ本体で、型を
"text/plain"に設定します。これはapply()に渡される関数で行うため、オブジェクトの識別子であるintentを参照する必要はありません。
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
}
putExtra()を呼び出して、EXTRA_SUBJECTに subject を渡します。
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
}
putExtra()を呼び出して、EXTRA_TEXTに summary を渡します。
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, summary)
}
- コンテキストの
startActivity()メソッドを呼び出します。
context.startActivity(
)
startActivity()に渡されるラムダ内で、クラスメソッドcreateChooser()を呼び出してインテントからアクティビティを作成します。最初の引数とnew_cupcake_order文字列リソースに intent を渡します。
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
CupcakeAppコンポーザブルにおいて、CucpakeScreen.Summary.nameのcomposable()の呼び出しで、コンテキスト オブジェクトへの参照を取得し、shareOrder()関数に渡せるようにします。
composable(route = CupcakeScreen.Summary.name) {
val context = LocalContext.current
...
}
onSendButtonClicked()のラムダ本体でshareOrder()を呼び出し、引数としてcontext、subject、summaryを渡します。
onSendButtonClicked = { subject: String, summary: String ->
shareOrder(context, subject = subject, summary = summary)
}
- アプリを実行し、画面を移動します。
[Send Order to Another App] をクリックすると、エクストラとして指定した件名と概要に加えて、[メッセージ] や [Bluetooth] などの共有アクションがボトムシートに表示されます。

7. アプリバーがナビゲーションに反応するようにする
アプリは機能し、どの画面間でも移動できるようにもなりましたが、この Codelab の冒頭で示したスクリーンショットと比べると欠けているものがまだいくつかあります。アプリバーは自動的にはナビゲーションに反応しません。アプリが新しいルートに移動したときにタイトルが更新されず、必要に応じてタイトルの前に「上へ」ボタンが表示されることもありません。
スターター コードには、CupcakeAppBar という AppBar を管理するためのコンポーザブルが含まれています。アプリにナビゲーションを実装したので、バックスタックからの情報を使用して正しいタイトルを表示し、必要に応じて「上へ」ボタンを表示できます。タイトルが適切に更新されるように CupcakeAppBar コンポーザブルは現在の画面を認識する必要があります。
- CupcakeScreen.kt の
CupcakeScreen列挙型で、@StringResアノテーションを使用してInt型のtitleという名前のパラメータを追加します。
import androidx.annotation.StringRes
enum class CupcakeScreen(@StringRes val title: Int) {
Start,
Flavor,
Pickup,
Summary
}
- 各画面のタイトル テキストに対応する各列挙型のリソース値を追加します。
app_nameをStart画面に、choose_flavorをFlavor画面に、choose_pickup_dateをPickup画面に、さらにorder_summaryをSummary画面に使用します。
enum class CupcakeScreen(@StringRes val title: Int) {
Start(title = R.string.app_name),
Flavor(title = R.string.choose_flavor),
Pickup(title = R.string.choose_pickup_date),
Summary(title = R.string.order_summary)
}
CupcakeScreen型のcurrentScreenというパラメータをCupcakeAppBarコンポーザブルに追加します。
fun CupcakeAppBar(
currentScreen: CupcakeScreen,
canNavigateBack: Boolean,
navigateUp: () -> Unit = {},
modifier: Modifier = Modifier
)
CupcakeAppBar内で、currentScreen.titleをTopAppBarのタイトル パラメータのstringResource()の呼び出しに渡すことによって、ハードコードされたアプリ名を現在の画面のタイトルに置き換えます。
TopAppBar(
title = { Text(stringResource(currentScreen.title)) },
modifier = modifier,
navigationIcon = {
if (canNavigateBack) {
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
}
}
)
「上へ」ボタンは、バックスタックにコンポーザブルがある場合にのみ表示されます。アプリの画面がバックスタックにない場合(StartOrderScreen が表示されている場合)、「上へ」ボタンは表示されません。これを確認するには、バックスタックへの参照が必要です。
CupcakeAppコンポーザブルで、navController変数の下にbackStackEntryという変数を作成し、byデリゲートを使用してnavControllerのcurrentBackStackEntryAsState()メソッドを呼び出します。
import androidx.navigation.compose.currentBackStackEntryAsState
@Composable
fun CupcakeApp(
viewModel: OrderViewModel = viewModel(),
navController: NavHostController = rememberNavController()
){
val backStackEntry by navController.currentBackStackEntryAsState()
...
}
- 現在の画面のタイトルを
CupcakeScreenの値に変換します。backStackEntry変数の下で、CupcakeScreenのvalueOf()クラス関数の呼び出し結果と同等のcurrentScreenという名前のvalを使用して変数を作成し、backStackEntryの宛先のルートに渡します。エルビス演算子を使用してCupcakeScreen.Start.nameのデフォルト値を指定します。
val currentScreen = CupcakeScreen.valueOf(
backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
currentScreen変数の値を同じ名前のCupcakeAppBarコンポーザブルのパラメータに渡します。
CupcakeAppBar(
currentScreen = currentScreen,
canNavigateBack = false,
navigateUp = {}
)
バックスタックで現在の画面の背後に画面がある限り、「上へ」ボタンが表示されます。ブール式を使用して、「上へ」ボタンを表示するかどうかを指定できます。
canNavigateBackパラメータに、navControllerのpreviousBackStackEntryプロパティが null と等しくないかどうかを確認するブール式を渡します。
canNavigateBack = navController.previousBackStackEntry != null,
- 実際に前の画面に戻るには、
navControllerのnavigateUp()メソッドを呼び出します。
navigateUp = { navController.navigateUp() }
- アプリを実行します。
AppBar のタイトルが現在の画面を反映するように更新されました。StartOrderScreen 以外の画面に移動すると「上へ」ボタンが表示され、前の画面に戻ることができます。

8. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、以下の git コマンドを使用します。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git $ cd basic-android-kotlin-compose-training-cupcake $ git checkout navigation
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
この Codelab の解答コードを確認する場合は、GitHub で表示します。
9. まとめ
お疲れさまでした。簡単な単一画面のアプリから、Jetpack Navigation コンポーネントを使用して複数の画面間を移動する複雑なマルチスクリーン アプリに飛躍しました。ルートを定義し、NavHost で処理して、関数型のパラメータを使用してナビゲーション ロジックを個々の画面から分離しました。また、インテントを使用して別のアプリにデータを送信する方法や、ナビゲーションに応じてアプリバーをカスタマイズする方法も学習しました。今後のユニットでは、こうしたスキルを使用して、さらに複雑化するマルチスクリーン アプリに取り組んでいきます。









