Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
49
Help us understand the problem. What are the problem?

FlutterのThemeに任せてMaterial Designを適用する

Material Designとは?

Material Designとは、Googleが提唱している、デザインの方針を定める一連のガイドラインみたいなものです。

これに従ってアプリをデザインすると、美しくて使いやすいアプリをデザインすることができます。

Material Design

FlutterのデザインやレイアウトのシステムはMaterial Designに準拠していますので、自分のアプリを作るときも極力それに逆らわず、流れに乗り、製作者の意図を汲んで、Material Designに準拠したアプリを作っていきたいものです。もっと細かくこだわりたい場合は別ですけれどもね。

Material DesignにおけるTheme

Implementing your theme

ざっくりリンク先のページを見ていただくと、次の4つを決めてアプリ全体に適用すると、統一感のある美しいアプリにできるという旨が書いてあります。

  • Color
  • Typography
  • Shape
  • Icons

一つずつ簡単に説明します。

Color

配色のことです。次の12個の色を決め、アプリ全体で使用すると良いです。

12個全部が別の色になっていなくてもいいです。

Material Designデフォルトの配色例がこれです。

Material Designデフォルトの配色

Implementing your themeのColorの所より引用しました。

Typography

文字組みのことです。次の13個のスタイル(フォント・大きさ・太さ・色など)を決め、アプリ全体で使用すると良いです。

Material Designデフォルトのタイポグラフィ設定例がこれです。

Material Designデフォルトのタイポグラフィ設定例

Implementing your themeのTypographyの所より引用しました。

Shape

ボタンをはじめとする様々な要素・コンポーネントの縁の形です。

「直角にする」「丸くする」「左上だけ斜めに切り落とす」などが考えられます。

一つ方針を決めて、アプリ全体で使用すると良いです。

Icons

同じ目的のアイコンでも、「丸っこいもの」「角張ったもの」「中が塗られたもの」「塗られてないもの」など、いくつか種類があります。

これも一つ方針を決めて、アプリ全体で使用すると良いです。

FlutterにおけるTheme

FlutterにはThemeという仕組みがあり、これを指定するとアプリ全体に自動的に適用され、統一感のあるアプリを作ることができます。

ThemeDataという、一通りの設定を持ったクラスのインスタンスを、MaterialAppThemeというWidgetに渡してWidgetツリーに組み込めば、その傘下のWidgetすべてに適用してくれます。

しかし、このThemeDataには大量の設定項目(プロパティ)があり、一つずつ設定するのはとても大変です。

こちらのリンク先に飛んだ瞬間、引数の多さに驚きます。

ThemeData constructor

また、各プロパティにバラバラの色を設定することができてしまうので、統一感がなくなるリスクもあります。

FlutterのThemeにMaterial Designを適用する

ThemeData.fromコンストラクタ

ThemeDataクラスには、ThemeData.fromというコンストラクタがあります。

このコンストラクタには、colorSchemetextThemeという引数を与えることができ、これがまさにMaterial DesignにおけるThemeのColorとTypographyに対応しているのです!

ThemeData.from constructor

リンク先のドキュメントにも

This is the recommended method to theme your application.

と、オススメであるとハッキリ書いてあります!

ということで、これを使いましょう。

MaterialApptheme引数に与えて

MaterialApp(
  theme: ThemeData.from(
    colorScheme: 何かしらのColorScheme,
    textTheme: 何かしらのTextTheme,
  ),
  home: ScaffoldとかのWidget
)

という感じの構造にします。

Color

ColorScheme class

ColorSchemeクラスのコンストラクタは、まさにMaterial Designで規定されている12個の色を引数に取ります。

12個全て指定してもいいですが、下記のようにすると、基本はデフォルトの色を使いつつ、一部だけ異なる色を指定することができます。

この例では、primaryのみColors.redに変え、あとはデフォルトの色を使うことになります。

ThemeData.from(colorScheme:ColorScheme.light(primary:Colors.red))

また、Widgetツリーの子孫から呼び出して読み取る場合もThemeDataから直接読み取るのではなく、一度ColorSchemeを経由するといいでしょう。

