1. 始める前に
Android デバイスはあらゆる形状とサイズで提供されているため、アプリのレイアウトをさまざまな画面サイズに合わせ、1 つの Android パッケージ(APK)または Android App Bundle(AAB)でさまざまなユーザーとデバイスに対応できるようにする必要があります。そのためには、特定の画面サイズやアスペクト比を前提とした固定ディメンションではなく、レスポンシブかつアダプティブなレイアウトでアプリを定義する必要があります。アダプティブ レイアウトは、使用可能な画面スペースに応じて変化します。
この Codelab では、アダプティブ UI を作成し、スポーツのリストと各スポーツの詳細情報を表示するアプリを、大画面デバイス向けに適応させる基本的な方法を説明します。このスポーツアプリは、ホーム、お気に入り、設定の 3 つの画面で構成されています。ホーム画面にはスポーツのリストが表示され、リストからスポーツを選択すると、ニュースのプレースホルダが表示されます。お気に入り画面と設定画面にもプレースホルダ テキストが表示されます。画面を切り替えるには、下部のナビゲーション メニューで関連する項目を選択します。
アプリでは、大画面で次のようなレイアウトの問題が発生します。
- 縦向きで使用できない。
- 大画面で空白スペースが多く表示される。
- 大画面で下部のナビゲーション メニューが常に表示される。
次のようにアプリの適応性を改善します。
- 横向きと縦向きの両方をサポートする。
- 水平方向のスペースが十分にある場合は、スポーツのリストと各スポーツに関するニュースを並べて表示する。
- マテリアル デザイン ガイドラインに沿ってナビゲーション コンポーネントを表示する。
このアプリは、複数のフラグメントを持つ単一アクティビティのアプリです。操作するファイルは次のとおりです。
AndroidManifest.xmlファイル。スポーツアプリに関するメタデータを提供します。MainActivity.ktファイル。activity_main.xmlファイルで生成されたコード、overrideアノテーション、幅のウィンドウ クラスサイズを表すenumクラス、アプリ ウィンドウの幅のウィンドウ クラスサイズを取得するメソッド定義が含まれています。下部のナビゲーション メニューは、アクティビティが作成されると初期化されます。activity_main.xmlファイル。Mainアクティビティのデフォルト レイアウトを定義します。layout-sw600dp/activity_main.xmlファイル。Mainアクティビティの代替レイアウトを定義します。代替レイアウトは、アプリ ウィンドウの幅が600dp値以上の場合に有効となります。その内容は、最初のデフォルト レイアウトと同じです。SportsListFragment.ktファイル。スポーツリストの実装とカスタムの「戻る」ナビゲーションが含まれます。fragment_sports_list.xmlファイル。スポーツリストのレイアウトを定義します。navigation_menu.xmlファイル。下部のナビゲーション メニュー項目を定義します。
図 1. このスポーツアプリは、1 つの APK または AAB でさまざまなウィンドウ サイズをサポートします。
前提条件
- ビューベースの UI 開発に関する基本的な知識
- ラムダ関数を含む Kotlin 構文の使用経験
- Jetpack Compose の基本の Codelab を完了していること
学習内容
- 構成の変更をサポートする方法。
- コード変更の少ない代替レイアウトを追加する方法。
- ウィンドウ サイズによって異なる動作をするリストと詳細の UI を実装する方法。
作成するアプリの概要
以下をサポートする Android アプリ。
- デバイスの横向き
- タブレット、パソコン、モバイル デバイス
- 画面サイズに応じたリストと詳細の動作
必要なもの
- Android Studio Bumblebee | 2021.1.1 以上
- Android タブレットまたはエミュレータ
2. 設定する
この Codelab のコードをダウンロードして、プロジェクトを設定します。
- コマンドラインから、この GitHub リポジトリのコードのクローンを作成します。
$ git clone https://github.com/android/add-adaptive-layouts
- Android Studio で
AddingAdaptiveLayoutプロジェクトを開きます。このプロジェクトには複数の git ブランチがあります。
mainブランチには、このプロジェクトのスターター コードが含まれています。このブランチに変更を加えて Codelab を完了します。endブランチには、この Codelab の解答があります。
3. トップ ナビゲーション コンポーネントを Compose に移行する
このスポーツアプリでは、下部のナビゲーション メニューをトップ ナビゲーション コンポーネントとして使用します。このナビゲーション コンポーネントは、BottomNavigationView クラスで実装されます。このセクションでは、トップ ナビゲーション コンポーネントを Compose に移行します。
関連付けられているメニュー リソースの内容をシールクラスとして表す
navigation_menu.xmlファイルの内容を表すシールされたMenuItemクラスを作成し、iconIdパラメータ、labelIdパラメータ、destinationIdパラメータを渡します。- デスティネーションに対応するサブクラスとして、
Homeオブジェクト、Favoriteオブジェクト、Settingsオブジェクトを追加します。
MenuItem.kt
sealed class MenuItem(
// Resource ID of the icon for the menu item
@DrawableRes val iconId: Int,
// Resource ID of the label text for the menu item
@StringRes val labelId: Int,
// ID of a destination to navigate users
@IdRes val destinationId: Int
) {
object Home: MenuItem(
R.drawable.ic_baseline_home_24,
R.string.home,
R.id.SportsListFragment
)
object Favorites: MenuItem(
R.drawable.ic_baseline_favorite_24,
R.string.favorites,
R.id.FavoritesFragment
)
object Settings: MenuItem(
R.drawable.ic_baseline_settings_24,
R.string.settings,
R.id.SettingsFragment
)
}
ボトム ナビゲーション メニューをコンポーズ可能な関数として実装する
List<MenuItem>値に設定されたmenuItemsオブジェクト、Modifier = Modifier値に設定されたmodifierオブジェクト、(MenuItem) -> Unit = {}ラムダ関数に設定されたonMenuSelectedオブジェクトの 3 つのパラメータを受け取るコンポーザブル可能なBottomNavigationBar関数を定義します。- コンポーザブル可能な
BottomNavigationBar関数の本文で、modifierパラメータを受け取るNavigationBar()関数を呼び出します。 NavigationBar()関数に渡されたラムダ関数で、menuItemsパラメータでforEach()メソッドを呼び出してから、foreach()メソッド呼び出しに設定されたラムダ関数でNavigationBarItem()関数を呼び出します。NavigationBarItem()関数に、false値に設定されたselectedパラメータ、MenuItemパラメータを持つonMenuSelected関数を含むラムダ関数に設定されたonClickパラメータ、painter = painterResource(id = menuItem.iconId)パラメータとcontentDescription = nullパラメータを受け取るIcon関数を含むラムダ関数に設定されたiconパラメータ、(text = stringResource(id = menuItem.labelId)パラメータを受け取るText関数を含むラムダ関数に設定されたlabelパラメータを渡します。
Navigation.kt
@Composable
fun BottomNavigationBar(
menuItems: List<MenuItem>,
modifier: Modifier = Modifier,
onMenuSelected: (MenuItem) -> Unit = {}
) {
NavigationBar(modifier = modifier) {
menuItems.forEach { menuItem ->
NavigationBarItem(
selected = false,
onClick = { onMenuSelected(menuItem) },
icon = {
Icon(
painter = painterResource(id = menuItem.iconId),
contentDescription = null)
},
label = { Text(text = stringResource(id = menuItem.labelId))}
)
}
}
}
BottomNavigationView 要素をレイアウト リソース ファイル内の ComposeView 要素に置き換える
activity_main.xmlファイルで、BottomNavigationView要素をComposeView要素に置き換えます。これにより、Compose で、ビューベースの UI レイアウトにボトム ナビゲーション コンポーネントが埋め込まれます。
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/top_navigation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/top_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Compose で定義したボトム ナビゲーション メニューをビューベースの UI レイアウトと統合する
MainActivity.ktファイルで、MenuItemオブジェクトのリストに設定されたnavigationMenuItems変数を定義します。MenuItemsオブジェクトは、下部のナビゲーション メニューにリスト順に表示されます。BottomNavigationBar()関数を呼び出して、下部のナビゲーション メニューをComposeViewオブジェクトに埋め込みます。BottomNavigationBar関数に渡されたコールバック関数で、ユーザーが選択した項目に関連付けられたデスティネーションに移動します。
MainActivity.kt
val navigationMenuItems = listOf(
MenuItem.Home,
MenuItem.Favorites,
MenuItem.Settings
)
binding.navigation.setContent {
MaterialTheme {
BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
}
}
4. 横向きをサポートする
アプリが大画面デバイスをサポートしている場合は、横向きと縦向きをサポートすることが求められます。現時点では、アプリには MainActivity という 1 つのアクティビティのみがあります。
デバイス上のアクティビティの表示の向きは、portrait 値に設定された android:screenOrientation 属性を含む AndroidManifest.xml ファイルで設定されます。
アプリが横向きをサポートするようにします。
android:screenOrientation属性をfullUser値に設定します。この設定により、ユーザーは画面の向きをロックできます。画面の向きは、デバイスの方向センサーによって 4 つの向きのいずれかに決定されます。
AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="fullUser">

図 2. AndroidManifest.xml ファイルを更新すると、アプリは横向きで実行されます。
5. ウィンドウ サイズクラス
ウィンドウ サイズクラスは、アプリで使用できる未加工のウィンドウ サイズを使って、事前定義されたサイズクラス(コンパクト、中程度、拡大)にウィンドウ サイズを分類するためのブレークポイント値です。このサイズクラスは、アダプティブ レイアウトを設計、開発、テストする場合に使用します。
利用可能な幅と高さは個別に分割されるため、アプリには常に、幅のウィンドウ サイズクラスと高さのウィンドウ サイズクラスという、2 つのウィンドウ サイズクラスが関連付けられています。

図 3. 幅のウィンドウ サイズクラスと、それに関連するブレークポイント

図 4. 高さのウィンドウ サイズクラスと、それに関連するブレークポイント
ウィンドウ サイズクラスは、アプリの現在のウィンドウ サイズを表します。つまり、物理デバイスのサイズでウィンドウ サイズクラスを判断することはできません。アプリが同じデバイスで実行されている場合でも、関連するウィンドウ サイズクラスは、アプリを分割画面モードで実行する場合など、構成によって異なります。その結果、次の 2 点が重要となります。
- 物理デバイスで、特定のウィンドウ サイズクラスが保証されるわけではありません。
- ウィンドウ サイズクラスは、アプリの全期間を通じて変わる可能性があります。
アプリにアダプティブ レイアウトを追加したら、すべてのウィンドウ サイズ(特にコンパクト、中程度、拡大のウィンドウ サイズクラス)でアプリをテストします。ウィンドウ サイズクラスごとにテストが必要ですが、多くの場合、十分ではありません。UI を正しくスケーリングできるように、アプリをさまざまなウィンドウ サイズでテストすることが重要です。詳しくは、大画面のレイアウトと大画面のアプリの品質をご覧ください。
6. 大画面でリストペインと詳細ペインを並べて配置する
リストと詳細の UI では、現在の幅のウィンドウ サイズクラスによって異なる動作が必要になる場合があります。中程度の幅または拡大幅のウィンドウ サイズクラスがアプリに関連付けられている場合、アプリには、リストペインと詳細ペインを並べて表示するのに十分なスペースがあり、ユーザーは画面遷移なしで項目のリストと選択した項目の詳細情報を確認できます。しかし、小さな画面では表示が過密になる場合があるため、1 つずつペインを表示することをおすすめします(最初はリストペインを表示)。ユーザーがリスト内の項目をタップすると、詳細ペインに選択した項目の詳細が表示されます。SlidingPaneLayout クラスは、これら 2 つのユーザー エクスペリエンスのどちらが現在のウィンドウ サイズに適しているかを判断するロジックを管理します。
リストペインのレイアウトを設定する
SlidingPaneLayout クラスはビューベースの UI コンポーネントです。リストペインのレイアウト リソース ファイル(この Codelab では fragment_sports_list.xml ファイル)を変更します。
SlidingPaneLayout クラスは 2 つの子要素を受け取ります。各子要素の幅と重みの属性は、ウィンドウが両方のビューを並べて表示するのに十分な大きさであるかどうかを判断するための SlidingPaneLayout クラスの重要な要素です。ウィンドウが十分な大きさでない場合は、全画面表示のリストが全画面表示の詳細 UI に置き換えられます。ウィンドウ サイズがペインを並べて表示するための最小要件よりも大きい場合、2 つのペインのサイズを比率で指定するために weight 値が参照されます。
SlidingPaneLayout クラスが fragment_sports_list.xml ファイルに適用されました。リストペインの幅は 1280dp になるように構成されています。そのため、リストペインと詳細が並んで表示されません。
画面の幅が 580dp より大きい場合、ペインを並べて表示します。
RecyclerViewを280dpの幅に設定します。
fragment_sports_list.xml
<androidx.slidingpanelayout.widget.SlidingPaneLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SportsListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<androidx.fragment.app.FragmentContainerView
android:layout_height="match_parent"
android:layout_width="300dp"
android:layout_weight="1"
android:id="@+id/detail_container"
android:name="com.example.android.sports.NewsDetailsFragment"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
図 5. レイアウト リソース ファイルを更新すると、リストペインと詳細ペインが並んで表示されます。
詳細ペインを入れ替える
中程度の幅または拡大幅のウィンドウ サイズクラスをアプリに関連付けると、リストペインと詳細ペインが並んで表示されるようになりました。ただし、ユーザーがリストペインから項目を選択すると、画面は詳細ペインに完全に遷移します。

図 6. リストからスポーツを選択すると、画面が詳細ペインに遷移します。
この問題は、ユーザーがリストペインから項目を選択したときにトリガーされるナビゲーションが原因で発生します。関連するコードは SportsListFragment.kt ファイルにあります。
SportsListFragment.kt
val adapter = SportsAdapter {
sportsViewModel.updateCurrentSport(it)
// Navigate to the details pane.
val action =
SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
}
リストペインと詳細ペインを並べて表示するための十分なスペースがない場合にのみ、画面が詳細ペインに完全に遷移するようにします。
adapter関数変数に、SlidingPaneLayoutクラスのisSlidable属性が true であり、SlidingPaneLayoutクラスのisOpen属性が false であるかどうかをチェックするifステートメントを追加します。
SportsListFragment.kt
val adapter = SportsAdapter {
sportsViewModel.updateCurrentSport(it)
if(slidingPaneLayout.isSlidable && !slidingPaneLayout.isOpen){
// Navigate to the details pane.
val action =
SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
}
}
7. 幅のウィンドウ サイズクラスに基づいて適切なナビゲーション コンポーネントを選択する
マテリアル デザインでは、アプリは適応的にコンポーネントを選択する必要があります。このセクションでは、現在の幅のウィンドウ サイズクラスに基づいて、上部のナビゲーション バーのナビゲーション コンポーネントを選択します。次の表に、各ウィンドウ サイズクラスで想定されるナビゲーション コンポーネントを示します。
幅のウィンドウ サイズクラス | ナビゲーション コンポーネント |
コンパクト | ボトム ナビゲーション |
中 | ナビゲーション レール |
拡張 | 固定的なナビゲーション ドロワー |
ナビゲーション レールを実装する
List<MenuItem>値に設定されたmenuItemsオブジェクト、Modifier値に設定されたmodifierオブジェクト、onMenuSelectedラムダ関数の 3 つのパラメータを受け取るコンポーズ可能なNavRail()関数を作成します。- 関数の本文で、
modifierオブジェクトをパラメータとして受け取るNavigationRail()関数を呼び出します。 BottomNavigationBar()関数内のNavigationBarItem関数の場合と同様に、menuItemsオブジェクト内の各MenuItemオブジェクトに対してNavigationRailItem()関数を呼び出します。
固定的なナビゲーション ドロワーを実装する
List<MenuItem>値に設定されたmenuItemsオブジェクト、Modifier値に設定されたmodifierオブジェクト、onMenuSelectedラムダ関数の 3 つのパラメータを受け取るコンポーズ可能なNavigationDrawer()関数を作成します。- 関数の本文で、
modifierオブジェクトをパラメータとして受け取るColumn()関数を呼び出します。 menuItemsオブジェクト内の各MenuItemオブジェクトに対してRow()関数を呼び出します。BottomNavigationBar()関数内のNavigationBarItem関数の場合と同様に、Row()関数の本文で、iconラベルとtextラベルを追加します。
幅のウィンドウ サイズクラスに基づいて適切なナビゲーション コンポーネントを選択する
- 現在の幅のウィンドウ サイズクラスを取得するには、
ComposeViewオブジェクトのsetContent()メソッドに渡されたコンポーズ可能な関数内でrememberWidthSizeClass()関数を呼び出します。 - 条件付きブランチを作成して、取得した幅のウィンドウ サイズクラスに基づいてナビゲーション コンポーネントを選択し、選択したコンポーネントを呼び出します。
ModifierオブジェクトをNavigationDrawer関数に渡して、その幅を256dp値として指定します。
ActivityMain.kt
binding.navigation.setContent {
MaterialTheme {
when(rememberWidthSizeClass()){
WidthSizeClass.COMPACT ->
BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
WidthSizeClass.MEDIUM ->
NavRail(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
WidthSizeClass.EXPANDED ->
NavigationDrawer(
menuItems = navigationMenuItems,
modifier = Modifier.width(256.dp)
) { menuItem ->
navController.navigate(screen.destinationId)
}
}
}
}
ナビゲーション コンポーネントを適切な位置に配置する
これで、アプリは現在のウィンドウ サイズクラスに基づいて適切なナビゲーション コンポーネントを選択できるようになりましたが、選択したコンポーネントは期待どおりに配置できません。これは、ComposeView 要素が FragmentViewContainer 要素の下に配置されているためです。
MainActivity クラスの代替レイアウト リソースを更新します。
layout-sw600dp/activity_main.xmlファイルを開きます。ComposeView要素とFragmentContainer要素を水平方向に配置するように制約を更新します。
layout-sw600dp/activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintLeft_toRightOf="@+id/top_navigation"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:navGraph="@navigation/nav_graph" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/top_navigation"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/top_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
変更後、アプリは、関連付けられた幅のウィンドウ サイズクラスに基づいて適切なナビゲーション コンポーネントを選択します。1 つ目のスクリーンショットは、中程度の幅のウィンドウ サイズクラスの画面を示しています。2 つ目のスクリーンショットは、拡大幅のウィンドウ サイズクラスの画面を示しています。

図 7. 中程度の幅および拡大幅のウィンドウ サイズクラスの画面
8. 完了
お疲れさまでした。この Codelab は終了です。Compose を使用してビューベースの Android アプリにアダプティブ レイアウトを追加する方法を学習しました。その過程で、SlidingPaneLayout クラス、ウィンドウ サイズクラス、幅のウィンドウ サイズクラスに基づくナビゲーション コンポーネントの選択について学習しました。
