Compose で画面間を移動する

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

1. 始める前に

ここまで取り組んできたアプリは、1 つの画面で構成されていました。しかし、普段使用している多くのアプリは画面が複数あり、画面間を移動できるようになっているはずです。たとえば設定アプリには、複数の画面にまたがるコンテンツ ページが多数あります。

Android 設定アプリの最初のページ。

最初のページで [接続済みのデバイス] を選択した後の設定ページ。

前のページで [新しいデバイスとペア設定] を選択した後の設定ページ。

最新の 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

3. アプリの内容

Cupcake アプリは、これまで取り組んできたアプリとは少し異なります。すべてのコンテンツが 1 つの画面に表示されるのではなく、個別の画面が 4 つあり、ユーザーは各画面を移動しながらカップケーキを注文します。

注文開始画面

最初の画面には、注文するカップケーキの数量に対応した 3 つのボタンが表示されます。

1 個、6 個、12 個のカップケーキの注文を開始するオプションが表示された、Cupcake アプリの最初の画面。

コードでは、これは StartOrderScreen.ktStartOrderScreen コンポーザブルで表現します。

画面は、画像とテキストを含む 1 つの列と、異なる量のカップケーキを注文するための 3 つのカスタムボタンで構成されています。カスタムボタンは、StartOrderScreen.ktSelectQuantityButton コンポーザブルで実装します。

フレーバー選択画面

数量を選択すると、カップケーキのフレーバーを選択するよう促されます。このアプリでは、ラジオボタンと呼ばれるものを使用して、さまざまなオプションを表示します。ユーザーは複数のフレーバーの中から 1 つを選択できます。

さまざまなフレーバーの選択肢を提示する Cupcake アプリ。

フレーバーのリストは、文字列リソース ID のリストとして data.DataSource.kt に格納されます。

受け取り日選択画面

フレーバーを選択すると、受け取り日を選択する別のラジオボタン群が表示されます。受け取りオプションは、OrderViewModelpickupOptions() 関数が返すリストに由来します。

受け取り日の選択肢を提示する Cupcake アプリ。

フレーバー選択画面と受け取り日選択画面は、同じ SelectOptionScreen.ktSelectOptionScreen コンポーザブルで表現します。なぜ同じコンポーザブルを使用するのかというと、これらの画面のレイアウトがまったく同じだからです。唯一、データが異なりますが、同じコンポーザブルを使用して、フレーバー画面と受け取り日画面の両方を表示できます。

注文概要画面

受け取り日を選択すると、アプリに注文概要画面が表示され、ユーザーは注文を確認して完了できます。

数量、フレーバー、受け取り日、小計が記載された注文概要に加え、注文を別のアプリに送信するか、注文をキャンセルするかというオプションを表示する Cupcake アプリ。

この画面は、OrderSummaryScreen.ktOrderSummaryScreen コンポーザブルで実装します。

このレイアウトは、注文に関するすべての情報を含む Column、小計のための Text コンポーザブル、注文を別のアプリに送信するボタン、注文をキャンセルして最初の画面に戻るボタンで構成されています。

ユーザーが別のアプリに注文を送信することを選択した場合、Cupcake アプリは、さまざまな共有オプションを示すボトムシートを表示します。

SMS やメールなどの共有オプションを提示する Cupcake アプリ。

アプリの現在の状態は 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: 選択内容を確認し、注文を送信またはキャンセルします。

列挙型クラスを追加してルートを定義します。

  1. CupcakeScreen.ktCupcakeAppBar コンポーザブルの上に、CupcakeScreen という列挙型クラスを追加します。
enum class CupcakeScreen() {

}
  1. この列挙型クラスに、StartFlavorPickupSummary という 4 つのケースを追加します。
enum class CupcakeScreen() {
    Start,
    Flavor,
    Pickup,
    Summary
}

アプリに NavHost を追加する

NavHost は、所定のルートに基づいて他のコンポーザブルのデスティネーションを表示するコンポーザブルです。たとえばルートが Flavor であれば、NavHost はカップケーキのフレーバーを選択する画面を表示します。ルートが Summary であれば、アプリは概要画面を表示します。

