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