1. はじめに
Android デバイスのエコシステムは常に進化しています。ハードウェア キーボードが内蔵されていた初期の頃から時代は変わり、現在では、フリップ式、折りたたみ式、タブレット、サイズ変更可能なフリーフォーム ウィンドウなど、かつてないほど多様なデバイス上で Android アプリが稼働しています。
これはデベロッパーにとって朗報と言えますが、さまざまな画面サイズで期待に沿ったユーザビリティや優れたユーザー エクスペリエンスを実現するには、アプリに対して各種の最適化を行う必要があります。レスポンシブ / アダプティブ UI や復元性に優れたアーキテクチャを利用すれば、新しいデバイスを一つずつ対象とするのではなく、現在および今後ユーザーが使用するあらゆるサイズや形状のデバイスに、アプリの見た目と動作を対応させられます。
サイズ変更可能なフリーフォームの Android 環境の導入は、あらゆるデバイスに対応できるようにレスポンシブ / アダプティブ UI に対してプレッシャー テストを実施する良い方法です。この Codelab では、サイズ変更の影響、およびアプリのサイズを確実かつ簡単に変更するためのベスト プラクティスの実装について説明します。
作成するアプリの概要
フリーフォームのサイズ変更の影響を学び、サイズ変更のベスト プラクティスを示すために Android アプリを最適化します。作成するアプリの機能は次のとおりです。
互換性のあるマニフェストを備える
- アプリの自由なサイズ変更を阻む制限を取り除きます
サイズ変更時に状態を維持する
- rememberSaveable を使用してサイズ変更時に UI 状態を維持します
- UI を初期化するバックグラウンド作業の不要な重複を回避します
必要なもの
- 基本的な Android アプリの作成に関する知識
- Compose での ViewModel と状態の知識
- フリーフォーム ウィンドウのサイズ変更をサポートする、次のいずれかのテストデバイス
- ADB がセットアップされた Chromebook
- Samsung DeX モードまたは本番環境モード対応のタブレット
- Android Studio の Desktop Android Virtual Device エミュレータ
この Codelab で問題(コードのバグ、文法的な誤り、不明確な表現など)が見つかった場合は、Codelab の左下隅にある [誤りを報告] から問題を報告してください。
2. はじめに
GitHub からリポジトリのクローンを作成します。
git clone https://github.com/android/large-screen-codelabs/
または、リポジトリの ZIP ファイルをダウンロードして展開します。
プロジェクトをインポートする
- Android Studio を開きます。
- [Import Project] または [File] -> [New] -> [Import Project] を選択します。
- プロジェクトのクローンを作成した場所、またはプロジェクトを解凍した場所に移動します。
- resizing フォルダを開きます。
- start フォルダ内のプロジェクトを開きます。中にスターター コードがあります。
アプリを試す
- アプリをビルドして実行します
- アプリのサイズ変更を試します
考えてみましょう
お使いのテストデバイスの互換性サポートによっては、望ましいユーザー エクスペリエンスではなかったかもしれません。アプリはサイズ変更できず、初期のアスペクト比のままになっています。原因は何でしょうか?
マニフェストの制限
アプリの AndroidManifest.xml
ファイルを見てみると、フリーフォーム ウィンドウのサイズ変更環境においてアプリが正常に動作するのを妨げる制限がいくつか加えられています。
AndroidManifest.xml
android:maxAspectRatio="1.4"
android:resizeableActivity="false"
android:screenOrientation="portrait">
問題のある 3 行をマニフェストから削除し、アプリを再ビルドして、テストデバイスでもう一度試してみましょう。フリーフォームのサイズ変更の制限が解除されていることがわかります。このような制限をマニフェストから削除することは、フリーフォーム ウィンドウのサイズ変更のためにアプリを最適化する際の重要なステップです。
3. サイズ変更による構成変更
アプリのウィンドウをサイズ変更すると、アプリの Configuration がアップデートされます。このアップデートはアプリに影響を与えます。影響を理解し、予期することで、ユーザーに良い体験を提供できます。アプリ ウィンドウの幅と高さの変更は明らかですが、アスペクト比と画面の向きにも影響があります。
構成変更の確認
Android ビューシステムでビルドしたアプリでの変更を確認するには、View.onConfigurationChanged
をオーバーライドできます。Jetpack Compose では、LocalConfiguration.current
にアクセスできます。これは、View.onConfigurationChanged
が呼び出されるたびに自動的にアップデートされます。
サンプルアプリでの設定の変更を確認するには、LocalConfiguration.current
の値を表示するコンポーザブルをアプリに追加するか、そうしたコンポーザブルを使って新しいサンプル プロジェクトを作成します。UI 例は次のようになります。
val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
when (configuration.screenLayout and
Configuration.SCREENLAYOUT_SIZE_MASK) {
SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
else -> "undefined value"
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text("screenWidthDp: ${configuration.screenWidthDp}")
Text("screenHeightDp: ${configuration.screenHeightDp}")
Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
Text("screenLayout SIZE: $screenLayoutSize")
}
observing-configuration-changes プロジェクト フォルダで実装例を確認できます。アプリの UI に追加してテストデバイスで実行し、アプリの構成変更に応じて UI がアップデートされるのを確認しましょう。
このようにアプリの構成を変更することで、小さなハンドセットの分割画面からタブレットやパソコンの全画面表示まで極端な画面サイズ間の素早い変更をシミュレートできます。この方法は、さまざまな画面でのアプリのレイアウトをテストするのに適しているだけでなく、素早い構成変更イベントをアプリがどの程度処理できるかもテストできます。
4. ロギング アクティビティのライフサイクル イベント
アプリにおけるフリーフォーム ウィンドウのサイズ変更による別の影響として、アプリで発生するさまざまな Activity
ライフサイクルの変更があります。リアルタイムで変更を確認するには、ライフサイクル オブザーバーを onCreate
メソッドに追加し、onStateChanged
をオーバーライドして新しいライフサイクル イベントを都度記録します。
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Log.d("resizing-codelab-lifecycle", "$event was called")
}
})
ロギングを実施した状態で、テストデバイスでアプリを再度実行し、アプリを最小化したりフォアグラウンドに戻したりしながら Logcat を確認します。
最小化するとアプリが停止し、フォアグラウンドに戻すと再開するのを確認します。このアプリに対する影響は、この Codelab 内の継続性についてのセクションで扱います。
次に、アプリを最小サイズから最大サイズに変更する際に呼び出されるアクティビティのライフサイクル コールバックを Logcat で確認します。
お使いのテストデバイスによって動作が異なる可能性がありますが、アプリのウィンドウ サイズが大幅に変更された場合はアクティビティが破棄、再作成される一方、若干のサイズ変更の場合はアクティビティの破棄、再作成が行われないことに気づくかもしれません。これは、API レベル 24 以降では、サイズが大幅に変更された場合にのみ Activity
が再作成されるためです。
フリーフォーム ウィンドウ処理環境で想定されるよくある構成変更をいくつか説明しましたが、他にも留意しておくべき変更があります。たとえば、テストデバイスに接続された外部モニタがある場合、ディスプレイ密度などの構成が変更されると Activity
が破棄されて、アカウントに再作成されます。
構成変更に関連する複雑さをある程度抽象化するには、WindowSizeClass のようなより高いレベルの API を使用してアダプティブ UI を実装します(各種の画面サイズのサポートもご覧ください)。
5. 継続性 - サイズ変更時にコンポーザブルの内部状態を維持
前セクションでは、フリーフォーム ウィンドウのサイズ変更環境で想定されるアプリの構成変更を確認しました。このセクションでは、こうした変更を通じてアプリの UI 状態を継続的に維持します。
まず、クリックされると開いてメールアドレスを表示するようにコンポーズ可能な関数 NavigationDrawerHeader
(ReplyHomeScreen.kt
にあります)を設定します。
@Composable
private fun NavigationDrawerHeader(
modifier: Modifier = Modifier
) {
var showDetails by remember { mutableStateOf(false) }
Column(
modifier = modifier.clickable {
showDetails = !showDetails
}
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
ReplyLogo(
modifier = Modifier
.size(dimensionResource(R.dimen.reply_logo_size))
)
ReplyProfileImage(
drawableResource = LocalAccountsDataProvider
.userAccount.avatar,
description = stringResource(id = R.string.profile),
modifier = Modifier
.size(dimensionResource(R.dimen.profile_image_size))
)
}
AnimatedVisibility (showDetails) {
Text(
text = stringResource(id = LocalAccountsDataProvider
.userAccount.email),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier
.padding(
start = dimensionResource(
R.dimen.drawer_padding_header),
end = dimensionResource(
R.dimen.drawer_padding_header),
bottom = dimensionResource(
R.dimen.drawer_padding_header)
),
)
}
}
}
アプリに展開可能なヘッダーを追加し、次のように操作します。
- テストデバイスでアプリを実行します
- ヘッダーをタップして開きます
- ウィンドウ サイズを変更します
大幅にサイズを変更すると、ヘッダーの状態が失われることがわかります。
remember
は再コンポーズ後も状態を維持しますが、アクティビティやプロセスの再作成後は状態を維持しないため、UI の状態は失われます。一般的には、状態ホイスティングを使用して、コンポーズ可能な関数の呼び出し元に状態を移動してコンポーズ可能な関数をステートレスにします。これにより、この問題全体を回避できます。ただし、UI 要素の状態をコンポーズ可能な関数の内部に維持する場合は remember
を使用できます。
この問題を解決するには、remember
を rememberSaveable
に置き換えます。これは、rememberSaveable
が savedInstanceState
に remember で保存された値を保存、復元するためです。remember
を rememberSaveable
に変更し、テストデバイスでアプリを実行して、アプリのサイズを再度変更してみてください。意図したとおり、展開可能なヘッダーの状態がサイズ変更を通して維持されることが確認できます。
6. バックグラウンド作業の不要な重複の回避
rememberSaveable
を使用して、構成変更を通してコンポーザブル内部の UI 状態を保持する方法を説明しました。構成変更は、フリーフォーム ウィンドウのサイズ変更により頻繁に発生します。しかし、多くの場合、アプリでコンポーザブルから UI の状態とロジックをホイスティングする必要があります。状態のオーナー権限を ViewModel に移すことは、サイズ変更時も状態を保持する良い方法です。状態を ViewModel
にホイスティングすると、重いファイル システムへのアクセスや画面の初期化に必要なネットワーク呼び出しなど、長時間実行のバックグラウンド作業で問題が発生するかもしれません。
発生する可能性のある問題の例については、ReplyViewModel
内の initializeUIState
メソッドにログ ステートメントを追加して確認してください。
fun initializeUIState() {
Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
val mailboxes: Map<MailboxType, List<Email>> =
LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
_uiState.value =
ReplyUiState(
mailboxes = mailboxes,
currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
?: LocalEmailsDataProvider.defaultEmail
)
}
次に、テストデバイスでアプリを実行し、アプリのウィンドウ サイズを何度か変更してみましょう。
Logcat を見ると、アプリで初期化メソッドが複数回実行されていることが確認できます。これは、一度だけ実行して UI を初期化したい作業において、問題になる可能性があります。追加のネットワーク呼び出し、ファイル I/O、その他の作業により、デバイスのパフォーマンスが低下し、その他の意図しない問題が起こる場合があります。
不要なバックグラウンド作業を回避するには、アクティビティの onCreate()
メソッドから initializeUIState()
の呼び出しを削除します。代わりに、ViewModel
の init
メソッド内のデータを初期化します。これにより、ReplyViewModel
が最初にインスタンス化される際に初期化メソッドが一度のみ実行されるようにします。
init {
initializeUIState()
}
アプリを再度実行してみましょう。アプリのウィンドウ サイズを変更した回数にかかわらず、不要なシミュレートされた初期化タスクが一度のみ実行されることが確認できます。これは、Activity
のライフサイクルを超えて ViewModel が保持されるためです。ViewModel
の作成時に初期化コードを一度のみ実行することで、Activity
の再作成から区分し、不要な作業を防ぎます。UI を初期化するのにコストの高いサーバー呼び出しや重いファイルの I/O オペレーションが伴う場合、リソースを大幅に節約しユーザー エクスペリエンスを改善できます。
7. 完了
以上でこの Codelab は終了です。ChromeOS やその他のマルチウィンドウ環境、マルチスクリーン環境で、Android アプリを適切にサイズ変更するためのベスト プラクティスを実装できました。
サンプル ソースコード
GitHub からリポジトリのクローンを作成します。
git clone https://github.com/android/large-screen-codelabs/
または、リポジトリの ZIP ファイルをダウンロードして展開します。