NavHost の構文は、他のコンポーザブルの場合と同じです。

fae7688d6dd53de9.png

注目すべきパラメータは 2 つあります。

  • navController: NavHostController クラスのインスタンス。このオブジェクトを使用して画面間を移動できます(navigate() メソッドを呼び出して別のデスティネーションに移動するなど)。NavHostController は、コンポーズ可能な関数から rememberNavController() を呼び出すことで取得できます。
  • startDestination: アプリが最初に NavHost を表示したときにデフォルトで表示されるデスティネーションを定義する文字列ルート。Cupcake アプリの場合、これは Start ルートになります。

他のコンポーザブルと同様に、NavHostmodifier パラメータを受け取ります。

CupcakeScreen.ktCupcakeApp コンポーザブルに NavHost を追加します。まず、ナビゲーション コントローラへの参照が必要です。ナビゲーション コントローラは、これから追加する NavHost と後のステップで追加する AppBar の両方で使用できます。そのため、変数を CupcakeApp() コンポーザブルで宣言する必要があります。

  1. CupcakeScreen.kt を開きます。
  2. CupcakeApp コンポーザブルの viewModel 変数の上に、navController という val を使用して新しい変数を作成し、rememberNavController() を呼び出した結果を割り当てます。
@Composable
fun CupcakeApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()

    ...
}
  1. ScaffolduiState 変数の下に NavHost コンポーザブルを追加します。
Scaffold(
    ...
) { innerPadding ->
    val uiState by viewModel.uiState.collectAsState()

    NavHost()
}
  1. navController パラメータに navController 変数を渡し、startDestination パラメータに CupcakeScreen.Start.name を渡します。CupcakeApp() に渡された修飾子を修飾子パラメータに渡します。最後のパラメータに空の後置ラムダを渡します。
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
}

NavHost でルートを処理する

他のコンポーザブルと同様に、NavHost のコンテンツは関数型です。

f67974b7fb3f0377.png

NavHost のコンテンツ関数内で、composable() 関数を呼び出します。composable() 関数には必須のパラメータが 2 つあります。

  • route: ルートの名前に対応する文字列。一意の文字列を指定できます。CupcakeScreen 列挙型の定数の name プロパティを使用します。
  • content: ここで、所定のルートに対して表示するコンポーザブルを呼び出すことができます。

4 つのルートそれぞれについて、composable() 関数を 1 回ずつ呼び出します。

  1. composable() 関数を呼び出し、routeCupcakeScreen.Start.name を渡します。
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {

    }
}
  1. 後置ラムダ内で、StartOrderScreen コンポーザブルを呼び出し、quantityOptions プロパティに quantityOptions を渡します。
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {
        StartOrderScreen(
            quantityOptions = quantityOptions
        )
    }
}
  1. 最初の composable() の呼び出しの下で、composable() を再度呼び出し、routeCupcakeScreen.Flavor.name を渡します。
composable(route = CupcakeScreen.Flavor.name) {

}
  1. 後置ラムダ内で、LocalContext.current への参照を取得し、context という変数に格納します。この変数を使用することで、ビューモデルのリソース ID リストから文字列を取得し、フレーバーのリストを表示できます。
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current

}
  1. SelectOptionScreen コンポーザブルを呼び出します。
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(

    )
}
  1. ユーザーがフレーバーを選択したとき、フレーバー画面が小計を表示、更新する必要があります。subtotal パラメータに uiState.price を渡します。
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price
    )
}
  1. フレーバー画面では、アプリの文字列リソースからフレーバーのリストを取得します。ビューモデルのフレーバー リストから文字列のリストを作成します。map() 関数を使用し、stringResource() を呼び出すことで、リソース ID のリストを文字列のリストに変換できます。
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> stringResource(id) }
    )
}
  1. onSelectionChanged パラメータについては、ビューモデルの setFlavor() を呼び出すラムダ式を渡し、itonSelectionChanged() に渡された引数)を渡します。
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}

