Flutterで共通スタイルのボタンクラスを実装する

Flutterの共通ボタンクラスの一例です。
早速全体像を記載した上で解説を入れていきます。

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

enum _ButtonType {
  filled,
  outlined,
  icon,
  text,
  ;
}

class CustomButton extends StatelessWidget {
  const CustomButton.filled({
    super.key,
    required this.label,
    required this.onPressed,
    this.textStyle,
    this.prefixIcon,
    this.suffixIcon,
    this.labelPadding = EdgeInsets.zero,
    this.color = Colors.blue,
    this.centerTitle = true,
  })  : type = _ButtonType.filled,
        icon = null;

  const CustomButton.outlined({
    super.key,
    required this.label,
    required this.onPressed,
    this.textStyle,
    this.prefixIcon,
    this.suffixIcon,
    this.color = Colors.blue,
    this.labelPadding = EdgeInsets.zero,
    this.centerTitle = true,
  })  : type = _ButtonType.outlined,
        icon = null;

  const CustomButton.icon({
    super.key,
    required this.icon,
    this.label = '',
    required this.onPressed,
    this.textStyle,
  })  : type = _ButtonType.icon,
        prefixIcon = null,
        suffixIcon = null,
        color = null,
        labelPadding = EdgeInsets.zero,
        centerTitle = false;

  const CustomButton.text({
    super.key,
    required this.label,
    required this.onPressed,
    this.textStyle,
    this.color = Colors.blue,
    this.labelPadding = EdgeInsets.zero,
  })  : type = _ButtonType.text,
        icon = null,
        prefixIcon = null,
        suffixIcon = null,
        centerTitle = false;

  final String label;
  final TextStyle? textStyle;
  final Widget? prefixIcon;
  final Widget? suffixIcon;
  final Color? color;
  final _ButtonType type;
  final void Function()? onPressed;
  final Widget? icon;
  final EdgeInsets labelPadding;
  final bool centerTitle;

  TextAlign get textAlign => centerTitle ? TextAlign.center : TextAlign.start;

  @override
  Widget build(BuildContext context) {
    return switch (type) {
      _ButtonType.filled => _buildFilledButton(),
      _ButtonType.outlined => _buildOutlinedButton(),
      _ButtonType.icon => _buildIconButton(),
      _ButtonType.text => _buildTextButton(),
    };
  }

  Widget _buildFilledButton() {
    final hasPrefixIcon = prefixIcon != null;
    final hasSuffixIcon = suffixIcon != null;
    return FilledButton(
      onPressed: onPressed,
      style: FilledButton.styleFrom(
        backgroundColor: color,
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          if (hasPrefixIcon || (hasSuffixIcon && centerTitle))
            Padding(
              padding: const EdgeInsets.only(right: 10),
              child: SizedBox(
                height: 16,
                width: 16,
                child: prefixIcon,
              ),
            ),
          Expanded(
            child: Padding(
              padding: labelPadding,
              child: Text(
                label,
                style: textStyle,
                textAlign: textAlign,
              ),
            ),
          ),
          if (hasSuffixIcon || (hasPrefixIcon && centerTitle))
            Padding(
              padding: const EdgeInsets.only(left: 10),
              child: SizedBox(
                height: 16,
                width: 16,
                child: suffixIcon,
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildOutlinedButton() {
    final hasPrefixIcon = prefixIcon != null;
    final hasSuffixIcon = suffixIcon != null;
    return OutlinedButton(
      onPressed: onPressed,
      style: OutlinedButton.styleFrom(
        foregroundColor: color,
        side: BorderSide(color: color ?? Colors.blue),
      ),
      child: Row(
        children: [
          if (hasPrefixIcon || (hasSuffixIcon && centerTitle))
            SizedBox.square(
              dimension: 16,
              child: prefixIcon,
            ),
          Expanded(
            child: Padding(
              padding: labelPadding,
              child: Text(
                label,
                textAlign: textAlign,
                style: textStyle,
              ),
            ),
          ),
          if (hasSuffixIcon || (hasPrefixIcon && centerTitle))
            SizedBox.square(
              dimension: 16,
              child: suffixIcon,
            ),
        ],
      ),
    );
  }

  Widget _buildIconButton() {
    // labelが空文字でない場合、上にアイコン下にテキストを表示するボタンを表示する。
    if (label.isNotEmpty) {
      return TextButton(
        style: TextButton.styleFrom(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8),
          ),
        ),
        onPressed: onPressed,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            icon ?? const SizedBox.shrink(),
            const Gap(7),
            Text(
              label,
              style:
                  (textStyle ?? CustomTypography.style10N12.textStyle).copyWith(
                color: color,
              ),
            ),
          ],
        ),
      );
    }
    return IconButton(
      onPressed: onPressed,
      icon: icon ?? const SizedBox.shrink(),
    );
  }

  Widget _buildTextButton() {
    return TextButton(
      style: TextButton.styleFrom(
        shape: LinearBorder.none,
        tapTargetSize: MaterialTapTargetSize.shrinkWrap,
        minimumSize: Size.zero,
        padding: EdgeInsets.zero,
        splashFactory: NoSplash.splashFactory,
      ),
      onPressed: onPressed,
      child: Padding(
        padding: labelPadding,
        child: Text(
          label,
          textAlign: textAlign,
          style: (textStyle ?? CustomTypography.style14N17.textStyle).copyWith(
            color: color,
          ),
        ),
      ),
    );
  }
}
```

## 解説

まず、今回デザインで定番のボタン4種を共通化しています。

```dart
enum _ButtonType {
  filled, // 塗りつぶしボタン
  outlined, // 外枠ボタン
  icon, // アイコンボタン
  text, // テキストボタン
  ;
}
```

次に、クラスの中で名前付きコンストラクタの定義をします

```dart
class CustomButton extends StatelessWidget {
  const CustomButton.filled({
    super.key,
    required this.label,
    required this.onPressed,
    // ...
  })  : type = _ButtonType.filled;

  const CustomButton.outlined({
    super.key,
    required this.label,
    required this.onPressed,
    // ...
  })  : type = _ButtonType.outlined;

  const CustomButton.icon({
    super.key,
    required this.icon,
    this.label = '',
    required this.onPressed,
    // ...
  })  : type = _ButtonType.icon;

  const CustomButton.text({
    super.key,
    required this.label,
    required this.onPressed,
    // ...
  })  : type = _ButtonType.text;
```

あとはbuildメソッドの中で`_ButtonType`を用いたswitch式を記述します

```dart
  @override
  Widget build(BuildContext context) {
    return switch (type) {
      _ButtonType.filled => _buildFilledButton(),
      _ButtonType.outlined => _buildOutlinedButton(),
      _ButtonType.icon => _buildIconButton(),
      _ButtonType.text => _buildTextButton(),
    };
  }
```


以上です！
プロダクトのデザインによっては、FilledButtonにも3パターンあるようなケースがあります。
そういった場合はFilledButtonのlearge, medium, smallでこのような実装を用いても良いと思います。
Flutterにはさまざまな共通化のパターンがあるのであくまで一例として参考になればと思います。