ダイナミック ナビゲーションを使用してアダプティブ アプリを作成する

1. 概要

Android プラットフォームでアプリを開発することの大きなメリットの 1 つは、ウェアラブル、折りたたみ式、タブレット、デスクトップ、さらにはテレビなど、さまざまなフォーム ファクタでユーザーにリーチできることです。アプリを使用するユーザーは、同じアプリを大画面デバイスで使用して、広くなったスペースのメリットを享受できます。Android ユーザーが、画面サイズの異なる複数のデバイスでアプリを使用することが多くなり、すべてのデバイスで質の高いユーザー エクスペリエンスを期待するようになっています。

ここまでは、主にモバイル デバイス向けアプリを作成する方法を学習しましたが、この Codelab では、アプリを他の画面サイズに適応させる方法を学びます。ここでは、アダプティブ ナビゲーション レイアウト パターンという、折りたたみ式、タブレット、パソコンなど、モバイル デバイスと大画面デバイスのどちらでも使用可能で見栄えも良いパターンを使用します。

前提条件

  • クラス、関数、条件文など、Kotlin プログラミングに精通していること
  • ViewModel クラスの使用経験
  • Composables 関数の作成に精通していること
  • Jetpack Compose でレイアウトを作成した経験
  • デバイスまたはエミュレータでアプリを実行した経験

学習内容

  • シンプルなアプリで、ナビゲーション グラフを使用せずに画面間のナビゲーションを作成する方法
  • Jetpack Compose を使用してアダプティブ ナビゲーション レイアウトを作成する方法
  • カスタムの「戻る」ハンドラを作成する方法

作成するアプリの概要

  • 既存の Reply アプリにダイナミック ナビゲーションを実装し、そのレイアウトがすべての画面サイズに適応するようにする

完成すると以下の画像のようになります。

​​ この Codelab の最後の Reply アプリのイラスト。左側にはナビゲーション ドロワーが表示されています。ナビゲーション ドロワーには「Inbox」「Sent」「Drafts」「Spam」の 4 つのタブが表示されています。ナビゲーション ドロワーの右側に、サンプルメールのリストが表示されています。

必要なもの

  • インターネットにアクセスできるパソコン、ウェブブラウザ、Android Studio
  • GitHub へのアクセス

2. アプリの概要

Reply アプリの概要

Reply アプリは、メール クライアントに似たマルチスクリーン アプリです。

Reply アプリがスマートフォン モードで表示されます。ユーザーが通読できるサンプルメールのリストが表示されます。画面下部に、受信トレイ、送信済み、下書き、迷惑メールを表す 4 つのアイコンがあります。

4 つのカテゴリがあり、それぞれが受信トレイ、送信済み、下書き、迷惑メールの各タブに表示されます。

スターター コードをダウンロードする

Android Studio で basic-android-kotlin-compose-training-reply-app フォルダを開きます。

  1. プロジェクト用に提供されている GitHub リポジトリ ページに移動します。
  2. ブランチ名が Codelab で指定されたブランチ名と一致していることを確認します。たとえば、次のスクリーンショットでは、ブランチ名は main です。

1e4c0d2c081a8fd2.png

  1. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ポップアップが表示されます。

1debcf330fd04c7b.png

  1. ポップアップで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ちます。
  2. パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
  3. ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで、[Open] をクリックします。

d8e9dbdeafe9038a.png

注: Android Studio がすでに開いている場合は、メニューから [File] > [Open] を選択します。

8d1fda7396afe8e5.png

  1. ファイル ブラウザで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開かれるまで待ちます。
  4. 実行ボタン 8de56cba7583251f.png をクリックして、アプリをビルドし、実行します。期待どおりにビルドされることを確認します。

3. スターター コードのチュートリアル

Reply アプリの重要なディレクトリ

Reply アプリのファイル ディレクトリには、開かれた状態の 2 つのサブディレクトリ「data」と「ui」が表示されています。ui ディレクトリで、MainActivity.kt が選択されています。MainActivity.kt は、内容のリストの最後に表示されています。

Reply アプリ プロジェクトのデータと UI レイヤは、別々のディレクトリに分割されています。ReplyViewModelReplyUiState などのコンポーザブルは、ui ディレクトリにあります。データレイヤとデータ プロバイダのクラスを定義する data クラスと enum クラスは data ディレクトリにあります。

Reply アプリのデータ初期化

