さまざまなディスプレイ サイズをサポートする

さまざまなディスプレイ サイズをサポートすると、数多くのユーザーがさまざまなデバイスでアプリを利用できるようになります。

さまざまなデバイスの画面やマルチウィンドウ モードのさまざまなアプリ ウィンドウなど、できるだけ多くのディスプレイ サイズをサポートするには、アプリのレイアウトがレスポンシブ / アダプティブになるように設計する必要があります。レスポンシブ/アダプティブ レイアウトは、ディスプレイ サイズにかかわらず、最適なユーザー エクスペリエンスを提供します。これにより、アプリはスマートフォン、タブレット、折りたたみ式デバイス、ChromeOS デバイス、縦向きと横向き、サイズ変更が可能なディスプレイ構成(分割画面モードやデスクトップ ウィンドウなど)に対応できます。

レスポンシブ/アダプティブ レイアウトは、使用可能な表示スペースに応じて変化します。変更は、スペースを埋める小さなレイアウト調整(レスポンシブ デザイン)から、アプリがさまざまなディスプレイ サイズに最適に対応できるようにレイアウトを完全に置き換えること(アダプティブ デザイン)まで多岐にわたります。

宣言型 UI ツールキットである Jetpack Compose は、さまざまなディスプレイ サイズでコンテンツを異なる方法でレンダリングするように動的に変化するレイアウトを設計および実装するのに最適です。

コンテンツ レベルのコンポーザブルの大規模なレイアウト変更を明示的にする

アプリレベルとコンテンツ レベルのコンポーザブルは、アプリで使用可能なディスプレイ領域全体を占有します。これらのタイプのコンポーザブルでは、大画面でのアプリの全体的なレイアウトを変更することが理にかなっている場合があります。

レイアウトに関する決定を行う際は、物理的なハードウェア値を使用しないでください。固定された具体的な値(デバイスはタブレットかどうか、物理画面に特定のアスペクト比があるかなど)に基づいて決定したくなることもあるでしょうが、こうした問いに対する答えは、UI が使用できるスペースを決定するうえで必ずしも有用ではありません。

図 1. スマートフォン、折りたたみ式デバイス、タブレット、ノートパソコンのフォーム ファクタ

タブレットでは、アプリがマルチウィンドウ モードで実行される(つまり、アプリが別のアプリと画面を分割している)場合があります。デスクトップ ウィンドウ モードまたは ChromeOS では、サイズ変更可能なウィンドウ内でアプリが動作する場合があります。折りたたみ式デバイスのように、複数の物理画面が存在する場合もあります。上記のいずれの場合も、物理画面サイズはコンテンツの表示方法の決定とは無関係です。

そうした決定は、Jetpack の WindowManager ライブラリによって提供される現在のウィンドウ指標で記述される、アプリに割り当てられる画面の実際の領域に基づいて行う必要があります。Compose アプリで WindowManager を使用する方法の例については、JetNews サンプルをご覧ください。

また、使用可能なディスプレイ スペースに対応できるようにレイアウトをアダプティブにすることで、ChromeOS などのプラットフォームと、タブレットや折りたたみ式デバイスなどのフォーム ファクタをサポートするために必要な特別な処理の量も削減されます。

アプリで使用できるスペースの指標を特定したら、ウィンドウ サイズクラスを使用するで説明しているように、未加工のサイズをウィンドウ サイズクラスに変換します。ウィンドウ サイズクラスは、アプリのロジックのシンプルさと、ほとんどのディスプレイ サイズに合わせてアプリを最適化できる柔軟性のバランスを取るように設計されたブレークポイントです。

ウィンドウ サイズクラスはアプリのウィンドウ全体を参照するので、アプリのレイアウト全体に影響するレイアウトを決定する際に使用してください。ウィンドウ サイズクラスは状態として渡すことができます。また、追加のロジックを実行して、ネストされたコンポーザブルに渡す派生状態を作成することもできます。

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Decide whether to show the top app bar based on window size class.
    val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND)

    // MyScreen logic is based on the showTopAppBar boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

階層化されたアプローチでは、表示サイズのロジックを単一の場所にとどめておくことができます。アプリのさまざまな場所にロジックを分散させてそれらの同期を維持する必要はありません。単一の場所で状態が生成され、他のアプリの状態と同様に、その状態を他のコンポーザブルに明示的に渡すことができます。状態を明示的に渡すことで、個別のコンポーザブルが簡素化されます。コンポーザブルは、他のデータとともにウィンドウ サイズクラスまたは指定された構成を受け取るからです。

柔軟なネストされたコンポーザブルは再利用が可能

コンポーザブルは、さまざまな場所に配置できる場合、より再利用しやすくなります。コンポーザブルを特定のサイズで特定の場所に配置する必要がある場合、そのコンポーザブルは他のコンテキストで再利用できない可能性があります。これは、再利用可能な個別のコンポーザブルがグローバルなディスプレイ サイズ情報に暗黙的に依存しないようにする必要があることも意味します。

