Skip to content

Updating the Material Buttons and their Themes #54776

@HansMuller

Description

@HansMuller

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.

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'),
        ),
      ],
    );
  },
);

Screen Shot 2020-05-11 at 11 31 04 AM

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'),
        ),
      ],
    );
  },
);

Screen Shot 2020-05-11 at 11 33 19 AM

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    c: proposalA detailed proposal for a change to Flutterf: material designflutter/packages/flutter/material repository.frameworkflutter/packages/flutter repository. See also f: labels.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions