1. はじめに
Android プラットフォームでアプリを開発することの大きなメリットの 1 つは、ウェアラブル、折りたたみ式、タブレット、デスクトップ、さらにはテレビなど、さまざまなフォーム ファクタでユーザーにリーチできることです。アプリを使用するユーザーは、同じアプリを大画面デバイスで使用して、広くなったスペースのメリットを享受できます。Android ユーザーが、画面サイズの異なる複数のデバイスでアプリを使用することが多くなり、すべてのデバイスで質の高いユーザー エクスペリエンスを期待するようになっています。
ここまでは、主にモバイル デバイス向けアプリを作成する方法を学習しましたが、この Codelab では、アプリを他の画面サイズに適応させる方法を学びます。ここでは、アダプティブ ナビゲーション レイアウト パターンという、折りたたみ式、タブレット、パソコンなど、モバイル デバイスと大画面デバイスのどちらでも使用可能で見栄えも良いパターンを使用します。
前提条件
- クラス、関数、条件文など、Kotlin プログラミングに精通していること
ViewModelクラスの使用経験Composable関数の作成に精通していること- Jetpack Compose でレイアウトを作成した経験
- デバイスまたはエミュレータでアプリを実行した経験
学習内容
- シンプルなアプリで、ナビゲーション グラフを使用せずに画面間のナビゲーションを作成する方法
- Jetpack Compose を使用してアダプティブ ナビゲーション レイアウトを作成する方法
- カスタムの「戻る」ハンドラを作成する方法
作成するアプリの概要
- 既存の Reply アプリにダイナミック ナビゲーションを実装し、そのレイアウトがすべての画面サイズに適応するようにする
完成すると以下の画像のようになります。

必要なもの
- インターネットにアクセスできるパソコン、ウェブブラウザ、Android Studio
- GitHub へのアクセス
2. アプリの概要
Reply アプリの概要
Reply アプリは、メール クライアントに似たマルチスクリーン アプリです。

4 つのカテゴリからなり、それぞれ別のタブ(受信トレイ、送信済み、下書き、迷惑メール)で表示されます
スターター コードをダウンロードする
Android Studio で basic-android-kotlin-compose-training-reply-app フォルダを開きます。
3. スターター コードのチュートリアル
Reply アプリの重要なディレクトリ