Reply アプリは ReplyViewModelinitilizeUIState() メソッドを使用してデータが初期化されます。このメソッドは 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 アプリは viewModeluiState が宣言されるメインのコンポーザブルとして 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 アプリを変更して、状態変更時に画面を変更できるようにします。手順は次のとおりです。

  1. Android Studio でスターター コードを開きます。
  2. ReplyHomeScreen.ktReplyHomeScreen コンポーザブルで、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

        )
    }
}

詳細画面を表示することで、ユーザーがホーム画面にいないときのシナリオを考慮する必要があります。

  1. 本体に ReplyDetailsScreen コンポーザブルが含まれる else 分岐を追加します。ReplyDetailsScreen コンポーザブルの引数として replyUIStateonDetailScreenBackPressedmodifier を追加します。

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 クラスを使用することなく画面間のナビゲーションに対応しています。

スマートフォン エミュレータに表示された Reply アプリのアニメーション。画面がホーム画面から詳細ページに変わります。ホーム画面には、メールのリストと、下部に 4 つのアイコン(受信トレイ、送信済み、下書き、迷惑メール)が表示されています。詳細ページには、サンプルメールの全文が表示され、その下に [Reply] ボタンと [Reply All] ボタンがあります。

カスタムの「戻る」ハンドラを作成する

NavHost コンポーザブルを使用して画面を切り替えることの利点の 1 つは、前の画面の履歴がバックスタックに保存されることです。これらの保存された画面により、システムの [戻る] ボタンを押すと前の画面に簡単に移動できるようになります。Reply アプリでは NavHost を使用しないため、[戻る] ボタンを手動で処理するためのコードを追加する必要があります。これは次で行います。

次の手順に沿って、Reply アプリでカスタムの「戻る」ハンドラを作成します。

  1. ReplyDetailsScreen コンポーザブルの 1 行目に、BackHandler コンポーザブルを追加します。
  2. BackHandler コンポーザブルの本体で onBackPressed() 関数を呼び出します。

ReplyDetailsScreen.kt