受け取り日画面はフレーバー画面に似ています。唯一の違いは、SelectOptionScreen コンポーザブルに渡されるデータです。

  1. composable() 関数を再度呼び出し、route パラメータに CupcakeScreen.Pickup.name を渡します。
composable(route = CupcakeScreen.Pickup.name) {

}
  1. 後置ラムダで、SelectOptionScreen コンポーザブルを呼び出し、前と同じように subtotaluiState.price を渡します。options パラメータには uiState.pickupOptions を渡し、onSelectionChanged パラメータには viewModelsetDate() を呼び出すラムダ式を渡します。
SelectOptionScreen(
    subtotal = uiState.price,
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  1. composable() をもう一度呼び出し、routeCupcakeScreen.Summary.name を渡します。
composable(route = CupcakeScreen.Summary.name) {

}
  1. 後置ラムダで、OrderSummaryScreen() コンポーザブルを呼び出し、orderUiState パラメータに uiState 変数を渡します。
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState
    )
}

NavHost のセットアップは以上です。次のセクションでは、アプリがルートを変更して、ユーザーが各ボタンをタップしたときに画面間を移動するようにします。

5. ルート間を移動する

ルートを定義し、NavHost でコンポーザブルにマッピングしたところで、今度は画面間を移動しましょう。NavHostControllerrememberNavController() を呼び出す際の navController プロパティ)は、ルート間の移動を担います。ただし、このプロパティは CupcakeApp コンポーザブルで定義されています。アプリのさまざまな画面からアクセスする方法が必要です。

単に、navController をパラメータとして各コンポーザブルに渡すだけです。

このアプローチは機能しますが、アプリの設計に理想的な方法ではありません。NavHost を使用してアプリのナビゲーションを処理するメリットは、ナビゲーション ロジックが個々の UI とは別に維持されることです。このオプションでは、navController をパラメータとして渡す場合の大きな欠点を回避できます。

  • ナビゲーション ロジックが 1 か所にまとまるため、コードをメンテナンスしやすくなり、誤って個々の画面からアプリ内を自由に移動できるようにしないことで、バグを防止できます。
  • さまざまなフォーム ファクタ(縦向きのスマートフォン、折りたたみ式スマートフォン、大画面のタブレットなど)で動作する必要があるアプリでは、アプリのレイアウトに応じて、ボタンがナビゲーションをトリガーする場合もあれば、しない場合もあります。個々の画面は自己完結している必要があります。アプリ内の他の画面を認識する必要はありません。

代わりに、ユーザーがボタンをクリックしたときの動作について、関数型を各コンポーザブルに渡します。これにより、コンポーザブルとその子コンポーザブルは、関数を呼び出すタイミングを決定します。ただし、ナビゲーション ロジックはアプリの個々の画面には公開されません。ナビゲーション動作はすべて NavHost で処理されます。

StartOrderScreen にボタンハンドラを追加する

まず、最初の画面で数量ボタンのいずれかが押されたときに呼び出される関数型のパラメータを追加します。この関数は StartOrderScreen コンポーザブルに渡され、ビューモデルの更新と次の画面への移動を担います。

  1. StartOrderScreen.kt を開きます。
  2. quantityOptions パラメータの下、修飾子パラメータの前に、() -> Unit 型の onNextButtonClicked というパラメータを追加します。
@Composable
fun StartOrderScreen(
    quantityOptions: List<Pair<Int, Int>>,
    onNextButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
...
}

各ボタンはそれぞれ異なる数量のカップケーキに対応しています。この情報は、onNextButtonClicked に渡される関数がそれに応じて適切にビューモデルを更新できるようにするために必要です。

  1. Int パラメータを受け取るように onNextButtonClicked パラメータの型を変更します。
onNextButtonClicked: (Int) -> Unit,

onNextButtonClicked() を呼び出すときに渡す Int を確認するために、quantityOptions パラメータの型を見てみましょう。

型は List<Pair<Int, Int>> か、Pair<Int, Int> のリストです。Pair 型にはなじみがないかもしれませんが、その名のとおり、値のペアです。Pair は汎用型パラメータを 2 つ受け取ります。この場合、どちらも Int 型です。

8326701a77706258.png

