flutter/examples/api
Hans Muller ff6c8f5d37
Added ButtonStyle.foregroundBuilder and ButtonStyle.backgroundBuilder (#141818)
Fixes https://github.com/flutter/flutter/issues/139456, https://github.com/flutter/flutter/issues/130335, https://github.com/flutter/flutter/issues/89563.

Two new properties have been added to ButtonStyle to make it possible to insert arbitrary state-dependent widgets in a button's background or foreground. These properties can be specified for an individual button, using the style parameter, or for all buttons using a button theme's style parameter.

The new ButtonStyle properties are `backgroundBuilder` and `foregroundBuilder` and their (function) types are:

```dart
typedef ButtonLayerBuilder = Widget Function(
  BuildContext context,
  Set<MaterialState> states,
  Widget? child
);
```

The new builder functions are called whenever the button is built and the `states` parameter communicates the pressed/hovered/etc state fo the button.

## `backgroundBuilder`

Creates a widget that becomes the child of the button's Material and whose child is the rest of the button, including the button's `child` parameter.  By default the returned widget is clipped to the Material's ButtonStyle.shape.

The `backgroundBuilder` can be used to add a gradient to the button's background. Here's an example that creates a yellow/orange gradient background:

![opaque-gradient-bg](https://github.com/flutter/flutter/assets/1377460/80df8368-e7cf-49ef-aee7-2776a573644c)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [Colors.orange, Colors.yellow]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

Because the background widget becomes the child of the button's Material, if it's opaque (as it is in this case) then it obscures the overlay highlights which are painted on the button's Material. To ensure that the highlights show through one can decorate the background with an `Ink` widget.  This version also overrides the overlay color to be (shades of) red, because that makes the highlights look a little nicer with the yellow/orange background.

![ink-gradient-bg](https://github.com/flutter/flutter/assets/1377460/68a49733-f30e-44a1-a948-dc8cc95e1716)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.red,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return Ink(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [Colors.orange, Colors.yellow]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

Now the button's overlay highlights are painted on the Ink widget. An Ink widget isn't needed if the background is sufficiently translucent. This version of the example creates a translucent backround widget. 

![translucent-graident-bg](https://github.com/flutter/flutter/assets/1377460/3b016e1f-200a-4d07-8111-e20d29f18014)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.red,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [
            Colors.orange.withOpacity(0.5),
            Colors.yellow.withOpacity(0.5),
          ]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

One can also decorate the background with an image. In this example, the button's background is an burlap texture image. The foreground color has been changed to black to make the button's text a little clearer relative to the mottled brown backround.

![burlap-bg](https://github.com/flutter/flutter/assets/1377460/f2f61ab1-10d9-43a4-bd63-beecdce33b45)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.black,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return Ink(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(burlapUrl),
            fit: BoxFit.cover,
          ),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The background widget can depend on the `states` parameter. In this example the blue/orange gradient flips horizontally when the button is hovered/pressed.

![gradient-flip](https://github.com/flutter/flutter/assets/1377460/c6c6fe26-ae47-445b-b82d-4605d9583bd8)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final Color color1 = Colors.blue.withOpacity(0.5);
      final Color color2 = Colors.orange.withOpacity(0.5);
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: switch (states.contains(MaterialState.hovered)) {
              true => <Color>[color1, color2],
              false => <Color>[color2, color1],
            },
          ),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The preceeding examples have not included a BoxDecoration border because ButtonStyle already supports `ButtonStyle.shape` and `ButtonStyle.side` parameters that can be uesd to define state-dependent borders. Borders defined with the ButtonStyle side parameter match the button's shape. To add a border that changes color when the button is hovered or pressed, one must specify the side property using `copyWith`, since there's no `styleFrom` shorthand for this case.

![border-gradient-bg](https://github.com/flutter/flutter/assets/1377460/63cffcd3-0dcf-4eb1-aed5-d14adf1e57f6)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.indigo,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final Color color1 = Colors.blue.withOpacity(0.5);
      final Color color2 = Colors.orange.withOpacity(0.5);
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: switch (states.contains(MaterialState.hovered)) {
              true => <Color>[color1, color2],
              false => <Color>[color2, color1],
            },
          ),
        ),
        child: child,
      );
    },
  ).copyWith(
    side: MaterialStateProperty.resolveWith<BorderSide?>((Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered)) {
        return BorderSide(width: 3, color: Colors.yellow);
      }
      return null; // defer to the default
    }),
  ),
  child: Text('Text Button'),
)
```

Although all of the examples have created a ButtonStyle locally and only applied it to one button, they could have configured the `ThemeData.textButtonTheme` instead and applied the style to all TextButtons. And, of course, all of this works for all of the ButtonStyleButton classes, not just TextButton.

## `foregroundBuilder`

Creates a Widget that contains the button's child parameter. The returned widget is clipped by the button's [ButtonStyle.shape] inset by the button's [ButtonStyle.padding] and aligned by the button's [ButtonStyle.alignment].

The `foregroundBuilder` can be used to wrap the button's child, e.g. with a border or a `ShaderMask` or as a state-dependent substitute for the child.

This example adds a border that's just applied to the child. The border only appears when the button is hovered/pressed.

![border-fg](https://github.com/flutter/flutter/assets/1377460/687a3245-fe68-4983-a04e-5fcc77f8aa21)

```dart
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final ColorScheme colorScheme = Theme.of(context).colorScheme;
      return DecoratedBox(
        decoration: BoxDecoration(
          border: states.contains(MaterialState.hovered)
            ? Border(bottom: BorderSide(color: colorScheme.primary))
            : Border(), // essentially "no border"
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The foregroundBuilder can be used with `ShaderMask` to change the way the button's child is rendered. In this example the ShaderMask's gradient causes the button's child to fade out on top.

![shader_mask_fg](https://github.com/flutter/flutter/assets/1377460/54010f24-e65d-4551-ae58-712135df3d8d)

```dart
ElevatedButton(
  onPressed: () { },
  style: ElevatedButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final ColorScheme colorScheme = Theme.of(context).colorScheme;
      return ShaderMask(
        shaderCallback: (Rect bounds) {
          return LinearGradient(
            begin: Alignment.bottomCenter,
            end: Alignment.topCenter,
            colors: <Color>[
              colorScheme.primary,
              colorScheme.primaryContainer,
            ],
          ).createShader(bounds);
        },
        blendMode: BlendMode.srcATop,
        child: child,
      );
    },
  ),
  child:  const Text('Elevated Button'),
)
```

A commonly requested configuration for butttons has the developer provide images, one for pressed/hovered/normal state. You can use the foregroundBuilder to create a button that fades between a normal image and another image when the button is pressed. In this case the foregroundBuilder doesn't use the child it's passed, even though we've provided the required TextButton child parameter.

![image-button](https://github.com/flutter/flutter/assets/1377460/f5b1a22f-43ce-4be3-8e70-06de4c958380)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final String url = states.contains(MaterialState.pressed) ? smiley2Url : smiley1Url;
      return AnimatedContainer(
        width: 100,
        height: 100,
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(url),
            fit: BoxFit.contain,
          ),
        ),
      );
    },
  ),
  child: Text('No Child'),
)
```

In this example the button's default overlay appears when the button is hovered and pressed. Another image can be used to indicate the hovered state and the default overlay can be defeated by specifying `Colors.transparent` for the `overlayColor`:

![image-per-state](https://github.com/flutter/flutter/assets/1377460/7ab9da2f-f661-4374-b395-c2e0c7c4cf13)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.transparent,
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      String url = states.contains(MaterialState.hovered) ? smiley3Url : smiley1Url;
      if (states.contains(MaterialState.pressed)) {
        url = smiley2Url;
      }
      return AnimatedContainer(
        width: 100,
        height: 100,
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(url),
            fit: BoxFit.contain,
          ),
        ),
      );
    },
  ),
  child: Text('No Child'),
)
```
2024-02-01 00:02:23 +00:00
..
android Update Android minSdkVersion to 21 (#142267) 2024-01-29 09:49:09 -08:00
ios Migrate Xcode projects last version checks to Xcode 15.1 (#140256) 2024-01-03 23:05:46 +00:00
lib Added ButtonStyle.foregroundBuilder and ButtonStyle.backgroundBuilder (#141818) 2024-02-01 00:02:23 +00:00
linux Deletes files that should be ignored (#127984) 2023-06-29 19:45:22 +00:00
macos Migrate Xcode projects last version checks to Xcode 15.1 (#140256) 2024-01-03 23:05:46 +00:00
test Added ButtonStyle.foregroundBuilder and ButtonStyle.backgroundBuilder (#141818) 2024-02-01 00:02:23 +00:00
test_driver Add smoke tests for all the examples, fix 17 broken examples. (#89021) 2021-09-28 09:32:06 -07:00
web Fix example app names and copyrights (#100795) 2022-03-25 17:40:11 -07:00
windows [Windows] Add target architecture to build path (#131843) 2023-08-31 09:09:02 -07:00
.metadata Fix example app names and copyrights (#100795) 2022-03-25 17:40:11 -07:00
analysis_options.yaml Enable unreachable_from_main lint - it is stable now!!1 (#129854) 2023-07-06 00:09:01 +00:00
pubspec.yaml Upgrade leak_tracker. (#142162) 2024-01-24 15:33:17 -08:00
README.md Add example examples (#111276) 2022-09-12 20:28:09 +00:00

API Example Code

This directory contains the API sample code that is referenced from the API documentation in the framework.

The examples can be run individually by just specifying the path to the example on the command line (or in the run configuration of an IDE).

For example (no pun intended!), to run the first example from the Curve2D class in Chrome, you would run it like so from the api directory:

% flutter run -d chrome lib/animation/curves/curve2_d.0.dart

All of these same examples are available on the API docs site. For instance, the example above is available on this page. Most of the samples are available as interactive examples in Dartpad, but some (the ones marked with {@tool sample} in the framework source code), just don't make sense on the web, and so are available as standalone examples that can be run here. For instance, setting the system overlay style doesn't make sense on the web (it only changes the notification area background color on Android), so you can run the example for that on an Android device like so:

% flutter run -d MyAndroidDevice lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart

Naming

lib/library/file/class_name.n.dart

lib/library/file/class_name.member_name.n.dart

The naming scheme for the files is similar to the hierarchy under packages/flutter/lib/src, except that the files are represented as directories (without the .dart suffix), and each sample in the file is a separate file in that directory. So, for the example above, where the examples are from the packages/flutter/lib/src/animation/curves.dart file, the Curve2D class, the first sample (hence the index "0") for that symbol resides in the file named lib/animation/curves/curve2_d.0.dart.

Symbol names are converted from "CamelCase" to "snake_case". Dots are left between symbol names, so the first example for symbol InputDecoration.prefixIconConstraints would be converted to input_decoration.prefix_icon_constraints.0.dart.

If the same example is linked to from multiple symbols, the source will be in the canonical location for one of the symbols, and the link in the API docs block for the other symbols will point to the first symbol's example location.

Authoring

For more detailed information about authoring examples, see the snippets package.

When authoring examples, first place a block in the Dartdoc documentation for the symbol you would like to attach it to. Here's what it might look like if you wanted to add a new example to the Curve2D class:

/// {@tool dartpad}
/// Write a description of the example here. This description will appear in the
/// API web documentation to introduce the example.
///
/// ** See code in examples/api/lib/animation/curves/curve2_d.0.dart **
/// {@end-tool}

The "See code in" line needs to be formatted exactly as above, with no wrapping or newlines, one space after the "**" at the beginning, and one space before the "**" at the end, and the words "See code in" at the beginning of the line. This is what the snippets tool use when finding the example source code that you are creating.

Use {@tool dartpad} for Dartpad examples, and use {@tool sample} for examples that shouldn't be run/shown in Dartpad.

Once that comment block is inserted in the source code, create a new file at the appropriate path under examples/api. See the sample_templates directory for examples of different types of samples with some best practices applied.

The filename should match the location of the source file it is linked from, and is named for the symbol it is attached to, in lower_snake_case, with an index relating to their order within the doc comment. So, for the Curve2D example above, since it's in the animation library, in a file called curves.dart, and it's the first example, it should have the name examples/api/lib/animation/curves/curve2_d.0.dart.

You should also add tests for your sample code under examples/api/test, that matches their location under lib, ending in _test.dart. See the section on writing tests for more information on what kinds of tests to write.

The entire example should be in a single file, so that Dartpad can load it.

Only packages that can be loaded by Dartpad may be imported. If you use one that hasn't been used in an example before, you may have to add it to the pubspec.yaml in the api directory.

Snippets

There is another type of example that can also be authored, using {@tool snippet}. Snippet examples are just written inline in the source, like so:

/// {@tool dartpad}
/// Write a description of the example here. This description will appear in the
/// API web documentation to introduce the example.
///
/// ```dart
/// // Sample code goes here, e.g.:
/// const Widget emptyBox = SizedBox();
/// ```
/// {@end-tool}

The source for these snippets isn't stored under the examples/api directory, or available in Dartpad in the API docs, since they're not intended to be runnable, they just show some incomplete snippet of example code. It must compile (in the context of the sample analyzer), but doesn't need to do anything. See the snippets documentation for more information about the context that the analyzer uses.

Writing Tests

Examples are required to have tests. There is already a "smoke test" that simply builds and runs all the API examples, just to make sure that they start up without crashing. Functionality tests are required the examples, and generally just do what is normally done for writing tests. The one thing that makes it more challenging to do for examples is that they can't really be written for testability in any obvious way, since that would complicate the examples and make them harder to explain.

As an example, in regular framework code, you might include a parameter for a Platform object that can be overridden by a test to supply a dummy platform, but in the example. This would be unnecessarily complex for the example. In all other ways, these are just normal tests. You don't need to re-test the functionality of the widget being used in the example, but you should test the functionality and integrity of the example itself.

Tests go into a directory under test that matches their location under lib. They are named the same as the example they are testing, with _test.dart at the end, like other tests. For instance, a LayoutBuilder example that resides in lib/widgets/layout_builder/layout_builder.0.dart would have its tests in a file named test/widgets/layout_builder/layout_builder.0_test.dart