color: Theme.of(context).colorScheme.primary,

Typography

TextTheme class

TextThemeクラスは、まさにMaterial Designで規定されている13種類のスタイルを引数に取ります。

以前はそうなっていませんでしたが、最近変更されてMaterial Design準拠になりました(執筆日:2021年2月11日)。古い引数はdeprecatedになっています。その経緯により、一部、Material Designで規定されているものと名前が違います。

最新のMaterial Designに準拠したデフォルトのTextTheme

Typography.material2018().black
Typography.material2018().white

の2つです。明るい色を基調としたアプリの場合は文字色はblackにするため前者を、ダークテーマの場合は後者を選ぶといいでしょう。

これらを元に、色を変える場合はcopyWithを使ってこんな感じにしましょう。

Typography.material2018().black.copyWith(
          headline1: TextStyle(color: Colors.brown),
          subtitle1: TextStyle(color: Colors.orange), 
          bodyText1: TextStyle(color: Colors.pink),
          ...
        )

GoogleFontsを併用する場合はこんな感じ。

GoogleFonts.abrilFatfaceTextTheme(Typography.material2018().black.copyWith(
          headline1: TextStyle(color: Colors.brown),
          ...
        ))

なお、このTextThemeを使う場合、フォントサイズや太さは変えるべきではないです。

根拠はこちらのページblack property

This TextTheme should provide color but not geometry (font size, weight, etc). A text theme's geometry depends on the locale.

とある通り、各プロパティのTextStyleのフォントサイズや太さは、言語設定(Locale)に依存してFlutterが自動で設定するからです。

ScriptCategoryについて

Internationalizing Flutter appsに従って正しくローカリゼーション設定をした場合、Flutterが言語設定に従ってTextThemeの各TextStyleのフォントサイズや太さやを指定してくれます。

その設定はScriptCategory enumとして次の3種類が定義されています。

  • englishLike
    • 英語など
  • dense
    • 日本語など
  • tall
    • タイ語など

開発者である我々はこれを深く意識することなく、先程の

Typography.material2018().black
Typography.material2018().white

を使っておけば、Flutterが勝手に設定してくれます。

指定したい場合は

Typography.material2018().englishLike

などとすればよいです。

ShapeとIconsについて

ShapeとIconsについては、全体的なテーマを一括で指定する方法はなさそうです。

Shape

ThemeDataの引数CardThemeElevatedButtonThemeなどをそれぞれ指定するしかないと思います。ThemeData.fromコンストラクタにはこれらの引数はありませんので、.copyWithを使って

ThemeData.from(
  colorScheme: yourColorScheme,
  textTheme: yourTextTheme,
).copyWith(
  elevatedButtonTheme: yourElevatedButtonThemeData,
  outlinedButtonTheme: yourOutlinedButtonTheme,
  cardTheme: yourCardTheme,
  dialogTheme: yourDialogTheme,
  ...
)

という感じです。面倒ですね。

Buttonにスタイルを適用する方法についてはこの記事がわかりやすいと思います。

FlutterのButton系Widgetが置き換えられて変わったところ

Icons

IconWidgetを使う度に、自分で決めた統一的な基準に従ってアイコンを選択しましょう。

IconThemeData classというものもありますが、これはcolor, opacity, sizeを指定するものなので、Material Designで言われているアイコンの形状に関するものとは別物です。

何がどこに適用されるか

ColorSchemeTextThemeを使って色やフォントを指定した場合、デフォルトではどこに適用されるのでしょう?

検証用アプリを作って検証してみましたのでその結果をご紹介します。なお、全てを列挙できているわけではないのでご了承ください。

下記のリストにないもの(ColorSchemeのprimaryVariantやTextThemeのheadline1など)は恐らくデフォルトで使われていないと思われますが、自分でそのプロパティをWidgetツリーの下部から指定すればもちろん使用可能です。

