Jetpack Compose でアプリを適応させてアクセス可能にする

1. はじめに

この Codelab では、スマートフォン、タブレット、折りたたみ式デバイスに向けたアダプティブ アプリを作成する方法、Jetpack Compose を使用してユーザー補助を中核に維持する方法について学びます。また、マテリアル 3 コンポーネントとテーマの使用に関するベスト プラクティスもご紹介します。

本題に入る前に、適応性とアクセシビリティとは何かを理解することが重要です。

適応性

アプリの UI は、さまざまな画面サイズ、向き、フォーム ファクタに対応するためにレスポンシブでなければなりません。アダプティブ レイアウトは、使用可能な画面スペースに応じて変化します。今回の変更は、スペースを埋めるためのシンプルなレイアウト調整、各ナビゲーション スタイルの選択、追加のスペースを使用するためのレイアウトの完全な変更など、多岐にわたります。

ユーザー補助機能

Android アプリは、ユーザー補助機能を必要とするユーザーを含めて、誰もが利用できるようにする必要があります。アプリは、さまざまなシナリオに適応して、色のコントラストやネットワーク到達性などから最高のユーザー エクスペリエンスを提供する必要があります。

この Codelab では、Jetpack Compose を使用する際の適応性とユーザー補助の使用方法と方法について説明します。あらゆる種類の画面に対応する方法を示す REPLY というアプリケーションを作成します。適応性とユーザー補助が連携して、最適なユーザー エクスペリエンスを実現する仕組みを紹介します。

学習内容

  • Jetpack Compose を使用してすべての画面サイズをターゲットとするようにアプリを設計する方法。
  • 各種の折りたたみ式デバイスでアプリのターゲットを設定する方法
  • さまざまなナビを使って操作性と使いやすさを改善する方法。
  • 最適なユーザー補助エクスペリエンスを提供するために、マテリアル 3 のカラーパターンと動的テーマを設計する方法。
  • マテリアル 3 コンポーネントを使用してあらゆる画面サイズに最適な方法を実現する方法

必要なもの

  • Android Studio Bumblebee
  • Kotlin に関する知識。
  • Compose に関する基礎知識(@Composable アノテーションなど)
  • Compose レイアウトの基本的な知識( RowColumn など)との整合性を取りました。
  • 修飾子(例: Modifier.padding)が対象)。

Compose についてよく知らない場合は、この Codelab を完了する前に Jetpack Compose の基本の Codelab を受講することを検討してください。

作成するアプリの概要

  • マテリアル 3、動的なテーマ設定、適応可能なデザインにおすすめの方法を使用したインタラクティブな返信メール クライアント アプリ。

この Codelab では複数のデバイスをサポートしていることを示すショーケース

2. 設定する

サンプルアプリを次のいずれかの方法でダウンロードします。

または、次のコマンドを使用して、コマンドラインから GitHub リポジトリのクローンを作成します。

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ReplyAdaptabilityCodelab

どちらのモジュールも、Android Studio のツールバーで実行構成を変更することでいつでも実行できます。

b059413b0cf9113a.png

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

  1. [Welcome to Android Studio] ウィンドウで、c01826594f360d94.png [Open an Existing Project] を選択します。
  2. フォルダ [Download Location]/ReplyAdaptabilityCodelab を選択します(build.gradle を含む ReplyAdaptabilityCodlab ディレクトリを選択してください)。
  3. Android Studio にプロジェクトがインポートされたら、start モジュールと finished モジュールを実行できるかどうかテストします。

開始コードを調べる

開始コードには、次の 4 つのパッケージが含まれています。

  • MainActivity - ReplyApp を起動したエントリ ポイント アクティビティ。このファイル内で変更を加えます。
  • ui - テーマ、コンポーネント、Compose UI が開始される ReplyApp が含まれます。このパッケージで変更を加えます。
  • util - プロジェクトのヘルパーコードが格納されます。このパッケージを編集する必要はありません。

この Codelab では、reply パッケージのファイルに焦点を当てます。start モジュールに、これから学習するファイルがあります。