...
import androidx.activity.compose.BackHandler
...
@Composable
fun ReplyDetailsScreen(
    replyUiState: ReplyUiState,
    modifier: Modifier = Modifier,
    onBackPressed: () -> Unit = {},
) {
    BackHandler {
        onBackPressed()
    }
...

5. 大画面デバイスでアプリを実行する

サイズ変更可能なエミュレータでアプリを確認する

使いやすいアプリを作成するには、さまざまなフォーム ファクタでのユーザー エクスペリエンスを理解する必要があります。そのため、開発プロセスの初期から、さまざまなフォーム ファクタでアプリをテストする必要があります。

さまざまな画面サイズのエミュレータを使用することで、この目標を達成できます。特に複数の画面サイズに対応するアプリを一度に構築する場合には、面倒な作業になる可能性があります。また、実行中のアプリの画面サイズの変化(画面の向きの変更、デスクトップでのウィンドウ サイズの変更、折りたたみ式デバイスでの折りたたみ状態の変化など)に対する反応をテストする必要があります。

Android Studio では、サイズ変更可能なエミュレータを導入することで、このようなシナリオのテストが可能になりました。

サイズ変更可能なエミュレータは、次の手順でセットアップします。

  1. Android Studio Chipmunk | 2021.2.1 以降が実行されるようにします。
  2. Android Studio で、[Tools] > [Device Manager] を選択します。

[Tools] メニューに、オプションのリストが表示されています。Device Manager が、リストの中ほどに表示され、選択されています。

  1. [Device Manager] で [Create device] をクリックします。デバイス マネージャー ツールバーに、[Virtual] と [Physical] の 2 つのメニュー オプションが表示されています。これらのオプションの下に、[Create Device] ボタンがあります。
  2. [Phone] カテゴリと [Resizable (Experimental)] デバイスを選択します。
  3. [Next] をクリックします。

[Device Manager] ウィンドウに、デバイス定義を選ぶプロンプトが表示されています。選択肢のリストが表示され、その上に検索フィールドがあります。カテゴリ「Phone」が選択され、デバイス定義名「Resizable (Experimental)」が選択されています。

  1. API レベル 33 を選択します。
  2. [Next] をクリックします。

[Virtual Device Configuration] ウィンドウに、システム イメージを選択するプロンプトが表示されています。Tiramisu API が選択されています。

  1. 新しい Android Virtual Device に名前を付けます。
  2. [Finish] をクリックします。

Android Virtural Device(AVD)の [Virtual Configration] 画面が表示されています。設定画面には、AVD 名を入力するためのテキスト フィールドがあります。名前フィールドの下には、デバイス定義(Resizable Experimental)、システム イメージ(Tiramisu)、画面の向き(デフォルトの Portrait が選択されている)などのデバイス オプションのリストがあります。デバイス定義とシステム イメージ情報の右側に [Change] ボタンが表示され、選択した Portrait オプションの右側に Landscape オプションが表示されています。右下に [Cancel]、[Previous]、[Next](グレー表示され、選択不可)、[Finish] の 4 つのボタンがあります。

大画面エミュレータでアプリを実行する

サイズ変更可能なエミュレータのセットアップが完了したので、大画面でアプリがどのように表示されるのかを見てみましょう。

  1. サイズ変更可能なエミュレータでアプリを実行します。
  2. 表示モードに [Tablet] を選択します。

サイズ変更可能なエミュレータが、スマートフォンの画面に Reply アプリを表示しています。メッセージのリストと、画面下部に受信トレイ、送信済み、下書き、迷惑メールの 4 つのアイコンが表示されています。

  1. 横表示のタブレット モードでアプリを調べます。

サイズ変更可能なエミュレータ。タブレット画面に Reply アプリが表示されており、本文が引き伸ばされています。受信トレイ、送信済み、下書き、迷惑メールの各アイコンが画面の下部に表示されています。

タブレット画面の表示が横方向に引き伸ばされています。この向きは機能的には問題ありませんが、大画面のスペースを最大限に活用できてはいません。次はこれに対処しましょう。

大画面用に設計する

タブレットでこのアプリを見たとき、デザインが悪く、魅力的でないと思ったのではないでしょうか。そうです。このレイアウトは大画面での使用に適したデザインになっていません。

タブレットや折りたたみ式などの大画面向けに設計する際には、ユーザーの使い勝手や、ユーザーの指と画面との距離を考慮する必要があります。モバイル デバイスの場合、ユーザーの指は画面のほとんどの場所に簡単に届くため、インタラクティブ要素(ボタンやナビゲーション要素など)の位置はそれほど重要ではありません。しかし、大画面の場合、重要なインタラクティブ要素が画面の中央に配置されると、指が届きづらくなります。

この Reply アプリで見たように、画面に合わせて UI 要素を広げたり引き伸ばしたりするだけでは、大画面向けのデザインとは言えません。広くなったスペースを活用して、別のユーザー エクスペリエンスを生み出すチャンスです。たとえば、同じ画面に別のレイアウトを追加して別の画面への移動を避けることや、マルチタスクを可能にすることが考えられます。

Reply アプリのホーム画面に、ナビゲーション ドロワー、メール一覧といっしょに詳細画面が表示されています。メール一覧の右側にサンプルメールが表示されています。[Reply] ボタンと [Reply All] ボタンがサンプルメールの下に表示されています。

この設計により、ユーザーの生産性が高まり、エンゲージメントが促進されます。ただし、このデザインを採用する前に、まず画面サイズごとに異なるレイアウトを作成する方法を確認してください。

6. さまざまな画面サイズにレイアウトを合わせる

ブレークポイントとは

同じアプリで異なるレイアウトをどのように表示できるのか疑問に思われるかもしれません。簡単に言えば、この Codelab の最初に行ったように、さまざまな状態の条件分岐を使用するということです。

アダプティブ アプリを作成するには、画面サイズに応じてレイアウトを変更する必要があります。レイアウト変更の基準となる測定ポイントをブレークポイントと呼びます。マテリアル デザインで、ほとんどの Android 画面に対応する独自のブレークポイント範囲が作成されました。

デバイスの種類と設定ごとのブレークポイント範囲(dp)を表す表。0~599 dp の対象は、縦向きモードのハンドセット、横向きのスマートフォン、コンパクトなウィンドウ サイズ、4 列、最小マージン 8 です。600~839 dp の対象は、縦向きモードまたは横向きモードの折りたたみ式小型タブレット、中程度のウィンドウ サイズクラス、12 列、最小マージン 12 です。840 dp 以上の対象は、縦向きモードまたは横向きモードの大画面タブレットで、拡大されたウィンドウ サイズクラス、12 列、最小マージン 32 です。表の注記には、マージンとガターは柔軟であり、サイズが同じである必要はないこと、また、横向きのスマートフォンが 0~599 dp のブレークポイント範囲に収まるという例外であることが記載されています。

このブレークポイント範囲の表は、たとえば、画面サイズが 600 dp 未満のデバイスでアプリが実行されている場合は、モバイル レイアウトが表示される必要があることが示されています。

ウィンドウ サイズクラスを使用する

Compose に導入された WindowSizeClass API を使用すると、マテリアル デザインのブレークポイントを簡単に実装できます。

ウィンドウ サイズクラスには、幅と高さの両方について、コンパクト、中程度、拡大の 3 つのカテゴリのサイズがあります。

幅に基づくウィンドウ サイズクラスを表す図。 高さに基づくウィンドウ サイズクラスを表す図。

次の手順に沿って、Reply アプリに WindowSizeClass API を実装します。

  1. material3-window-size-class 依存関係をモジュール build.gradle ファイルに追加します。

build.gradle

...
dependencies {
...
"androidx.compose.material3:material3-window-size-class:$material3_version"
...
  1. [Sync Now] をクリックして、依存関係を追加した後に Gradle を同期させます。

.kt ファイルと .gradle ファイルを選択するタブの下に、[Sync Now] ボタンが表示されています。[Sync Now] ボタンの右側に「Ignore these changes」という別のボタンがあります。

build.grade ファイルが最新の状態になると、いつでもアプリのウィンドウ サイズを格納する変数を作成できます。

  1. MainActivity.kt ファイルの onCreate() 関数内で、パラメータとして this コンテキストを渡して calculateWindowSizeClass メソッドを呼び出した結果を windowSize という変数に代入します。
  2. 適切な calculateWindowSizeClass パッケージをインポートします。

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

...

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ReplyTheme {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp()

...
  1. calculateWindowSizeClass の箇所に赤い下線が引かれ、赤い電球が表示されています。windowSize 変数の左側にある赤い電球のアイコンをクリックし、[Opt in for ‘ExperimentalMaterial3WindowSizeClassApi' on ‘onCreate'] を選択すると、onCreate() メソッドの上にアノテーションが作成されます。

コードでは、「val windowSize = calculateWindowSizeClass(this)」という行が選択されていて、赤い電球のアイコンがコードの行の左側に表示されます。選択されている電球の下に、エラーを解決するための選択肢が表示されていて、[Opt in for 'ExperimentalMaterial3WindowSizeClassApi' on 'onCreate'] が選択されています。

MainActivity.ktWindowWidthSizeClass 変数を使用して、さまざまなコンポーザブルに表示するレイアウトを決定できます。この値を受け取るように ReplyApp コンポーザブルを準備しましょう。

  1. ReplyApp.kt ファイルで、WindowWidthSizeClass をパラメータとして受け取るように ReplyApp コンポーザブルを変更し、適切なパッケージをインポートします。

ReplyApp.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
...
  1. MainActivity.kt ファイルの onCreate() メソッドで、windowSize 変数を ReplyApp コンポーネントに渡します。

MainActivity.kt

...
         setContent {
            ReplyTheme {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp(
                    windowSize = windowSize.widthSizeClass
                )
...

windowSize パラメータに関しては、アプリのプレビューも更新する必要があります。

  1. WindowWidthSizeClass.CompactwindowSize パラメータとしてプレビュー コンポーネントの ReplyApp コンポーザブルに渡し、適切なパッケージをインポートします。

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Preview(showBackground = true)
@Composable
fun ReplyAppPreview() {
    ReplyTheme {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Compact,
        )
    }
}
  1. 画面のサイズに基づいてアプリのレイアウトを変更するために、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 ナビゲーションを実装する

現在は、すべての画面サイズでボトム ナビゲーションが使用されます。

Reply アプリのボトム ナビゲーション。

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

表には、ウィンドウ サイズクラスと表示される項目が記載されています。幅がコンパクトの場合、ボトム ナビゲーション バーが表示されます。幅が中程度の場合、ナビゲーション レールが表示されます。幅が拡大の場合、前端付きの永続ナビゲーション ドロワーが表示されます。

マテリアル デザインのナビゲーション コンポーネントであるナビゲーション レールでは、主要なデスティネーション用のコンパクトなナビゲーション オプションにアプリの端からアクセスできます。

Reply アプリのナビゲーション レールの例に、受信トレイ、送信済み、下書き、迷惑メールの 4 つのアイコンが縦に並んで表示されています。

同様に、永続 / 固定ナビゲーション ドロワーは、大画面でアクセスしやすいように作られたマテリアル デザインの別のオプションです。

Reply アプリの永続ナビゲーション ドロワーに、4 つのタブが縦に並んで表示され、それぞれにアイコンと名前が表示されています(Inbox、Sent、Drafts、Spam)。

ナビゲーション ドロワーを実装する

拡大画面用のナビゲーション ドロワーを作成するために、navigationType パラメータを使用します。以下の手順に沿って進めます。

  1. ナビゲーション要素の種類を表現するために、ui ディレクトリにある新しいパッケージ utils に新しいファイル WindowStateUtils.kt を作成します。
  2. ナビゲーション要素の種類を表す Enum クラスを追加します。

WindowStateUtils.kt

package com.example.reply.ui.utils

enum class ReplyNavigationType {
    BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}

ナビゲーション ドロワーを実装するには、アプリのウィンドウ サイズに基づいてナビゲーション タイプを決定する必要があります。

  1. ReplyApp コンポーザブルで、navigationType 変数を作成し、when 文の画面サイズに応じて適切な ReplyNavigationType の値を代入します。

ReplyApp.kt

...
import com.example.reply.ui.utils.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 の値を使用できるようになりました。実際に使用するために、それをコンポーザブルのパラメータにします。

  1. ReplyHomeScreen コンポーザブルで、navigationType をパラメータとして追加します。

ReplyHomeScreen.kt

...
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
)

...

  1. navigationTypeReplyHomeScreen コンポーザブルに渡します。

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
    )
...

次に、分岐を作成して、ユーザーが拡張画面でアプリを開いてホーム画面を表示したときに、アプリのコンテンツとともにナビゲーション ドロワーを表示するようにします。

  1. ReplyHomeScreen コンポーザブルの本体で、navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepage という条件の if 文を追加します。

ReplyHomeScreen.kt

import androidx.compose.material3.PermanentNavigationDrawer
...
@OptIn(ExperimentalMaterial3Api::class)
@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,
...
  1. 永続ドロワーを作成するために、if 文の本体に PermanentNavigationDrawer コンポーザブルを作成し、drawerContent パラメータの入力として NavigationDrawerContent コンポーザブルを追加します。
  2. ReplyAppContent コンポーザブルを、PermanentNavigationDrawer の最後のラムダ引数として追加します。

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                NavigationDrawerContent(
                    selectedDestination = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier

            )
        }
    }