Reply アプリ プロジェクトのデータと UI レイヤは、別々のディレクトリに分割されています。ReplyViewModel、ReplyUiState などのコンポーザブルは、ui ディレクトリにあります。データレイヤとデータ プロバイダのクラスを定義する data クラスと enum クラスは data ディレクトリにあります。
Reply アプリのデータ初期化
Reply アプリは ReplyViewModel の initializeUIState() メソッドを使用してデータが初期化されます。このメソッドは init 関数で実行されます。
ReplyViewModel.kt
...
init {
initializeUIState()
}
private fun initializeUIState() {
var mailboxes: Map<MailboxType, List<Email>> =
LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
_uiState.value = ReplyUiState(
mailboxes = mailboxes,
currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
?: LocalEmailsDataProvider.defaultEmail
)
}
...
画面レベルのコンポーザブル
他のアプリと同様に、Reply アプリは viewModel と uiState が宣言されるメインのコンポーザブルとして ReplyApp コンポーザブルを使用します。さまざまな viewModel() 関数も、ReplyHomeScreen コンポーザブルのラムダ引数として渡されます。
ReplyApp.kt
...
@Composable
fun ReplyApp(modifier: Modifier = Modifier) {
val viewModel: ReplyViewModel = viewModel()
val replyUiState = viewModel.uiState.collectAsState().value
ReplyHomeScreen(
replyUiState = replyUiState,
onTabPressed = { mailboxType: MailboxType ->
viewModel.updateCurrentMailbox(mailboxType = mailboxType)
viewModel.resetHomeScreenStates()
},
onEmailCardPressed = { email: Email ->
viewModel.updateDetailsScreenStates(
email = email
)
},
onDetailScreenBackPressed = {
viewModel.resetHomeScreenStates()
},
modifier = modifier
)
}
その他のコンポーザブル
ReplyHomeScreen.kt: ナビゲーション要素など、ホーム画面用の画面コンポーザブルが含まれています。ReplyHomeContent.kt: ホーム画面の詳細なコンポーザブルを定義するコンポーザブルが含まれています。ReplyDetailsScreen.kt: 画面コンポーザブルと、詳細画面用のより小さなコンポーザブルが含まれています。
この Codelab の次のセクションに進む前に、各ファイルについて詳しく確認して、これらのコンポーザブルの理解を深めることをおすすめします。
4. ナビゲーション グラフを使用せずに画面を変更する
前のパスウェイでは、NavHostController クラスを使用して、ある画面から別の画面に移動する方法を学びました。Compose では、実行時の可変状態を利用し、単純な条件文で画面を変更することもできます。これは、Reply アプリのような 2 つの画面間のみで切り替える小さなアプリなどに特に有用です。
状態変更で画面を変更する
Compose では、状態変更が発生すると画面が再コンポーズされます。単純な条件文を使用して画面を変更することで、状態の変化に対応できます。
条件文を使用して、ユーザーがホーム画面にいるときにホーム画面の内容を表示し、ユーザーがホーム画面にいないときには詳細画面の内容を表示しましょう。
Reply アプリを変更して、状態変更時に画面を変更できるようにします。手順は次のとおりです。
- Android Studio でスターター コードを開きます。
ReplyHomeScreen.ktのReplyHomeScreenコンポーザブルで、ReplyAppContentコンポーザブルを、replyUiStateオブジェクトのisShowingHomepageプロパティがtrueの場合のif文で囲みます。
ReplyHomeScreen.kt
@Composable
fun ReplyHomeScreen(
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Int) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
...
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
}
詳細画面を表示することで、ユーザーがホーム画面にいないときのシナリオを考慮する必要があります。
- 本体に
ReplyDetailsScreenコンポーザブルが含まれるelse分岐を追加します。ReplyDetailsScreenコンポーザブルの引数としてreplyUIState、onDetailScreenBackPressed、modifierを追加します。
ReplyHomeScreen.kt
@Composable
fun ReplyHomeScreen(
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Int) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
...
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
}
replyUiState オブジェクトは状態オブジェクトです。そのため、実行時には、replyUiState オブジェクトの isShowingHomepage プロパティが変化すると、ReplyHomeScreen コンポーザブルが再コンポーズされ、if/else 文が再評価されます。このアプローチでは、NavHostController クラスを使用することなく画面間のナビゲーションに対応しています。

