Reverts flutter/flutter#141818 Initiated by: XilaiZhang This change reverts the following previous change: Original Description: 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:  ```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.  ```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.  ```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.  ```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.  ```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.  ```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.  ```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.  ```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.  ```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`:  ```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'), ) ```
Flutter's Build Infrastructure
This directory exists to support building Flutter on our build infrastructure.
Flutter build results are available at:
- https://flutter-dashboard.appspot.com/#/build
- Aggregate dashboard of the separate CI systems used by Flutter.
- https://cirrus-ci.com/github/flutter/flutter/master
- Testing is done on PRs and submitted changes on GitHub.
Flutter infra requires special permissions to retrigger builds on the build dashboard. File an infra ticket to request permission.
The Cirrus-based bots run the test.dart
script for each PR and submission. This does testing for the tools, for the
framework, and (for submitted changes only) rebuilds and updates the main
branch API docs staging site.
For tagged dev and beta builds, it also builds and deploys the gallery app to
the app stores. It is configured by the .cirrus.yml.
The build dashboard includes post-commit testing run on physical devices. See //dev/devicelab for more information.
LUCI (Layered Universal Continuous Integration)
A set of infra scripts run on Windows, Linux, and Mac machines. The configuration for how many machines and what kind are managed internally by Google. File an infra ticket to request new machine types to be added. Both of these technologies are highly specific to the LUCI project, which is the successor to Chromium's infra and the foundation to Flutter's infrastructure.
Prerequisites
To work on this infrastructure you will need:
- depot_tools
- Python package installer:
sudo apt-get install python-pip - Python coverage package (only needed for
training_simulation):sudo pip install coverage
To run prepare_package.dart locally:
- Make sure the
depot_toolsis in yourPATH. If you're on Windows, you also need an environment variable calledDEPOT_TOOLSwith the path todepot_toolsas value. - Run
gsutil.py config(orpython3 %DEPOT_TOOLS%\gsutil.pyon Windows) to authenticate with your auth token. - Create a local temp directory.
cdinto it. - Run
dart [path to your normal Flutter repo]/dev/bots/prepare_package.dart --temp_dir=. --revision=[revision to package] --branch=[branch to deploy to] --publish. - If you're running into
gsutilpermission issues, check with @Hixie to make sure you have the right push permissions.
Editing a recipe
Flutter has several recipes depending on the test. The recipes share common
actions through recipe_modules. Searching the builder config in infra
will indicate the recipe used for a test.
Recipes are just Python with some limitations on what can be imported. They are documented by the luci/recipes-py GitHub project.
The typical cycle for editing a recipe is:
- Check out the recipes project using
git clone https://flutter.googlesource.com/recipes. - Make your edits (probably to files in
//recipes/recipes). - Update the tests. Run
recipes.py test trainto update the existing expected output to match the new output. Verify completely new test cases by altering theGenTestsmethod of the recipe. The recipe is required to have 100% test coverage. - Run
led get-builder 'luci.flutter.staging:BUILDER_NAME' | led edit -pa git_ref='refs/pull/<PR number>/head' | led edit -pa git_url='https://github.com/flutter/<repo>' | led edit-recipe-bundle | led launch, whereBUILDER_NAMEis the builder name (e.g.Linux Engine), andgit_ref/git_urlis the ref/url of the intended changes to build.- If
ledfails, ensure that yourdepot_toolscheckout is up to date.
- If
- To submit a CL, you need a local branch first (
git checkout -b [some branch name]). - Upload the patch (
git commit,git cl upload), and open the outputted URL to the CL. - Use "Find owners" to get reviewers for the CL
Android Tools
The Android SDK and NDK used by Flutter's Chrome infra bots are stored in Google
Cloud. During the build, a bot runs the download_android_tools.py script that
downloads the required version of the Android SDK into dev/bots/android_tools.
To check which components are currently installed, download the current SDK
stored in Google Cloud using the download_android_tools.py script, then
dev/bots/android_tools/sdk/tools/bin/sdkmanager --list. If you find that some
components need to be updated or installed, follow the steps below:
How to update Android SDK on Google Cloud Storage
-
Run Android SDK Manager and update packages
$ dev/bots/android_tools/sdk/tools/android update sdkUseandroid.baton Windows. -
Use the UI to choose the packages you want to install and/or update.
-
Run
dev/bots/android_tools/sdk/tools/bin/sdkmanager --update. On Windows, runsdkmanager.batinstead. If the process fails with an error saying that it is unable to move files (Windows makes files and directories read-only when another process is holding them open), make a copy of thedev/bots/android_tools/sdk/toolsdirectory, run thesdkmanager.batfrom the copy, and use the--sdk_rootoption pointing atdev/bots/android_tools/sdk. -
Run
dev/bots/android_tools/sdk/tools/bin/sdkmanager --licensesand accept the licenses for the newly installed components. It also helps to run this command a second time and make sure that it prints "All SDK package licenses accepted". -
Run upload_android_tools.py -t sdk
$ dev/bots/upload_android_tools.py -t sdk
How to update Android NDK on Google Cloud Storage
-
Download a new NDK binary (e.g. android-ndk-r10e-linux-x86_64.bin)
-
cd dev/bots/android_tools
$ cd dev/bots/android_tools -
Remove the old ndk directory
$ rm -rf ndk -
Run the new NDK binary file
$ ./android-ndk-r10e-linux-x86_64.bin -
Rename the extracted directory to ndk
$ mv android-ndk-r10e ndk -
Run upload_android_tools.py -t ndk
$ cd ../..$ dev/bots/upload_android_tools.py -t ndk
Flutter codelabs build test
The Flutter codelabs exercise Material Components in the form of a demo application. The code for the codelabs is similar to, but distinct from, the code for the Shrine demo app in Flutter Gallery.
The Flutter codelabs build test ensures that the final version of the Material Components for Flutter Codelabs can be built. This test serves as a smoke test for the Flutter framework and should not fail. If it does, please address any issues in your PR and rerun the test. If you feel that the test failing is not a direct result of changes made in your PR or that breaking this test is absolutely necessary, escalate this issue by submitting an issue to the MDC-Flutter Team.
Unpublishing published archives
Flutter downloadable archives are built for each release by our continuous
integration systems using the prepare_package.dart
script, but if something goes very wrong, and a release is published that wasn't
intended to be published, the unpublish_package.dart
script may be used to remove the package or packages from the channels in which
they were published.
For example To remove a published package corresponding to the git hash
d444a455de87a2e40b7f576dc12ffd9ab82fd491, first do a dry run of the script to
see what it will do:
$ dart ./unpublish_package.dart --temp_dir=/tmp/foo --revision d444a455de87a2e40b7f576dc12ffd9ab82fd491
And once you've verified the output of the dry run to be sure it is what you want to do, run:
$ dart ./unpublish_package.dart --confirm --temp_dir=/tmp/foo --revision d444a455de87a2e40b7f576dc12ffd9ab82fd491
and it will perform the actions. You will of course need to have access
to the cloud storage server and have gsutil installed to perform this
operation. Only runs on Linux or macOS systems.
See dart ./unpublish_package.dart --help for more details.
Once the package is unpublished, it will not be available from the website for download, and will not be rebuilt (even though there is a tagged revision in the repo still) unless someone forces the packaging build to run again at that revision to rebuild the package.