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

1. 概要

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

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

前提条件

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

学習内容

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

作成するアプリの概要

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

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

56cfa13ef31d0b59.png

​​

必要なもの

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

2. アプリの概要

Reply アプリの概要

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

a1af0f9193718abf.png

4 つのカテゴリからなり、それぞれ別のタブ(受信トレイ、送信済み、下書き、迷惑メール)で表示されます

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

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

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

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

Reply アプリのファイル ディレクトリには、開かれた状態の 2 つのサブディレクトリが表示されています:

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

Reply アプリのデータ初期化

Reply アプリは ReplyViewModelinitializeUIState() メソッドを使用してデータが初期化されます。このメソッドは 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 クラスを使用することなく画面間のナビゲーションに対応しています。

8443a3ef1a239f6e.gif

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

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

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

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

ReplyDetailsScreen.kt

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

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

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

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

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

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

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

  1. Android Studio で、[Tools] > [Device Manager] を選択します。

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

  1. [Device Manager] で [+] アイコンをクリックして、仮想デバイスを作成します。

デバイス マネージャー ツールバーに、仮想デバイスの作成を含む 2 つのメニュー オプションが表示されています。

  1. [Phone] カテゴリと [Resizable (Experimental)] デバイスを選択します。
  2. [Next] をクリックします。

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

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

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

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

Android Virtural Device(AVD)の [Virtual Configration] 画面が表示されています。設定画面には、AVD 名を入力するためのテキスト フィールドがあります。名前フィールドの下には、デバイス定義(Resizable Experimental)、システム イメージ(Tiramisu)、画面の向き(デフォルトの Portrait が選択されている)などのデバイス オプションのリストがあります。ボタンの読み上げ

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

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

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

bfacf9c20a30b06b.png

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

bb0fa5e954f6ca4b.png

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

大画面用に設計する

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

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

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

f50e77a4ffd923a.png

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

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.kts ファイルに追加します。

build.gradle.kts

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

b4c912a45fa8b7f4.png

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

  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 layoutDirection = LocalLayoutDirection.current
            Surface (
               // ...
            ) {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp()
...  
  1. calculateWindowSizeClass の箇所に赤い下線が引かれ、赤い電球が表示されています。windowSize 変数の左側にある赤い電球のアイコンをクリックし、[Opt in for ‘ExperimentalMaterial3WindowSizeClassApi' on ‘onCreate'] を選択すると、onCreate() メソッドの上にアノテーションが作成されます。

f8029f61dfad0306.png

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 {
                Surface {
                    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 ReplyAppCompactPreview() {
    ReplyTheme {
        Surface {
            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 ナビゲーションを実装する

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

f39984211e4dd665.png

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

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

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

1c73d20ace67811c.png

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

6795fb31e6d4a564.png

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

拡大画面用のナビゲーション ドロワーを作成するために、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
...
    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 の値を使用できるようになりました。実際に使用するために、それをコンポーザブルのパラメータにします。

  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
...
@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 = {
                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
            )
        }
    }

...
  1. 前のコンポーザブルの本体を使用する 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
            )
        }
    }
}
...
  1. アプリをタブレット モードで実行します。以下に示す画面が表示されます。

2dbbc2f88d08f6a.png

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

ナビゲーション ドロワーの実装と同様に、ナビゲーション要素間の切り替えを行うには 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 の値が 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()
            )
        }
    }
}     
... 
  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) {
        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()
            )
        }
    }
}

... 

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

  1. ReplyListOnlyContent コンポーザブルの後で、ReplyBottomNavigationBar コンポーザブルを AnimatedVisibility コンポーザブルで囲みます。
  2. 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()
    )
}

... 
  1. アプリを開いた状態の折りたたみ式デバイスで実行します。以下に示す画面が表示されます。

bfacf9c20a30b06b.png

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

関連リンク