New Button Universe by HansMuller · Pull Request #59702 · flutter/flutter
Warning
The widget and theme called ContainedButton are being changed to ElevatedButton. See #61262.
Overview
Adds a new set of button widgets and themes that address the problems outlined in flutter.dev/go/material-button-system-updates. The overall goal is to make buttons more flexible, and easier to configure via constructor parameters or themes.
Demo
If you'd like to skip the explanation and jump right to the visuals or the source code,
there's a prototype DartPad demo. The source code is there (and all in one file), and the Flutter framework part is hosted here: https://github.com/HansMuller/flutter_buttons. It's (now) somewhat out of date wrt this PR. However, if you want interactively experiment with changes, doing so from within the DartPad demo may be useful.
Summary: Updating the Material Buttons and their Themes
Rather than try and evolve the existing button classes and their theme in-place, this PR introduces new replacement button widgets and themes. In addition to freeing us from the backwards compatibility labyrinthe that evolving the existing classes in-place would entail, the new names sync Flutter back up with the Material Design spec, which uses the new names for the button components.
| Old Widget | Old Theme | New Widget | New Theme |
|---|---|---|---|
| FlatButton | ButtonTheme | TextButton | TextButtonTheme |
| RaisedButton | ButtonTheme | ContainedButton | ContainedButtonTheme |
| OutlineButton | ButtonTheme | OutlinedButton | OutlinedButtonTheme |
The new themes follow the "normalized" pattern that Flutter adopted for new Material widgets about a year ago. Theme properties and widget constructor parameters are null by default. Non-null theme properties and widget parameters specify an override of the component's default value. Implementing and documenting default values is the sole responsibility of the button component widgets. The defaults themselves are based primarily on the overall Theme's colorScheme and textTheme.
ButtonStyle
We've added a new class called ButtonStyle which aggregates the buttons' visual properties. Most of ButtonStyle's properties are defined with MaterialStateProperty, so that they can represent different values for different button states.
Each of the new button widget classes has a static styleFrom() method that returns a ButtonStyle. The styleFrom method's parameters are simple values (not MaterialStateProperties) that include overrides for the ColorScheme colors that the button's style depends on. The styleFrom() method computes all of the dependent colors for all of the button's states.
Using the Theme to override a Button property like textColor
An app that uses TextButton and ContainedButton (nee FlatButton and RaisedButton) can configure the text color for all buttons by specifying a textButtonTheme and containedButtonTheme in the app's overall theme.
MaterialApp( theme: ThemeData( textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom(primary: Colors.green) ), containedButtonTheme: ContainedButtonThemeData( style: ContainedButton.styleFrom(onPrimary: Colors.green) ), ), // ... )
The TextButton's text is rendered in the ColorScheme's primary color by default, and the ContainedButton's text is rendered with the onPrimary color. We've created a new ButtonStyle for each of the corresponding themes that effectively overrides the text color. ContainedButtons use the primary color as background, so one would probably want to set that as well:
MaterialApp( theme: ThemeData( textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom(primary: Colors.green) ), containedButtonTheme: ContainedButtonThemeData( style: ContainedButton.styleFrom( primary: Colors.yellow, onPrimary: Colors.green, ) ), ), // ... )
In both cases this approach creates a ButtonStyle where the other colors that depend on the primary or onPrimary colors have been updated as well. For example the highlight color that's shown when the button is tapped or focused is also included in the ButtonStyle because it depends on the primary color too.
This is usually what you want. However there are times when an app needs to more precisely control its buttons' appearance. For example, you might really want to only change the ContainedButton's text color, and for all possible states (focused, pressed, disabled, etc). To do that, create a ButtonStyle that specifies textColor:
MaterialApp( theme: ThemeData( containedButtonTheme: ContainedButtonThemeData( style: ButtonStyle( textColor: MaterialStateProperty.resolveWith<Color>( (Set<MaterialState> states) => Colors.green, ), ), ), ), // ... )
The ButtonStyle's textColor is a MaterialProperty<Color> and in this case the property just maps all possible states to green. To only override the button's text color when the button was enabled:
MaterialApp( theme: ThemeData( containedButtonTheme: ContainedButtonThemeData( style: ButtonStyle( textColor: MaterialStateProperty.resolveWith<Color>( (Set<MaterialState> states) { return (states.contains(MaterialState.disabled)) ? null : Colors.green; }, ), ), ), ), // ... )
The MaterialStateProperty that we've created for the text color returns null when its button is disabled. That means that the component will use the default: either the widget's ButtonStyle parameter, or, if that's null too, then the widget's internal default.
Using the Theme to override Button shapes
ButtonStyle objects allow one to override all of the visual properties including the buttons' shapes. As noted below, one can give all of an app's buttons the "stadium" shape like this.
MaterialApp( home: Home(), theme: ThemeData( textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( shape: StadiumBorder(), ), ), containedButtonTheme: ContainedButtonThemeData( style: ContainedButton.styleFrom( shape: StadiumBorder(), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( shape: StadiumBorder(), ), ), ), )
Using ButtonStyle to change the appearance of individual buttons
A ButtonStyle can also be applied to individual buttons. For example, to create an AlertDialog with "stadium" shaped action buttons, rather than wrapping the dialog's contents in a theme, one could just specify the same style for both buttons.
ButtonStyle style = OutlinedButton.styleFrom(shape: StadiumBorder()); showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('AlertDialog Title'), content: Text('Stadium shaped action buttons, default outline'), actions: <Widget>[ OutlinedButton( style: style, onPressed: () { dismissDialog(); }, child: Text('Approve'), ), OutlinedButton( style: style, onPressed: () { dismissDialog(); }, child: Text('Really Approve'), ), ], ); }, );
In this case, just like the others, the style only overrides the button shapes, all of of the other properties get their context-specific defaults in the usual way. To give one of the buttons a heavier primary colored outline, instead of the default thin gray outline:
showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('AlertDialog Title'), content: Text('One Stadium shaped action button, with a heavy, primary color outline.', actions: <Widget>[ OutlinedButton( style: OutlinedButton.styleFrom(shape: StadiumBorder()), onPressed: () { dismissDialog(); }, child: Text('Approve'), ), OutlinedButton( style: OutlinedButton.styleFrom( shape: StadiumBorder(), side: BorderSide( width: 2, color: Theme.of(context).colorScheme.primary, ), ), onPressed: () { dismissDialog(); }, child: Text('Really Approve'), ), ], ); }, );
Most of the button visual properties are specified in terms of MaterialStateProperty, which means that the property can have different values depending on its button's state. Using the convenient static styleFrom methods delegates creating the MaterialStateProperty values to the button class. It's easy enough to create them directly, to construct ButtonStyles with state-specific values. For example, to set up the second dialog buttons so that it only shows the heavier primary colored outline when it's hovered or focused:
OutlinedButton( style: OutlinedButton.styleFrom( shape: StadiumBorder(), ).copyWith( side: MaterialStateProperty.resolveWith<BorderSide>((Set<MaterialState> states) { if (states.contains(MaterialState.hovered) || states.contains(MaterialState.focused)) { return BorderSide( width: 2, color: Theme.of(context).colorScheme.primary, ); } return null; // defer to the default }, )), onPressed: () { dismissDialog(); }, child: Text('Really Approve'), ),
In this case we've used the resolveWith() utility method to create a MaterialStateProperty that only overrides the default outline appearance when the button is either focused or hovered.
Fixes #54776.


