OSバージョン間の差分を考慮したAndroidのThemeの定義方法

OSバージョン間の差分を考慮したAndroidのThemeの定義方法について整理してみました。基本的にはAppCompatライブラリの定義方法を参考にしています。
Themeについては以下の公式ドキュメントをご参照ください。

developer.android.com


以降は、Android 7.0で追加されたマルチウィンドウAndroid 9で追加されたディスプレイ カットアウトに関する設定をThemeに追加するケースを例とします。


1. Themeの定義専用のリソースファイルを作成し、Themeの定義を移動する

Android Studioでプロジェクトを作成した後に特に変更を加えていなければ、Themeはstyles.xmlに定義されています。

f:id:tkhs0604:20200626012613p:plain


StyleとThemeはともに<style>タグによって定義されますが、これらを同一ファイル内に定義すると、どれがStyleの定義でどれがThemeの定義かが分かりづらくなります。
そこで、Themeの定義専用にthemes_base.xmlthemes.xmlというファイルをres/valuesディレクトリに作成します。

ファイルを作成したら、styles.xmlのThemeに関する定義をthemes_base.xmlに移動します。リソース名はAppThemeからBase.MyAppThemeに変更します。
themes_base.xmlには全OSバージョン共通で適用するThemeを定義します。

<!-- res/values/themes_base.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <!-- Base application theme. -->
  <style name="Base.MyAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
  </style>
</resources>


次に、Base.MyAppThemeを継承したThemeをthemes.xmlに定義します。リソース名はMyAppThemeとします。
Themeの継承には<style>タグのparent属性を使用します。

<!-- res/values/themes.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style name="MyAppTheme" parent="Base.MyAppTheme" />
</resources>


そして、このMyAppThemeマニフェストファイルの<application>タグのandroid:theme属性に指定します。
これにより、MyAppThemeに定義した内容がアプリケーション全体のThemeとして適用されます


2. 各OSバージョンに対応する代替リソースを作成する

ベースとなるリソースファイルの準備ができたので、次は各OSバージョンに対応する代替リソースを作成します。
代替リソースについては以下の公式ドキュメントをご参照ください。

developer.android.com


代替リソースを追加するディレクトリのsuffixは、OSバージョンではなくAPI Levelを指定します。OSバージョンとサポートされるAPI Levelは1対1で対応しています。
両者の対応関係は以下の公式ドキュメントをご参照ください。

developer.android.com


上記のドキュメントから、Android 7.0でサポートされるAPI Levelは24、Android 9でサポートされるAPI Levelは28と分かります。
よって、res/values-v24ディレクトリとres/values-v28ディレクトリを作成し、各ディレクトリにthemes.xmlを作成します。
なお、themes_base.xmlを作成する必要はありません。

f:id:tkhs0604:20200626083621p:plain


その後、先ほどと同様にthemes.xmlにThemeを定義していきます。
res/values-v24ディレクトリ内のthemes.xmlには、Base.MyAppThemeを継承したBase.V24.MyAppThemeを作成し、マルチウィンドウの設定を追加します。
また、Base.V24.MyAppThemeを継承したMyAppThemeを作成します。

<!-- res/values-v24/themes.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style name="MyAppTheme" parent="Base.V24.MyAppTheme" />

  <style name="Base.V24.MyAppTheme" parent="Base.MyAppTheme">
    <!-- Android 7.0(API Level 24)以降で使用可能な属性を追加する -->
    <item name="android:resizeableActivity">false</item>
  </style>
</resources>


同様に、res/values-v28ディレクトリ内のthemes.xmlには、Base.V24.MyAppThemeを継承したBase.V28.MyAppThemeを作成し、ディスプレイ カットアウトの設定を追加します。
また、Base.V28.MyAppThemeを継承したMyAppThemeを作成します。

<!-- res/values-v28/themes.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style name="MyAppTheme" parent="Base.V28.MyAppTheme" />

  <style name="Base.V28.MyAppTheme" parent="Base.V24.MyAppTheme">
    <!-- Android 9(API Level 28)以降で使用可能な属性を追加する -->
    <item name="android:windowLayoutInDisplayCutoutMode">never</item>
  </style>
</resources>


上記の結果、各OSバージョンに対応するMyAppThemeの継承関係は以下の通りになります。
ポイントは、より新しいOSバージョンに対応するMyAppThemeが古いOSバージョンに対応するThemeを全て含んでいる点です。

・全OSバージョン共通
Base.MyAppTheme
 └MyAppTheme

Android 7.0(API Level 24)以降
Base.MyAppTheme
 └Base.V24.MyAppTheme
  └MyAppTheme

Android 9(API Level 28)以降
Base.MyAppTheme
 └Base.V24.MyAppTheme
  └Base.V28.MyAppTheme
   └MyAppTheme


代替リソースを利用する場合、例えばAndroid 7.0の端末であればres/values-v24themes.xmlが利用されます。
言語化などを行うときは共通する定義が基本的にないので問題ないのですが、Themeの場合は共通する定義があるので、何も考えないと全OSバージョン共通のThemeを何度も定義しなければなりません。

今回のような形でThemeを定義することによって、OSバージョン間の差分を考慮してAndroidのThemeが適用できるようになります。