文脈
触ったのでついでに調べてまとめることにした。
Per-app language preferences って何
端末全体の言語設定とは別にアプリ毎の言語設定を system 側で管理してくれる機能。
System settings から設定できるようにする
Android 13+ で有効化するためにやることは以下の2つ:
res/xml/locale_config.xml
を作成する- AndroidManifest の
application
要素にandroid:localeConfig
属性を追加し、作成した config xml を指定する
細かいことはドキュメントを読んでもらうとして、 locale_config.xml
は以下のような内容のファイルになっている。
<?xml version="1.0" encoding="utf-8"?> <locale-config xmlns:android="http://schemas.android.com/apk/res/android"> <locale android:name="en"/> <locale android:name="ja"/> <locale android:name="zh-CN"/> ... </locale-config>
ドキュメントで言及のある build.gradle
の resConfigs
は単に per-app preferences で指定したサポートする言語とアプリに含める言語 resource を一致させるための設定なので、書かなくても有効化される。
上記の変更によって per-app language preferences が有効化されると、以下の場所からアプリの使用言語を設定できるようになる:
- Settings > Apps > 対象のアプリ > Language
- Settings > System > Languages & input > App Languages > 対象のアプリ
アプリ独自の言語設定機能と連携させる
以前からアプリ内の言語設定を変更する機能を独自に実装していたアプリや、言語設定のためにいちいち Settings に移動させずに自アプリ内で操作を完結させたいという要求に対して、この per-app language preferences と連携させるための API が追加されている。
追加された API は framework の LocaleManager
と AndroidX AppCompat library の AppCompatDelegate#getApplicationLocales
/AppCompatDelegate#setApplicationLocales
で、 LocaleManager
は added in API level 33 なので当然公式の推奨は AppCompat の方になる。 AppCompat library の方は 1.6.0
で追加されている (2022-09-23 時点で RC01)。
LocaleManager
: https://developer.android.com/reference/android/app/LocaleManagerAppCompatDelegate
: https://developer.android.com/reference/androidx/appcompat/app/AppCompatDelegate
することに特に難しいことはなく、現在設定されている locale を取得する時に getApplicationLocales
を呼び、 locale を変更する時は設定したい言語を LocaleList
/ LocaleListCompat
として setApplicationLocales
に渡すだけで OK。ちなみに、言語設定が system default の時は空の locale list が取得でき、 system default に設定したい時は空の locale list をセットすることで実現できる。
// アプリの言語が「日本語」に設定されているとする // 中身は [ja] val currentLocales = AppCompatDelegate.getApplicationLocales() // アプリの言語設定が「English」になる AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags("en")) // アプリの言語設定が「System Default」になる // LocaleListCompat.forLanguageTags("") も getEmptyLocaleList() と同義になる AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()) // 中身は [] val localesChangedToDefault = AppCompatDelegate.getApplicationLocales()
以下は DroidKaigi2022 のアプリで実装したもののデモ動画。アプリ内で変更した言語設定が system settings の方の言語設定にも反映されていることがわかる。
AppCompat の API を使う際の注意点
追記 (2022-09-24): Android 12 以下での挙動の記述が誤っていたので修正
あまり無いケースだと思うが AppCompatDelegate
の方を使う場合の注意点として、API reference にも記述があるように、getApplicationLocales
/setApplicationLocales
は Activity#onCreate
以降でのみ呼ぶようにすべきというものがある。これは Android 13+ 向けの実装の都合で、内部で LocaleManager
を取得するのに必要な Context が手に入るようになるのが onCreate
以降*1となり、それまでは呼んでも実際の get/set が実行されないから。 Android 12 以下の場合は get/set で受け渡しする値は AppCompatDelegate
内部のメンバとして管理されているため、 onCreate
以前でも AppCompatDelegate
との値の受け渡しは正常に行うことができる。この際、実際に framework 側に設定が同期されるのは onCreate
呼び出し時である。後述するがこの挙動を利用して Android 12 以下向けに後方互換対応をする場合がある。
また、これは地味かつ当然といえば当然な話だが、 AppCompat の API を使う時はそれが呼ばれる Activity が AppCompatActivity
を継承したものである必要がある*2。 AppCompatActivity
が用いられなかった場合の挙動は大体上記と同じだが、こちらに関しては Android 12 以下でも framework に言語設定が同期されなくなるので設定の変更がアプリに反映されない。
この辺の仕組みの詳細は長くなるし横道に逸れるので書かないが気になる人は AppCompatDelegate
の sAppContext
や sActivityDelegates
に入る値はどこから来るかや framework との同期はどこで行われるかを追いかけてみるとわかると思う。
Android 12 以下でも使えるようにする
Android 12 以下には per-app language preferences の機能が存在しないので、「アプリ独自の言語設定機能と連携させる」で書いたように AppCompat library を使ってアプリ内に言語設定の機能を実装する必要があるが、これだけだとアプリのプロセスが終了する度に設定した言語の情報が揮発してしまうので、アプリ内で設定情報を保存しておく必要がある。これに関しては AppCompat library を使った自動保存を opt-in するか自前の保存機構を使って起動時に連携するかという二通りの対応方法がある。
言及箇所へのリンク:
- https://developer.android.com/guide/topics/resources/app-languages#android12-impl
- https://developer.android.com/reference/androidx/appcompat/app/AppCompatDelegate#setApplicationLocales(androidx.core.os.LocaleListCompat)
自動保存の opt-in
Manifest に以下のように AppLocalesMetadataHolderService
を登録するとライブラリの方で上手くやってくれる。ただしこの設定をすると blocking な disk read/write が発生するようで、 StrictMode
で検知するようにしている場合は引っかかってしまうとのこと。 DroidKaigi2022 アプリは自前の言語設定機能をまだ持っていなかったし、新規にローンチされるものでデータの引き継ぎを考慮する必要もなかったのでこちらを採用した。
<application ... <service android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:enabled="false" android:exported="false"> <meta-data android:name="autoStoreLocales" android:value="true" /> </service> ... </application>
自前の保存機構と連携
既に自前の言語設定機能を持っていたり、 blocking な disk read/write が許容できないなどの理由で AppLocalesMetadataHolderService
を manifest に登録しないか、した上で autoStoreLocales
を false
した場合はこちらの選択肢となる。やることは以下の通り:
- Activity の
Activity#onCreate
が呼ばれる前*3に保存されている言語設定を読み出してAppCompatDelegate#setApplicationLocales
に与える - 上記の操作を Android 13+ で実行されないように制御する
- アプリ操作での
setApplicationLocales
実行時に自前の storage にも設定を保存する
Android 13+ では同期処理を実行させないようにするのは、 per-app language preferences の方で言語設定を保存しているので初期化する必要がないのと、1. の時点で呼んでも機能しないから。
デバイスの OS version 更新時の設定引き継ぎ
アプリ利用中にデバイスの OS が 12 から 13 に更新された際にそれまでアプリ内で管理していた言語設定を per-app language preferences に引き継ぐ方法について:
autoStoreLocales
を有効にする対応をした場合は AppCompat library が自動的に同期してくれる- 自前の storage で管理していた場合は適当なタイミングで設定を取り出して
AppCompatDelegate#setApplicationLocales
に与える one-time の処理を仕込んでおく
まとめ
- Android 13 以降でだけ動けばいいなら xml 作って manifest に設定すれば OK
- 12 以下でも同じような体験をさせたい場合はアプリに言語設定機能を作って AppCompat library を使う、12 以下向けの設定保存をどうするか考える
- AppCompat library に設定保存まで任せると実装や OS version up 時の引き継ぎが楽、ただし blocking disk R/W を許容する必要がある
- 設定保存をアプリで実装して設定の取得/変更をする時だけ AppCompat の API を呼ぶ方法もある
- Android 12 以下から 13 以降への更新時に言語設定を per-app language preferences に引き継ぐことを考慮しよう