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