ペアの各アイテムには、1 つ目のプロパティまたは 2 つ目のプロパティでアクセスします。StartOrderScreen コンポーザブルの quantityOptions パラメータの場合、1 つ目の Int は、各ボタンに表示する文字列のリソース ID です。2 つ目の Int は、カップケーキの実際の数量です。

onNextButtonClicked() 関数を呼び出すときに、選択したペアの 2 つ目のプロパティを渡します。

  1. SelectQuantityButtononClick パラメータにラムダ式を渡します。
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = {  }
    )
}
  1. そのラムダ式で onNextButtonClicked を呼び出し、item.second(カップケーキの数)を渡します。
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = { onNextButtonClicked(item.second) }
    )
}

SelectOptionScreen にボタンハンドラを追加する

  1. SelectOptionScreen.kt で、SelectOptionScreen コンポーザブルの onSelectionChanged パラメータの下に、() -> Unit 型の onCancelButtonClicked というパラメータを追加します。
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. onCancelButtonClicked パラメータの下に、() -> Unit 型の onNextButtonClicked という別のパラメータを追加します。
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    onNextButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Cancel ボタンの onClick パラメータに onCancelButtonClicked を渡します。
OutlinedButton(modifier = Modifier.weight(1f), onClick = onCancelButtonClicked) {
    Text(stringResource(R.string.cancel))
}
  1. Next ボタンの onClick パラメータに onNextButtonClicked を渡します。
Button(
    modifier = Modifier.weight(1f),
    enabled = selectedValue.isNotEmpty(),
    onClick = onNextButtonClicked
) {
    Text(stringResource(R.string.next))
}

SummaryScreen にボタンハンドラを追加する

最後に、概要画面の [Cancel] ボタンと [Send] ボタンのボタンハンドラ関数を追加します。

  1. OrderSummaryScreen.ktOrderSummaryScreen コンポーザブルに、() -> Unit 型の onCancelButtonClicked というパラメータを追加します。
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. () -> Unit 型のパラメータを追加し、名前を onSendButtonClicked にします。
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    onSendButtonClicked: (String, String) -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. [Send] ボタンの onClick パラメータに onSendButtonClicked を渡します。newOrderorderSummary(前に OrderSummaryScreen で定義した 2 つの変数)を渡します。これらの文字列は、ユーザーが別のアプリと共有できる実際のデータで構成されます。
Button(
    modifier = Modifier.fillMaxWidth(),
    onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
    Text(stringResource(R.string.send))
}
  1. [Cancel] ボタンの onClick パラメータに onCancelButtonClicked を渡します。
OutlinedButton(
    modifier = Modifier.fillMaxWidth(),
    onClick = onCancelButtonClicked
) {
    Text(stringResource(R.string.cancel))
}

別のルートに移動するには、単に NavHostController のインスタンスで navigate() メソッドを呼び出します。

fc8aae3911a6a25d.png

navigate メソッドは、1 つのパラメータ(NavHost で定義したルートに対応する文字列)を受け取ります。ルートが NavHost の composable() の呼び出しのいずれかと一致する場合、アプリはその画面に移動します。

StartFlavorPickup の各画面でユーザーがボタンを押したときに、navigate() を呼び出す関数を渡します。

  1. CupcakeScreen.kt で、開始画面の composable() の呼び出しを見つけます。onNextButtonClicked パラメータにラムダ式を渡します。
StartOrderScreen(
    quantityOptions = quantityOptions,
    onNextButtonClicked = {
    }
)

カップケーキの数について、この関数に渡した Int プロパティを覚えているでしょうか。次の画面に移動する前に、ビューモデルを更新してアプリに正しい小計が表示されるようにする必要があります。

  1. viewModel に対して setQuantity を呼び出し、it を渡します。
onNextButtonClicked = {
    viewModel.setQuantity(it)
}
  1. navController に対して navigate() を呼び出し、routeCupcakeScreen.Flavor.name を渡します。
