Buttons are one of the most used Widgets in Flutter. But between ElevatedButton, TextButton, and OutlinedButton, you might end up with incoherent designs between the different buttons.
Moreover, you always need to remember which one to use, and onboarding juniors might be a little bit harder. They begin to type ++code>Button++/code> and see multiple choices. Which one is the better?
Finally, there is the theming of the buttons. If you want to add color or an icon, you need to change the style props. It might make your code less readable and less consistent.
This article aims at showing how we like to handle buttons at Bam. It might not be perfect, but It helps us have a consistent design and easy onboarding. Moreover, by not completely reimplementing the buttons, we get great support for the web and semantics by default!
Imagine I'm working on the project ++code>project++/code>I'll call most of my custom UI elements ++code>ProjectSomething++/code>. Let's create a ++code>ProjectButton++/code> class that extends the ++code>StatelessWidget++/code> class:
++pre>++code>++code>enum _StyleEnum { primary, outlined, text }
class ProjectButton extends StatelessWidget {
const ProjectButton._({
Key? key,
this.icon,
required this.label,
required this.onPressed,
required _StyleEnum styleEnum,
required this.padding,
}) : _styleEnum = styleEnum,
super(key: key);
/// Icon of the button
final IconData? icon;
/// Label of the button
final String label;
/// Padding of the inside of the button
final EdgeInsets? padding;
/// Action of the button, if null, will go to disabled state
final Function()? onPressed;
final _StyleEnum _styleEnum;
...++/code>++/pre>
You can see that I define a private enum. It will help us correctly choose the style in the build method.
Also, You cannot create the button outside of this class since the default constructor is private (see the ++code>ProjectButton._++/code> ).
Let's create the logic of the build method for the primary variant of our button.
++pre>++code>++code> @override
Widget build(BuildContext context) {
switch (_styleEnum) {
case _StyleEnum.primary:
final style = ElevatedButton.styleFrom(
elevation: 0,
padding: padding,
);
if (icon != null) {
return ElevatedButton.icon(
style: style,
onPressed: onPressed,
icon: Icon(icon),
label: Text(label),
);
}
return ElevatedButton(
style: style,
onPressed: onPressed,
child: Text(label),
);
...++/code>++/pre>
You can see here that we are reusing the ++code>ElevatedButton++/code> and ++code>ElevatedButton.icon++/code> to not reimplement the custom logic of showing an icon.
But now, we miss a single last thing. How do we call this button? We're going to add a factory to our class, that will make handling the button easy:
++pre>++code>++code> /// Primary button
factory ProjectButton.primary({
IconData? icon,
required String label,
required Function()? onPressed,
EdgeInsets? padding,
}) {
return ProjectButton._(
label: label,
onPressed: onPressed,
icon: icon,
styleEnum: _StyleEnum.primary,
padding: padding,
);
}++/code>++/pre>
And that's it! You can now call your button by simply using
++pre>++code>ProjectButton.primary(label: "Hey", onPressed: () {})++/code>++/pre>
anywhere in your code!
You should now be able to complete the ++code>switch++/code> statement and have a coherent Button for your app!
You can even extend the ++code>_StyleEnum++/code> to add more variants of your button ??.
Find below the complete code. Bear in mind that you might want to customize the buttons a little more to give your app a unique feel!
++pre>++code>++code>class ProjectButton extends StatelessWidget {
const ProjectButton._({
Key? key,
this.icon,
required this.label,
required this.onPressed,
required _StyleEnum styleEnum,
required this.padding,
}) : _styleEnum = styleEnum,
super(key: key);
/// Primary button
factory ProjectButton.primary({
IconData? icon,
required String label,
required Function()? onPressed,
EdgeInsets? padding,
}) {
return ProjectButton._(
label: label,
onPressed: onPressed,
icon: icon,
styleEnum: _StyleEnum.primary,
padding: padding,
);
}
/// Outlined button
factory ProjectButton.outlined({
IconData? icon,
required String label,
required Function()? onPressed,
EdgeInsets? padding,
}) {
return ProjectButton._(
label: label,
onPressed: onPressed,
icon: icon,
styleEnum: _StyleEnum.outlined,
padding: padding,
);
}
/// Text button
factory ProjectButton.text({
IconData? icon,
required String label,
required Function()? onPressed,
EdgeInsets? padding,
}) {
return ProjectButton._(
label: label,
onPressed: onPressed,
icon: icon,
styleEnum: _StyleEnum.text,
padding: padding,
);
}
/// Icon of the button
final IconData? icon;
/// Label of the button
final String label;
/// Padding of the inside of the button
final EdgeInsets? padding;
/// Action of the button, if null, will go to disabled state
final Function()? onPressed;
final _StyleEnum _styleEnum;
@override
Widget build(BuildContext context) {
switch (_styleEnum) {
case _StyleEnum.primary:
final style = ElevatedButton.styleFrom(
elevation: 0,
padding: padding,
);
if (icon != null) {
return ElevatedButton.icon(
style: style,
onPressed: onPressed,
icon: Icon(icon),
label: Text(label),
);
}
return ElevatedButton(
style: style,
onPressed: onPressed,
child: Text(label),
);
case _StyleEnum.outlined:
final style = OutlinedButton.styleFrom(
elevation: 0,
padding: padding,
);
if (icon != null) {
return OutlinedButton.icon(
style: style,
onPressed: onPressed,
icon: Icon(icon),
label: Text(label),
);
}
return OutlinedButton(
style: style,
onPressed: onPressed,
child: Text(label),
);
case _StyleEnum.text:
final style = TextButton.styleFrom(
elevation: 0,
padding: padding,
);
if (icon != null) {
return TextButton.icon(
style: style,
onPressed: onPressed,
icon: Icon(icon),
label: Text(label),
);
}
return TextButton(
style: style,
onPressed: onPressed,
child: Text(label),
);
}
}
}
++/code>++/pre>