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

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

できるだけ多くの画面サイズをサポートするには、レスポンシブかつアダプティブになるようにアプリ レイアウトを設計します。レスポンシブ/アダプティブ レイアウトは、画面サイズに関係なく最適化されたユーザー エクスペリエンスを提供し、アプリはスマートフォン、タブレット、折りたたみ式デバイス、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 は、アダプティブ レイアウトを使用してさまざまな画面サイズをサポートするアプリです。

動画