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

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

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

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

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

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

アプリレベルとコンテンツレベルのコンポーザブルは、アプリで使用可能なディスプレイ領域全体を占有します。このようなタイプのコンポーザブルの場合は、大きなディスプレイでアプリの全体的なレイアウトを変更することをおすすめします。

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

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

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

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

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

アプリで使用可能なスペースの指標を決定したら、ウィンドウ サイズクラスを使用するで説明されているように、未加工のサイズをウィンドウサイズクラスに変換します。ウィンドウ サイズクラスは、アプリロジックのシンプルさと、ほとんどのディスプレイサイズに合わせてアプリを最適化できる柔軟性のバランスを保つように設計されたブレークポイントです。ウィンドウ サイズクラスはアプリのウィンドウ全体を参照するため、アプリのレイアウト全体に影響するレイアウトを決定する際に使用してください。ウィンドウのサイズクラスは状態として渡すことができます。また、追加のロジックを実行して、ネストされたコンポーザブルに渡す派生状態を作成することもできます。

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

    // MyScreen knows nothing about window sizes, and performs logic based on a 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 は、モバイル、タブレット、折りたたみ式デバイスをサポートする適応型のサンプルです。
  • Android の新機能は、アダプティブ レイアウトを使用してさまざまなディスプレイ サイズをサポートするアプリです。

動画