Flutter|カラー、テキストスタイル、ウィジェットスタイルの設定

Devlog
Devlog

スタイル設定をまとめて記述してみる

Flutterアプリを作成中、こんな課題にぶつかったので、思い切ってスタイル設定を全て一箇所にまとめてみました。

  • テーマカラーのカラーコードを直接記載するのが面倒
  • テーマカラーを調整するときに全て修正しなくてはいけないのが非効率
  • 登場箇所が多すぎて、テキストの色を揃えきれない
  • テキストサイズやスタイルの記載が面倒だしコードが長くなる
  • パーツのサイズ調整をするときに、複数のファイルを横断して数字を書き換えなくてはいけない
  • レスポンシブなデザインにするために、サイズ指定の数値を調整する(可能性がある)ので、多くのファイルに散らばっていると漏れがありそう

他の記事のコードでも登場してしまうので、都度言い訳もくどいなあと思い、まとめて記事にしてみます!

設定ファイル

全て自作クラスを作成し、パラメータを持たせていきます。使うときはクラス+変数名で呼び出します(使う時のコードは後述)。

カラー設定

テーマカラー、グレートーンを複数種類、文字色、その他アクセントやアイコンに使うカラーを作成。ダイヤログの背景やグレーアウトなどで透過色も使うので、同じクラスに設定します。

また、4色で色分けするエリアやメッセージアイコンの種類別の色分けもここで設定。都度コード内で色を呼び出すよりも、管理がしやすいと思います。メッセージアイコンの色は、messageType という整数で自動的に色分けできるようにしています。

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

class MyTheme {
  // ベース
  static const green = Color(0xFF94CDC7);
  static const viGreen = Color.fromARGB(255, 82, 137, 131);
  static const red = Color(0xFFff3503);
  static const grey = Color(0xFF808080);
  static const background = Color(0xFFEDEDED);
  static const onBackground = Color(0xFFFFFFFF);
  static const middleBackground = Color(0xFFF8F8F8);
  static const greyShadow = Color(0x80808080);
  static const middleGrey = Color(0xFFCACACA);
  static const text = Colors.black87;
  static const blue = Color(0xFF03CDFF);
  static final paleBlue = Color(0xFFabcad1);
  static const pink = Color(0xFFE52B50);
  static const systemIconColor = Color(0xFF3f4947);
  // Opacity
  static const opacity = Colors.transparent;
  static final requestDialogColor = MyTheme.green.withValues(alpha: 0.8);
  static final userItemIconColor = MyTheme.red.withValues(alpha: 0.6);
  static final userItemIconBackGroundColor = MyTheme.red.withValues(alpha: 0.1);
  static final whiteOpacColor = MyTheme.onBackground.withValues(alpha: 0.5);
  static final whiteOutColor = MyTheme.onBackground.withValues(alpha: 0.8);
  static final backGroundOutColor = MyTheme.background.withValues(alpha: 0.7);
  static final greyOutColor = MyTheme.grey.withValues(alpha: 0.3);
  static final blackOutColor = Colors.black.withValues(alpha: 0.5);
  static final multiOneInputAreaColor = MyTheme.middleBackground.withValues(alpha: 0.8);
}

Map<int, Color> partsColor = {
  0: MyTheme.green.withValues(alpha: 0.9),
  1: MyTheme.pink.withValues(alpha: 0.6),
  2: MyTheme.red.withValues(alpha: 0.6),
  3: MyTheme.blue.withValues(alpha: 0.6),
};

Color iconColor(int messageType) {
  Color returnColor = switch (messageType) {
    0 => MyTheme.green, // お知らせ
    1 => MyTheme.blue.withValues(alpha: 0.6), // 問い合わせの受付履歴
    2 => MyTheme.pink.withValues(alpha: 0.6), // AIヒント
    _ => MyTheme.grey,
  };
  return returnColor;
}

サイズ設定

基本的にはコードに実数を埋め込まないように、ウィジェットサイズや比率などをまとめていきます。量が多いので命名規則が難しかったですが、レスポンシブ対応を考えると、一箇所にまとまっている方がやりやすいと思います!