onNextButtonClicked = {
    viewModel.setQuantity(it)
    navController.navigate(CupcakeScreen.Flavor.name)
}
  1. フレーバー画面の onNextButtonClicked パラメータについては、単に navigate() を呼び出すラムダを渡し、routeCupcakeScreen.Pickup.name を渡します。
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = {
            navController.navigate(CupcakeScreen.Pickup.name) },
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}
  1. 次に実装する onCancelButtonClicked に空のラムダを渡します。
SelectOptionScreen(
     subtotal = uiState.price,
    onNextButtonClicked = {
        navController.navigate(CupcakeScreen.Pickup.name) },
    onCancelButtonClicked = {},
    options = flavors.map { id -> context.resources.getString(id) },
    onSelectionChanged = { viewModel.setFlavor(it) }
)
  1. 受け取り画面の onNextButtonClicked パラメータについては、navigate() を呼び出すラムダを渡し、routeCupcakeScreen.Summary.name を渡します。
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = {
            navController.navigate(CupcakeScreen.Summary.name)
        },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) }
    )
}
  1. 再度、onCancelButtonClicked() に空のラムダを渡します。
SelectOptionScreen(
    subtotal = uiState.price,
    onNextButtonClicked = {
        navController.navigate(CupcakeScreen.Summary.name) },
    onCancelButtonClicked = {},
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  1. OrderSummaryScreen については、onCancelButtonClickedonSendButtonClicked に空のラムダを渡します。onSendButtonClicked に渡される subjectsummary のパラメータを追加します。これらはまもなく実装します。
composable(route = CupcakeScreen.Summary.name) {
   val context = LocalContext.current
   OrderSummaryScreen(
       orderUiState = uiState,
       onCancelButtonClicked = {},
       onSendButtonClicked = { subject: String, summary: String ->

       }
   )
}

これで、アプリの各画面を移動できるようになりました。navigate() を呼び出すと、画面が変更されるだけでなく、実際にバックスタックの上に配置されます。また、システムの「戻る」ボタンを押すと、前の画面に戻ることができます。

表示された画面は一つ一つ前の画面の上に積み重ねられ、「戻る」ボタン(bade5f3ecb71e4a2.png)で削除できます。一番下の startDestination から、表示されている一番上の画面までの履歴を、バックスタックといいます。

開始画面までポップする

システムの「戻る」ボタンとは異なり、[Cancel] ボタンを押しても前の画面に戻りません。バックスタックからすべての画面をポップ(削除)して、開始画面に戻る必要があります。

そのためには、popBackStack() メソッドを呼び出します。

2f382e5eb319b4b8.png

popBackStack() メソッドには必須のパラメータが 2 つあります。

  • route: 戻るデスティネーションのルートを表す文字列。
  • inclusive: ブール値。true の場合、指定したルートもポップ(削除)します。false の場合、popBackStack() は開始デスティネーションより上にあるデスティネーションをすべて削除し、ユーザーが目にする一番上の画面として開始デスティネーションを残します。

ユーザーがいずれかの画面で [Cancel] ボタンを押すと、アプリはビューモデルの状態をリセットし、popBackStack() を呼び出します。まずこれを行うメソッドを実装してから、[Cancel] ボタンを備えた 3 つの画面すべてで、適切なパラメータに渡します。

  1. CupcakeApp() 関数の後に、cancelOrderAndNavigateToStart() というプライベート関数を定義します。
private fun cancelOrderAndNavigateToStart() {
}
  1. OrderViewModel 型の viewModelNavHostController 型の navController という 2 つのパラメータを追加します。
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
}
  1. 関数本体で、viewModel に対して resetOrder() を呼び出します。
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
}
  1. navController に対して popBackStack() を呼び出し、routeCupcakeScreen.Start.name を渡して、inclusivefalse を渡します。
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
    navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
  1. CupcakeApp() コンポーザブルで、2 つの SelectOptionScreen コンポーザブルと OrderSummaryScreen コンポーザブルの onCancelButtonClicked パラメータに cancelOrderAndNavigateToStart を渡します。
