構成の変更を処理する(ビュー)

コンセプトと Jetpack Compose の実装

アプリの実行中に、デバイス構成の一部が変更される場合があります。変更の可能性がある構成の一部を以下に示します。

  • アプリの表示サイズ
  • 画面の向き
  • フォントのサイズと太さ
  • 言語 / 地域
  • ダークモードとライトモード
  • キーボードを使用できるかどうか

このような構成変更のほとんどは、なんらかのユーザー操作を原因として発生します。たとえば、デバイスの回転や折りたたみを行うと、アプリが使用できる画面内のスペースが変わります。同様に、フォントサイズ、言語、テーマなどのデバイスの設定を変更すると、Configuration オブジェクト内の該当する値が変更されます。

このようなパラメータには通常、アプリケーションの UI に大幅な変更を加えることを要求します。そのため、Android プラットフォームには、そのような変更に対応するための専用のメカニズムがあります。 このメカニズムは Activity の再作成です。

アクティビティの再作成

構成の変更があると、システムが Activity を再作成します。これを行うため、システムは onDestroy を呼び出して既存の Activity インスタンスを破棄します。その後、onCreate を使用して新しいインスタンスが作成され、この新しい Activity インスタンスは、更新された新しい構成で初期化されます。つまり、システムは新しい構成で UI も再作成します。

この再作成動作が、新しいデバイス構成と一致する代替リソースを使用してアプリを自動的に再読み込みすることで、アプリが新しい構成に適応するようにします。

再作成の例

レイアウト XML ファイルの定義に従い、android:text="@string/title" を使用して静的タイトルを表示する TextView について考えてみます。ビューが作成される際、現在の言語に基づいて 1 回だけテキストが設定されます。言語が変更されると、システムがアクティビティを再作成します。その結果、ビューも再作成され、新しい言語に基づいた適切な値に初期化されます。

この再作成により、Activity 内、またはそこに含まれる FragmentView などのオブジェクト内のフィールドとして保持されている状態もすべて消去されます。これは、Activity の再作成によって Activity と UI の完全に新しいインスタンスが作成されるためです。さらに、古い Activity は表示されず無効になります。そのため、それに対するリファレンスや含まれているオブジェクトへのリファレンスが残っていても、それらは最新の状態ではありません。これにより、バグ、メモリリーク、クラッシュが発生する場合があります。

ユーザーの想定

アプリのユーザーは、状態が保持されることを想定しています。ユーザーがフォームに入力中で、 マルチウィンドウ モードで別のアプリを開いて情報を調べる場合、 ユーザーが戻ったときにフォームが空欄になっていたり、アプリ内の別の場所に移動させられたりすると、ユーザー エクスペリエンスが低下することになります。デベロッパーは、構成の変更やアクティビティの再作成を通じて、一貫したユーザー エクスペリエンスを提供する必要があります。

アプリ内で状態が保持されているかどうかを確認するには、アプリがフォアグラウンドとバックグラウンドの両方で動作しているときに、構成を変更するアクションを実行できます。そのようなアクションには以下があります。

  • デバイスを回転させる
  • マルチ ウィンドウ モードを開始する
  • マルチウィンドウ モードまたはフリーフォーム ウィンドウでアプリケーションのサイズ変更を行う
  • 画面が複数ある折りたたみ式デバイスを折りたたむ
  • ダークモードとライトモードなど、システムのテーマを変更する
  • フォントサイズを変更する
  • システムやアプリの言語を変更する
  • ハードウェア キーボードの接続または接続解除を行う
  • ホルダーとの接続または接続解除を行う

Activity の再作成を通じて関連する状態を保持する方法は、主に 3 つあります。どちらを使用するかは、保持する状態の種類によって異なります。

  • ローカル永続性により、複雑なデータまたは大規模なデータのプロセスの終了を処理します。 永続ローカル ストレージには、データベースや DataStore があります。
  • 保持オブジェクト。ユーザーがアプリを積極的に使用している間、メモリ内の UI 関連の状態を処理する ViewModel インスタンスなど。
  • 保存済みインスタンスの状態。システムが開始したプロセスの終了を処理し、ユーザー入力やナビゲーションに依存する一時的な状態を維持します。

各 API の詳細や API を適切に使用するタイミングについては、UI の状態を保存するをご覧ください。

View システムで構成の変更に対応する

View システムでは、Activity の再作成を無効にした構成変更が発生した場合、アクティビティが Activity.onConfigurationChanged に対する呼び出しを受け取ります。添付ビューもすべて、a call to View.onConfigurationChangedを受け取ります。android:configChanges に追加していない構成変更の場合、システムは通常どおりアクティビティを再作成します。

onConfigurationChanged コールバック メソッドは、新しいデバイス構成を指定する Configuration オブジェクトを受け取ります。Configuration オブジェクトのフィールドを読み取り、新しい構成を決定します。以降の変更を行うには、インターフェースで使用するリソースを更新します。このメソッドが呼び出されると、アクティビティの Resources オブジェクトが更新され、新しい構成に基づいてリソースが返されます。これにより、アクティビティを再起動することなく UI の要素を再設定できるようになります。

たとえば、次の onConfigurationChanged の実装は、キーボードが使用可能かどうかを確認します。

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show()
    } else if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_NO) {
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show()
    }
}

Java

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show();
    } else if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO){
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show();
    }
}

この構成変更に基づいてアプリを更新する必要がない場合は、代わりに onConfigurationChanged を実装しないことも可能です。その場合は、構成の変更前に使用したすべてのリソースが引き続き使用され、アクティビティの再起動だけが回避されます。たとえば、Bluetooth キーボードの取り付けや取り外し時に対応する必要のない TV アプリもあります。