ColorScheme

  • primary
    • appBarの背景
    • ElevatedButtonの色
    • OutlinedButton, TextButtonのテキストの色
    • BottomNavigationBarの選択されているもの
    • ダイアログのテキストボタン
  • secondary
    • FloatingActionButtonの色
    • スクロールが端までいった事を示すエフェクト(Androidのみ)
  • surface
    • Cardの色
    • ライセンスページのロード完了後の背景色
  • background
    • Scaffoldのbodyの背景色
    • ダイアログの背景色
    • Drawerの背景色
    • BottomNavigationBarの背景色
    • ライセンスページのロード中の背景色
  • error
    • TextFormFieldのvalidation失敗時の色
  • onPrimary
    • ElevatedButtonのテキストの色
    • AppBar内の選択中のタブを示す横棒
  • onSecondary
    • FloatingActionButtonのchildの色

TextTheme

  • headline5
    • AboutDialogやライセンスページのapplicationName
  • headline6
    • AlertDialogのtitle
  • subtitle1
    • ListTileのtitle
    • AlertDialogのcontent
    • AboutDialogのchildren
    • ライセンスページのパッケージ名
  • bodyText1
    • DrawerHeaderのchild
    • Drawer内のListTileのtitle
  • bodyText2
    • 通常のText
    • Cardのchild
    • AboutDialogやライセンスページのバージョン名
  • caption
    • ListTileのsubtitle
    • BottomNavigationBarの選択されていないもの
    • AboutDialogのapplicationLegalese

BottomNavigationBarの中身が、選択時はColorSchemeprimaryColorで、非選択時はTextThemecaptionの色になるのは面白いと思いました。

通常の本文テキストがbodyText1ではなくbodyText2なのも面白い、というか罠ですね。

何も適用されない主な場所

  • AppBar内の諸項目
    • title
    • leading
    • actions
    • bottomに配置するTabBar内のIconやText

AppBarの中では、背景色がprimary、選択中のタブを示す横棒がonPrimaryになる以外、どの色も適用されないようです。
AppBarにTextThemeを適用する方法については後述します。

  • ListTile両端のアイコン
  • 非選択時のTextFormFieldの下線やラベルの色
  • disabled状態のボタン

これらは灰色のままです。

検証用アプリの紹介

今回の検証にはこちらのアプリを使用しました。

GitHubからcloneして起動すれば使えるはずです。

このアプリの各所に指定されている色と、ソースコードで指定している色を見比べてみてください。

BottomNavigationBarからcolorSchemeというボタンを押すと、colorSchemeだけをいろいろ指定したモードになります。

Simulator Screen Shot - iPhone 8 - 2021-02-14 at 21.22.20.png

BottomNavigationBarからtextThemeというボタンを押すと、textThemeだけをいろいろ指定したモードになります。この時のcolorSchemeには白・黒・グレーだけを使用したColorSchemeを指定しているので、色がついているところはtextTheme由来だということが明確になります。

Simulator Screen Shot - iPhone 8 - 2021-02-14 at 21.22.39.png

なお、アイコンの選択は完全にテキトーです。

また、アプリの目的上できるだけバラバラに色を指定していますので、デザインとして美しいかどうかはお察しください。特にtextThemeだけ指定してある方、ひどいですね。

それから、Localeを変化させることでScriptCategoryを3通り(englishLike, dense, tall)変化させることができるようにしてありますが、その際表示する文字列は変化しません(英語から日本語やタイ語になったりはしません)。同じ文字列で比較することでScriptCategoryによる違いを見るためです。

ただし、Flutterデフォルトの文字列が使われている所(AboutDialogのライセンス表示ボタンなど)は言語が変化します。

Drawerを開いて、AboutDialogやライセンスページも見てみてくださいね。

じゃあAppBarにTextThemeを適用する方法は?

最後に、AppBarにTextThemeを適用する方法をご紹介します。

良くない方法

「そんなん、MaterialAppのtheme引数に渡すThemeDataのappBarThemeに適当なAppThemeDataを渡すだけじゃん」と思うかもしれません。

その通りなのですが、そこに罠があります。

