🐈

Flutterで少しでもデザイン調整のコードを簡潔にするために

2022/02/25に公開

Flutter(に限らず)、アプリケーションを構築していく上で欠かせないのが、要素間の余白を調整したり、角丸を活用したりと、デザインの微調整が必要になってきます。

例えば、次のように余白や角丸の設定を行ってデザインの微調整を行うことを考えてみます。

Container(
  padding: EdgeInsets.symmetric(
    horizontal: 16.0,
    vertical: 12.0,
  ),
)

Column(
  children: [
    SomeWidget(),
    SizedBox(height: 32.0),
  ],
)

ClipRRect(
  borderRadius: BorderRadius.all(8.0),
  child: ...,
),

基本的にはこれでも何の問題もありません。ただ長期的にデザインをメンテナスしていったり、他の実装箇所とディテールを整えていく場合や、よりたくさんコードを書いて実装していく上で

  • 毎回 EdgeInsets.xxxBorderRadius.xxxを書かないと行けない
  • マジックナンバーを直接書いているため、今後デザインの変更で値を調整した場合に、書き換えるのが大変

といった問題が(人によっては)出てきます。特にEdgeInsetsは使用頻度がとても高く、都度あれを書いて定義していくのは辛いですよね。

そこで今回は私が実践している極力デザイン調整のための記述を簡潔にし、今後細かい値の変更をしても困らないようにする方法の一例を紹介してみようと思います。あくまでも一例として見てもらえればと。🙌

値の定義を集約する

次のように、Measureクラスを作成し、const定義を使って値の定義を行っていきます。
自分の場合は、単純な数値としての余白(Space)、RowやColumnの内部で使う余白(Gap)、Container Widgetなどに指定するEdgeInsets, 角丸やshapeの設定などに使うRadius,BorderRadiusを定義しています。

また、自分のアプリでは原則4の倍数で調整するようにしているため、基本的には4の倍数で値を揃えています。(Radiusに関しては2の倍数で細かく調整)

import 'package:flutter/material.dart';
import 'package:gap/gap.dart';

class Measure {
  // MARK: - Space

  static const double s_4 = 4;
  static const double s_8 = 8;
  static const double s_12 = 12;
  static const double s_16 = 16;

  // MARK: - Gap

  static const Gap g_4 = Gap(Measure.s_4);
  static const Gap g_8 = Gap(Measure.s_8);
  static const Gap g_12 = Gap(Measure.s_12);
  static const Gap g_16 = Gap(Measure.s_16);

  // MARK: - EdgeInsets(padding)

  static const EdgeInsets p_l4 = EdgeInsets.only(left: Measure.s_4);
  static const EdgeInsets p_t4 = EdgeInsets.only(top: Measure.s_4);
  static const EdgeInsets p_r4 = EdgeInsets.only(right: Measure.s_4);
  static const EdgeInsets p_b4 = EdgeInsets.only(bottom: Measure.s_4);
  static const EdgeInsets p_h4 = EdgeInsets.symmetric(horizontal: Measure.s_4);
  static const EdgeInsets p_v4 = EdgeInsets.symmetric(vertical: Measure.s_4);
  static const EdgeInsets p_a4 = EdgeInsets.all(Measure.s_4);
  static const EdgeInsets p_l8 = EdgeInsets.only(left: Measure.s_8);
  static const EdgeInsets p_t8 = EdgeInsets.only(top: Measure.s_8);
  static const EdgeInsets p_r8 = EdgeInsets.only(right: Measure.s_8);
  static const EdgeInsets p_b8 = EdgeInsets.only(bottom: Measure.s_8);
  static const EdgeInsets p_h8 = EdgeInsets.symmetric(horizontal: Measure.s_8);
  static const EdgeInsets p_v8 = EdgeInsets.symmetric(vertical: Measure.s_8);
  static const EdgeInsets p_a8 = EdgeInsets.all(Measure.s_8);
  static const EdgeInsets p_l12 = EdgeInsets.only(left: Measure.s_12);
  static const EdgeInsets p_t12 = EdgeInsets.only(top: Measure.s_12);
  static const EdgeInsets p_r12 = EdgeInsets.only(right: Measure.s_12);
  static const EdgeInsets p_b12 = EdgeInsets.only(bottom: Measure.s_12);
  static const EdgeInsets p_h12 = EdgeInsets.symmetric(horizontal: Measure.s_12);
  static const EdgeInsets p_v12 = EdgeInsets.symmetric(vertical: Measure.s_12);
  static const EdgeInsets p_a12 = EdgeInsets.all(Measure.s_12);
  static const EdgeInsets p_l16 = EdgeInsets.only(left: Measure.s_16);
  static const EdgeInsets p_t16 = EdgeInsets.only(top: Measure.s_16);
  static const EdgeInsets p_r16 = EdgeInsets.only(right: Measure.s_16);
  static const EdgeInsets p_b16 = EdgeInsets.only(bottom: Measure.s_16);
  static const EdgeInsets p_h16 = EdgeInsets.symmetric(horizontal: Measure.s_16);
  static const EdgeInsets p_v16 = EdgeInsets.symmetric(vertical: Measure.s_16);
  static const EdgeInsets p_a16 = EdgeInsets.all(Measure.s_16);