composable(route = CupcakeScreen.Start.name) {
   StartOrderScreen(
       quantityOptions = quantityOptions,
       onNextButtonClicked = {
           viewModel.setQuantity(it)
           navController.navigate(CupcakeScreen.Flavor.name)
       }
   )
}
composable(route = CupcakeScreen.Flavor.name) {
   val context = LocalContext.current
   SelectOptionScreen(
       subtotal = uiState.price,
       onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
       onCancelButtonClicked = {
           cancelOrderAndNavigateToStart(viewModel, navController)
       },
       options = flavors.map { id -> context.resources.getString(id) },
       onSelectionChanged = { viewModel.setFlavor(it) }
   )
}
  1. アプリを実行し、いずれかの画面で [Cancel] ボタンを押すと最初の画面に戻ることを確認します。

6. 別のアプリに移動する

ここまでで、アプリ内の別の画面に移動する方法と、root 画面に戻る方法を学習しました。Cupcake アプリにナビゲーションを実装するには、もう 1 つだけステップがあります。注文概要画面で、ユーザーは別のアプリに注文を送信できます。この選択により、ボトムシート(画面の下部を覆うユーザー インターフェース コンポーネント)が表示され、共有オプションが表示されます。

この UI 部分は、Cupcake アプリの一部ではありません。実際は Android オペレーティング システムが提供しています。共有画面などのシステム UI は、navController で呼び出すわけではありません。代わりに、インテントというものを使用します。

インテントはシステムになんらかのアクションを行わせるリクエストです。通常は、新しいアクティビティを提示します。さまざまなインテントが存在します。包括的なリストについては、ドキュメントをご覧になることをおすすめします。ここでは、ACTION_SEND というインテントを活用します。このインテントに文字列などのデータを指定し、そのデータに適した共有アクションを提示できます。

インテントをセットアップする基本的なプロセスは次のとおりです。

  1. インテント オブジェクトを作成し、インテントを指定します(ACTION_SEND など)。
  2. インテントで送信する追加データの型を指定します。単純なテキストには "text/plain" を使用できますが、"image/*""video/*" など他のタイプも使用できます。
  3. putExtra() メソッドを呼び出して、共有するテキストや画像などの追加データをインテントに渡します。このインテントは EXTRA_SUBJECTEXTRA_TEXT という 2 つのエクストラを受け取ります。
  4. コンテキストの startActivity() メソッドを呼び出し、インテントから作成したアクティビティを渡します。

ここでは共有アクション インテントの作成方法を説明しますが、プロセスは他のタイプのインテントでも同じです。今後のプロジェクトでは必要に応じて、特定の種類のデータと必要なエクストラについてドキュメントをご覧になることをおすすめします。

次の手順を実施して、カップケーキの注文を別のアプリに送信するインテントを作成します。

  1. CupcakeScreen.ktCupcakeApp コンポーザブルの下に shareOrder() というプライベート関数を作成します。
private fun shareOrder()
  1. Context 型の context というパラメータを追加します。
private fun shareOrder(context: Context) {
}
  1. subjectsummary の 2 つの String パラメータを追加します。これらの文字列は共有アクション シートに表示されます。
private fun shareOrder(context: Context, subject: String, summary: String) {
}
  1. 関数本体内に intent というインテントを作成し、引数として Intent.ACTION_SEND を渡します。
val intent = Intent(Intent.ACTION_SEND)

この Intent オブジェクトは一度構成するだけで済むため、後続する数行のコードは、前の Codelab で学習した apply() 関数を使用してさらに簡潔にできます。

  1. 新しく作成したインテントに対して apply() を呼び出し、ラムダ式を渡します。
val intent = Intent(Intent.ACTION_SEND).apply {

}
  1. ラムダ本体で、型を "text/plain" に設定します。これは apply() に渡される関数で行うため、オブジェクトの識別子である intent を参照する必要はありません。
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
}
  1. putExtra() を呼び出して、EXTRA_SUBJECT に subject を渡します。
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
}
  1. putExtra() を呼び出して、EXTRA_TEXT に summary を渡します。
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
    putExtra(Intent.EXTRA_TEXT, summary)
}
  1. コンテキストの startActivity() メソッドを呼び出します。
context.startActivity(

)
  1. startActivity() に渡されるラムダ内で、クラスメソッド createChooser() を呼び出してインテントからアクティビティを作成します。最初の引数と new_cupcake_order 文字列リソースに intent を渡します。
context.startActivity(
    Intent.createChooser(
        intent,
        context.getString(R.string.new_cupcake_order)
    )
)
  1. CupcakeApp コンポーザブルにおいて、CucpakeScreen.Summary.namecomposable() の呼び出しで、コンテキスト オブジェクトへの参照を取得し、shareOrder() 関数に渡せるようにします。
composable(route = CupcakeScreen.Summary.name) {
    val context = LocalContext.current

    ...
}
  1. onSendButtonClicked() のラムダ本体で shareOrder() を呼び出し、引数として contextsubjectsummary を渡します。
onSendButtonClicked = { subject: String, summary: String ->
    shareOrder(context, subject = subject, summary = summary)
}
  1. アプリを実行し、画面を移動します。

[Send Order to Another App] をクリックすると、エクストラとして指定した件名と概要に加えて、[メッセージ] や [Bluetooth] などの共有アクションがボトムシートに表示されます。

SMS やメールなどの共有オプションを提示する Cupcake アプリ。

7. AppBar がナビゲーションに反応するようにする

アプリは機能し、どの画面間でも移動できるようにもなりましたが、この Codelab の冒頭で示したスクリーンショットと比べると欠けているものがまだいくつかあります。AppBar がナビゲーションに自動的に反応しません。アプリが新しいルートに移動したときにタイトルが更新されず、必要に応じてタイトルの前に「上へ」ボタンが表示されることもありません。

スターター コードには、CupcakeAppBar という AppBar を管理するためのコンポーザブルが含まれています。アプリにナビゲーションを実装したので、バックスタックからの情報を使用して正しいタイトルを表示し、必要に応じて「上へ」ボタンを表示できます。

「上へ」ボタンは、バックスタックにコンポーザブルがある場合にのみ表示されます。アプリの画面がバックスタックにない場合(StartOrderScreen が表示されている場合)、「上へ」ボタンは表示されません。これを確認するには、バックスタックへの参照が必要です。

  1. CupcakeApp コンポーザブルで、navController 変数の下に backStackEntry という変数を作成し、by デリゲートを使用して navControllercurrentBackStackEntry() メソッドを呼び出します。
@Composable
fun CupcakeApp(modifier: Modifier = Modifier, viewModel: OrderViewModel = viewModel()){

    val navController = rememberNavController()

    val backStackEntry by navController.currentBackStackEntryAsState()

    ...
}
  1. CupcakeAppBarcurrentScreen パラメータに backStackEntry?.destination?.route を渡します。これは null 許容であるため、エルビス演算子(?:)を使用して CupcakeScreen.Start.name をデフォルトとして指定します。
currentScreen = backStackEntry?.destination?.route ?: CupcakeScreen.Start.name,

バックスタックで現在の画面の背後に画面がある限り、「上へ」ボタンが表示されます。ブール式を使用して、「上へ」ボタンを表示するかどうかを指定できます。

  1. canNavigateBack パラメータに、navControllerpreviousBackStackEntry プロパティが null と等しくないかどうかを確認するブール式を渡します。
canNavigateBack = navController.previousBackStackEntry != null,
  1. 実際に前の画面に戻るには、navControllernavigateUp() メソッドを呼び出します。
navigateUp = { navController.navigateUp() }
  1. アプリを実行します。

AppBar のタイトルが現在の画面を反映するように更新されました。StartOrderScreen 以外の画面に移動すると「上へ」ボタンが表示され、前の画面に戻ることができます。

完成した Cupcake アプリで各画面間を移動する様子を表したアニメーション。

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 で処理して、関数型のパラメータを使用してナビゲーション ロジックを個々の画面から分離しました。また、インテントを使用して別のアプリにデータを送信する方法や、ナビゲーションに応じてアプリバーをカスタマイズする方法も学習しました。今後のユニットでは、こうしたスキルを使用して、さらに複雑化するマルチスクリーン アプリに取り組んでいきます。

詳細