sz は、screenutilで比率を設定するためにサイズ指定箇所に掛け算をしています。比率は変える必要がないのでsz倍にはしていません。変数にせずに直接 .w を記載しても良いと思います。

MediaQuery.of(context).size.width, MediaQuery.of(context).size.height は、画面回転や折りたたみデバイスの調整時にいろいろ書き換えて実験したため変数をかませています。これも本来は変数を介さなくて良いと思います。

Dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

class MySize {
  // -----------------------------------------
  // Pages
  // -----------------------------------------
  ////////// Home /////////////////////
  final double noMessageContainerHeight = 100 * sz;

  ////////// カレンダー ////////////////
  final double calenderColorBarHeight = 3 * sz;
  final double calenderDropDownWidth = 200 * sz;
  final double calenderDropDownHeight = 50 * sz;
  final double calenderLineWidth = 0.5 * sz;
  static double calenderDayWidth(BuildContext context) => fieldWidth(context) / 7;
  static double calenderPaddingSize(BuildContext context) => calenderDayWidth(context) / 35;
  // ドロップダウンサイズ
  static double dropDownListWidth(BuildContext context) => fieldWidth(context) * 0.4;
  final double dropDownListAllGreyOutHeight = 235 * sz;
  static double dropDownListHelpTextWidth(BuildContext context) =>
      dropDownListWidth(context) * 1.03;

  // -----------------------------------------
  // Record
  // -----------------------------------------
  ////////// Record icon ///////////////
  static const int iconCrossAxisCount = 4;
  static const int homeIconsNumber = 11;
  static const double iconMainAxisExtentRatio = 2; // 横に対し縦の長さ
  final double iconSize = 55.0 * sz;

  ////////// Record input / AllData /////////////
  final double recordTableRowPad = 4.0 * sz;
  static const double recordMyTableTitleHeight = 0;
  final double recordInputPad = 16 * sz;
  final double recordTitleWidthS = 100 * sz;
  final double recordTitleWidthM = 120 * sz;
  final double recordTitleWidgetRadius = 8 * sz;
  final double recordMultiAreaNumberWidth = 70 * sz;
  static const double recordMultiAreaNumberTileRatio = 0.7;
  final double recordMultiAreaHeight = 600 * sz;
  final double alldataTitlePad = 12 * sz;

  ////////// Record Widgets /////////////
  final double selectChipLineWid = 0.5 * sz;
  final double selectChipWrapSpacing = 6.0 * sz;
  final double selectChipWrapRunSpacing = 1.0 * sz;
  final double segmentedLineWid = 0.5 * sz;
  final double segmentedHeight = 25.0 * sz;
  final double segmentedRadius = 20.0 * sz;
  static double halfInputArea(BuildContext context) => fieldHeight(context) * 0.4;
  // number = 1の幅
  static double segmentedWidth(BuildContext context) {
    final tableWidth = fieldWidth(context) - 16 * sz * 2;
    final ratio = recordTableRatio / (1 + recordTableRatio);
    final widgetAreaWidth = tableWidth * ratio;
    return widgetAreaWidth - 4.0 * sz * 2;
  }

  // number != 1の幅
  static double multiSegmentedWidth(BuildContext context) {
    final ratio = recordTableRatio / (1 + recordTableRatio);
    return segmentedWidth(context) - (16 * sz * 2) * ratio;
  }

  // -----------------------------------------
  // Setting
  // -----------------------------------------
  ////////// signUpAndIn ///////////////
  final double logoSize = 75 * sz;
  final double signinButtonHeight = 40 * sz;
  static double signinButtonWidth(BuildContext context) => fieldWidth(context) * 0.7;

  ////////// SettingMenu ///////////////
  final double settingMenuWidth = 260 * sz;
  final double settingOffsetWidth = 20 * sz;
  final double settingIconSize = 22 * sz;

  ////////// User Data /////////////////
  final double userDataPartsHeight = 100 * sz;
  static const double userDataTableRatio = 3;

  ////////// Routine Setting ///////////
  final double routineTileExtent = 45 * sz;
  final double medicineTileExtent = 50 * sz;
  static const double medicineTextFieldRatio = 0.75;
  static double medicineTextWidth(BuildContext context) => fieldWidth(context) * 0.6;