  // MARK: - Corner Radius

  static const r_2 = Radius.circular(2.0);
  static const r_4 = Radius.circular(4.0);
  static const r_6 = Radius.circular(6.0);
  static const r_8 = Radius.circular(8.0);

  // MARK: - Border Radius

  static const br_2 = BorderRadius.all(Measure.r_2);
  static const br_4 = BorderRadius.all(Measure.r_4);
  static const br_6 = BorderRadius.all(Measure.r_6);
  static const br_8 = BorderRadius.all(Measure.r_8);
}

確定数の命名としては、 {それぞれの種類を表すprefix}_{値}で統一しています。
EdgeInsetsに関してはLeft,Top,Right,Bottom,Horizontal,Vertical,Allをそれぞれ定義しているため定義料が多く、{値}の前にどこに余白がつくかの方向を示す英語の頭文字を付けています。

_(スネークケース)についてや、変数名の付け方は好みがあると思うので、

  • Measure.s16
  • Measure.space16
  • Measure.s_16px
  • Measure.p_horizontal_16

のようにカスタマイズしても良いと思います。自分が使っていて混乱しづらいものであれば良いかと思います。
また、xsmall, small, medium, large,...といったSML記法もありだと思います。

さらに、人によってはより簡素な形にするために、Measureというクラスに入れずに、直接トップレベルにs_16などと定義するのもありだと思います。(名前衝突には注意)

💡 Gapについて

Gapは、RowやColumnの中で、SizedBox(width/height:)の代わりに使用できる、大変便利なWidgetです。
SizedBox Widgetのように、複数のWidgetを束ねるWidgetのdirectionを考えてwidth/heightを設定して余白を作り出すことが不要で、Gap Widget自身がdirectionを判断して、指定した長さの余白を表現してくれます。

pubspec.yamlでgap: ^2.0.0と書くだけで簡単に導入できます。

(Optional: EdgeInsetsにcustom operatorを付与しておく)

Dartでは、Custom Operatorを定義することができ、EdgeInsetsに + operatorを定義してみようと思います。

extension CustomAddOperator on EdgeInsets {
  EdgeInsets operator +(EdgeInsets other) {
    return EdgeInsets.fromLTRB(
      left + other.left,
      top + other.top,
      right + other.right,
      bottom + other.bottom,
    );
  }
}

これを定義しておくことで、

EdgeInsets.symmetric(horizontal: 16.0).copyWith(bottom: 16)

が、

Measure.p_h16 + Measure.p_b16

のように書くことができるようになります。

使ってみる

Container(
  padding: Measure.p_h16 + Measure.p_v12,
)

Column(
  children: [
    SomeWidget(),
    Measure.g_32,
  ],
)

ClipRRect(
  borderRadius: Measure.br_8,
  child: ...,
),

もし将来的に値を調整したくなった場合でも、それぞれの箇所でマジックナンバーを代入していない分、ある程度一括で機械的に書き換えて更新or削除することが容易になります。

また、原則すべてconst定義できているため、関数の引数のデフォルトの値としても設定できます。

class PrimaryButton extends StatelessWidget {
  PrimaryButton({this.outerPadding = Measure.p_a16});
  
  final EdgeInsets outerPadding;
}

説明はこれで以上になります。もしあなたのチームやプロダクトでよりよい方法で管理しているよ!などがあれば気軽に教えて下さい。


以下は余談です。

本来実現したかった形

Measure.s.px4のように、ドット記法でそれぞれの種類を分けつつ、値を取得できるようにするために、以下のように最初は実装することを試みました。

class Measure {
  static const s = Space._();
}

class Space {
  const Space._();
  
  final px4 = 4.0;
  ...
}  

しかし、このように実装した場合、 Measure.sはconstですが、Measure.s.px4はfinal指定になるため、引数のデフォルトの値として使用することができなくなります。

class PrimaryButton extends StatelessWidget {
  PrimaryButton({this.outerPadding = Measure.p.16}); // Error: Default values of an optional parameter must be constant.
  
  final EdgeInsets outerPadding;
}

また、static const px4 = 4.0;と定義した場合、今度はMeasure.sからはアクセスできなくなってしまいます。
次のように、Measure.sでSpaceのType自体を返せればよかったのですが、Type型はありつつも、そこからSpace型そのものを参照はできないためこれもだめでした。

// NOTE:これは仮のコードで実際にはこのようには書けません
class Measure {
  static const Type<Space> s = Space;
}

class Space {
  static const px4 = 4.0;
  ...
}  

これらを受けて、値の定義をconstにしつつ、うまく値を集約するには

  • Measureという方にすべて集約して、ネストさせない
  • Spaces,Gaps, Paddings, ...とそれぞれの種類ごとにclassを定義して値を集約する
  • class定義して中に入れるのをやめて、トップレベルに定数定義する

となり、自分の場合は一番目の案を採用することにしました。

Swiftなどの他の言語ではNested Class定義ができて、Measure.Padding.h16 みたいな記述で実現できるのですが、上記で挙げた問題があり、Dartでは実現が厳しそうということでこのアプローチは諦めました。const指定できなくなることに目を瞑れるなら、Measure.padding.h16Measure.g.px16のようにできるかと思います。

Discussion