リスト詳細レイアウトを実装するネストされたコンポーザブルが、1 つのペインまたは横に並んだ 2 つのペインを表示するとします。

横に並んだ 2 つのペインを表示するアプリ。
図 2. 一般的なリストと詳細レイアウトを表示するアプリ - 1 はリスト領域、2 は詳細領域です。

リスト / 詳細の決定はアプリの全体的なレイアウトに含める必要があるため、コンテンツ レベルのコンポーザブルから決定を渡します。

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

代わりに、使用可能な表示スペースに応じて、コンポーザブルが自身のレイアウトを個別に変更するようにしたい場合はどうでしょうか。たとえば、スペースに余裕があるときに追加の詳細を表示するカードを使用したい場合です。使用可能なディスプレイ サイズに応じてなんらかのロジックを実行する必要がありますが、具体的にはどのサイズを使用すればよいでしょうか。

図 3. アイコンとタイトルのみを表示する幅の狭いカードと、アイコン、タイトル、簡単な説明を表示する幅の広いカード。

デバイスの実際の画面のサイズを使用するべきではありません。その値は、さまざまなタイプの画面がある場合も、アプリが全画面表示でない場合も不適切です。

このコンポーザブルはコンテンツ レベルのコンポーザブルではないので、現在のウィンドウ指標を直接使用しないでください。

コンポーネントがパディング付きで配置されている場合(インセット用など)、またはアプリにナビゲーション レールやアプリバーなどのコンポーネントが含まれている場合は、コンポーザブルが使用できるディスプレイ スペースの量が、アプリが使用できる全体的なスペースと大きく異なることがあります。

コンポーザブルが自身のレンダリング用として実際に与えられる幅を使用します。この幅を取得するには、次の 2 つの方法があります。

  • コンテンツを表示する場所または方法を変更したい場合は、修飾子のコレクションまたはカスタム レイアウトを使用して、レイアウトをレスポンシブにすることができます。これは、すべての使用可能なスペースを子で埋めるか、十分な空間がある場合に複数の列に複数の子をレイアウトする単純な方法で実現できます。

  • 表示する内容を変更したい場合は、代わりにより強力な方法である BoxWithConstraints を使用できます。BoxWithConstraints は、使用可能なディスプレイ スペースに応じて異なるコンポーザブルを呼び出すために使用できる測定制約を提供します。ただし、この方法はある程度のコストがかかります。BoxWithConstraints はこれらの制約が認識されるレイアウト フェーズまでコンポジションを遅延するので、レイアウト時に、より多くの処理が必要になるからです。

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

さまざまなディスプレイ サイズですべてのデータを使用できるようにする

追加の表示スペースを活用するコンポーザブルを実装する際は、効率化を図るために、現在の表示サイズの副作用としてデータを読み込みたくなるかもしれません。

しかし、これは単方向データフローの原則に反します。単方向データフローでは、データをホイスティングしてコンポーザブルに提供することにより、適切なレンダリングが可能になります。コンテンツの一部が必ずしも使用されない場合でも、任意のディスプレイ サイズで表示するのに十分なコンテンツをコンポーザブルが常に保持できるように、コンポーザブルに十分なデータを提供する必要があります。

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

Card の例では、常に descriptionCard に渡していることに注意してください。description は、それを表示できる幅がある場合にのみ使用されますが、Card は、使用可能な幅と無関係に常に description を必要とします。

常に十分なコンテンツを渡すことで、アダプティブ レイアウトはよりステートフルでなくなり、よりシンプルになります。また、ディスプレイ サイズの切り替え(ウィンドウ サイズの変更、向きの変更、デバイスの折りたたみと展開によって発生)の際に副作用がトリガーされることを回避できます。

また、この原則により、レイアウトの変更後も状態を保持できます。すべてのディスプレイ サイズで使用されるわけではない情報をホイスティングすることで、レイアウト サイズの変更時もアプリの状態を保持できます。

たとえば、showMore ブール値フラグをホイスティングすると、ディスプレイのサイズ変更に応じてレイアウトがコンテンツの表示と非表示を切り替える際に、アプリの状態が保持されます。

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

詳細

Compose のアダプティブ レイアウトについて詳しくは、以下のリソースをご覧ください。

サンプルアプリ

  • CanonicalLayouts は、大画面ディスプレイで最適なユーザー エクスペリエンスを提供する実証済みのデザイン パターンのリポジトリです。
  • JetNews は、使用可能なディスプレイ スペースを利用するために UI を適応させるアプリの設計方法を示しています。
  • Reply は、モバイル、タブレット、折りたたみ式デバイスをサポートするアダプティブ サンプルです。
  • Now in Android は、アダプティブ レイアウトを使用してさまざまなディスプレイ サイズをサポートするアプリです。

動画