Android アプリのサイズ変更

1. はじめに

Android デバイスのエコシステムは常に進化しています。ハードウェア キーボードが内蔵されていた初期の頃から時代は変わり、現在では、フリップ式、折りたたみ式、タブレット、サイズ変更可能なフリーフォーム ウィンドウなど、かつてないほど多様なデバイス上で Android アプリが稼働しています。

これはデベロッパーにとって朗報と言えますが、さまざまな画面サイズで期待に沿ったユーザビリティや優れたユーザー エクスペリエンスを実現するには、アプリに対して各種の最適化を行う必要があります。レスポンシブ / アダプティブ UI や復元性に優れたアーキテクチャを利用すれば、新しいデバイスを一つずつ対象とするのではなく、現在および今後ユーザーが使用するあらゆるサイズや形状のデバイスに、アプリの見た目と動作を対応させられます。

サイズ変更可能なフリーフォームの Android 環境の導入は、あらゆるデバイスに対応できるようにレスポンシブ / アダプティブ UI に対してプレッシャー テストを実施する良い方法です。この Codelab では、サイズ変更の影響、およびアプリのサイズを確実かつ簡単に変更するためのベスト プラクティスの実装について説明します。

作成するアプリの概要

フリーフォームのサイズ変更の影響を学び、サイズ変更のベスト プラクティスを示すために Android アプリを最適化します。作成するアプリの機能は次のとおりです。

互換性のあるマニフェストを備える

  • アプリの自由なサイズ変更を阻む制限を取り除きます

サイズ変更時に状態を維持する

  • rememberSaveable を使用してサイズ変更時に UI 状態を維持します
  • UI を初期化するバックグラウンド作業の不要な重複を回避します

必要なもの

  1. 基本的な Android アプリの作成に関する知識
  2. Compose での ViewModel と状態の知識
  3. フリーフォーム ウィンドウのサイズ変更をサポートする、次のいずれかのテストデバイス

この 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

次に、アプリを最小サイズから最大サイズに変更する際に呼び出されるアクティビティのライフサイクル コールバックを Logcat で確認します。

お使いのテストデバイスによって動作が異なる可能性がありますが、アプリのウィンドウ サイズが大幅に変更された場合はアクティビティが破棄、再作成される一方、若干のサイズ変更の場合はアクティビティの破棄、再作成が行われないことに気づくかもしれません。これは、API レベル 24 以降では、サイズが大幅に変更された場合にのみ Activity が再作成されるためです。

フリーフォーム ウィンドウ処理環境で想定されるよくある構成変更をいくつか説明しましたが、他にも留意しておくべき変更があります。たとえば、テストデバイスに接続された外部モニタがある場合、ディスプレイ密度などの構成が変更されると Activity が破棄されて、アカウントに再作成されます。

構成変更に関連する複雑さをある程度抽象化するには、WindowSizeClass のようなより高いレベルの API を使用してアダプティブ UI を実装します(各種の画面サイズのサポートもご覧ください)。

5. 継続性 - サイズ変更時にコンポーザブルの内部状態を維持

前セクションでは、フリーフォーム ウィンドウのサイズ変更環境で想定されるアプリの構成変更を確認しました。このセクションでは、こうした変更を通じてアプリの UI 状態を継続的に維持します。

まず、クリックされると開いてメールアドレスを表示するようにコンポーズ可能な関数 NavigationDrawerHeaderReplyHomeScreen.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)
                ),


            )
        }
    }
}

アプリに展開可能なヘッダーを追加し、次のように操作します。

  1. テストデバイスでアプリを実行します
  2. ヘッダーをタップして開きます
  3. ウィンドウ サイズを変更します

大幅にサイズを変更すると、ヘッダーの状態が失われることがわかります。

アプリのナビゲーション ドロワー上のヘッダーはタップで開きますが、アプリがサイズ変更されると閉じます

remember は再コンポーズ後も状態を維持しますが、アクティビティやプロセスの再作成後は状態を維持しないため、UI の状態は失われます。一般的には、状態ホイスティングを使用して、コンポーズ可能な関数の呼び出し元に状態を移動してコンポーズ可能な関数をステートレスにします。これにより、この問題全体を回避できます。ただし、UI 要素の状態をコンポーズ可能な関数の内部に維持する場合は remember を使用できます。

この問題を解決するには、rememberrememberSaveable に置き換えます。これは、rememberSaveablesavedInstanceState に remember で保存された値を保存、復元するためです。rememberrememberSaveable に変更し、テストデバイスでアプリを実行して、アプリのサイズを再度変更してみてください。意図したとおり、展開可能なヘッダーの状態がサイズ変更を通して維持されることが確認できます。

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() の呼び出しを削除します。代わりに、ViewModelinit メソッド内のデータを初期化します。これにより、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 ファイルをダウンロードして展開します。