Flutterでアプリ全体を統一感のあるデザインにする方法

FlutterのThemeDataの使い勝手がとてもよかったので紹介しようと思います。

iOSもAndroidもアプリ全体を通してデザインの統一感をもたせようとすると地味に手間がかかります。
AndroidのStyleやThemeはいまいち使い勝手が良くないと思いますし、iOSではそもそも全体を統一するための仕組みが無いように思います。
アプリで使う色やフォントの定義ファイルを作って都度設定するような実装になってしまうことも多いのではないでしょうか？

FlutterのThemeはまずアプリ全体に適用するためのThemeDataを作成します。といってもデフォルト値が設定されているので、何もしなくてもよいです。
AppBarやFloathingActionButtonなど組み込みのウィジェットの多くは、Themeのスタイルを参照するようになっているので何も指定しなければThemeに準じた見た目になります。
またカスタムWidgetでもThemeに合うように色やフォントを指定したい場合がありますが、その場合は`Theme.of(context).primaryColor`のようにすることで簡単に色やStyleにアクセスできます。

具体的に使い方を見ていきましょう。


# ThemeDataの適用
MaterialApp作成時にThemeDataを適用します。

```dart
@override
Widget build(BuildContext context) {
return new MaterialApp(
  // ThemeDataでThemeを編集する
  theme: ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.lightBlue[800],
    accentColor: Colors.cyan[600],
  ),
  home: Center(
    child: Text("Hoge"),
  ),
);
}
```

# ThemeDataについて

ThemeDataはこんなにたくさんプロパティがあるので、必要なときに設定していくスタンスが良いと思います。

```dart
  factory ThemeData({
    Brightness brightness,
    MaterialColor primarySwatch,
    Color primaryColor,
    Brightness primaryColorBrightness,
    Color primaryColorLight,
    Color primaryColorDark,
    Color accentColor,
    Brightness accentColorBrightness,
    Color canvasColor,
    Color scaffoldBackgroundColor,
    Color bottomAppBarColor,
    Color cardColor,
    Color dividerColor,
    Color highlightColor,
    Color splashColor,
    InteractiveInkFeatureFactory splashFactory,
    Color selectedRowColor,
    Color unselectedWidgetColor,
    Color disabledColor,
    Color buttonColor,
    ButtonThemeData buttonTheme,
    Color secondaryHeaderColor,
    Color textSelectionColor,
    Color textSelectionHandleColor,
    Color backgroundColor,
    Color dialogBackgroundColor,
    Color indicatorColor,
    Color hintColor,
    Color errorColor,
    String fontFamily,
    TextTheme textTheme,
    TextTheme primaryTextTheme,
    TextTheme accentTextTheme,
    InputDecorationTheme inputDecorationTheme,
    IconThemeData iconTheme,
    IconThemeData primaryIconTheme,
    IconThemeData accentIconTheme,
    SliderThemeData sliderTheme,
    ChipThemeData chipTheme,
    TargetPlatform platform,
  }) {}
```

# 標準のWidgetでThemeDataの値が使用される仕組み

この状態でScaffoldを作成すると以下のような見た目になります。AppBarにはThemeDataのprimaryColorが使用されていますね。

<img src="https://qiita-image-store.s3.amazonaws.com/0/18273/815884a8-f041-d4f8-4d28-7561426f89f1.png" width="300px">

これは AppBar ウィジェットで次のようにbackgroundColorが指定されていない場合に、ThemeData.primaryColorを使用するという実装のためです。

```dart 
    return new Semantics(
      container: true,
      explicitChildNodes: true,
      child: new Material(
        color: widget.backgroundColor ?? themeData.primaryColor, // ここ
        elevation: widget.elevation,
        child: appBar,
      ),
    );
```

# ThemeDataの値を使用する
ThemeDataの値にアクセスしてThemeに準じた見た目にすることも簡単です。
Drawerウィジェットを例にとって説明してみます。

デフォルトの状態ではこのようにDrawerのヘッダの背景は白です。

```dart
  return Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: <Widget>[
        DrawerHeader(
          child: Text( 'Drawer Header'
          ),
        ),
        ListTile(
          selected: true,
          title: Text('Hoge'),
          onTap: () {
            Navigator.pop(context);
          },
        ),
      ],
    ),
  );
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/18273/4ea2006c-96f7-85a9-9e97-2fad1ebcfecb.png" width="300px">

ここでアプリのプライマリカラーをDrawerのヘッダの背景にしたい場合は次のようにします。
`Theme.of(context).primaryColor` でプライマリカラーを取得し、 `Theme.of(context).primaryTextTheme.title` でプライマリカラーの上にのるテキストのStyleを取得しています。

```dart
  return Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: <Widget>[
        DrawerHeader(
          child: Text( 'Drawer Header',
            style: Theme.of(context).primaryTextTheme.title,
          ),
          decoration: BoxDecoration(
            color: Theme.of(context).primaryColor,
          ),
        ),
        ListTile(
          selected: true,
          title: Text('Hoge'),
          onTap: () {
            Navigator.pop(context);
          },
        ),
      ],
    ),
  );
```

すると、このような見た目になりThemeDataが簡単に使用できたことがわかります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/18273/37168283-06ed-e931-e2c9-087be482e040.png" width="300px">

# テキストのテーマ
先ほどDrawerのヘッダのテキストに `Theme.of(context).primaryTextTheme.title` とスタイルを指定しました。
これはtitle用のスタイルを適用したことになります。
マテリアルデザインには [ここ](https://material.io/design/typography/the-type-system.html#type-scale) で説明されているように用途ごとにスタイルを分けるという考えがされています。
FlutterでもTextThemeクラスで display4, display3, display2, display1, headline, title, subhead, body2, body1, caption, button という用途のテーマが用意されているので、
基本的には個別のTextウィジェットにフォントサイズなどを指定するのでなく、ThemeDataから参照できるテキストのテーマから選択するのがよさそうです。
つまりこのリストのStyleを参照することで実装の手間も減り、アプリ全体がマテリアルデザインとして統一感を持つことになります。


# まとめ
ThemeDataにはよく考えられたデフォルトの設定があり、また設定値へのアクセスが簡単なのでアプリ全体で統一感のある見た目にすることが簡単になります。
マテリアルデザインの考え方を復習しつつFlutterアプリではThemeDataを積極的に活用していくことで、上質なマテリアルデザインのアプリはすばやく開発できるようになりそうですね。