React Native製の公開しているアプリにダークモードを実装してみた

以前Qiitaで紹介したアプリ**ペペロミア**にダークモードを実装してみたので、実装方法について紹介します。

■ React Native製、予定作成アプリ「ペペロミア」を公開しました
https://qiita.com/wheatandcat/items/3324dfd141729e46009f

# 対象のアプリ: ペペロミア

■ アプリ公式サイト
https://peperomia.app/

<img width="1362" alt="スクリーンショット 2019-09-20 00.02.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170824/9885ad82-22c4-acf8-908a-396160eff0d1.png">

# GitHub

■ Peperomia
https://github.com/wheatandcat/Peperomia

# 環境
 - Expo SDK 34

# 実装してみた
動画にまとめました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ペペロミアでダークモードを実装してみた <a href="https://t.co/dBmyHqNExw">https://t.co/dBmyHqNExw</a> <a href="https://twitter.com/YouTube?ref_src=twsrc%5Etfw">@YouTube</a>さんから</p>&mdash; 飯野陽平 (@wheatAndCat) <a href="https://twitter.com/wheatAndCat/status/1174706566370430976?ref_src=twsrc%5Etfw">September 19, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 対象のpull request

■ ダークモードを実装する
https://github.com/wheatandcat/Peperomia/pull/108

# 実装方法

# デザイン面

カラーを黒ベースに変えていきました。通常時とダークモード時を並べると、こんな感じです。

<img width="913" alt="通常画面.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170824/1c80fd55-38db-5fb3-ce18-7938b602d73b.png">


<img width="913" alt="ダークモード画面.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170824/dd043eb2-70d9-45ff-76a6-59e2b17592b8.png">


## カラー定義

以下のカラー設定を決めて各要素に設定

■ PeperomiaNative/src/config/theme.ts
https://github.com/wheatandcat/Peperomia/blob/e9a4beac729938b6f88366324dbb8873144736b7/PeperomiaNative/src/config/theme.ts#L149-L163

| 要素名       |       通常 |    Darkモード    |
|:-----------------|------------------:|:------------------:|
| バックグラウンド             |              white |        black        |
| バックグラウンド（サブ）          |            lightGray |       darkGray       |
| テキスト             |              black |        lightGray        |
| テキスト（サブ）               |                darkGray |         gray         |
| アイコン             |             black |       lightGray       |
| チップ          |          lightGray |      darkGray       |
| チップテキスト             |              black |        gray        |
| ヘッダーバックグラウンド          |          Green |      black       |
| タブバーバックグラウンド          |          lightGray |      darkGray       |


基本的には上記の要素のみカラーを変更して、それ以外は通常時とダークモードでカラーに差分は無いようしました。

## 画像の変更

例えば下のような画面を単純にカラー定義に当てはめると。。。
<img width="150" alt="アイコン画面.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170824/9a9c6026-f5ed-6d6a-29af-918caf5dfd58.png">

こんな感じになり画像と背景色が、かぶって画像が見えづらくなりました。
<img width="150" alt="Simulator Screen Shot - iPhone X - 2019-09-20 at 20.06.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170824/373fb997-ecda-2630-fbd4-2f1aa3ab2d83.png">

なので色を反転した画像を用意して、こんな感じになりました。（+ステータスバーのカラーも変えました）
<img width="150" alt="Simulator Screen Shot - iPhone X - 2019-09-20 at 20.16.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170824/201d1f92-b278-0f50-7a47-e760291efde4.png">

これで良さ気です。


# コード面

## react-native-extended-stylesheetを使う

一般的にReactNativeのstyleの宣言は**StyleSheet**を使用すると思います。

https://facebook.github.io/react-native/docs/stylesheet

StyleSheetは最初にstyleを宣言してしまうので、あとから変数でカラーの値を変更しても値は動的に変更されません。
なので、StyleSheetのラッパーである**react-native-extended-stylesheet**を使用してカラー設定をしていきます。

■ react-native-extended-stylesheet
https://github.com/vitalets/react-native-extended-stylesheet

こちらのライブラリは最初にカラー定義を設定して

```jsx
import EStyleSheet from "react-native-extended-stylesheet";

const setLight = () => {
  EStyleSheet.build({
    $theme: "light",
    $background: "#fff",
    $text: "#333631"
  });
}

const setDark = () => {
  EStyleSheet.build({
    $theme: "dark",
    $background: "#333631",
    $text: "#fff"
  });
}

```

以下のようにstyleを指定します。

```jsx
import React from "react";
import { View, Text } from "react-native";
import EStyleSheet from "react-native-extended-stylesheet";

export default () => (
  <View style={styles.root}>
    <Text style={styles.text}>テスト</Text>
  </View>
)

const styles = EStyleSheet.create({
  root: {
    height: "100%",
    backgroundColor: "$background", // ← EStyleSheet.buildで定義した変数
  },
  text: {
    color: "$text", // ← EStyleSheet.buildで定義した変数
  }
});
```

あとは、以下のメソッドを実行すれば通常（Light）モード

```jsx
setLight()
```

以下のメソッドを実行すればダークモードとstyle内のカラー変更が可能になります。

```jsx
setDark()
```

・・・と言いたいところですが、これだけでは完全には変わりません。

以下のドキュメントに記載している通り、**一度レンダリングされてしまったコンポーネントに関してはEStyleSheet.buildを実行してもstyleの変更はされません**。値が適応されるのは再レンダリングのタイミングです。

■ react-native-extended-stylesheetの反映タイミング
https://github.com/vitalets/react-native-extended-stylesheet#theming

なので、ドキュメントの通り一旦render部分をnullにして、再レンダリングをさせる必要があります。
ペペロミアでは、Providerでカラー設定の制御を持ち、一旦アプリケーションのレンダリングをnullにしてから再レンダリングする実装でダークモードを実現しています。

■ PeperomiaNative/src/containers/Theme.tsx （カラー設定を制御するProvider）
https://github.com/wheatandcat/Peperomia/blob/d1c9308e9ec2a27ca7cbd3afc865652106fcf784/PeperomiaNative/src/containers/Theme.tsx

ここまで実装すればダークモード完成です。

## その他の使えそうな設定

今回は使用していませんが、各ライブラリにThemeの設定が存在するので、この辺を利用するともっと良い感じに実装できるかもしれません。

■ React Navigation Theme設定
https://reactnavigation.org/docs/en/themes.html

■ React Native Elements  ThemeProvider設定
https://react-native-training.github.io/react-native-elements/docs/customization.html#using-themeprovider



# まとめ

最初は1日くらいで出来るだろうと思いましたが、stylesheetで結構ハマって結局3日くらいかかってしまいました。
一応ネタのため実装してみましたが、正直まだReact Nativeでダークモードを実装するのは時期尚早な気がします。

iOS13とAndroid10のリリースで両方ともダークモードが実装され需用が高まるはずなので、もっと良い実装方法が増えそうな気がするので、今は待ったほうが良いかなーと実装して思いました。
（再レンダリングを明示的に書かなくてはいけないのは、余り良い実装な気もしないので。。。）

# Expoでダークモードを使ってみる
まだダークモード搭載のアプリはリリースはしていませんが、Expoにはデプロイしているので以下から実機で起動すること可能です。

https://expo.io/@wheatandcat/peperomia