構成の変更に対処する

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

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

このような構成変更のほとんどは、なんらかのユーザー操作を原因として発生します。たとえば、デバイスの回転や折りたたみを行うと、アプリが使用できる画面内のスペースが変わります。同様に、フォントサイズ、言語、テーマなどのデバイス構成を変更すると、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 の状態を保存するをご覧ください。

アクティビティの再作成を制限する

特定の構成の変更に対しては、アクティビティの自動再作成を防ぐことができます。Activity が再作成されると、UI 全体と、その Activity から派生したオブジェクトが再作成されます。これを避ける正当な理由があるかもしれません。たとえば、特定の構成変更時にアプリでリソースを更新する必要がない場合や、パフォーマンスの制限がある場合です。そのような場合は、アクティビティ自体が構成の変更を処理することを宣言し、システムによるアクティビティの再起動を防ぐことができます。

特定の構成変更に関するアクティビティの再作成を無効にするには、AndroidManifest.xml ファイルの <activity> エントリで構成タイプを android:configChanges に追加します。有効な値は、android:configChanges 属性のドキュメントに記載されています。

次のマニフェスト コードは、画面の向きとキーボードの利用可能性が変更されたときに、MyActivityActivity 再作成を無効にします。

<activity
    android:name=".MyActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
    android:label="@string/app_name">

一部の構成変更では、アクティビティの再起動が必ず発生します。これを無効にすることはできません。たとえば、Android 12L(API レベル 32)で導入されたダイナミック カラーの変更を無効にすることはできません。

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

View システムでは、Activity の再作成を無効にした構成変更が発生した場合、アクティビティが Activity.onConfigurationChanged() に対する呼び出しを受け取ります。添付ビューもすべて、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 アプリもあります。

状態を保持する

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

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

Jetpack Compose で構成の変更に対応する

Jetpack Compose を使用すると、アプリは構成の変更に対応しやすくなります。ただし、可能なすべての構成変更に対して Activity の再作成を無効にした場合、アプリは構成の変更を適切に処理する必要があります。

Configuration オブジェクトは、LocalConfiguration コンポジション ローカルの Compose UI 階層で使用できます。変更があるたびに、LocalConfiguration.current から読み取るコンポーズ可能な関数が再コンポーズされます。コンポジション ローカルの仕組みについては、CompositionLocal でローカルにスコープされたデータをご覧ください。

次の例では、コンポーザブルが特定の形式で日付を表示します。コンポーザブルは、LocalConfiguration.currentConfigurationCompat.getLocales() を呼び出すことで、システムのロケール構成の変更に対応します。

@Composable
fun DateText(year: Int, dayOfYear: Int) {
    val dateTimeFormatter = DateTimeFormatter.ofPattern(
        "MMM dd",
        ConfigurationCompat.getLocales(LocalConfiguration.current)[0]
    )
    Text(
        dateTimeFormatter.format(LocalDate.ofYearDay(year, dayOfYear))
    )
}

ロケールが変更されたときに Activity の再作成を回避するには、Compose コードをホストする Activity でロケール構成の変更を無効にする必要があります。これを行うには、android:configChangeslocale|layoutDirection に設定します。

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

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

  • 構成: デバイス構成では、アプリの表示サイズ、ロケール、システムテーマなど、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() への呼び出しを受け取ります。

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

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

Android 7.0(API レベル 24)以降では、サイズが大幅に変更された場合にのみ、サイズベースの構成変更に対する Activity の再作成が発生します。 サイズが不十分なためにシステムが Activity を再作成しない場合、代わりに Activity.onConfigurationChanged()View.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 で対処されています。

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