11
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kurogoma939のひとりアドベントカレンダーAdvent Calendar 2024

Day 3

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

Last updated at Posted at 2024-12-02

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

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種を共通化しています。

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

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

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式を記述します

  @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にはさまざまな共通化のパターンがあるのであくまで一例として参考になればと思います。

11
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?