  ////////// User Item Setting //////////
  final double userItemCarouselHeight = 410 * sz;
  static const double userItemCarouselFractionRatio = 0.8;
  final EdgeInsets userItemCardPad = MyPad().ega16;
  final EdgeInsets userItemCardMargin = MyPad().ega16;
  final double userItemCardHeight = 375 * sz;
  final double userItemCardRadius = 25 * sz;
  final double userItemAddCardIconSize = 36 * sz;
  final double userItemAddCardIconButton = 60 * sz;
  final double userItemTextFieldHeight = 62 * sz;
  final double userItemAreaHeight = 200 * sz;
  final double userItemCompleteTextWidth = 75 * sz;
  final double userItemCompleteSegmentWidth = 230 * sz;
  final double userItemCompleteSegmentHeight = 30 * sz;
  final double userItemCompleteSegmentLineWid = 1 * sz;
  final double userItemCountDropDownWidth = 100 * sz;
  final double userItemCountCounterWidth = 150 * sz;
  final double userItemIconSegmentWidth = 320 * sz;

  ////////// Request /////////////////////
  static double requestInputWidth(BuildContext context) => fieldWidth(context) * 0.7;
  static double requestInputHeight(BuildContext context) => fieldHeight(context) * 0.4;

  ////////// アカウント削除 ////////////////
  final double deleteAccountWidth = 300 * sz;
  final double deleteAccountHeight = 50 * sz;

  // -----------------------------------------
  // その他
  // -----------------------------------------
  ////////// Common Widget ///////////////
  final double widgetElevation = 2 * sz;
  final double navigationBarHeight = 60 * sz;
  final double navigationIconSize = 20 * sz;
  final double dropDownMenuHeight = 300 * sz;
  final double minDropDownHeight = 45 * sz;
  final double dialogTextWidth = 230 * sz;
  final double saveButtonWidth = 50 * sz;
  final double saveButtonRadius = 8 * sz;
  final double deleteButtonWidth = 30 * sz;
  final double myDividerHeight = 8 * sz; // コンテンツ間の余白
  final double myDividerThickness = 0.5 * sz; // 線の太さ
  final double iconS = 10 * sz;
  final double iconM = 15 * sz;
  final double iconL = 20 * sz;
  final double iconLL = 25 * sz;
  final double myIndicatorSize = 60 * sz;
  static double notfDialogHeight(BuildContext context) => fieldHeight(context) * 0.4;
  static double notfDialogWidth(BuildContext context) => fieldWidth(context) * 0.7;
  static double errorPageHeight(BuildContext context) => fieldHeight(context);
  static double maintePageHeight(BuildContext context) => fieldHeight(context) * 0.5;
  static double maintePageWidth(BuildContext context) => fieldWidth(context) * 0.9;

  ////////// 汎用 アイコンやradius //////////
  final double three = 3 * sz;
  final double five = 5 * sz;
  final double fourteen = 14 * sz;
  final double fifteen = 15 * sz;
  final double twentyfour = 24 * sz;
}

// デバイスサイズに合わせて調整
final double sz = 1.w;

double fieldWidth(BuildContext context) => MediaQuery.of(context).size.width;
double fieldHeight(BuildContext context) => MediaQuery.of(context).size.height;

テキストスタイル

TextStyle型の設定を作成。文字色、サイズ、スタイル(通常と太字の2種類を使っています)の組み合わせの数だけ作成が必要です。リンクテキスト用にアンダーライン付きも用意。

また、色やサイズを変数として受け取るタイプも作成しています。これは、種類によって色を変えたり、文字数によってサイズを変えたりしている箇所があるため。

フォントサイズは、レスポンシブ対応の倍数を掛け算しておきます。

Dart
import 'package:flutter/material.dart';
import 'package:dayby/presentation/styles/sizes.dart';
import 'package:dayby/presentation/styles/theme.dart';

class MyText {
  // <------------- NavigationBar ------------->
  final navi = TextStyle(
    color: MyTheme.text,
    fontSize: myTextS,
    fontWeight: myLight,
  );

  // <---------------- Normal ----------------->
  // XXLサイズ 太字
  final xxlBold = TextStyle(
    color: MyTheme.text,
    fontSize: myTextXXl,
    fontWeight: myBold,
  );