AppBarのtitleテキストのフォントを、GoogleFontを使って変えたいとします。

先程のようにThemeData.fromコンストラクタを使ってThemeDataを作った後、.copyWithappBarThemeを上書きしてみます。

final myThemeData = ThemeData.from(
  colorScheme: ColorScheme.light(), // 好きなColorSchemeでOK
  textTheme: Typography.material2018().black, // 好きなTextThemeでOK
).copyWith(
    appBarTheme: AppBarTheme(textTheme: GoogleFonts.pacificoTextTheme()));

結果はこんな感じ。

スクリーンショット 2021-02-15 23.55.45.png

確かにフォントは変わっていますが、なんだか小さいです。

色も黒くて見にくいし、左のハンバーガーメニューと色が違っていて変です。

これは、もともと使われていたAppBar用のTextThemeを完全無視して、新たに生成したTextThemeを渡してしまったせいです。

解説

そもそもどうしてThemeData.fromコンストラクタに渡したTextThemeは、AppBarに適用されないのでしょうか?

実は、ThemeData.fromコンストラクタに渡したTextThemeはそのままThemeDataのtextThemeプロパティに渡されるのですが、

一方で、AppBarはThemeDataのprimaryTextThemeプロパティを読みに行くからです。

ではじゃあprimaryTextThemeにもtextThemeプロパティと同じTextThemeを渡せばいいかと言うと、そういうわけでもない。

primaryTextThemeは、primaryに設定した色の上に表示した時に見えやすい色を設定する必要があります。単にtextThemeプロパティと同じにしてしまうと、たいへん見づらい結果を招くことがあるのです。

ちなみに、iconThemeprimaryIconThemeというプロパティもあり、同様の構造になっています。

良い方法

ということで、もともとprimaryTextThemeに設定されているTextThemeを可能な限り活かしながら、最低限のフォント(と、ついでに色)を変更することにします。

primaryIconThemeも同様にします。

このようにすると良いでしょう。

まずThemeData.fromを使ってThemeDataをつくります。

final _tempThemeData = ThemeData.from(
  colorScheme: ColorScheme.light(), // 好きなColorSchemeでOK
  textTheme: Typography.material2018().black, // 好きなTextThemeでOK
);

これを元に、primaryTextThemeを、もともと設定されていたものを最大限残しながら部分的に変更します。(iconThemeも同様)

final themeData = _tempThemeData.copyWith(
  primaryTextTheme:
      GoogleFonts.pacificoTextTheme(_tempThemeData.primaryTextTheme).apply(
    bodyColor: Colors.yellow,
  ),
  primaryIconTheme:
      _tempThemeData.primaryIconTheme.copyWith(color: Colors.yellow),
);

GoogleFontsではなく手元のフォントファイルを指定する場合はこう。

final themeData2 = _tempThemeData.copyWith(
  primaryTextTheme: _tempThemeData.primaryTextTheme.apply(
    fontFamily: 'DancingScript',
    bodyColor: Colors.yellow,
  ),
  primaryIconTheme:
      _tempThemeData.primaryIconTheme.copyWith(color: Colors.yellow),
);

これでうまくいきます。

スクリーンショット 2021-02-16 0.15.06.png

大きさも適切で、色設定もフォント設定もうまくいきましたね。

「ハンバーガーメニューは元の色でいいわ!」という場合はprimaryIconThemeプロパティの設定をしなければいいです。

ちなみにですが、AppBarのtitleにはprimaryTextThemeに渡したTextThemeのheadline6プロパティにあるTextStyleが適用されます。へ〜。

補足

ThemeDataのappBarThemeプロパティにAppBarThemeを渡しても良かったのですが、primaryTextThemeプロパティを設定しておくほうがより全体に包括的に設定できるので良いと思います。

具体的には、CircleAvatarやFlexibleSpaceBar、SearchPageなどを使った場合に違いが出そうです(ソースコードより推察。未検証)。

おわり

おわりです。

用意されている機能を極力そのまま利用して、統一感のあるアプリを作っていきましょう!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
49
Help us understand the problem. What are the problem?