少ない開発リソースでAndroidアプリをダークモードに対応した話 

# 初めに
現在、開発している[ArtSticker](https://artsticker.app/)というアプリで、ダークモードの対応をしました。
リリースまでの流れや、技術的な事などを共有したいと思います。

### チームの開発状況
まず現在のチーム状況です。

- Androidエンジニア: 1名
- デザイナー: 1名

リソースは少ないけど、やりたいことはたくさんあるので、ダークモードの優先度は低い。
でも、勉強を兼ねてダークモードに対応したいという状況でした。
ということで、デザイナーさんのできる限り負担をかけずに、ダークモードに対応しようと考えました。

### 工数を減らすために
デザインの管理は基本的にSketchで行っています。通常の機能開発では、デザイナーさんにSketch上でデザインを作ってもらってから開発を進めていますが、今回はできる限りその作業を削りました。

# リリースまでの流れ
以下のような流れで進めました。

Step 1. 最低限必要な実装をしてゴールまでの距離を確認
Step 2. デザイナーさんとDark Themeの勉強会を開く
Step 3. Step 2ですり合わせたイメージを元に実装をすすめる
Step 4. デザインチェック
Step 5. リリース

# Step 1. 最低限必要な実装をして、ゴールまでの距離を確認

まずは、ダークモードの対応の全体感を掴むために、シンプルな設定を入れます。

  - NotNight/Night Modeを切り替えられる `DayNight` のThemeをActivityに設定

```style.xml
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
```

  - Debugビルドのときのみ、OSのTheme設定に従うようにApplicationクラスで設定
 
```MyApplication.kt
override fun onCreate() {
    super.onCreate()
    AppCompatDelegate.setDefaultNightMode(
        if (BuildConfig.DEBUG) {
            AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
        } else AppCompatDelegate.MODE_NIGHT_NO
    )
}
```
  - night用の最低限のリソースファイルを作り、最低限の設定を追加

```/values/colors.xml
  <color name="color_primary">@color/white</color>
  <color name="color_primary_dark">@color/black</color>
  <color name="color_accent">@color/black</color>
```
```/values-night/colors.xml
  <color name="color_primary">@color/black</color>
  <color name="color_primary_dark">@color/white</color>
  <color name="color_accent">@color/gray_cccccc</color>
```
  - 動作確認をする
    - Android 10の端末やエミュレーターで、ダークモードの設定を変え、動作を確認します

# Step 2. デザイナーさんとDark Themeの勉強会を開く
  - [Material DesignのDark Theme](https://material.io/design/color/dark-theme.html)の読み合わせをする
    - 今回は日本語に翻訳をかけて、段落毎に順番に読み、議論しながらすすめました
  - Material DesignのDark Themeのレイヤーの考え方を理解することがポイントです
    - Light ThemeとDark Themeでelevationの考え方を統一します
    - ベースカラーを決め、その上に白をベースに以下の透明度を設定したレイヤーを重ね、各dp毎の色を作成します
    - Material DesignのDefault Elevationから各コンポーネントの色を決定します
        - [Default Elevation](https://material.io/design/environment/elevation.html#default-elevations)

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247380/056e76f0-59dc-89cb-7c6a-f292b5b3a80a.png">
ベースカラーと白のレイヤーのイメージ

<img height="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247380/798599be-e8dd-b8ee-c1c0-40be958b2a7d.png"><img height="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247380/dbe7555e-4f0c-4088-8745-e253e485612f.png">
レイヤー毎の色のイメージ

  - 開発中のアプリだったらという前提でレイヤーや使用する色の意識合わせをする
    - 自然とそういう流れになると思いますが議論します
  - 他のアプリのダークモードをいくつか見てみる
    - ダークモードに対応したアプリが増えてきているので、ゴールを共有します
  - ダークモードが増えた場合の運用負荷について議論する
    - デザインの管理を今後どうするか共有します

# Step 3. すり合わせたイメージを元に実装をする

### 最低限の色の定義を行う
  - ベースカラーと各レイヤー毎の色を仮置で定義します

```/values/colors.xml
    <color name="black_00dp">#0f0f0f</color>
    <color name="black_01dp">#1a1a1a</color>
    <color name="black_02dp">#1c1c1c</color>
    <color name="black_03dp">#1e1e1e</color>
    <color name="black_04dp">#202020</color>
    <color name="black_05dp">#222222</color>
    <color name="black_06dp">#242424</color>
    <color name="black_08dp">#282828</color>
    <color name="black_12dp">#2f2f2f</color>
    <color name="black_16dp">#383838</color>
    <color name="black_24dp">#484848</color>
```
- テキストカラーをLightなアプリのルールに従って定義します

```/values-night/colors.xml
    <color name="text_color">@color/white</color>
    <color name="text_color_primary">@color/white</color>
    <color name="text_color_secondary">@color/gray</color>
```

- 各コンポーネントの色を定義します

```/values-night/colors.xml
    <color name="color_border">@color/black_01dp</color>
    <color name="color_surface">@color/black_02dp</color>
    <color name="color_card_layout">@color/black_03dp</color>
    <color name="color_app_bar_layout">@color/black_04dp</color>
    <color name="color_tab_layout">@color/black_04dp</color>
    <color name="color_bottom_navigation_view">@color/black_08dp</color>
    <color name="color_dialog">@color/black_24dp</color>
```

### Viewに設定していく

- 使用する色の方針が決まったら、Viewに設定します
- 共通のコンポーネント毎に、styleにまとめながら実装すると、きれいに書くことができます
- 既存のLightモードの実装がいまいちな場合はリファクタリングしながら設定します

```values/styles.xml
    <style name="App.Widget.AppBarLayout" parent="Widget.Design.AppBarLayout" >
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:background">@color/color_app_bar_layout</item>
    </style>
```

```fragment_home.xml
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        style="@style/App.Widget.AppBarLayout">
```

### Vector Drawableの対応方法

- Context Menuに設定したVector Drawableは以下のように `android:tint="?attr/colorControlNormal"` を `vector` tagに設定するとNightModeの設定に従って色が変更されます
- この時、設定したThemeの `android:textColorSecondary` に従って色が決定します

```ic_like_off_black.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="11dp"
    android:height="10dp"
    android:viewportWidth="11"
    android:viewportHeight="10"
    android:tint="?attr/colorControlNormal">
    <!--  省略 -->
</vector>
```
### pngやwebp画像の対応方法

- 自動で設定することはできないため、 `drawable-night` 配下に同じファイル名でリソースを置きます

### 設定画面に、切り替えメニューを作る

今回は設定画面にメニューを追加し、Dialogを表示してモードを切り替えられるようにしました。

- 変更後の実装

```SettingsFragment.kt
class SettingsFragment : Fragment(), NightModeDelegate {

    fun showChangeNightModeDialog() {
        ChangeNightModeDialogFragment.instance(nightMode).also {
            it.onClickNightMode = { selectedNightMode ->
                val shouldUpdateState = nightMode != selectedNightMode
                if (shouldUpdateState) {
                    // SharedPreferencesに設定を保存
                    actionCreator.selectNightMode(selectedNightMode)
                    // NightModeを変更
                    updateNightMode(selectedNightMode)
                }
            }
            it.show(childFragmentManager, ChangeNightModeDialogFragment::class.java.simpleName)
        }
    }
```

- 変更処理詳細

```NightModeDelegate.kt
interface NightModeDelegate {

    fun updateNightMode(nightMode: NightModeType) {
        AppCompatDelegate.setDefaultNightMode(
            when (nightMode) {
                NightModeType.NotNight -> AppCompatDelegate.MODE_NIGHT_NO
                NightModeType.Night -> AppCompatDelegate.MODE_NIGHT_YES
                NightModeType.FollowDeviceTheme -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
            }
        )
    }
}
```

- アプリ起動時の設定

```MyApplication.kt
class ArtStickerApplication : Application, NightModeDelegate {

  override fun onCreate() {
      super.onCreate()
      updateNightMode(currentNightMode)
  }
}
```    

今回は、Night Modeをオフ(NotNight)の状態を初期値としました。

### オーバードローのチェック
- オーバードローが少なくなるように実装します
    - ダークモードに限った話ではないですが、開発者向けオプションの「GPUオーバードローをデバッグ」をONにして、無駄なオーバードローが発生していないか確認します

### デザイナーさんに共有する
- 数画面作り、デザイナーさんに共有し、調整します
- ダークモードにした時に別のリソースにする必要のある画像を洗い出し、必要に応じて作成を依頼します
- 概ね良さそうであれば最後まで実装します

# Step 4. デザインチェック

- 最後に、デザイナさんに一通りチェックしてもらいます
    - Step 3で作成した色の設定を共有し調整してもらいます
        - 色の設定が変わってもViewのレイヤー構造が変わらなければ、大きな変更はせずに済みます
    - 画面毎に、フィードバックをもらいます
    - 修正したら、再度確認してもらい精度を上げていきます

# Step 5. リリース
- 各AndroidのVersionでテストをして、リリースします

# 最後に
-  少ないリソースのなかで、Android版のダークモードに対応することができました
- デザインの管理をコンポーネント毎の色の管理に留めることで、デザイナーさんの負荷を下げることができました
- Material Designに従い、共通のコンポーネントの色を統一し、共通のView毎にstyleを定義することで、開発の時間や、テストの負荷を下げることができました
- 実績として、Step 1 〜 Step 4までで、片手間で作業をして2週間程度で完了しました (アプリやチームによって、作業量が変わるため、あまり参考にならないかもしれません)
- すすめ方によっては、チームのメンバーに迷惑をかけることもあると思うので、コミュニケーションは十分にとることが大事だと感じました
- 社内のメンバーにダークモードをプレビューしたところ、反応がよく実装してよかったと感じています
- 参考までに、[ArtSticker](https://artsticker.app/)を是非使ってみてください(現在はAndroid版のみダークモードに対応しています)

# 更新履歴
(2019/12/30)
- colorの名前をsnake caseで揃えたほうが良いため変更しました
- Night Modeを切り替えたときは、Activityの再起動は必要ないため処理をけしました