ファイルを編集する ui パッケージ

  • MainActivity.kt - Android アクティビティが ReplyApp を開始して、折りたたみ状態、サイズ、レイアウト情報などの必要な情報を渡す開始点となる。
  • ReplyApp.kt - アプリのメイン UI 構造は ReplyApp.kt ファイル内にあるため、これを使用します。
  • ReplyAppContent.kt - アプリのコンテンツとリストの詳細を Compose で実装します。

まず、MainActivity.kt にフォーカスしましょう。start モジュールでは、アクティビティですでにコードが動作しているはずです。

MainActicity.kt

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

   setContent {
       ReplyTheme {
           val uiState = viewModel.uiState.collectAsState().value
           ReplyApp(uiState)
       }
   }
}

あらゆるサイズのデバイスでこのアプリを実行すると、UI 要素を変更することなく、同じ画面ストレッチが最大領域いっぱいに表示されます。 何も変更せずに ReplyApp の初期セットアップ。 さん

アクセシビリティを中心に据えながら、画面スペースを活用してユーザー エクスペリエンスを向上させるための改善に努めましょう。

3. アプリを適応可能にする

このセクションでは、アプリを適応させるとはどういう意味なのか、そしてこれらを簡単に実行できるようにマテリアル 3 が提供しているコンポーネントについて説明します。

また、スマートフォン、タブレット、大型タブレット、折りたたみ式デバイスなど、ターゲットとする画面や状態の種類についても説明します。

ウィンドウ サイズの処理

Reply アプリにアクセスする前に、Google のユーザーがアプリを使用するために市場でどのようなサイズやデバイスが存在するかを見てみましょう。

スマートフォンは 10 cm から 10 cm まであります。その後、タブレットを小型タブレットからノートパソコンに近いサイズのタブレットまでリリースしました。

まずは、WIndowSizeClass に基づいて、これらのサイズを 3 つのカテゴリに分類します。各カテゴリは、レイアウトのシンプルさと、独自のケースでアプリを最適化する柔軟性のバランスが取れるよう、特別に選択されました。ウィンドウ サイズクラスは、アプリが使用できる画面スペースによって常に決定されます。画面スペースは、マルチタスクやその他のセグメンテーションのための物理画面全体になるとは限りません。

WindowSizeClass に基づくデバイスサイズ分布

WindowStateUtils**.kt**

enum class WindowSize { COMPACT, MEDIUM, EXPANDED }

fun getWindowSizeClass(windowDpSize: DpSize): WindowSize = when {
   windowDpSize.width < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative")
   windowDpSize.width < 600.dp -> WindowSize.COMPACT
   windowDpSize.width < 840.dp -> WindowSize.MEDIUM
   else -> WindowSize.EXPANDED
}

WindowStateUtils.kt には rememberWindowSizeClass(), が用意されています。これは、Compose の記憶された状態を取得するため、サイズの構成が変更されるたびに新しいサイズに基づいて UI ツリーがレンダリングされます。

WindowStateUtils.kt

fun Activity.rememberWindowSizeClass(): WindowSize {
   // Get the size (in pixels) of the window
   val windowSize = rememberWindowSize()

   // Convert the window size to [Dp]
   val windowDpSize = with(LocalDensity.current) {
       windowSize.toDpSize()
   }

   // Calculate the window size class
   return getWindowSizeClass(windowDpSize)
}

アダプティブ バナーのサイズ対応を開始するには、Compose UI の先頭に rememberWindowSizeClass() を追加して、それを ReplyApp に渡すだけです。MainActivity.kt を以下のように変更できるようになりました。

MainActivity.kt

setContent {
   ReplyTheme(dynamicColor = false, darkTheme = false) {
       val windowSize = rememberWindowSizeClass()
       ReplyApp(windowSize, uiState)
   }
}

これらの変更により、ReplyApp にスペースを正しく使用するための最新のウィンドウ サイズに関する情報があることがわかります。

4.折りたたみ状態を処理する

また、アプリが画面サイズだけでなく折りたたみ状態の変化にも対応するようにします。折りたたんだ状態は多数ありますが、まずはこの状況に対処することから始めます。これらは util クラスですでに定義されています。

