-
Notifications
You must be signed in to change notification settings - Fork 29.1k
Description
To address the many problems that have been reported about the Material Theme system and, in particular, the button classes and their theme, we've made some long-term plans for improving things.
- Material Theme System Updates (flutter.dev/go/material-theme-system-updates) outlines the overall problem and provides detail in some areas.
- Updating the Material Buttons and their Themes (flutter.dev/go/material-button-system-updates) is a relatively detailed proposal for how to remedy the problems with buttons and their themes.
This issue is the Flutter GitHub connection to flutter.dev/go/material-button-system-updates proposal. I'll summarize how the proposal addresses problems that have been raised in other issues here. If you have feedback about the proposal you're welcome to add marginal comments the proposal document, or you can leave comments here. If you have you a lot to say, or if you'd like to incorporate code or screenshots into your comments, you may find that leaving your feedback here is more convenient.
Demo
If you'd like to skip the explanation and jump write to the visuals or the source code,
there's a DartPad demo of the prototype. The source code is in the DartPad demo of course (and all in one file), and the Flutter framework part is hosted here: https://github.com/HansMuller/flutter_buttons.
Summary: Updating the Material Buttons and their Themes
Rather than try and evolve the existing button classes and their theme in-place, the proposal 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 are null by default, and although ThemeData has a slot for each button theme, they're also null by default. Implementing and documenting default values are the sole responsibility of the button components. Themes can override the component defaults. If no override is specified the component defers to 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 given one or more ColorScheme colors. 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
This issue was raised in #19623
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.