View ベースの MVP アーキテクチャを採用しているアプリが画面ごとにテーマを割り当てる方法

# 概要

最近、View ベースの MVP アーキテクチャが MVVM と並んで流行しているようです。Square 社製のライブラリである「[Flow](https://github.com/square/flow)+[Mortar](https://github.com/square/mortar)」や、他にも [Scoop](https://github.com/lyft/scoop) など、View をベースにした MVP アーキテクチャのためのフレームワークを提供するライブラリはいろいろあるようです。

ところで、レイアウト上の UI パーツ（`View`）に割り当てる属性値のまとまりに名前をつけることができます。これはスタイルと呼ばれ、Web で言うところの CSS に相当するようなものです。アプリを作っていく中で、どのレイアウトでも共通する属性値を持つ UI パーツが出てくることも多いでしょう。この時愚直に、同じ属性値をあちこちのレイアウト上でコピペしまくっていると、後々その共通部分を修正するときにひたすら面倒な作業をあちこちに適用するはめになり、作業量も差分も大きくなってしまいます。そこでスタイルと呼ばれる、レイアウトとは独立したリソースを用意してやり、この UI パーツはこのスタイルを割り当てるということを宣言してさえおけば、どのレイアウトでも共通の属性値を持った UI パーツを作れます。

さて、Anddroid でいうところのスタイルには、実は 2 種類あります。ひとつはスタイル、もうひとつはテーマです。どちらも`<style>`タグを使って宣言するので紛らわしいのですが、ざっくり言ってしまうと、スタイルのまとまりのことをテーマと呼びます。テーマでは、どの種類の UI パーツにはどのスタイルを割り当てますよ、という宣言をします。こうすることで、レイアウトの中でいちいち「この UI パーツにはこのスタイルを割り当てる」ということを書かなくても、テーマを一発バチコンと当てるだけで指定した UI パーツに対応するスタイルが割り当てられていきます（もちろん、すべての場合でテーマ一発で解決するとは限りません）。

一般的に、と言うのが適切かはわかりませんが、多くの場合、テーマは`AndroidManifest`の`<application>`や`<activity>`の属性として割り当てていきます。テーマはその性質上、画面単位での見た目を決定するものなので、アプリ内の全画面で共通のテーマを使うなら`<application>`に、画面ごとに個別にテーマを割り当てたいときには`<activity>`に`android:theme`属性をつけます。

# 問題

これで何が困るかというと、View ベースの MVP アーキテクチャの場合、基本的に画面遷移は View のアニメーションで実現されるため、極端な話 Activity はひとつ用意しておけばよい作りになります（実際には複数用意することが多いかもしれませんが）。そうなると、この画面ではちょっとテーマを変えたい……という時に、どこでそのテーマの宣言をすればよいかが難しくなります。

# 対処法はふたつ

おおまかに 2 種類の対処法があります。ひとつは、MVP アーキテクチャを実現するフレームワークで `ContextThemeWrapper`を使う方法、もうひとつは`android:theme`属性をレイアウトで指定する方法です。おそらく後者の方が圧倒的に楽なはずです。

# ContextThemeWrapper を使う方法

MVP アーキテクチャを実現するフレームワークのうち、View をベースとしたものは必ずどこかで`LayoutInflater`を使って`View`を展開するはずです([Scoop.java#L134](https://github.com/lyft/scoop/blob/370abd83a78a409f20416d279078a3eec3efe6f7/scoop/src/main/java/com/lyft/scoop/Scoop.java#L134)や[FlowContextWrappter.java#L47](https://github.com/square/flow/blob/fffcb5a42aac388bef1c477e1f5b2c976d3ef5c3/flow/src/main/java/flow/FlowContextWrapper.java#L47)で`LayoutInflater`を取り出している様子が見えます)。この時、`LayoutInflater`は`from()`メソッドで取り出していますが、続く`cloneInContext()`メソッドの引数に渡す`Context`を`ContextThemeWrapper`にすることで、この`LayoutInflater`で展開する`View`に割り当てるテーマを指定することが出来ます。

```java

ContextThemeWrappter wrappedContext = new ContextThemeWrapper(baseContext, R.style.AppTheme_Something);
LayoutInflater inflater = LayoutInflater.from(baseContext).cloneInContext(wrappedContext);
```

よって、画面のライフサイクルの最初期に、その画面の`View`を展開する`Context`を`ContextThemeWrappter`にくるんでフレームワークに渡すことができれば、画面ごとにテーマの切り替えができます。

# android:theme を使う方法

こちらは XML で記述します。やり方は非常に簡単で、テーマを指定したい画面の一番親にあたる`View`に`android:theme`属性を書くだけです。

```xml

<!-- LinearLayout 配下のすべての View にテーマを指定するよ -->
<LinearLayout
  ...
  android:theme="@style/AppTheme.Something"/>
  <Button .../>
  <ListView .../>
</LinearLayout>
```

# どうしてこんな長々と書いたのか

「`android:theme`使え」だけじゃ素っ気なさすぎるじゃないですか！

というのが本音だったり建前だったりしますが、XML で`android:theme`を指定したら結局 Android がよしなに`ContextThemeWrapper`使ってテーマを割り当ててくれるので([このへん](https://github.com/android/platform_frameworks_base/blob/4535e11fb7010f2b104d3f8b3954407b9f330e0f/core/java/android/view/LayoutInflater.java#L733))
、勉強も兼ねてと言うお話でした。決して、`android:theme`が`View`にも使えることを知らずに MVP フレームワークに`ContextThemeWrapper`を渡して`View`を展開できるように必死こいてコードを読み書きした挙句`android:theme`で一発じゃん…と絶望した腹いせに書いたわけではありません。