  // XLサイズ 太字
  final xlBold = TextStyle(
    color: MyTheme.text,
    fontSize: myTextXl,
    fontWeight: myBold,
  );

  // XLサイズ
  final xl = TextStyle(
    color: MyTheme.text,
    fontSize: myTextXl,
    fontWeight: myLight,
  );

  // Lサイズ 太字
  final lBold = TextStyle(
    color: MyTheme.text,
    fontSize: myTextL,
    fontWeight: myBold,
  );

  // Lサイズ
  final l = TextStyle(
    color: MyTheme.text,
    fontSize: myTextL,
    fontWeight: myLight,
  );

  // Mサイズ
  final m = TextStyle(
    color: MyTheme.text,
    fontSize: myTextM,
    fontWeight: myLight,
  );

  // Mサイズ 太字
  final mBold = TextStyle(
    color: MyTheme.text,
    fontSize: myTextM,
    fontWeight: myBold,
  );

  // Sサイズ
  final s = TextStyle(
    color: MyTheme.text,
    fontSize: myTextS,
    fontWeight: myLight,
  );

  // XSサイズ
  final xs = TextStyle(
    color: MyTheme.text,
    fontSize: myTextXXS,
    fontWeight: myLight,
  );

  // <---------------- OnColor ----------------->
  // XLサイズ 太字
  final whxlBold = TextStyle(
    color: MyTheme.onGreen,
    fontSize: myTextXl,
    fontWeight: myBold,
  );

  // XLサイズ
  final whxl = TextStyle(
    color: MyTheme.onGreen,
    fontSize: myTextXl,
    fontWeight: myLight,
  );

  // Lサイズ 太字
  final whlBold = TextStyle(
    color: MyTheme.onGreen,
    fontSize: myTextL,
    fontWeight: myBold,
  );

  // Mサイズ
  final whm = TextStyle(
    color: MyTheme.onGreen,
    fontSize: myTextM,
    fontWeight: myLight,
  );

  // Mサイズ 太字
  final whmBold = TextStyle(
    color: MyTheme.onGreen,
    fontSize: myTextM,
    fontWeight: myBold,
  );

  // Sサイズ
  final whs = TextStyle(
    color: MyTheme.onGreen,
    fontSize: myTextS,
    fontWeight: myLight,
  );

  // <----------------- Color ----------------->
  // XLサイズ 太字
  final rexlBold = TextStyle(
    color: MyTheme.red,
    fontSize: myTextXl,
    fontWeight: myBold,
  );

  // Lサイズ 太字
  final relBold = TextStyle(
    color: MyTheme.red,
    fontSize: myTextL,
    fontWeight: myBold,
  );

  // Mサイズ
  final rem = TextStyle(
    color: MyTheme.red,
    fontSize: myTextM,
    fontWeight: myLight,
  );

  // Mサイズ 太字
  final remBold = TextStyle(
    color: MyTheme.red,
    fontSize: myTextM,
    fontWeight: myBold,
  );

  // Sサイズ
  final res = TextStyle(
    color: MyTheme.red,
    fontSize: myTextS,
    fontWeight: myLight,
  );

  // XLサイズ 太字
  final grxlBold = TextStyle(
    color: MyTheme.green,
    fontSize: myTextXl,
    fontWeight: myBold,
  );

  // Lサイズ 太字
  final grlBold = TextStyle(
    color: MyTheme.green,
    fontSize: myTextL,
    fontWeight: myBold,
  );

  // XLサイズ 太字
  final blxlBold = TextStyle(
    color: MyTheme.blue,
    fontSize: myTextXl,
    fontWeight: myBold,
  );

  // <-------------- GivenColor -------------->
  // Lサイズ 太字
  static TextStyle colBold(Color color) {
    return TextStyle(
      color: color,
      fontSize: myTextL,
      fontWeight: myBold,
    );
  }

  // Mサイズ 太字
  static TextStyle comBold(Color color) {
    return TextStyle(
      color: color,
      fontSize: myTextM,
      fontWeight: myBold,
    );
  }

  // Mサイズ
  static TextStyle com(Color color) {
    return TextStyle(
      color: color,
      fontSize: myTextM,
      fontWeight: myLight,
    );
  }