カスタムの「戻る」ハンドラを作成する
NavHost コンポーザブルを使用して画面を切り替えることの利点の 1 つは、前の画面の履歴がバックスタックに保存されることです。これらの保存された画面により、システムの [戻る] ボタンを押すと前の画面に簡単に移動できるようになります。Reply アプリでは NavHost を使用しないため、[戻る] ボタンを手動で処理するためのコードを追加する必要があります。これは次で行います。
次の手順に沿って、Reply アプリでカスタムの「戻る」ハンドラを作成します。
ReplyDetailsScreenコンポーザブルの 1 行目に、BackHandlerコンポーザブルを追加します。BackHandlerコンポーザブルの本体でonBackPressed()関数を呼び出します。
ReplyDetailsScreen.kt
...
import androidx.activity.compose.BackHandler
...
@Composable
fun ReplyDetailsScreen(
replyUiState: ReplyUiState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
BackHandler {
onBackPressed()
}
...
5. 大画面デバイスでアプリを実行する
サイズ変更可能なエミュレータでアプリを確認する
使いやすいアプリを作成するには、さまざまなフォーム ファクタでのユーザー エクスペリエンスを理解する必要があります。そのため、開発プロセスの初期から、さまざまなフォーム ファクタでアプリをテストする必要があります。
さまざまな画面サイズのエミュレータを使用することで、この目標を達成できます。特に複数の画面サイズに対応するアプリを一度に構築する場合には、面倒な作業になる可能性があります。また、実行中のアプリの画面サイズの変化(画面の向きの変更、デスクトップでのウィンドウ サイズの変更、折りたたみ式デバイスでの折りたたみ状態の変化など)に対する反応をテストする必要があります。
Android Studio では、サイズ変更可能なエミュレータを導入することで、このようなシナリオのテストが可能になりました。
サイズ変更可能なエミュレータは、次の手順でセットアップします。
- Android Studio で、[Tools] > [Device Manager] を選択します。
![[Tools] メニューに、オプションのリストが表示されています。Device Manager が、リストの中ほどに表示され、選択されています。](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-adaptive-navigation-for-large-screens/img/d5ad25dcd845441a.png?authuser=0000&hl=ja)
- [Device Manager] で [+] アイコンをクリックして、仮想デバイスを作成します。

- [Phone] カテゴリと [Resizable (Experimental)] デバイスを選択します。
- [次へ] をクリックします。
![[Device Manager] ウィンドウに、デバイス定義を選ぶプロンプトが表示されています。選択肢のリストが表示され、その上に検索フィールドがあります。カテゴリ](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-adaptive-navigation-for-large-screens/img/94af56858ef86e1c.png?authuser=0000&hl=ja)
- API レベル 34 以上を選択します。
- [次へ] をクリックします。
![[Virtual Device Configuration] ウィンドウに、システム イメージを選択するプロンプトが表示されています。API レベル 34 が選択されています。](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-adaptive-navigation-for-large-screens/img/9658c810f0be9988.png?authuser=0000&hl=ja)
- 新しい Android Virtual Device に名前を付けます。
- [完了] をクリックします。
![Android Virtural Device(AVD)の [Virtual Configration] 画面が表示されています。設定画面には、AVD 名を入力するためのテキスト フィールドがあります。名前フィールドの下には、デバイス定義(Resizable Experimental)、システム イメージ(Tiramisu)、画面の向き(デフォルトの Portrait が選択されている)などのデバイス オプションのリストがあります。ボタンの読み上げ](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-adaptive-navigation-for-large-screens/img/f6f40f18319df171.png?authuser=0000&hl=ja)
大画面のエミュレータでアプリを実行する
サイズ変更可能なエミュレータのセットアップが完了したので、大画面でアプリがどのように表示されるのかを見てみましょう。
- サイズ変更可能なエミュレータでアプリを実行します。
- 表示モードに [Tablet] を選択します。

- 横表示のタブレット モードでアプリを検査します。

タブレット画面の表示は横長であることに注意してください。この向きは機能的には問題ありませんが、大画面のスペースを最大限に活用できてはいません。次はこれに対処しましょう。
大画面用に設計する
タブレットでこのアプリを見たとき、デザインが悪く、魅力的でないと思ったのではないでしょうか。そうです。このレイアウトは大画面での使用に適したデザインになっていません。
タブレットや折りたたみ式などの大画面向けに設計する際には、ユーザーの使い勝手や、ユーザーの指と画面との距離を考慮する必要があります。モバイル デバイスの場合、ユーザーの指は画面のほとんどの場所に簡単に届くため、インタラクティブ要素(ボタンやナビゲーション要素など)の位置はそれほど重要ではありません。しかし、大画面の場合、重要なインタラクティブ要素が画面の中央に配置されると、指が届きづらくなります。
この Reply アプリで見たように、画面に合わせて UI 要素を広げたり引き伸ばしたりするだけでは、大画面向けのデザインとは言えません。広くなったスペースを活用して、別のユーザー エクスペリエンスを生み出すチャンスです。たとえば、同じ画面に別のレイアウトを追加して別の画面への移動を避けることや、マルチタスクを可能にすることが考えられます。

このデザインにより、ユーザーの生産性が高まり、エンゲージメントが促進されます。ただし、このデザインを採用する前に、まず画面サイズごとに異なるレイアウトを作成する方法を確認してください。
6. さまざまな画面サイズにレイアウトを合わせる
ブレークポイントとは
同じアプリで異なるレイアウトをどのように表示できるのか疑問に思われるかもしれません。簡単に言えば、この Codelab の最初に行ったように、さまざまな状態の条件分岐を使用するということです。
アダプティブ アプリを作成するには、画面サイズに応じてレイアウトを変更する必要があります。レイアウト変更の基準となる測定ポイントをブレークポイントと呼びます。マテリアル デザインで、ほとんどの Android 画面に対応する独自のブレークポイント範囲が作成されました。

このブレークポイント範囲の表は、たとえば、画面サイズが 600 dp 未満のデバイスでアプリが実行されている場合は、モバイル レイアウトが表示される必要があることが示されています。
ウィンドウ サイズクラスを使用する
Compose に導入された WindowSizeClass API を使用すると、マテリアル デザインのブレークポイントを簡単に実装できます。
ウィンドウ サイズクラスには、幅と高さの両方について、コンパクト、中程度、拡大の 3 つのカテゴリのサイズがあります。

次の手順に沿って、Reply アプリに WindowSizeClass API を実装します。
material3-window-size-class依存関係をモジュールbuild.gradle.ktsファイルに追加します。
build.gradle.kts
...
dependencies {
...
implementation("androidx.compose.material3:material3-window-size-class")
...
- 依存関係を追加した後、[Sync Now] をクリックして Gradle を同期します。

build.gradle.kts ファイルが最新の状態になると、随時アプリのウィンドウ サイズを格納する変数を作成できます。
MainActivity.ktファイルのonCreate()関数内で、パラメータとしてthisコンテキストを渡してcalculateWindowSizeClass()メソッドを呼び出した結果をwindowSizeという変数に代入します。- 適切な
calculateWindowSizeClassパッケージをインポートします。
MainActivity.kt
...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReplyTheme {
val layoutDirection = LocalLayoutDirection.current
Surface (
// ...
) {
val windowSize = calculateWindowSizeClass(this)
ReplyApp()
...
calculateWindowSizeClassの箇所に赤い下線が引かれ、赤い電球が表示されています。windowSize変数の左側にある赤い電球のアイコンをクリックし、[Opt in for ‘ExperimentalMaterial3WindowSizeClassApi' on ‘onCreate'] を選択すると、onCreate()メソッドの上にアノテーションが作成されます。

MainActivity.kt の WindowWidthSizeClass 変数を使用して、さまざまなコンポーザブルに表示するレイアウトを決定できます。この値を受け取るように ReplyApp コンポーザブルを準備しましょう。
ReplyApp.ktファイルで、WindowWidthSizeClassをパラメータとして受け取るようにReplyAppコンポーザブルを変更し、適切なパッケージをインポートします。
ReplyApp.kt
...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...
@Composable
fun ReplyApp(
windowSize: WindowWidthSizeClass,
modifier: Modifier = Modifier
) {
...
MainActivity.ktファイルのonCreate()メソッドで、windowSize変数をReplyAppコンポーネントに渡します。
MainActivity.kt
...
setContent {
ReplyTheme {
Surface {
val windowSize = calculateWindowSizeClass(this)
ReplyApp(
windowSize = windowSize.widthSizeClass
)
...
windowSize パラメータに関しては、アプリのプレビューも更新する必要があります。
WindowWidthSizeClass.CompactをwindowSizeパラメータとしてプレビュー コンポーネントのReplyAppコンポーザブルに渡し、適切なパッケージをインポートします。
MainActivity.kt
...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...
@Preview(showBackground = true)
@Composable
fun ReplyAppCompactPreview() {
ReplyTheme {
Surface {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact,
)
}
}
}
- 画面のサイズに基づいてアプリのレイアウトを変更するために、
ReplyAppコンポーザブルにWindowWidthSizeClass値に基づくwhen文を追加します。
ReplyApp.kt
...
@Composable
fun ReplyApp(
windowSize: WindowWidthSizeClass,
modifier: Modifier = Modifier
) {
val viewModel: ReplyViewModel = viewModel()
val replyUiState = viewModel.uiState.collectAsState().value
when (windowSize) {
WindowWidthSizeClass.Compact -> {
}
WindowWidthSizeClass.Medium -> {
}
WindowWidthSizeClass.Expanded -> {
}
else -> {
}
}
...
これで、WindowSizeClass の値を使用してアプリのレイアウトを変更するための基盤が出来上がりました。次のステップでは、各画面サイズでアプリをどのように表示するかを決定します。
7. アダプティブ ナビゲーション レイアウトを実装する
アダプティブ UI ナビゲーションを実装する
現時点では、すべての画面サイズでボトム ナビゲーションが使用されます。

前述のように、大画面になると、この重要なナビゲーション要素にアクセスするのが難しくなるため、理想的とは言えません。幸い、レスポンシブ UI のナビゲーションでは、ウィンドウ サイズクラスごとにナビゲーション要素の推奨パターンがあります。Reply アプリの場合は、以下の要素を実装できます。

マテリアル デザインのもう一つのナビゲーション コンポーネントであるナビゲーション レールでは、コンパクトなナビゲーションを選択可能で、アプリの横から主要な目的にアクセスできるようになります。

同様に、永続 / 固定ナビゲーション ドロワーもマテリアル デザインの一つで、大画面でも人間工学的に基づいた操作性が得られます。

ナビゲーション ドロワーを実装する
拡大画面用のナビゲーション ドロワーを作成するために、navigationType パラメータを使用します。以下の手順に沿って進めます。
- ナビゲーション要素の種類を表現するために、
uiディレクトリにある新しいパッケージutilsに新しいファイルWindowStateUtils.ktを作成します。 - ナビゲーション要素の種類を表す
Enumクラスを追加します。
WindowStateUtils.kt
package com.example.reply.ui.utils
enum class ReplyNavigationType {
BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}
ナビゲーション ドロワーを実装するには、アプリのウィンドウ サイズに基づいてナビゲーション タイプを決定する必要があります。
ReplyAppコンポーザブルで、navigationType変数を作成し、when文の画面サイズに応じて適切なReplyNavigationTypeの値を代入します。
ReplyApp.kt
...
import com.example.reply.ui.utils.ReplyNavigationType
...
val navigationType: ReplyNavigationType
when (windowSize) {
WindowWidthSizeClass.Compact -> {
navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
}
WindowWidthSizeClass.Medium -> {
navigationType = ReplyNavigationType.NAVIGATION_RAIL
}
WindowWidthSizeClass.Expanded -> {
navigationType = ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
}
else -> {
navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
}
}
...
これで ReplyHomeScreen コンポーザブルで navigationType の値を使用できるようになりました。実際に使用するために、それをコンポーザブルのパラメータにします。
ReplyHomeScreenコンポーザブルで、navigationTypeをパラメータとして追加します。
ReplyHomeScreen.kt
...
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Email) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
)
...
navigationTypeをReplyHomeScreenコンポーザブルに渡します。
ReplyApp.kt
...
ReplyHomeScreen(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = { mailboxType: MailboxType ->
viewModel.updateCurrentMailbox(mailboxType = mailboxType)
viewModel.resetHomeScreenStates()
},
onEmailCardPressed = { email: Email ->
viewModel.updateDetailsScreenStates(
email = email
)
},
onDetailScreenBackPressed = {
viewModel.resetHomeScreenStates()
},
modifier = modifier
)
...
次に、分岐を作成して、ユーザーが拡張画面でアプリを開いてホーム画面を表示したときに、アプリのコンテンツとともにナビゲーション ドロワーを表示するようにします。
ReplyHomeScreenコンポーザブルの本体で、navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepageという条件のif文を追加します。
ReplyHomeScreen.kt
import androidx.compose.material3.PermanentNavigationDrawer
...
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Email) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
}
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
...
- 永続ドロワーを作成するために、if 文の本体に
PermanentNavigationDrawerコンポーザブルを作成し、drawerContentパラメータの入力としてNavigationDrawerContentコンポーザブルを追加します。 ReplyAppContentコンポーザブルを、PermanentNavigationDrawerの最後のラムダ引数として追加します。
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.inverseOnSurface)
.padding(dimensionResource(R.dimen.drawer_padding_content))
)
}
}
) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
}
...
- 前のコンポーザブルの本体を使用する
else分岐を追加し、拡大以外の画面には以前の分岐をそのまま使用します。
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.inverseOnSurface)
.padding(dimensionResource(R.dimen.drawer_padding_content))
)
}
}
) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
} else {
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
}
}
...
- アプリをタブレット モードで実行します。以下に示す画面が表示されます。