状態を保持する

この手法を使用する場合でも、通常のアクティビティのライフサイクルで状態を保持する必要があります。その理由は次のとおりです。

  • 回避できない変更: 回避できない構成変更により、アプリが再起動される場合があります。
  • プロセスの終了: アプリは、システムが開始したプロセスの終了を処理できなければなりません。ユーザーがアプリから離れ、アプリがバックグラウンドに移動した場合、システムによってアプリが破棄されることがあります。

構成の変更: 主なコンセプトとベスト プラクティス

構成を変更する場合、次の重要なコンセプトを理解しておく必要があります。

  • 構成: デバイス構成では、アプリの表示サイズ、ロケール、システムテーマなど、UI をどのようにユーザーに表示するかを定義します。
  • 構成の変更: ユーザーの操作により構成が変更されます。たとえばユーザーが、デバイスの構成やデバイスとの物理的な通信手段を変更する場合があります。構成の変更を防ぐ方法はありません。
  • Activity の再作成: 構成を変更すると、デフォルトで Activity が再作成されます。これは、新しい構成に応じてアプリの状態を再初期化するための組み込みメカニズムです。
  • Activity の破棄: Activity の再作成により、古い Activity インスタンスは破棄され、新しいインスタンスが作成されます。現在、古いインスタンスは使われていません。なんらかのリファレンスが残っていると、メモリリーク、バグ、クラッシュにつながる恐れがあります。
  • 状態: 古い Activity インスタンスの状態は、新しい Activity インスタンスには存在しません。その 2 つは異なるオブジェクトのインスタンスだからです。UI の状態を保存するで説明しているように、アプリとユーザーの状態は保持されます。
  • オプトアウト: ある種の構成変更に対するアクティビティの再作成を無効にすることで、最適化できる可能性があります。そのため、新しい構成に応じてアプリを適切に更新する必要があります。

優れたユーザー エクスペリエンスを提供するために、次のベスト プラクティスを参考にしてください。

  • 頻繁な構成変更に備える: API レベル、フォーム ファクタ、UI ツールキットに関係なく、構成変更は滅多に行われないものであると思い込まないでください。ユーザーは構成を変更した際に、アプリも更新されて新たな構成で引き続き正常に動作するものと期待しています。
  • 状態を保持する: Activity の再作成時にユーザーの状態が失われないようにします。UI の状態を保存するで説明しているように、状態を保持します。
  • 簡単な修正としてのオプトアウトを避ける: 状態の喪失を避けるためのショートカットとして、Activity の再作成を無効にしないでください。アクティビティの再作成を無効にするには、変更を処理する promise を実行する必要があります。それでも依然として、他の構成変更、プロセスの終了、アプリの終了から生じる Activity の再作成により、状態が失われる可能性はあります。Activity の再作成を完全に無効にすることはできないのです。UI の状態を保存するで説明しているように、状態を保持します。
  • 構成の変更を回避しない: 構成の変更と Activity の再作成を回避する目的で、画面の向き、 アスペクト比、サイズ変更の可能性を制限しないでください。これは、アプリを快適に使用したいと考えるユーザーに悪影響を及ぼします。

サイズベースの構成変更を処理する

サイズベースの構成変更は、いつでも発生する可能性があります。マルチウィンドウ モードに入れるような大画面のデバイスでアプリが動作している場合は、その傾向が強くなります。ユーザーは、そのような状況でもアプリが適切に動作することを期待しています。

一般的にサイズ変更には、大幅な変更と軽微な変更の 2 種類があります。大幅なサイズ変更とは、画面サイズ(幅、高さ、最小幅など)の違いにより、新しい構成に別の代替リソースセットが適用されるものです。 これらのリソースには、アプリ自体が定義するものと、アプリのライブラリから発生したものが含まれています。

サイズベースの構成変更に関するアクティビティの再作成を制限する

サイズベースの構成変更に対して Activity の再作成を無効にすると、Activity は再作成されません。代わりに、Activity.onConfigurationChanged への呼び出しを受け取ります。添付ビューはすべて、 View.onConfigurationChanged への呼び出しを受け取ります。

Activity の再作成は、マニフェスト ファイルに android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout" が含まれていると、サイズベースの構成変更に対して無効になります。

サイズベースの構成変更に関するアクティビティの再作成を許可する

Android 7.0(API レベル 24)以降では、サイズが大幅に変更された場合にのみ、サイズベースの構成変更に対する Activity の再作成が発生します。 サイズが不十分なためにシステムが Activity を再作成しない場合、代わりに Activity.onConfigurationChangedView.onConfigurationChanged が呼び出されることがあります。

Activity が再作成されない場合、Activity コールバックと View コールバックについて、次の点に注意してください。

  • Android 11(API レベル 30)から Android 13(API レベル 33)では、Activity.onConfigurationChanged は呼び出されません。
  • Android 12L(API レベル 32)と Android 13(API レベル 33)の初期バージョンでは、View.onConfigurationChanged が呼び出されない可能性があるという既知の問題があります。詳細については、一般公開されている問題をご覧ください。この問題は、Android 13 の後期のリリースと Android 14 で対処されています。

サイズベースの構成 変更のリッスンに依存するコードについては、オーバーライドされた View.onConfigurationChangedでユーティリティViewを使用することをおすすめします。Activityの再作成や Activity.onConfigurationChangedに依存しないでください。

android:configChanges="uiMode"