...
  1. 前のコンポーザブルの本体を使用する else 分岐を追加し、拡大以外の画面には以前の分岐をそのまま使用します。

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                NavigationDrawerContent(
                    selectedDestination = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        ) {
            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
            )
        }
    }
}
...
  1. ReplyHomeScreen コンポーザブルに Experimental アノテーションを追加します。PermanentNavigationDrawer API はまだ試験運用版であるため、このアノテーションが必要です。

ReplyHomeScreen.kt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {
...
  1. アプリをタブレット モードで実行します。下の画面が表示されます。

Reply アプリはタブレット モードで表示され、画面の左側にナビゲーション ドロワー、右側にメール一覧が表示されています。

ナビゲーション レールを実装する

ナビゲーション ドロワーの実装と同様に、ナビゲーション要素間の切り替えを行うには navigationType パラメータを使用する必要があります。

まず、中程度の画面用のナビゲーション レールを追加しましょう。

  1. はじめに、ReplyAppContent コンポーザブルにパラメータとして navigationType を追加します。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
...
  1. 両方の 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
            )
...

次に、分岐を追加します。この分岐により、一部のシナリオでナビゲーション レールを表示できるようになります。

  1. ReplyAppContent コンポーザブル本体の 1 行目で、ReplyNavigationRail コンポーザブルを AnimatedVisibility コンポーザブルで囲み、ReplyNavigationType の値が NavigationRail の場合に visibility パラメータを true に設定します。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: 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)
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList

            )
        }

}
...
  1. コンポーザブルを正しく配置するために、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.fillMaxSize()) {
        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)
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList

            )
        }
    }
}
...

最後に、一部のシナリオでボトム ナビゲーションが表示されることを確認しましょう。

  1. ReplyListOnlyContent コンポーザブルの後で、ReplyBottomNavigationBar コンポーザブルを AnimatedVisibility コンポーザブルで囲みます。
  2. ReplyNavigationType の値が BOTTOM_NAVIGATION の場合、visible パラメータを設定します。

ReplyHomeScreen.kt

...
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
                ReplyBottomNavigationBar(
                    currentTab = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
...
  1. アプリを開いた状態の折りたたみ式デバイスで実行します。下の画面が表示されます。

Reply アプリが折りたたみ式デバイスで表示され、画面の左側にナビゲーション レール、右側にメール一覧が表示されています。

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 を付けて、ソーシャル メディアで共有しましょう。

関連リンク