各種の画面サイズのサポート

さまざまな画面サイズのサポートにより、さまざまなデバイスと多くのユーザーがアプリにアクセスできます。

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

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

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

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

Compose を使用してアプリ全体をレイアウトする場合、アプリレベルと画面レベルのコンポーザブルは、レンダリング用としてアプリに与えられるすべてのスペースを占有します。設計のこのレベルでは、大画面を利用できるように画面の全体的なレイアウトを変更することが合理的です。

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

スマートフォン、折りたたみ式デバイス、タブレット、ノートパソコンなど、さまざまなデバイスのフォーム ファクタを示す図。
図 1. スマートフォン、折りたたみ式デバイス、タブレット、ノートパソコンのフォーム ファクタ

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

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

このアプローチを採用すると、アプリが上記のすべてのシナリオで適切に動作するため、アプリの柔軟性が向上します。利用可能な画面スペースにレイアウトを適応させることで、ChromeOS などのプラットフォームや、タブレットや折りたたみ式などのフォーム ファクタをサポートするための特別な処理の量も削減できます。

アプリが使用できる関連スペースを確認したら、ウィンドウ サイズクラスで説明されているように、未加工のサイズを意味のあるサイズクラスに変換すると便利です。これにより、サイズが標準サイズのバケットにグループ化されます。このバケットは、ほとんどの固有のケースに合わせてアプリを最適化するための柔軟性とシンプルさのバランスを取るように設計されたブレークポイントです。これらのサイズクラスはアプリの全体的なウィンドウを示すため、画面レイアウト全体に影響するレイアウトを決定する際はこれらのクラスを使用します。これらのサイズクラスを状態として渡すことも、追加のロジックを実行して派生状態を作成し、ネストされたコンポーザブルに渡すこともできます。

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@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 つの場所に制限します。同期を維持する必要のある多くの場所にアプリ全体で分散させる必要はありません。この 1 つの場所で状態が生成され、他のアプリの状態と同様に、その状態を他のコンポーザブルに明示的に渡すことができます。状態を明示的に渡すことで、個別のコンポーザブルが簡素化されます。個別のコンポーザブルは、他のデータとともにサイズクラスまたは指定された構成を受け取る通常のコンポーズ可能な関数にすぎないからです。

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

コンポーザブルは、さまざまな場所に配置できる場合、より再利用しやすくなります。コンポーザブルが常に特定のサイズで特定の場所に配置されると想定している場合、別の場所にある別の場所や、使用可能なスペースが異なる場所で再利用するのが難しくなります。つまり、再利用可能な個々のコンポーザブルが「グローバル」なサイズ情報に暗黙的に依存しないようにする必要があります。

次の例について考えてみましょう。1 つのペインまたは 2 つのペインを並べて表示するリスト詳細レイアウトを実装する、ネストされたコンポーザブルについて考えてみましょう。

2 つのペインを並べて表示しているアプリのスクリーンショット。
図 2. 一般的なリスト / 詳細レイアウトを示すアプリのスクリーンショット - 1 はリスト領域、2 は詳細領域です。

この決定をアプリの全体的なレイアウトに含めたいので、前述のように、画面レベルのコンポーザブルから決定を渡します。

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

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

2 種類のカードの例。
図 3. アイコンとタイトルのみが表示された幅の狭いカードと、アイコン、タイトル、簡単な説明が表示された幅の広いカード。

前述のように、デバイスの実際の画面のサイズは使用しないでください。これは複数の画面では正確ではありません。また、アプリが全画面表示でない場合も正確ではありません。

このコンポーザブルは画面レベルのコンポーザブルではないので、再利用可能性を最大化するために、現在のウィンドウ指標も直接使用するべきではありません。コンポーネントがパディング付きで配置されている場合(インセット用など)、またはナビゲーション レールやアプリバーなどのコンポーネントが存在する場合は、コンポーザブルが使用できるスペースの量が、アプリが使用できる全体的なスペースと大きく異なることがあります。

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

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

表示する内容を変更したい場合は、代わりにより強力な方法である 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 は、使用可能な幅と無関係に常にそれを必要とします。

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

また、この原則により、レイアウトの変更後も状態を保持できます。一部のサイズでは使用できない情報をホイスティングすることで、レイアウト サイズの変化に応じてユーザーの状態を保持できます。たとえば、サイズ変更によってレイアウトの説明の表示と非表示が切り替わったときに、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 のカスタム レイアウトについて詳しくは、以下の参考情報をご覧ください。

サンプルアプリ

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

動画