WindowStateUtils.kt

/**
* Information about the posture of the device
*/
sealed interface DevicePosture {
   object NormalPosture : DevicePosture

   data class TableTopPosture(
       val hingePosition: Rect
   ) : DevicePosture

   data class BookPosture(
       val hingePosition: Rect
   ) : DevicePosture
}

折りたたまれた位置から広げられた位置に移動したときに UI が反応するようにします。また、BookPosture と TableTopPosture もヒンジ位置で考慮する必要があります。これは、ヒンジでテキストやその他の有用な情報をレンダリングしたくないためです。

アクティビティのライフサイクルの折りたたみ状態を確認してみましょう。setContent() を呼び出す前に、アクティビティの onCreate() メソッドにこのコードを追加します。

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

    /* Flow of [DevicePosture] that emits every time there is a change in the windowLayoutInfo
    */
   val devicePostureFlow =  WindowInfoTracker.getOrCreate(this).windowLayoutInfo(this)
       .flowWithLifecycle(this.lifecycle)
       .map { layoutInfo ->
           val foldingFeature =
               layoutInfo.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
           when {
               isTableTopPosture(foldingFeature) ->
                   DevicePosture.TableTopPosture(foldingFeature.bounds)
               isBookPosture(foldingFeature) ->
                   DevicePosture.BookPosture(foldingFeature.bounds)
               isSeparating(foldingFeature) ->
                   DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)
               else -> DevicePosture.NormalPosture
           }
       }
       .stateIn(
           scope = lifecycleScope,
           started = SharingStarted.Eagerly,
           initialValue = DevicePosture.NormalPosture
       )

これで、デバイスの形状フローを Compose の状態としてモニタリングするだけで、UI が折りたたみ状態の変化に対応できるようになります。setContent() に次の変更を追加します。

MainActivity.kt

setContent {
   ReplyTheme(dynamicColor = false, darkTheme = false) {
       val devicePosture = devicePostureFlow.collectAsState().value
       ReplyApp(windowSize, devicePosture, uiState)
   }
}

これで、Compose UI はデバイスのサイズと折りたたみ状態の変化の両方に対応する準備が整いました。ここから、さまざまな状態に合わせて UI を設計できます。折りたたみ状態が変更されるたびに、UI をこのように処理する必要があります。

折りたたみ式 UI 適応

5. ダイナミック ナビゲーション

前のセクションでは、UI のサイズ、構成、折りたたみ状態の変化に反応するようにしました。ここでは、さまざまな状態に遷移するユーザー インタラクションをデバイスに適応させる方法を学習しました。

まずはナビゲーションです。ユーザーが最初に操作するからです。デバイスによって保持されるデバイスの種類が異なるので注意してください。マテリアル ナビゲーションの component.navi を見てみましょう。

ボトム ナビゲーション

ボトム ナビゲーションは、親指がボトム ナビゲーションのすべてのタッチポイントに容易にアクセスできるデバイスを自然に保持するので、コンパクトなサイズに最適です。コンパクトなデバイス サイズの場合や、コンパクトな折りたたみ状態の折りたたみ式デバイスで使用することができます。

中程度のサイズのデバイス、または横向きのほとんどのスマートフォンには、ナビゲーション レールが最適です。ナビゲーション レールは自然に画面の左上にくるので、簡単にナビゲーションできます。ナビゲーション ドロワーとナビゲーション レールを使用して、詳細情報を表示することもできます。

ナビゲーション ドロワーは、ナビゲーション タブの詳しい情報を簡単に表示するツールです。タブレットやサイズの大きいデバイスを使用している場合は簡単にアクセスできます。ナビゲーション ドロワー(ボトム ナビゲーション)とボトム ナビゲーションの両方を使用できます。また、固定ナビゲーション ナビゲーションでは、固定幅の広いデバイスでナビゲーションを固定できます。

それでは、デバイスの状態とサイズの変化に応じて、別のタイプのナビゲーションを切り替えながら、ユーザー操作とユーザー補助機能を中心にしていきましょう。