  // Sサイズ
  static TextStyle cos(Color color) {
    return TextStyle(
      color: color,
      fontSize: myTextS,
      fontWeight: myLight,
    );
  }

  // <-------------- GivenSize --------------->
  // Lサイズ 太字
  static TextStyle freeSize(double size) {
    return TextStyle(
      color: MyTheme.onBackground,
      fontSize: size,
      fontWeight: myBold,
    );
  }

  // <------------ GivenColor&Size ------------>
  // Mサイズ 太字
  static TextStyle freeColorAndSize(Color color, double size) {
    return TextStyle(
      color: color,
      fontSize: size,
      fontWeight: myBold,
    );
  }

  // <----------------- NoSize ----------------->
  static const noSize = TextStyle(
    color: MyTheme.text,
    fontWeight: myLight,
  );

  // <---------------- NoColor ----------------->
  final segmentM = TextStyle(
    fontSize: myTextMS,
    fontWeight: myLight,
  );

  final segmentS = TextStyle(
    fontSize: myTextXS,
    fontWeight: myLight,
  );

  // <---------------- WebLink ----------------->
  final mLink = TextStyle(
    color: MyTheme.blue,
    fontSize: myTextM,
    fontWeight: myLight,
    decoration: TextDecoration.underline,
    decorationColor: MyTheme.blue,
  );

  final lBoldLink = TextStyle(
    color: MyTheme.blue,
    fontSize: myTextL,
    fontWeight: myBold,
    decoration: TextDecoration.underline,
    decorationColor: MyTheme.blue,
  );

  // <------------- メッセージの行間 -------------->
  final textHeight = TextStyle(height: myTextHeight);
}

final double myTextXXl = 20 * sz;
final double myTextXl = 16 * sz;
final double myTextL = 15 * sz;
final double myTextM = 13 * sz;
final double myTextMS = 12 * sz;
final double myTextS = 11 * sz;
final double myTextXS = 10 * sz;
final double myTextXXS = 8 * sz;
final double adTextSize = 13 * sz;
final double myTextHeight = 1.7 * sz;

const FontWeight myLight = FontWeight.w400;
const FontWeight myBold = FontWeight.w500;

使い方

色の呼び出し例

Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dayby/presentation/styles/theme.dart';

class Home extends ConsumerWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {

    return Scaffold(
      backgroundColor: MyTheme.background,
      body: ListView(
        shrinkWrap: true,
        children: [
          ...
        ],
      ),
    );
  }
}

サイズ設定の呼び出し例

Dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:dayby/presentation/styles/sizes.dart';
import 'package:dayby/presentation/styles/texts.dart';

class MailAndPass extends HookWidget {
  const MailAndPass({super.key});

  @override
  Widget build(BuildContext context) {
    // アドレス入力
    final TextEditingController emailController = useTextEditingController();
    final mailField = SizedBox(
      width: MySize.signinButtonWidth(context),
      height: MySize().minDropDownHeight,
      child: TextFormField(
        controller: emailController,
        style: MyText().m,
        decoration: InputDecoration(
          labelText: 'メールアドレス',
          labelStyle: MyText().m,
        ),
        onChanged: (String value) {
          emailController.text = value;
        },
      ),
    );

    return signinContainer(
      [
        mailField,
        ...
      ],
    );
  }
}

テキストスタイルの呼び出し例(文字数制限はUIに関わるところではないので埋め込んでいます)

Dart
import 'package:flutter/material.dart';
import 'package:dayby/presentation/styles/texts.dart';

// UserNameField
class UserNameField extends StatelessWidget {
  final TextEditingController userNameController;
  const UserNameField({super.key, required this.userNameController});

  @override
  Widget build(BuildContext context) {
    return TableCell(
      verticalAlignment: TableCellVerticalAlignment.bottom,
      child: TextFormField(
        controller: userNameController,
        autofocus: true, // 開いた途端にユーザー名を入力できるように
        decoration: const InputDecoration(
          border: OutlineInputBorder(),
        ),
        style: MyText().m,
        maxLength: 14,
        maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
      ),
    );
  }
}

参考になりましたら、ぜひコメントやXで教えてください!

コメント

タイトルとURLをコピーしました