ナビゲーション レールを実装する
ナビゲーション ドロワーの実装と同様に、ナビゲーション要素間の切り替えを行うには navigationType パラメータを使用する必要があります。
まず、中程度の画面用のナビゲーション レールを追加しましょう。
- はじめに、
ReplyAppContentコンポーザブルにパラメータとしてnavigationTypeを追加します。
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
...
- 両方の
ReplyAppContentコンポーザブルにnavigationTypeの値を渡します。
ReplyHomeScreen.kt
...
ReplyAppContent(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
} else {
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
...
次に、分岐を追加します。この分岐により、一部のシナリオでナビゲーション レールを表示できるようになります。
ReplyAppContentコンポーザブル本体の 1 行目で、ReplyNavigationRailコンポーザブルをAnimatedVisibilityコンポーザブルで囲み、ReplyNavigationTypeの値がNAVIGATION_RAILの場合にvisibleパラメータをtrueに設定します。
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
ReplyNavigationRail(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(
MaterialTheme.colorScheme.inverseOnSurface
)
) {
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
.padding(
horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
)
)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.fillMaxWidth()
)
}
}
}
...
- コンポーザブルを正しく配置するために、
ReplyAppContentの本体にあるAnimatedVisibilityコンポーザブルとColumnコンポーザブルの両方をRowコンポーザブルで囲みます。
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier) {
AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.inverseOnSurface)
) {
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
.padding(
horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
)
)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.fillMaxWidth()
)
}
}
}
...
最後に、一部のシナリオでボトム ナビゲーションが表示されることを確認しましょう。
ReplyListOnlyContentコンポーザブルの後で、ReplyBottomNavigationBarコンポーザブルをAnimatedVisibilityコンポーザブルで囲みます。ReplyNavigationTypeの値がBOTTOM_NAVIGATIONの場合、visibleパラメータを設定します。
ReplyHomeScreen.kt
...
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
.padding(
horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
)
)
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.fillMaxWidth()
)
}
...
- アプリを開いた状態の折りたたみ式デバイスで実行します。以下に示す画面が表示されます。

8. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、以下の git コマンドを使用します。
git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git cd basic-android-kotlin-compose-training-reply-app git checkout nav-update
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
解答コードを確認する場合は、GitHub で表示します。
9. まとめ
お疲れさまでした。アダプティブ ナビゲーション レイアウトを実装したことで、Reply アプリをあらゆる画面サイズに適応させることに一歩近づきました。また、さまざまな Android フォーム ファクタを使用するユーザー エクスペリエンスを向上させました。次の Codelab では、アダプティブ コンテンツ レイアウトの実装、テスト、プレビューを行い、アダプティブ アプリを扱うスキルをさらに高めます。
作成したら、#AndroidBasics を付けて、ソーシャル メディアで共有しましょう。