アプリにダイナミック ナビゲーションを追加しましょう。 ReplyApp.kt を開き、ReplyApp コンポーザブルに追加します。

ReplyApp.kt

/**
* This will help us select type of navigation depending on window size and
* fold state of the device.
*/
val navigationType: ReplyNavigationType

when (windowSize) {
   WindowSize.COMPACT -> {
       navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
   }
   WindowSize.MEDIUM -> {
       navigationType = ReplyNavigationType.NAVIGATION_RAIL
   }
   WindowSize.EXPANDED -> {
       navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) {
           ReplyNavigationType.NAVIGATION_RAIL
       } else {
           ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
       }
   }
}

ナビゲーション ドロワーがコンテナ コンテナのReplyAppContent,固定型またはモーダル ナビゲーション ドロワーでラップするnavigationType

ReplyApp.kt

if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
   PermanentNavigationDrawer(drawerContent = {                    NavigationDrawerContent(selectedDestination) }) {
       ReplyAppContent(navigationType, contentType, replyHomeUIState)
   }
} else {
   ModalNavigationDrawer(
       drawerContent = {
           NavigationDrawerContent(
               selectedDestination,
               onDrawerClicked = {
                   scope.launch {
                       drawerState.close()
                   }
               }
           )
       },
       drawerState = drawerState
   ) {
       ReplyAppContent(navigationType, contentType, replyHomeUIState,
           onDrawerClicked = {
               scope.launch {
                   drawerState.open()
               }
           }
       )
   }
}

これで、動的な NavigationType が作成されました。これにより、構成が変更されるたびにナビゲーションを変更できます。ナビゲーションを動的にするために、navigationTypeReplyAppContent() に追加しましょう。

ReplyApp.kt

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
           ReplyNavigationRail(
               onDrawerClicked = onDrawerClicked
           )
       }
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           // Reply List content

           AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
               ReplyBottomNavigationBar()
           }
       }
   }
}

アプリをもう一度実行して、ダイナミック ナビゲーションを試します。

アプリを再度実行すると、画面構成が変更されたり、折りたたみ式デバイスを広げたりすると、そのサイズに適したタイプにナビゲーションが変化することがわかります。

さまざまなサイズのデバイスに対する適応性の変化を示しています。

これで、各種の画面サイズと状態をサポートするナビゲーションの種類について理解できました。

次のセクションでは、同じ項目を画面全体に延ばす代わりに、残りの画面領域を活用する方法を見ていきます。

6. 画面スペースの使用

小さなタブレット、展開したデバイス、大きなタブレットのいずれでも、残りのスペースを埋めるように画面が引き伸ばされています。この画面スペースを活用して、ユーザーにより多くの情報を表示できるようにする必要があります。

navigationType と同様に、contentType を作成します。これにより、リスト コンテンツのみを決定するか、リスト コンテンツと詳細コンテンツの両方を動的に表示するかを決定できます。画面の状態の変化

ReplyApp.kt

val contentType: ReplyContentType
when (windowSize) {
   WindowSize.COMPACT -> {
       contentType = ReplyContentType.LIST_ONLY
   }
   WindowSize.MEDIUM -> {
       contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) {
           ReplyContentType.LIST_AND_DETAIL
       } else {
           ReplyContentType.LIST_ONLY
       }
   }
   WindowSize.EXPANDED -> {
       contentType = ReplyContentType.LIST_AND_DETAIL
   }
}

このコンテンツ タイプを ReplyAppContent, に渡せるようになり、構成が変更されるたびに適切なレイアウトに合わせて調整されます。また、折りたたみ形状とヒンジ位置を考慮して、リスト位置とディテール レイアウトの位置を決定して、ヒンジ位置にコンテンツが表示されないようにすることもできます。

ReplyApp.kt

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           if (contentType == ReplyContentType.LIST_AND_DETAIL) {
               ReplyListAndDetailContent(
                   replyHomeUIState = replyHomeUIState,
                   modifier = Modifier.weight(1f),
               )
           } else {
               ReplyListOnlyContent(replyHomeUIState = replyHomeUIState, modifier = Modifier.weight(1f))
           }
       }
   }
}

すべての変更を追加した後の ReplyApp の最終ビュー

アプリをもう一度実行して、完全に適応したアプリをお試しください。

アプリをもう一度実行して、画面構成が変更されたときや、折りたたみ式デバイスを広げたときは、デバイスの状態の変化に応じてナビゲーションと画面の内容を動的に変化させることに注意してください。Jetpack Compose では、このような宣言型パターンで非常に簡単に記述できます。

これで、デバイスの種類やサイズを問わず、アプリを順応させることができました。折りたたみ式デバイス、タブレット、その他のモバイル デバイスでアプリを実行してみましょう。

以降のセクションでは、こうした適応性の変更を通じて、ユーザー補助のための構造がどのように調整されるかについて見ていきます。

7. ユーザー補助機能を強化する

ネットワーク到達性

ネットワーク到達性とは、極端な手の位置を必要とせずに、デバイスを操作する、またはアプリを使用する際の手の配置を変えることができる機能です。

Reply アプリのダイナミック ナビゲーション セクションで、画面の状態に応じて使用するナビゲーション モードを複数追加しました。ボトム ナビゲーション バー、ナビゲーション レール、ナビゲーション ドロワーなどのマテリアル コンポーネントは、さまざまなフォーム ファクタのデバイスを保持する方法に基づいて、ナビゲーションに簡単にアクセスできます。

タブレット サイズのナビゲーション レールとナビゲーション ドロワーを示すネットワーク到達性デモ。

また、リストと詳細のフォーム ファクタを追加したことで、ユーザーがスレッド間を簡単に移動したり、配置を変えずに左右の手を使って大きなデバイスをスクロールしたりできるようになりました。

色のコントラスト

Reply アプリは Android 12 以降で動的なテーマ設定をサポートします。Android 12 では、壁紙の選択やその他のカスタマイズ設定でカラーパターンが生成されます。ダイナミック カラーを使用するプロダクトは、エンドユーザーが体験できるアルゴリズムの組み合わせがこうした基準を満たすように設計されているため、ユーザー補助の要件を満たしています。

詳しくは、ダイナミック カラーをご覧ください。

ライトモードとダークモードのマテリアル 3 カラーパターン

このアプリでも、マテリアル 3 のカラーパターンを使用しています。カラー コントラストのユーザー補助基準を満たすためにすでに設計されています。色調パレットのシステムは、デフォルトでカラーパターンにアクセスしやすくするための中心的な機能です。

マテリアル 3 のテーマが設定された色のコントラストのデモ。

16 進数値または色相ではなく、色調に基づく色の組み合わせは、任意の色出力にアクセスできるようにする主要なシステムの 1 つです。プライマリ カラー、セカンダリ カラー、ターマル カラーの適切なセットを選択し、マテリアル テーマビルダーを使用してライト カラーとダーク バリエーションの両方にマテリアル 3 カラーパターンを作成することで、いつでも完全なマテリアル 3 カラーパターンを作成できます。生成されたバリエーションは、色のコントラストについてユーザー補助基準にすでに準拠しています。

動的テーマ設定を使用できない Android 11 以前では、マテリアル テーマビルダーで生成された固定のマテリアル 3 カラーパターンにフォールバックします。

マテリアル テーマビルダーを使用して、新しいカラーテーマを試すことができます。

生成された色を ui/theme/Color.kt ファイルに直接配置して、実際の動作を確認することができます。

8. 完了

これで、この Codelab は終了です。Jetpack Compose を使用して、適応性が高くアクセスしやすいアプリを設計する方法を学習しました。

デバイスのサイズと折りたたみ状態を確認し、それに応じてアプリの UI、ナビゲーション、その他の機能を更新する方法を学習しました。また、マテリアル 3 のカラーパターンとタイポグラフィを活用して、ユーザー エクスペリエンスとユーザー補助機能を改善する方法も学びました。

次のステップ

Compose パスウェイに関する他の Codelab をご覧ください。

サンプルアプリ

  • サンプルアプリは、Codelab で紹介されているベスト プラクティスを取り入れたアプリのコレクションです。

リファレンス ドキュメント