[framework,web] add FlutterTimeline and semantics benchmarks that use it (#128366)
## FlutterTimeline
Add a new class `FlutterTimeline` that's a drop-in replacement for `Timeline` from `dart:developer`. In addition to forwarding invocations of `startSync`, `finishSync`, `timeSync`, and `instantSync` to `dart:developer`, provides the following extra methods that make is easy to collect timings for code blocks on a frame-by-frame basis:
* `debugCollect()` - aggregates timings since the last reset, or since the app launched.
* `debugReset()` - forgets all data collected since the previous reset, or since the app launched. This allows clearing data from previous frames so timings can be attributed to the current frame.
* `now` - this was enhanced so that it works on the web by calling `window.performance.now` (in `Timeline` this is a noop in Dart web compilers).
* `collectionEnabled` - a field that controls whether `FlutterTimeline` stores timings in memory. By default this is disabled to avoid unexpected overhead (although the class is designed for minimal and predictable overhead). Specific benchmarks can enable collection to report to Skia Perf.
## Semantics benchmarks
Add `BenchMaterial3Semantics` that benchmarks the cost of semantics when constructing a screen full of Material 3 widgets from nothing. It is expected that semantics will have non-trivial cost in this case, but we should strive to keep it much lower than the rendering cost. This is the case already. This benchmark shows that the cost of semantics is <10%.
Add `BenchMaterial3ScrollSemantics` that benchmarks the cost of scrolling a previously constructed screen full of Material 3 widgets. The expectation should be that semantics will have trivial cost, since we're just shifting some widgets around. As of today, the numbers are not great, with semantics taking >50% of frame time, which is what prompted this PR in the first place. As we optimize this, we want to see this number improve.
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart
index a531281..882ae92 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart
@@ -3,8 +3,8 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
+import 'material3.dart';
import 'recorder.dart';
/// Measures how expensive it is to construct the material 3 components screen.
@@ -15,2330 +15,6 @@
@override
Widget createWidget() {
- return const Material3Components();
- }
-}
-
-const SizedBox rowDivider = SizedBox(width: 20);
-const SizedBox colDivider = SizedBox(height: 10);
-const double smallSpacing = 10.0;
-const double cardWidth = 115;
-const double widthConstraint = 450;
-
-class Material3Components extends StatefulWidget {
- const Material3Components({super.key});
-
- @override
- State<Material3Components> createState() => _Material3ComponentsState();
-}
-
-class _Material3ComponentsState extends State<Material3Components> {
- final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
-
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- home: Scaffold(
- key: scaffoldKey,
- body: Row(
- children: <Widget>[
- Expanded(
- child: FirstComponentList(
- showNavBottomBar: true,
- scaffoldKey: scaffoldKey,
- showSecondList: true,
- ),
- ),
- Expanded(
- child: SecondComponentList(scaffoldKey: scaffoldKey),
- ),
- ],
- ),
- ),
- );
- }
-}
-
-class FirstComponentList extends StatelessWidget {
- const FirstComponentList({
- super.key,
- required this.showNavBottomBar,
- required this.scaffoldKey,
- required this.showSecondList,
- });
-
- final bool showNavBottomBar;
- final GlobalKey<ScaffoldState> scaffoldKey;
- final bool showSecondList;
-
- @override
- Widget build(BuildContext context) {
- // Fully traverse this list before moving on.
- return FocusTraversalGroup(
- child: ListView(
- padding: showSecondList
- ? const EdgeInsetsDirectional.only(end: smallSpacing)
- : EdgeInsets.zero,
- children: <Widget>[
- const Actions(),
- colDivider,
- const Communication(),
- colDivider,
- const Containment(),
- if (!showSecondList) ...<Widget>[
- colDivider,
- Navigation(scaffoldKey: scaffoldKey),
- colDivider,
- const Selection(),
- colDivider,
- const TextInputs()
- ],
- ],
- ),
- );
- }
-}
-
-class SecondComponentList extends StatelessWidget {
- const SecondComponentList({
- super.key,
- required this.scaffoldKey,
- });
-
- final GlobalKey<ScaffoldState> scaffoldKey;
-
- @override
- Widget build(BuildContext context) {
- // Fully traverse this list before moving on.
- return FocusTraversalGroup(
- child: ListView(
- padding: const EdgeInsetsDirectional.only(end: smallSpacing),
- children: <Widget>[
- Navigation(scaffoldKey: scaffoldKey),
- colDivider,
- const Selection(),
- colDivider,
- const TextInputs(),
- ],
- ),
- );
- }
-}
-
-class Actions extends StatelessWidget {
- const Actions({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentGroupDecoration(label: 'Actions', children: <Widget>[
- Buttons(),
- FloatingActionButtons(),
- IconToggleButtons(),
- SegmentedButtons(),
- ]);
- }
-}
-
-class Communication extends StatelessWidget {
- const Communication({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentGroupDecoration(label: 'Communication', children: <Widget>[
- NavigationBars(
- selectedIndex: 1,
- isExampleBar: true,
- isBadgeExample: true,
- ),
- ProgressIndicators(),
- SnackBarSection(),
- ]);
- }
-}
-
-class Containment extends StatelessWidget {
- const Containment({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentGroupDecoration(label: 'Containment', children: <Widget>[
- BottomSheetSection(),
- Cards(),
- Dialogs(),
- Dividers(),
- ]);
- }
-}
-
-class Navigation extends StatelessWidget {
- const Navigation({super.key, required this.scaffoldKey});
-
- final GlobalKey<ScaffoldState> scaffoldKey;
-
- @override
- Widget build(BuildContext context) {
- return ComponentGroupDecoration(label: 'Navigation', children: <Widget>[
- const BottomAppBars(),
- const NavigationBars(
- selectedIndex: 0,
- isExampleBar: true,
- ),
- NavigationDrawers(scaffoldKey: scaffoldKey),
- const NavigationRails(),
- const Tabs(),
- const TopAppBars(),
- ]);
- }
-}
-
-class Selection extends StatelessWidget {
- const Selection({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentGroupDecoration(label: 'Selection', children: <Widget>[
- Checkboxes(),
- Chips(),
- Menus(),
- Radios(),
- Sliders(),
- Switches(),
- ]);
- }
-}
-
-class TextInputs extends StatelessWidget {
- const TextInputs({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentGroupDecoration(
- label: 'Text inputs',
- children: <Widget>[TextFields()],
- );
- }
-}
-
-class Buttons extends StatefulWidget {
- const Buttons({super.key});
-
- @override
- State<Buttons> createState() => _ButtonsState();
-}
-
-class _ButtonsState extends State<Buttons> {
- @override
- Widget build(BuildContext context) {
- return const ComponentDecoration(
- label: 'Common buttons',
- tooltipMessage:
- 'Use ElevatedButton, FilledButton, FilledButton.tonal, OutlinedButton, or TextButton',
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: <Widget>[
- ButtonsWithoutIcon(isDisabled: false),
- ButtonsWithIcon(),
- ButtonsWithoutIcon(isDisabled: true),
- ],
- ),
- ),
- );
- }
-}
-
-class ButtonsWithoutIcon extends StatelessWidget {
- const ButtonsWithoutIcon({super.key, required this.isDisabled});
-
- final bool isDisabled;
-
- @override
- Widget build(BuildContext context) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 5.0),
- child: IntrinsicWidth(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- ElevatedButton(
- onPressed: isDisabled ? null : () {},
- child: const Text('Elevated'),
- ),
- colDivider,
- FilledButton(
- onPressed: isDisabled ? null : () {},
- child: const Text('Filled'),
- ),
- colDivider,
- FilledButton.tonal(
- onPressed: isDisabled ? null : () {},
- child: const Text('Filled tonal'),
- ),
- colDivider,
- OutlinedButton(
- onPressed: isDisabled ? null : () {},
- child: const Text('Outlined'),
- ),
- colDivider,
- TextButton(
- onPressed: isDisabled ? null : () {},
- child: const Text('Text'),
- ),
- ],
- ),
- ),
- );
- }
-}
-
-class ButtonsWithIcon extends StatelessWidget {
- const ButtonsWithIcon({super.key});
-
- @override
- Widget build(BuildContext context) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 10.0),
- child: IntrinsicWidth(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- ElevatedButton.icon(
- onPressed: () {},
- icon: const Icon(Icons.add),
- label: const Text('Icon'),
- ),
- colDivider,
- FilledButton.icon(
- onPressed: () {},
- label: const Text('Icon'),
- icon: const Icon(Icons.add),
- ),
- colDivider,
- FilledButton.tonalIcon(
- onPressed: () {},
- label: const Text('Icon'),
- icon: const Icon(Icons.add),
- ),
- colDivider,
- OutlinedButton.icon(
- onPressed: () {},
- icon: const Icon(Icons.add),
- label: const Text('Icon'),
- ),
- colDivider,
- TextButton.icon(
- onPressed: () {},
- icon: const Icon(Icons.add),
- label: const Text('Icon'),
- )
- ],
- ),
- ),
- );
- }
-}
-
-class FloatingActionButtons extends StatelessWidget {
- const FloatingActionButtons({super.key});
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Floating action buttons',
- tooltipMessage:
- 'Use FloatingActionButton or FloatingActionButton.extended',
- child: Wrap(
- crossAxisAlignment: WrapCrossAlignment.center,
- runSpacing: smallSpacing,
- spacing: smallSpacing,
- children: <Widget>[
- FloatingActionButton.small(
- onPressed: () {},
- tooltip: 'Small',
- child: const Icon(Icons.add),
- ),
- FloatingActionButton.extended(
- onPressed: () {},
- tooltip: 'Extended',
- icon: const Icon(Icons.add),
- label: const Text('Create'),
- ),
- FloatingActionButton(
- onPressed: () {},
- tooltip: 'Standard',
- child: const Icon(Icons.add),
- ),
- FloatingActionButton.large(
- onPressed: () {},
- tooltip: 'Large',
- child: const Icon(Icons.add),
- ),
- ],
- ),
- );
- }
-}
-
-class Cards extends StatelessWidget {
- const Cards({super.key});
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Cards',
- tooltipMessage: 'Use Card',
- child: Wrap(
- alignment: WrapAlignment.spaceEvenly,
- children: <Widget>[
- SizedBox(
- width: cardWidth,
- child: Card(
- child: Container(
- padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
- child: Column(
- children: <Widget>[
- Align(
- alignment: Alignment.topRight,
- child: IconButton(
- icon: const Icon(Icons.more_vert),
- onPressed: () {},
- ),
- ),
- const SizedBox(height: 20),
- const Align(
- alignment: Alignment.bottomLeft,
- child: Text('Elevated'),
- )
- ],
- ),
- ),
- ),
- ),
- SizedBox(
- width: cardWidth,
- child: Card(
- color: Theme.of(context).colorScheme.surfaceVariant,
- elevation: 0,
- child: Container(
- padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
- child: Column(
- children: <Widget>[
- Align(
- alignment: Alignment.topRight,
- child: IconButton(
- icon: const Icon(Icons.more_vert),
- onPressed: () {},
- ),
- ),
- const SizedBox(height: 20),
- const Align(
- alignment: Alignment.bottomLeft,
- child: Text('Filled'),
- )
- ],
- ),
- ),
- ),
- ),
- SizedBox(
- width: cardWidth,
- child: Card(
- elevation: 0,
- shape: RoundedRectangleBorder(
- side: BorderSide(
- color: Theme.of(context).colorScheme.outline,
- ),
- borderRadius: const BorderRadius.all(Radius.circular(12)),
- ),
- child: Container(
- padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
- child: Column(
- children: <Widget>[
- Align(
- alignment: Alignment.topRight,
- child: IconButton(
- icon: const Icon(Icons.more_vert),
- onPressed: () {},
- ),
- ),
- const SizedBox(height: 20),
- const Align(
- alignment: Alignment.bottomLeft,
- child: Text('Outlined'),
- )
- ],
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
-}
-
-class _ClearButton extends StatelessWidget {
- const _ClearButton({required this.controller});
-
- final TextEditingController controller;
-
- @override
- Widget build(BuildContext context) => IconButton(
- icon: const Icon(Icons.clear),
- onPressed: () => controller.clear(),
- );
-}
-
-class TextFields extends StatefulWidget {
- const TextFields({super.key});
-
- @override
- State<TextFields> createState() => _TextFieldsState();
-}
-
-class _TextFieldsState extends State<TextFields> {
- final TextEditingController _controllerFilled = TextEditingController();
- final TextEditingController _controllerOutlined = TextEditingController();
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Text fields',
- tooltipMessage: 'Use TextField with different InputDecoration',
- child: Column(
- children: <Widget>[
- Padding(
- padding: const EdgeInsets.all(smallSpacing),
- child: TextField(
- controller: _controllerFilled,
- decoration: InputDecoration(
- prefixIcon: const Icon(Icons.search),
- suffixIcon: _ClearButton(controller: _controllerFilled),
- labelText: 'Filled',
- hintText: 'hint text',
- helperText: 'supporting text',
- filled: true,
- ),
- ),
- ),
- Padding(
- padding: const EdgeInsets.all(smallSpacing),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: <Widget>[
- Flexible(
- child: SizedBox(
- width: 200,
- child: TextField(
- maxLength: 10,
- maxLengthEnforcement: MaxLengthEnforcement.none,
- controller: _controllerFilled,
- decoration: InputDecoration(
- prefixIcon: const Icon(Icons.search),
- suffixIcon: _ClearButton(controller: _controllerFilled),
- labelText: 'Filled',
- hintText: 'hint text',
- helperText: 'supporting text',
- filled: true,
- errorText: 'error text',
- ),
- ),
- ),
- ),
- const SizedBox(width: smallSpacing),
- Flexible(
- child: SizedBox(
- width: 200,
- child: TextField(
- controller: _controllerFilled,
- enabled: false,
- decoration: InputDecoration(
- prefixIcon: const Icon(Icons.search),
- suffixIcon: _ClearButton(controller: _controllerFilled),
- labelText: 'Disabled',
- hintText: 'hint text',
- helperText: 'supporting text',
- filled: true,
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- Padding(
- padding: const EdgeInsets.all(smallSpacing),
- child: TextField(
- controller: _controllerOutlined,
- decoration: InputDecoration(
- prefixIcon: const Icon(Icons.search),
- suffixIcon: _ClearButton(controller: _controllerOutlined),
- labelText: 'Outlined',
- hintText: 'hint text',
- helperText: 'supporting text',
- border: const OutlineInputBorder(),
- ),
- ),
- ),
- Padding(
- padding: const EdgeInsets.all(smallSpacing),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: <Widget>[
- Flexible(
- child: SizedBox(
- width: 200,
- child: TextField(
- controller: _controllerOutlined,
- decoration: InputDecoration(
- prefixIcon: const Icon(Icons.search),
- suffixIcon:
- _ClearButton(controller: _controllerOutlined),
- labelText: 'Outlined',
- hintText: 'hint text',
- helperText: 'supporting text',
- errorText: 'error text',
- border: const OutlineInputBorder(),
- filled: true,
- ),
- ),
- ),
- ),
- const SizedBox(width: smallSpacing),
- Flexible(
- child: SizedBox(
- width: 200,
- child: TextField(
- controller: _controllerOutlined,
- enabled: false,
- decoration: InputDecoration(
- prefixIcon: const Icon(Icons.search),
- suffixIcon:
- _ClearButton(controller: _controllerOutlined),
- labelText: 'Disabled',
- hintText: 'hint text',
- helperText: 'supporting text',
- border: const OutlineInputBorder(),
- filled: true,
- ),
- ),
- ),
- ),
- ])),
- ],
- ),
- );
- }
-}
-
-class Dialogs extends StatefulWidget {
- const Dialogs({super.key});
-
- @override
- State<Dialogs> createState() => _DialogsState();
-}
-
-class _DialogsState extends State<Dialogs> {
- void openDialog(BuildContext context) {
- showDialog<void>(
- context: context,
- builder: (BuildContext context) => AlertDialog(
- title: const Text('What is a dialog?'),
- content: const Text(
- 'A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made.'),
- actions: <Widget>[
- TextButton(
- child: const Text('Okay'),
- onPressed: () => Navigator.of(context).pop(),
- ),
- FilledButton(
- child: const Text('Dismiss'),
- onPressed: () => Navigator.of(context).pop(),
- ),
- ],
- ),
- );
- }
-
- void openFullscreenDialog(BuildContext context) {
- showDialog<void>(
- context: context,
- builder: (BuildContext context) => Dialog.fullscreen(
- child: Padding(
- padding: const EdgeInsets.all(20.0),
- child: Scaffold(
- appBar: AppBar(
- title: const Text('Full-screen dialog'),
- centerTitle: false,
- leading: IconButton(
- icon: const Icon(Icons.close),
- onPressed: () => Navigator.of(context).pop(),
- ),
- actions: <Widget>[
- TextButton(
- child: const Text('Close'),
- onPressed: () => Navigator.of(context).pop(),
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Dialog',
- tooltipMessage:
- 'Use showDialog with Dialog.fullscreen, AlertDialog, or SimpleDialog',
- child: Wrap(
- alignment: WrapAlignment.spaceBetween,
- children: <Widget>[
- TextButton(
- child: const Text(
- 'Show dialog',
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- onPressed: () => openDialog(context),
- ),
- TextButton(
- child: const Text(
- 'Show full-screen dialog',
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- onPressed: () => openFullscreenDialog(context),
- ),
- ],
- ),
- );
- }
-}
-
-class Dividers extends StatelessWidget {
- const Dividers({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentDecoration(
- label: 'Dividers',
- tooltipMessage: 'Use Divider or VerticalDivider',
- child: Column(
- children: <Widget>[
- Divider(key: Key('divider')),
- ],
- ),
- );
- }
-}
-
-class Switches extends StatelessWidget {
- const Switches({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentDecoration(
- label: 'Switches',
- tooltipMessage: 'Use SwitchListTile or Switch',
- child: Column(
- children: <Widget>[
- SwitchRow(isEnabled: true),
- SwitchRow(isEnabled: false),
- ],
- ),
- );
- }
-}
-
-class SwitchRow extends StatefulWidget {
- const SwitchRow({super.key, required this.isEnabled});
-
- final bool isEnabled;
-
- @override
- State<SwitchRow> createState() => _SwitchRowState();
-}
-
-class _SwitchRowState extends State<SwitchRow> {
- bool value0 = false;
- bool value1 = true;
-
- final MaterialStateProperty<Icon?> thumbIcon =
- MaterialStateProperty.resolveWith<Icon?>((Set<MaterialState> states) {
- if (states.contains(MaterialState.selected)) {
- return const Icon(Icons.check);
- }
- return const Icon(Icons.close);
- });
-
- @override
- Widget build(BuildContext context) {
- return Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: <Widget>[
- Switch(
- value: value0,
- onChanged: widget.isEnabled
- ? (bool value) {
- setState(() {
- value0 = value;
- });
- }
- : null,
- ),
- Switch(
- thumbIcon: thumbIcon,
- value: value1,
- onChanged: widget.isEnabled
- ? (bool value) {
- setState(() {
- value1 = value;
- });
- }
- : null,
- ),
- ],
- );
- }
-}
-
-class Checkboxes extends StatefulWidget {
- const Checkboxes({super.key});
-
- @override
- State<Checkboxes> createState() => _CheckboxesState();
-}
-
-class _CheckboxesState extends State<Checkboxes> {
- bool? isChecked0 = true;
- bool? isChecked1;
- bool? isChecked2 = false;
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Checkboxes',
- tooltipMessage: 'Use CheckboxListTile or Checkbox',
- child: Column(
- children: <Widget>[
- CheckboxListTile(
- tristate: true,
- value: isChecked0,
- title: const Text('Option 1'),
- onChanged: (bool? value) {
- setState(() {
- isChecked0 = value;
- });
- },
- ),
- CheckboxListTile(
- tristate: true,
- value: isChecked1,
- title: const Text('Option 2'),
- onChanged: (bool? value) {
- setState(() {
- isChecked1 = value;
- });
- },
- ),
- CheckboxListTile(
- tristate: true,
- value: isChecked2,
- title: const Text('Option 3'),
- onChanged: (bool? value) {
- setState(() {
- isChecked2 = value;
- });
- },
- ),
- const CheckboxListTile(
- tristate: true,
- title: Text('Option 4'),
- value: true,
- onChanged: null,
- ),
- ],
- ),
- );
- }
-}
-
-enum Value { first, second }
-
-class Radios extends StatefulWidget {
- const Radios({super.key});
-
- @override
- State<Radios> createState() => _RadiosState();
-}
-
-enum Options { option1, option2, option3 }
-
-class _RadiosState extends State<Radios> {
- Options? _selectedOption = Options.option1;
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Radio buttons',
- tooltipMessage: 'Use RadioListTile<T> or Radio<T>',
- child: Column(
- children: <Widget>[
- RadioListTile<Options>(
- title: const Text('Option 1'),
- value: Options.option1,
- groupValue: _selectedOption,
- onChanged: (Options? value) {
- setState(() {
- _selectedOption = value;
- });
- },
- ),
- RadioListTile<Options>(
- title: const Text('Option 2'),
- value: Options.option2,
- groupValue: _selectedOption,
- onChanged: (Options? value) {
- setState(() {
- _selectedOption = value;
- });
- },
- ),
- RadioListTile<Options>(
- title: const Text('Option 3'),
- value: Options.option3,
- groupValue: _selectedOption,
- onChanged: null,
- ),
- ],
- ),
- );
- }
-}
-
-class ProgressIndicators extends StatefulWidget {
- const ProgressIndicators({super.key});
-
- @override
- State<ProgressIndicators> createState() => _ProgressIndicatorsState();
-}
-
-class _ProgressIndicatorsState extends State<ProgressIndicators> {
- bool playProgressIndicator = false;
-
- @override
- Widget build(BuildContext context) {
- final double? progressValue = playProgressIndicator ? null : 0.7;
-
- return ComponentDecoration(
- label: 'Progress indicators',
- tooltipMessage:
- 'Use CircularProgressIndicator or LinearProgressIndicator',
- child: Column(
- children: <Widget>[
- Row(
- children: <Widget>[
- IconButton(
- isSelected: playProgressIndicator,
- selectedIcon: const Icon(Icons.pause),
- icon: const Icon(Icons.play_arrow),
- onPressed: () {
- setState(() {
- playProgressIndicator = !playProgressIndicator;
- });
- },
- ),
- Expanded(
- child: Row(
- children: <Widget>[
- rowDivider,
- CircularProgressIndicator(
- value: progressValue,
- ),
- rowDivider,
- Expanded(
- child: LinearProgressIndicator(
- value: progressValue,
- ),
- ),
- rowDivider,
- ],
- ),
- ),
- ],
- ),
- ],
- ),
- );
- }
-}
-
-const List<NavigationDestination> appBarDestinations = <NavigationDestination>[
- NavigationDestination(
- tooltip: '',
- icon: Icon(Icons.widgets_outlined),
- label: 'Components',
- selectedIcon: Icon(Icons.widgets),
- ),
- NavigationDestination(
- tooltip: '',
- icon: Icon(Icons.format_paint_outlined),
- label: 'Color',
- selectedIcon: Icon(Icons.format_paint),
- ),
- NavigationDestination(
- tooltip: '',
- icon: Icon(Icons.text_snippet_outlined),
- label: 'Typography',
- selectedIcon: Icon(Icons.text_snippet),
- ),
- NavigationDestination(
- tooltip: '',
- icon: Icon(Icons.invert_colors_on_outlined),
- label: 'Elevation',
- selectedIcon: Icon(Icons.opacity),
- )
-];
-
-const List<Widget> exampleBarDestinations = <Widget>[
- NavigationDestination(
- tooltip: '',
- icon: Icon(Icons.explore_outlined),
- label: 'Explore',
- selectedIcon: Icon(Icons.explore),
- ),
- NavigationDestination(
- tooltip: '',
- icon: Icon(Icons.pets_outlined),
- label: 'Pets',
- selectedIcon: Icon(Icons.pets),
- ),
- NavigationDestination(
- tooltip: '',
- icon: Icon(Icons.account_box_outlined),
- label: 'Account',
- selectedIcon: Icon(Icons.account_box),
- )
-];
-
-List<Widget> barWithBadgeDestinations = <Widget>[
- NavigationDestination(
- tooltip: '',
- icon: Badge.count(count: 1000, child: const Icon(Icons.mail_outlined)),
- label: 'Mail',
- selectedIcon: Badge.count(count: 1000, child: const Icon(Icons.mail)),
- ),
- const NavigationDestination(
- tooltip: '',
- icon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble_outline)),
- label: 'Chat',
- selectedIcon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble)),
- ),
- const NavigationDestination(
- tooltip: '',
- icon: Badge(child: Icon(Icons.group_outlined)),
- label: 'Rooms',
- selectedIcon: Badge(child: Icon(Icons.group_rounded)),
- ),
- NavigationDestination(
- tooltip: '',
- icon: Badge.count(count: 3, child: const Icon(Icons.videocam_outlined)),
- label: 'Meet',
- selectedIcon: Badge.count(count: 3, child: const Icon(Icons.videocam)),
- )
-];
-
-class NavigationBars extends StatefulWidget {
- const NavigationBars({
- super.key,
- this.onSelectItem,
- required this.selectedIndex,
- required this.isExampleBar,
- this.isBadgeExample = false,
- });
-
- final void Function(int)? onSelectItem;
- final int selectedIndex;
- final bool isExampleBar;
- final bool isBadgeExample;
-
- @override
- State<NavigationBars> createState() => _NavigationBarsState();
-}
-
-class _NavigationBarsState extends State<NavigationBars> {
- late int selectedIndex;
-
- @override
- void initState() {
- super.initState();
- selectedIndex = widget.selectedIndex;
- }
-
- @override
- void didUpdateWidget(covariant NavigationBars oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (widget.selectedIndex != oldWidget.selectedIndex) {
- selectedIndex = widget.selectedIndex;
- }
- }
-
- @override
- Widget build(BuildContext context) {
- // App NavigationBar should get first focus.
- Widget navigationBar = Focus(
- autofocus: !(widget.isExampleBar || widget.isBadgeExample),
- child: NavigationBar(
- selectedIndex: selectedIndex,
- onDestinationSelected: (int index) {
- setState(() {
- selectedIndex = index;
- });
- if (!widget.isExampleBar) {
- widget.onSelectItem!(index);
- }
- },
- destinations: widget.isExampleBar && widget.isBadgeExample
- ? barWithBadgeDestinations
- : widget.isExampleBar
- ? exampleBarDestinations
- : appBarDestinations,
- ),
- );
-
- if (widget.isExampleBar && widget.isBadgeExample) {
- navigationBar = ComponentDecoration(
- label: 'Badges',
- tooltipMessage: 'Use Badge or Badge.count',
- child: navigationBar);
- } else if (widget.isExampleBar) {
- navigationBar = ComponentDecoration(
- label: 'Navigation bar',
- tooltipMessage: 'Use NavigationBar',
- child: navigationBar);
- }
-
- return navigationBar;
- }
-}
-
-class IconToggleButtons extends StatefulWidget {
- const IconToggleButtons({super.key});
-
- @override
- State<IconToggleButtons> createState() => _IconToggleButtonsState();
-}
-
-class _IconToggleButtonsState extends State<IconToggleButtons> {
- @override
- Widget build(BuildContext context) {
- return const ComponentDecoration(
- label: 'Icon buttons',
- tooltipMessage: 'Use IconButton',
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: <Widget>[
- Column(
- // Standard IconButton
- children: <Widget>[
- IconToggleButton(
- isEnabled: true,
- tooltip: 'Standard',
- ),
- colDivider,
- IconToggleButton(
- isEnabled: false,
- tooltip: 'Standard (disabled)',
- ),
- ],
- ),
- Column(
- children: <Widget>[
- // Filled IconButton
- IconToggleButton(
- isEnabled: true,
- tooltip: 'Filled',
- getDefaultStyle: enabledFilledButtonStyle,
- ),
- colDivider,
- IconToggleButton(
- isEnabled: false,
- tooltip: 'Filled (disabled)',
- getDefaultStyle: disabledFilledButtonStyle,
- ),
- ],
- ),
- Column(
- children: <Widget>[
- // Filled Tonal IconButton
- IconToggleButton(
- isEnabled: true,
- tooltip: 'Filled tonal',
- getDefaultStyle: enabledFilledTonalButtonStyle,
- ),
- colDivider,
- IconToggleButton(
- isEnabled: false,
- tooltip: 'Filled tonal (disabled)',
- getDefaultStyle: disabledFilledTonalButtonStyle,
- ),
- ],
- ),
- Column(
- children: <Widget>[
- // Outlined IconButton
- IconToggleButton(
- isEnabled: true,
- tooltip: 'Outlined',
- getDefaultStyle: enabledOutlinedButtonStyle,
- ),
- colDivider,
- IconToggleButton(
- isEnabled: false,
- tooltip: 'Outlined (disabled)',
- getDefaultStyle: disabledOutlinedButtonStyle,
- ),
- ],
- ),
- ],
- ),
- );
- }
-}
-
-class IconToggleButton extends StatefulWidget {
- const IconToggleButton({
- required this.isEnabled,
- required this.tooltip,
- this.getDefaultStyle,
- super.key,
- });
-
- final bool isEnabled;
- final String tooltip;
- final ButtonStyle? Function(bool, ColorScheme)? getDefaultStyle;
-
- @override
- State<IconToggleButton> createState() => _IconToggleButtonState();
-}
-
-class _IconToggleButtonState extends State<IconToggleButton> {
- bool selected = false;
-
- @override
- Widget build(BuildContext context) {
- final ColorScheme colors = Theme.of(context).colorScheme;
- final VoidCallback? onPressed = widget.isEnabled
- ? () {
- setState(() {
- selected = !selected;
- });
- }
- : null;
- final ButtonStyle? style = widget.getDefaultStyle?.call(selected, colors);
-
- return IconButton(
- visualDensity: VisualDensity.standard,
- isSelected: selected,
- tooltip: widget.tooltip,
- icon: const Icon(Icons.settings_outlined),
- selectedIcon: const Icon(Icons.settings),
- onPressed: onPressed,
- style: style,
- );
- }
-}
-
-ButtonStyle enabledFilledButtonStyle(bool selected, ColorScheme colors) {
- return IconButton.styleFrom(
- foregroundColor: selected ? colors.onPrimary : colors.primary,
- backgroundColor: selected ? colors.primary : colors.surfaceVariant,
- disabledForegroundColor: colors.onSurface.withOpacity(0.38),
- disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
- hoverColor: selected
- ? colors.onPrimary.withOpacity(0.08)
- : colors.primary.withOpacity(0.08),
- focusColor: selected
- ? colors.onPrimary.withOpacity(0.12)
- : colors.primary.withOpacity(0.12),
- highlightColor: selected
- ? colors.onPrimary.withOpacity(0.12)
- : colors.primary.withOpacity(0.12),
- );
-}
-
-ButtonStyle disabledFilledButtonStyle(bool selected, ColorScheme colors) {
- return IconButton.styleFrom(
- disabledForegroundColor: colors.onSurface.withOpacity(0.38),
- disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
- );
-}
-
-ButtonStyle enabledFilledTonalButtonStyle(bool selected, ColorScheme colors) {
- return IconButton.styleFrom(
- foregroundColor:
- selected ? colors.onSecondaryContainer : colors.onSurfaceVariant,
- backgroundColor:
- selected ? colors.secondaryContainer : colors.surfaceVariant,
- hoverColor: selected
- ? colors.onSecondaryContainer.withOpacity(0.08)
- : colors.onSurfaceVariant.withOpacity(0.08),
- focusColor: selected
- ? colors.onSecondaryContainer.withOpacity(0.12)
- : colors.onSurfaceVariant.withOpacity(0.12),
- highlightColor: selected
- ? colors.onSecondaryContainer.withOpacity(0.12)
- : colors.onSurfaceVariant.withOpacity(0.12),
- );
-}
-
-ButtonStyle disabledFilledTonalButtonStyle(bool selected, ColorScheme colors) {
- return IconButton.styleFrom(
- disabledForegroundColor: colors.onSurface.withOpacity(0.38),
- disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
- );
-}
-
-ButtonStyle enabledOutlinedButtonStyle(bool selected, ColorScheme colors) {
- return IconButton.styleFrom(
- backgroundColor: selected ? colors.inverseSurface : null,
- hoverColor: selected
- ? colors.onInverseSurface.withOpacity(0.08)
- : colors.onSurfaceVariant.withOpacity(0.08),
- focusColor: selected
- ? colors.onInverseSurface.withOpacity(0.12)
- : colors.onSurfaceVariant.withOpacity(0.12),
- highlightColor: selected
- ? colors.onInverseSurface.withOpacity(0.12)
- : colors.onSurface.withOpacity(0.12),
- side: BorderSide(color: colors.outline),
- ).copyWith(
- foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
- if (states.contains(MaterialState.selected)) {
- return colors.onInverseSurface;
- }
- if (states.contains(MaterialState.pressed)) {
- return colors.onSurface;
- }
- return null;
- }),
- );
-}
-
-ButtonStyle disabledOutlinedButtonStyle(bool selected, ColorScheme colors) {
- return IconButton.styleFrom(
- disabledForegroundColor: colors.onSurface.withOpacity(0.38),
- disabledBackgroundColor:
- selected ? colors.onSurface.withOpacity(0.12) : null,
- side: selected ? null : BorderSide(color: colors.outline.withOpacity(0.12)),
- );
-}
-
-class Chips extends StatefulWidget {
- const Chips({super.key});
-
- @override
- State<Chips> createState() => _ChipsState();
-}
-
-class _ChipsState extends State<Chips> {
- bool isFiltered = true;
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Chips',
- tooltipMessage:
- 'Use ActionChip, FilterChip, or InputChip. \nActionChip can also be used for suggestion chip',
- child: Column(
- children: <Widget>[
- Wrap(
- spacing: smallSpacing,
- runSpacing: smallSpacing,
- children: <Widget>[
- ActionChip(
- label: const Text('Assist'),
- avatar: const Icon(Icons.event),
- onPressed: () {},
- ),
- FilterChip(
- label: const Text('Filter'),
- selected: isFiltered,
- onSelected: (bool selected) {
- setState(() => isFiltered = selected);
- },
- ),
- InputChip(
- label: const Text('Input'),
- onPressed: () {},
- onDeleted: () {},
- ),
- ActionChip(
- label: const Text('Suggestion'),
- onPressed: () {},
- ),
- ],
- ),
- colDivider,
- Wrap(
- spacing: smallSpacing,
- runSpacing: smallSpacing,
- children: <Widget>[
- const ActionChip(
- label: Text('Assist'),
- avatar: Icon(Icons.event),
- ),
- FilterChip(
- label: const Text('Filter'),
- selected: isFiltered,
- onSelected: null,
- ),
- InputChip(
- label: const Text('Input'),
- onDeleted: () {},
- isEnabled: false,
- ),
- const ActionChip(
- label: Text('Suggestion'),
- ),
- ],
- ),
- ],
- ),
- );
- }
-}
-
-class SegmentedButtons extends StatelessWidget {
- const SegmentedButtons({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentDecoration(
- label: 'Segmented buttons',
- tooltipMessage: 'Use SegmentedButton<T>',
- child: Column(
- children: <Widget>[
- SingleChoice(),
- colDivider,
- MultipleChoice(),
- ],
- ),
- );
- }
-}
-
-enum Calendar { day, week, month, year }
-
-class SingleChoice extends StatefulWidget {
- const SingleChoice({super.key});
-
- @override
- State<SingleChoice> createState() => _SingleChoiceState();
-}
-
-class _SingleChoiceState extends State<SingleChoice> {
- Calendar calendarView = Calendar.day;
-
- @override
- Widget build(BuildContext context) {
- return SegmentedButton<Calendar>(
- segments: const <ButtonSegment<Calendar>>[
- ButtonSegment<Calendar>(
- value: Calendar.day,
- label: Text('Day'),
- icon: Icon(Icons.calendar_view_day)),
- ButtonSegment<Calendar>(
- value: Calendar.week,
- label: Text('Week'),
- icon: Icon(Icons.calendar_view_week)),
- ButtonSegment<Calendar>(
- value: Calendar.month,
- label: Text('Month'),
- icon: Icon(Icons.calendar_view_month)),
- ButtonSegment<Calendar>(
- value: Calendar.year,
- label: Text('Year'),
- icon: Icon(Icons.calendar_today)),
- ],
- selected: <Calendar>{calendarView},
- onSelectionChanged: (Set<Calendar> newSelection) {
- setState(() {
- // By default there is only a single segment that can be
- // selected at one time, so its value is always the first
- // item in the selected set.
- calendarView = newSelection.first;
- });
- },
- );
- }
-}
-
-enum Sizes { extraSmall, small, medium, large, extraLarge }
-
-class MultipleChoice extends StatefulWidget {
- const MultipleChoice({super.key});
-
- @override
- State<MultipleChoice> createState() => _MultipleChoiceState();
-}
-
-class _MultipleChoiceState extends State<MultipleChoice> {
- Set<Sizes> selection = <Sizes>{Sizes.large, Sizes.extraLarge};
-
- @override
- Widget build(BuildContext context) {
- return SegmentedButton<Sizes>(
- segments: const <ButtonSegment<Sizes>>[
- ButtonSegment<Sizes>(value: Sizes.extraSmall, label: Text('XS')),
- ButtonSegment<Sizes>(value: Sizes.small, label: Text('S')),
- ButtonSegment<Sizes>(value: Sizes.medium, label: Text('M')),
- ButtonSegment<Sizes>(
- value: Sizes.large,
- label: Text('L'),
- ),
- ButtonSegment<Sizes>(value: Sizes.extraLarge, label: Text('XL')),
- ],
- selected: selection,
- onSelectionChanged: (Set<Sizes> newSelection) {
- setState(() {
- selection = newSelection;
- });
- },
- multiSelectionEnabled: true,
- );
- }
-}
-
-class SnackBarSection extends StatelessWidget {
- const SnackBarSection({super.key});
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Snackbar',
- tooltipMessage:
- 'Use ScaffoldMessenger.of(context).showSnackBar with SnackBar',
- child: TextButton(
- onPressed: () {
- final SnackBar snackBar = SnackBar(
- behavior: SnackBarBehavior.floating,
- width: 400.0,
- content: const Text('This is a snackbar'),
- action: SnackBarAction(
- label: 'Close',
- onPressed: () {},
- ),
- );
-
- ScaffoldMessenger.of(context).hideCurrentSnackBar();
- ScaffoldMessenger.of(context).showSnackBar(snackBar);
- },
- child: const Text(
- 'Show snackbar',
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- ),
- );
- }
-}
-
-class BottomSheetSection extends StatefulWidget {
- const BottomSheetSection({super.key});
-
- @override
- State<BottomSheetSection> createState() => _BottomSheetSectionState();
-}
-
-class _BottomSheetSectionState extends State<BottomSheetSection> {
- bool isNonModalBottomSheetOpen = false;
- PersistentBottomSheetController<void>? _nonModalBottomSheetController;
-
- @override
- Widget build(BuildContext context) {
- List<Widget> buttonList = <Widget>[
- IconButton(onPressed: () {}, icon: const Icon(Icons.share_outlined)),
- IconButton(onPressed: () {}, icon: const Icon(Icons.add)),
- IconButton(onPressed: () {}, icon: const Icon(Icons.delete_outline)),
- IconButton(onPressed: () {}, icon: const Icon(Icons.archive_outlined)),
- IconButton(onPressed: () {}, icon: const Icon(Icons.settings_outlined)),
- IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)),
- ];
- const List<Text> labelList = <Text>[
- Text('Share'),
- Text('Add to'),
- Text('Trash'),
- Text('Archive'),
- Text('Settings'),
- Text('Favorite')
- ];
-
- buttonList = List<Widget>.generate(
- buttonList.length,
- (int index) => Padding(
- padding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0),
- child: Column(
- children: <Widget>[
- buttonList[index],
- labelList[index],
- ],
- ),
- ));
-
- return ComponentDecoration(
- label: 'Bottom sheet',
- tooltipMessage: 'Use showModalBottomSheet<T> or showBottomSheet<T>',
- child: Wrap(
- alignment: WrapAlignment.spaceEvenly,
- children: <Widget>[
- TextButton(
- child: const Text(
- 'Show modal bottom sheet',
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- onPressed: () {
- showModalBottomSheet<void>(
- context: context,
- constraints: const BoxConstraints(maxWidth: 640),
- builder: (BuildContext context) {
- return SizedBox(
- height: 150,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32.0),
- child: ListView(
- shrinkWrap: true,
- scrollDirection: Axis.horizontal,
- children: buttonList,
- ),
- ),
- );
- },
- );
- },
- ),
- TextButton(
- child: Text(
- isNonModalBottomSheetOpen
- ? 'Hide bottom sheet'
- : 'Show bottom sheet',
- style: const TextStyle(fontWeight: FontWeight.bold),
- ),
- onPressed: () {
- if (isNonModalBottomSheetOpen) {
- _nonModalBottomSheetController?.close();
- setState(() {
- isNonModalBottomSheetOpen = false;
- });
- return;
- } else {
- setState(() {
- isNonModalBottomSheetOpen = true;
- });
- }
-
- _nonModalBottomSheetController = showBottomSheet<void>(
- elevation: 8.0,
- context: context,
- constraints: const BoxConstraints(maxWidth: 640),
- builder: (BuildContext context) {
- return SizedBox(
- height: 150,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32.0),
- child: ListView(
- shrinkWrap: true,
- scrollDirection: Axis.horizontal,
- children: buttonList,
- ),
- ),
- );
- },
- );
- },
- ),
- ],
- ),
- );
- }
-}
-
-class BottomAppBars extends StatelessWidget {
- const BottomAppBars({super.key});
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Bottom app bar',
- tooltipMessage: 'Use BottomAppBar',
- child: Column(
- children: <Widget>[
- SizedBox(
- height: 80,
- child: Scaffold(
- floatingActionButton: FloatingActionButton(
- onPressed: () {},
- elevation: 0.0,
- child: const Icon(Icons.add),
- ),
- floatingActionButtonLocation:
- FloatingActionButtonLocation.endContained,
- bottomNavigationBar: BottomAppBar(
- child: Row(
- children: <Widget>[
- const IconButtonAnchorExample(),
- IconButton(
- tooltip: 'Search',
- icon: const Icon(Icons.search),
- onPressed: () {},
- ),
- IconButton(
- tooltip: 'Favorite',
- icon: const Icon(Icons.favorite),
- onPressed: () {},
- ),
- ],
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
-}
-
-class IconButtonAnchorExample extends StatelessWidget {
- const IconButtonAnchorExample({super.key});
-
- @override
- Widget build(BuildContext context) {
- return MenuAnchor(
- builder: (BuildContext context, MenuController controller, Widget? child) {
- return IconButton(
- onPressed: () {
- if (controller.isOpen) {
- controller.close();
- } else {
- controller.open();
- }
- },
- icon: const Icon(Icons.more_vert),
- );
- },
- menuChildren: <Widget>[
- MenuItemButton(
- child: const Text('Menu 1'),
- onPressed: () {},
- ),
- MenuItemButton(
- child: const Text('Menu 2'),
- onPressed: () {},
- ),
- SubmenuButton(
- menuChildren: <Widget>[
- MenuItemButton(
- onPressed: () {},
- child: const Text('Menu 3.1'),
- ),
- MenuItemButton(
- onPressed: () {},
- child: const Text('Menu 3.2'),
- ),
- MenuItemButton(
- onPressed: () {},
- child: const Text('Menu 3.3'),
- ),
- ],
- child: const Text('Menu 3'),
- ),
- ],
- );
- }
-}
-
-class ButtonAnchorExample extends StatelessWidget {
- const ButtonAnchorExample({super.key});
-
- @override
- Widget build(BuildContext context) {
- return MenuAnchor(
- builder: (BuildContext context, MenuController controller, Widget? child) {
- return FilledButton.tonal(
- onPressed: () {
- if (controller.isOpen) {
- controller.close();
- } else {
- controller.open();
- }
- },
- child: const Text('Show menu'),
- );
- },
- menuChildren: <Widget>[
- MenuItemButton(
- leadingIcon: const Icon(Icons.people_alt_outlined),
- child: const Text('Item 1'),
- onPressed: () {},
- ),
- MenuItemButton(
- leadingIcon: const Icon(Icons.remove_red_eye_outlined),
- child: const Text('Item 2'),
- onPressed: () {},
- ),
- MenuItemButton(
- leadingIcon: const Icon(Icons.refresh),
- onPressed: () {},
- child: const Text('Item 3'),
- ),
- ],
- );
- }
-}
-
-class NavigationDrawers extends StatelessWidget {
- const NavigationDrawers({super.key, required this.scaffoldKey});
- final GlobalKey<ScaffoldState> scaffoldKey;
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Navigation drawer',
- tooltipMessage:
- 'Use NavigationDrawer. For modal navigation drawers, see Scaffold.endDrawer',
- child: Column(
- children: <Widget>[
- const SizedBox(height: 520, child: NavigationDrawerSection()),
- colDivider,
- colDivider,
- TextButton(
- child: const Text('Show modal navigation drawer',
- style: TextStyle(fontWeight: FontWeight.bold)),
- onPressed: () {
- scaffoldKey.currentState!.openEndDrawer();
- },
- ),
- ],
- ),
- );
- }
-}
-
-class NavigationDrawerSection extends StatefulWidget {
- const NavigationDrawerSection({super.key});
-
- @override
- State<NavigationDrawerSection> createState() =>
- _NavigationDrawerSectionState();
-}
-
-class _NavigationDrawerSectionState extends State<NavigationDrawerSection> {
- int navDrawerIndex = 0;
-
- @override
- Widget build(BuildContext context) {
- return NavigationDrawer(
- onDestinationSelected: (int selectedIndex) {
- setState(() {
- navDrawerIndex = selectedIndex;
- });
- },
- selectedIndex: navDrawerIndex,
- children: <Widget>[
- Padding(
- padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
- child: Text(
- 'Mail',
- style: Theme.of(context).textTheme.titleSmall,
- ),
- ),
- ...destinations.map((ExampleDestination destination) {
- return NavigationDrawerDestination(
- label: Text(destination.label),
- icon: destination.icon,
- selectedIcon: destination.selectedIcon,
- );
- }),
- const Divider(indent: 28, endIndent: 28),
- Padding(
- padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
- child: Text(
- 'Labels',
- style: Theme.of(context).textTheme.titleSmall,
- ),
- ),
- ...labelDestinations.map((ExampleDestination destination) {
- return NavigationDrawerDestination(
- label: Text(destination.label),
- icon: destination.icon,
- selectedIcon: destination.selectedIcon,
- );
- }),
- ],
- );
- }
-}
-
-class ExampleDestination {
- const ExampleDestination(this.label, this.icon, this.selectedIcon);
-
- final String label;
- final Widget icon;
- final Widget selectedIcon;
-}
-
-const List<ExampleDestination> destinations = <ExampleDestination>[
- ExampleDestination('Inbox', Icon(Icons.inbox_outlined), Icon(Icons.inbox)),
- ExampleDestination('Outbox', Icon(Icons.send_outlined), Icon(Icons.send)),
- ExampleDestination(
- 'Favorites', Icon(Icons.favorite_outline), Icon(Icons.favorite)),
- ExampleDestination('Trash', Icon(Icons.delete_outline), Icon(Icons.delete)),
-];
-
-const List<ExampleDestination> labelDestinations = <ExampleDestination>[
- ExampleDestination(
- 'Family', Icon(Icons.bookmark_border), Icon(Icons.bookmark)),
- ExampleDestination(
- 'School', Icon(Icons.bookmark_border), Icon(Icons.bookmark)),
- ExampleDestination('Work', Icon(Icons.bookmark_border), Icon(Icons.bookmark)),
-];
-
-class NavigationRails extends StatelessWidget {
- const NavigationRails({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const ComponentDecoration(
- label: 'Navigation rail',
- tooltipMessage: 'Use NavigationRail',
- child: IntrinsicWidth(
- child: SizedBox(height: 420, child: NavigationRailSection())),
- );
- }
-}
-
-class NavigationRailSection extends StatefulWidget {
- const NavigationRailSection({super.key});
-
- @override
- State<NavigationRailSection> createState() => _NavigationRailSectionState();
-}
-
-class _NavigationRailSectionState extends State<NavigationRailSection> {
- int navRailIndex = 0;
-
- @override
- Widget build(BuildContext context) {
- return NavigationRail(
- onDestinationSelected: (int selectedIndex) {
- setState(() {
- navRailIndex = selectedIndex;
- });
- },
- elevation: 4,
- leading: FloatingActionButton(
- child: const Icon(Icons.create), onPressed: () {}),
- groupAlignment: 0.0,
- selectedIndex: navRailIndex,
- labelType: NavigationRailLabelType.selected,
- destinations: <NavigationRailDestination>[
- ...destinations.map((ExampleDestination destination) {
- return NavigationRailDestination(
- label: Text(destination.label),
- icon: destination.icon,
- selectedIcon: destination.selectedIcon,
- );
- }),
- ],
- );
- }
-}
-
-class Tabs extends StatefulWidget {
- const Tabs({super.key});
-
- @override
- State<Tabs> createState() => _TabsState();
-}
-
-class _TabsState extends State<Tabs> with TickerProviderStateMixin {
- late TabController _tabController;
-
- @override
- void initState() {
- super.initState();
- _tabController = TabController(length: 3, vsync: this);
- }
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Tabs',
- tooltipMessage: 'Use TabBar',
- child: SizedBox(
- height: 80,
- child: Scaffold(
- appBar: AppBar(
- bottom: TabBar(
- controller: _tabController,
- tabs: const <Widget>[
- Tab(
- icon: Icon(Icons.videocam_outlined),
- text: 'Video',
- iconMargin: EdgeInsets.zero,
- ),
- Tab(
- icon: Icon(Icons.photo_outlined),
- text: 'Photos',
- iconMargin: EdgeInsets.zero,
- ),
- Tab(
- icon: Icon(Icons.audiotrack_sharp),
- text: 'Audio',
- iconMargin: EdgeInsets.zero,
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
-}
-
-class TopAppBars extends StatelessWidget {
- const TopAppBars({super.key});
-
- static final List<IconButton> actions = <IconButton>[
- IconButton(icon: const Icon(Icons.attach_file), onPressed: () {}),
- IconButton(icon: const Icon(Icons.event), onPressed: () {}),
- IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
- ];
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Top app bars',
- tooltipMessage:
- 'Use AppBar, SliverAppBar, SliverAppBar.medium, or SliverAppBar.large',
- child: Column(
- children: <Widget>[
- AppBar(
- title: const Text('Center-aligned'),
- leading: const BackButton(),
- actions: <Widget>[
- IconButton(
- iconSize: 32,
- icon: const Icon(Icons.account_circle_outlined),
- onPressed: () {},
- ),
- ],
- centerTitle: true,
- ),
- colDivider,
- AppBar(
- title: const Text('Small'),
- leading: const BackButton(),
- actions: actions,
- centerTitle: false,
- ),
- colDivider,
- SizedBox(
- height: 100,
- child: CustomScrollView(
- slivers: <Widget>[
- SliverAppBar.medium(
- title: const Text('Medium'),
- leading: const BackButton(),
- actions: actions,
- ),
- const SliverFillRemaining(),
- ],
- ),
- ),
- colDivider,
- SizedBox(
- height: 130,
- child: CustomScrollView(
- slivers: <Widget>[
- SliverAppBar.large(
- title: const Text('Large'),
- leading: const BackButton(),
- actions: actions,
- ),
- const SliverFillRemaining(),
- ],
- ),
- ),
- ],
- ),
- );
- }
-}
-
-class Menus extends StatefulWidget {
- const Menus({super.key});
-
- @override
- State<Menus> createState() => _MenusState();
-}
-
-class _MenusState extends State<Menus> {
- final TextEditingController colorController = TextEditingController();
- final TextEditingController iconController = TextEditingController();
- IconLabel? selectedIcon = IconLabel.smile;
- ColorLabel? selectedColor;
-
- @override
- Widget build(BuildContext context) {
- final List<DropdownMenuEntry<ColorLabel>> colorEntries =
- <DropdownMenuEntry<ColorLabel>>[];
- for (final ColorLabel color in ColorLabel.values) {
- colorEntries.add(DropdownMenuEntry<ColorLabel>(
- value: color, label: color.label, enabled: color.label != 'Grey'));
- }
-
- final List<DropdownMenuEntry<IconLabel>> iconEntries =
- <DropdownMenuEntry<IconLabel>>[];
- for (final IconLabel icon in IconLabel.values) {
- iconEntries
- .add(DropdownMenuEntry<IconLabel>(value: icon, label: icon.label));
- }
-
- return ComponentDecoration(
- label: 'Menus',
- tooltipMessage: 'Use MenuAnchor or DropdownMenu<T>',
- child: Column(
- children: <Widget>[
- const Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- ButtonAnchorExample(),
- rowDivider,
- IconButtonAnchorExample(),
- ],
- ),
- colDivider,
- Wrap(
- alignment: WrapAlignment.spaceAround,
- runAlignment: WrapAlignment.center,
- crossAxisAlignment: WrapCrossAlignment.center,
- spacing: smallSpacing,
- runSpacing: smallSpacing,
- children: <Widget>[
- DropdownMenu<ColorLabel>(
- controller: colorController,
- label: const Text('Color'),
- enableFilter: true,
- dropdownMenuEntries: colorEntries,
- inputDecorationTheme: const InputDecorationTheme(filled: true),
- onSelected: (ColorLabel? color) {
- setState(() {
- selectedColor = color;
- });
- },
- ),
- DropdownMenu<IconLabel>(
- initialSelection: IconLabel.smile,
- controller: iconController,
- leadingIcon: const Icon(Icons.search),
- label: const Text('Icon'),
- dropdownMenuEntries: iconEntries,
- onSelected: (IconLabel? icon) {
- setState(() {
- selectedIcon = icon;
- });
- },
- ),
- Icon(
- selectedIcon?.icon,
- color: selectedColor?.color ?? Colors.grey.withOpacity(0.5),
- )
- ],
- ),
- ],
- ),
- );
- }
-}
-
-enum ColorLabel {
- blue('Blue', Colors.blue),
- pink('Pink', Colors.pink),
- green('Green', Colors.green),
- yellow('Yellow', Colors.yellow),
- grey('Grey', Colors.grey);
-
- const ColorLabel(this.label, this.color);
- final String label;
- final Color color;
-}
-
-enum IconLabel {
- smile('Smile', Icons.sentiment_satisfied_outlined),
- cloud(
- 'Cloud',
- Icons.cloud_outlined,
- ),
- brush('Brush', Icons.brush_outlined),
- heart('Heart', Icons.favorite);
-
- const IconLabel(this.label, this.icon);
- final String label;
- final IconData icon;
-}
-
-class Sliders extends StatefulWidget {
- const Sliders({super.key});
-
- @override
- State<Sliders> createState() => _SlidersState();
-}
-
-class _SlidersState extends State<Sliders> {
- double sliderValue0 = 30.0;
- double sliderValue1 = 20.0;
-
- @override
- Widget build(BuildContext context) {
- return ComponentDecoration(
- label: 'Sliders',
- tooltipMessage: 'Use Slider or RangeSlider',
- child: Column(
- children: <Widget>[
- Slider(
- max: 100,
- value: sliderValue0,
- onChanged: (double value) {
- setState(() {
- sliderValue0 = value;
- });
- },
- ),
- const SizedBox(height: 20),
- Slider(
- max: 100,
- divisions: 5,
- value: sliderValue1,
- label: sliderValue1.round().toString(),
- onChanged: (double value) {
- setState(() {
- sliderValue1 = value;
- });
- },
- ),
- ],
- ));
- }
-}
-
-class ComponentDecoration extends StatefulWidget {
- const ComponentDecoration({
- super.key,
- required this.label,
- required this.child,
- this.tooltipMessage = '',
- });
-
- final String label;
- final Widget child;
- final String? tooltipMessage;
-
- @override
- State<ComponentDecoration> createState() => _ComponentDecorationState();
-}
-
-class _ComponentDecorationState extends State<ComponentDecoration> {
- final FocusNode focusNode = FocusNode();
-
- @override
- Widget build(BuildContext context) {
- return RepaintBoundary(
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: smallSpacing),
- child: Column(
- children: <Widget>[
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- Text(widget.label,
- style: Theme.of(context).textTheme.titleSmall),
- Tooltip(
- message: widget.tooltipMessage,
- child: const Padding(
- padding: EdgeInsets.symmetric(horizontal: 5.0),
- child: Icon(Icons.info_outline, size: 16)),
- ),
- ],
- ),
- ConstrainedBox(
- constraints:
- const BoxConstraints.tightFor(width: widthConstraint),
- // Tapping within the a component card should request focus
- // for that component's children.
- child: Focus(
- focusNode: focusNode,
- canRequestFocus: true,
- child: GestureDetector(
- onTapDown: (_) {
- focusNode.requestFocus();
- },
- behavior: HitTestBehavior.opaque,
- child: Card(
- elevation: 0,
- shape: RoundedRectangleBorder(
- side: BorderSide(
- color: Theme.of(context).colorScheme.outlineVariant,
- ),
- borderRadius: const BorderRadius.all(Radius.circular(12)),
- ),
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 5.0, vertical: 20.0),
- child: Center(
- child: widget.child,
- ),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
-
-class ComponentGroupDecoration extends StatelessWidget {
- const ComponentGroupDecoration(
- {super.key, required this.label, required this.children});
-
- final String label;
- final List<Widget> children;
-
- @override
- Widget build(BuildContext context) {
- // Fully traverse this component group before moving on
- return FocusTraversalGroup(
- child: Card(
- margin: EdgeInsets.zero,
- elevation: 0,
- color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 20.0),
- child: Center(
- child: Column(
- children: <Widget>[
- Text(label, style: Theme.of(context).textTheme.titleLarge),
- colDivider,
- ...children
- ],
- ),
- ),
- ),
- ),
- );
+ return const TwoColumnMaterial3Components();
}
}
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3_semantics.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3_semantics.dart
new file mode 100644
index 0000000..8b23080
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3_semantics.dart
@@ -0,0 +1,147 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/semantics.dart';
+
+import 'material3.dart';
+import 'recorder.dart';
+
+/// Measures the cost of semantics when constructing screens containing
+/// Material 3 widgets.
+class BenchMaterial3Semantics extends WidgetBuildRecorder {
+ BenchMaterial3Semantics() : super(name: benchmarkName);
+
+ static const String benchmarkName = 'bench_material3_semantics';
+
+ @override
+ Future<void> setUpAll() async {
+ FlutterTimeline.debugCollectionEnabled = true;
+ super.setUpAll();
+ SemanticsBinding.instance.ensureSemantics();
+ }
+
+ @override
+ Future<void> tearDownAll() async {
+ FlutterTimeline.debugReset();
+ }
+
+ @override
+ void frameDidDraw() {
+ // Only record frames that show the widget. Frames that remove the widget
+ // are not interesting.
+ if (showWidget) {
+ final AggregatedTimings timings = FlutterTimeline.debugCollect();
+ final AggregatedTimedBlock semanticsBlock = timings.getAggregated('SEMANTICS');
+ final AggregatedTimedBlock getFragmentBlock = timings.getAggregated('Semantics.GetFragment');
+ final AggregatedTimedBlock compileChildrenBlock = timings.getAggregated('Semantics.compileChildren');
+ profile!.addTimedBlock(semanticsBlock, reported: true);
+ profile!.addTimedBlock(getFragmentBlock, reported: true);
+ profile!.addTimedBlock(compileChildrenBlock, reported: true);
+ }
+
+ super.frameDidDraw();
+ FlutterTimeline.debugReset();
+ }
+
+ @override
+ Widget createWidget() {
+ return const SingleColumnMaterial3Components();
+ }
+}
+
+/// Measures the cost of semantics when scrolling screens containing Material 3
+/// widgets.
+///
+/// The implementation uses a ListView that jumps the scroll position between
+/// 0 and 1 every frame. Such a small delta is not enough for lazy rendering to
+/// add/remove widgets, but its enough to trigger the framework to recompute
+/// some of the semantics.
+///
+/// The expected output numbers of this benchmarks should be very small as
+/// scrolling a list view should be a matter of shifting some widgets and
+/// updating the projected clip imposed by the viewport. As of June 2023, the
+/// numbers are not great. Semantics consumes >50% of frame time.
+class BenchMaterial3ScrollSemantics extends WidgetRecorder {
+ BenchMaterial3ScrollSemantics() : super(name: benchmarkName);
+
+ static const String benchmarkName = 'bench_material3_scroll_semantics';
+
+ @override
+ Future<void> setUpAll() async {
+ FlutterTimeline.debugCollectionEnabled = true;
+ super.setUpAll();
+ SemanticsBinding.instance.ensureSemantics();
+ }
+
+ @override
+ Future<void> tearDownAll() async {
+ FlutterTimeline.debugReset();
+ }
+
+ @override
+ void frameDidDraw() {
+ final AggregatedTimings timings = FlutterTimeline.debugCollect();
+ final AggregatedTimedBlock semanticsBlock = timings.getAggregated('SEMANTICS');
+ final AggregatedTimedBlock getFragmentBlock = timings.getAggregated('Semantics.GetFragment');
+ final AggregatedTimedBlock compileChildrenBlock = timings.getAggregated('Semantics.compileChildren');
+ profile!.addTimedBlock(semanticsBlock, reported: true);
+ profile!.addTimedBlock(getFragmentBlock, reported: true);
+ profile!.addTimedBlock(compileChildrenBlock, reported: true);
+
+ super.frameDidDraw();
+ FlutterTimeline.debugReset();
+ }
+
+ @override
+ Widget createWidget() => _ScrollTest();
+}
+
+class _ScrollTest extends StatefulWidget {
+ @override
+ State<_ScrollTest> createState() => _ScrollTestState();
+}
+
+class _ScrollTestState extends State<_ScrollTest> with SingleTickerProviderStateMixin {
+ late final Ticker ticker;
+ late final ScrollController scrollController;
+
+ @override
+ void initState() {
+ super.initState();
+
+ scrollController = ScrollController();
+
+ bool forward = true;
+
+ // A one-off timer is necessary to allow the framework to measure the
+ // available scroll extents before the scroll controller can be exercised
+ // to change the scroll position.
+ Timer.run(() {
+ ticker = createTicker((_) {
+ scrollController.jumpTo(forward ? 1 : 0);
+ forward = !forward;
+ });
+ ticker.start();
+ });
+ }
+
+ @override
+ void dispose() {
+ ticker.dispose();
+ scrollController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SingleColumnMaterial3Components(
+ scrollController: scrollController,
+ );
+ }
+}
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart
index e1156dd..6788ec8 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart
@@ -203,6 +203,7 @@
@override
Future<void> setUpAll() async {
+ super.setUpAll();
registerEngineBenchmarkValueListener('text_layout', (num value) {
_textLayoutMicros += value;
});
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/material3.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/material3.dart
new file mode 100644
index 0000000..1ec8ae1
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/material3.dart
@@ -0,0 +1,2369 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+const SizedBox rowDivider = SizedBox(width: 20);
+const SizedBox colDivider = SizedBox(height: 10);
+const double smallSpacing = 10.0;
+const double cardWidth = 115;
+const double widthConstraint = 450;
+final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
+
+class SingleColumnMaterial3Components extends StatelessWidget {
+ const SingleColumnMaterial3Components({
+ super.key,
+ this.scrollController,
+ });
+
+ final ScrollController? scrollController;
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ key: scaffoldKey,
+ body: ListView(
+ controller: scrollController,
+ children: <Widget>[
+ const Actions(),
+ colDivider,
+ const Communication(),
+ colDivider,
+ const Containment(),
+ colDivider,
+ Navigation(scaffoldKey: scaffoldKey),
+ colDivider,
+ const Selection(),
+ colDivider,
+ const TextInputs(),
+ colDivider,
+ Navigation(scaffoldKey: scaffoldKey),
+ colDivider,
+ const Selection(),
+ colDivider,
+ const TextInputs(),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class TwoColumnMaterial3Components extends StatefulWidget {
+ const TwoColumnMaterial3Components({super.key});
+
+ @override
+ State<TwoColumnMaterial3Components> createState() => _TwoColumnMaterial3ComponentsState();
+}
+
+class _TwoColumnMaterial3ComponentsState extends State<TwoColumnMaterial3Components> {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ key: scaffoldKey,
+ body: Row(
+ children: <Widget>[
+ Expanded(
+ child: FirstComponentList(
+ showNavBottomBar: true,
+ scaffoldKey: scaffoldKey,
+ showSecondList: true,
+ ),
+ ),
+ Expanded(
+ child: SecondComponentList(scaffoldKey: scaffoldKey),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class FirstComponentList extends StatelessWidget {
+ const FirstComponentList({
+ super.key,
+ required this.showNavBottomBar,
+ required this.scaffoldKey,
+ required this.showSecondList,
+ });
+
+ final bool showNavBottomBar;
+ final GlobalKey<ScaffoldState> scaffoldKey;
+ final bool showSecondList;
+
+ @override
+ Widget build(BuildContext context) {
+ // Fully traverse this list before moving on.
+ return FocusTraversalGroup(
+ child: ListView(
+ padding: showSecondList
+ ? const EdgeInsetsDirectional.only(end: smallSpacing)
+ : EdgeInsets.zero,
+ children: <Widget>[
+ const Actions(),
+ colDivider,
+ const Communication(),
+ colDivider,
+ const Containment(),
+ if (!showSecondList) ...<Widget>[
+ colDivider,
+ Navigation(scaffoldKey: scaffoldKey),
+ colDivider,
+ const Selection(),
+ colDivider,
+ const TextInputs()
+ ],
+ ],
+ ),
+ );
+ }
+}
+
+class SecondComponentList extends StatelessWidget {
+ const SecondComponentList({
+ super.key,
+ required this.scaffoldKey,
+ });
+
+ final GlobalKey<ScaffoldState> scaffoldKey;
+
+ @override
+ Widget build(BuildContext context) {
+ // Fully traverse this list before moving on.
+ return FocusTraversalGroup(
+ child: ListView(
+ padding: const EdgeInsetsDirectional.only(end: smallSpacing),
+ children: <Widget>[
+ Navigation(scaffoldKey: scaffoldKey),
+ colDivider,
+ const Selection(),
+ colDivider,
+ const TextInputs(),
+ ],
+ ),
+ );
+ }
+}
+
+class Actions extends StatelessWidget {
+ const Actions({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentGroupDecoration(label: 'Actions', children: <Widget>[
+ Buttons(),
+ FloatingActionButtons(),
+ IconToggleButtons(),
+ SegmentedButtons(),
+ ]);
+ }
+}
+
+class Communication extends StatelessWidget {
+ const Communication({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentGroupDecoration(label: 'Communication', children: <Widget>[
+ NavigationBars(
+ selectedIndex: 1,
+ isExampleBar: true,
+ isBadgeExample: true,
+ ),
+ ProgressIndicators(),
+ SnackBarSection(),
+ ]);
+ }
+}
+
+class Containment extends StatelessWidget {
+ const Containment({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentGroupDecoration(label: 'Containment', children: <Widget>[
+ BottomSheetSection(),
+ Cards(),
+ Dialogs(),
+ Dividers(),
+ ]);
+ }
+}
+
+class Navigation extends StatelessWidget {
+ const Navigation({super.key, required this.scaffoldKey});
+
+ final GlobalKey<ScaffoldState> scaffoldKey;
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentGroupDecoration(label: 'Navigation', children: <Widget>[
+ const BottomAppBars(),
+ const NavigationBars(
+ selectedIndex: 0,
+ isExampleBar: true,
+ ),
+ NavigationDrawers(scaffoldKey: scaffoldKey),
+ const NavigationRails(),
+ const Tabs(),
+ const TopAppBars(),
+ ]);
+ }
+}
+
+class Selection extends StatelessWidget {
+ const Selection({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentGroupDecoration(label: 'Selection', children: <Widget>[
+ Checkboxes(),
+ Chips(),
+ Menus(),
+ Radios(),
+ Sliders(),
+ Switches(),
+ ]);
+ }
+}
+
+class TextInputs extends StatelessWidget {
+ const TextInputs({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentGroupDecoration(
+ label: 'Text inputs',
+ children: <Widget>[TextFields()],
+ );
+ }
+}
+
+class Buttons extends StatefulWidget {
+ const Buttons({super.key});
+
+ @override
+ State<Buttons> createState() => _ButtonsState();
+}
+
+class _ButtonsState extends State<Buttons> {
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentDecoration(
+ label: 'Common buttons',
+ tooltipMessage:
+ 'Use ElevatedButton, FilledButton, FilledButton.tonal, OutlinedButton, or TextButton',
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: <Widget>[
+ ButtonsWithoutIcon(isDisabled: false),
+ ButtonsWithIcon(),
+ ButtonsWithoutIcon(isDisabled: true),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class ButtonsWithoutIcon extends StatelessWidget {
+ const ButtonsWithoutIcon({super.key, required this.isDisabled});
+
+ final bool isDisabled;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 5.0),
+ child: IntrinsicWidth(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ ElevatedButton(
+ onPressed: isDisabled ? null : () {},
+ child: const Text('Elevated'),
+ ),
+ colDivider,
+ FilledButton(
+ onPressed: isDisabled ? null : () {},
+ child: const Text('Filled'),
+ ),
+ colDivider,
+ FilledButton.tonal(
+ onPressed: isDisabled ? null : () {},
+ child: const Text('Filled tonal'),
+ ),
+ colDivider,
+ OutlinedButton(
+ onPressed: isDisabled ? null : () {},
+ child: const Text('Outlined'),
+ ),
+ colDivider,
+ TextButton(
+ onPressed: isDisabled ? null : () {},
+ child: const Text('Text'),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class ButtonsWithIcon extends StatelessWidget {
+ const ButtonsWithIcon({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 10.0),
+ child: IntrinsicWidth(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ ElevatedButton.icon(
+ onPressed: () {},
+ icon: const Icon(Icons.add),
+ label: const Text('Icon'),
+ ),
+ colDivider,
+ FilledButton.icon(
+ onPressed: () {},
+ label: const Text('Icon'),
+ icon: const Icon(Icons.add),
+ ),
+ colDivider,
+ FilledButton.tonalIcon(
+ onPressed: () {},
+ label: const Text('Icon'),
+ icon: const Icon(Icons.add),
+ ),
+ colDivider,
+ OutlinedButton.icon(
+ onPressed: () {},
+ icon: const Icon(Icons.add),
+ label: const Text('Icon'),
+ ),
+ colDivider,
+ TextButton.icon(
+ onPressed: () {},
+ icon: const Icon(Icons.add),
+ label: const Text('Icon'),
+ )
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class FloatingActionButtons extends StatelessWidget {
+ const FloatingActionButtons({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Floating action buttons',
+ tooltipMessage:
+ 'Use FloatingActionButton or FloatingActionButton.extended',
+ child: Wrap(
+ crossAxisAlignment: WrapCrossAlignment.center,
+ runSpacing: smallSpacing,
+ spacing: smallSpacing,
+ children: <Widget>[
+ FloatingActionButton.small(
+ onPressed: () {},
+ tooltip: 'Small',
+ child: const Icon(Icons.add),
+ ),
+ FloatingActionButton.extended(
+ onPressed: () {},
+ tooltip: 'Extended',
+ icon: const Icon(Icons.add),
+ label: const Text('Create'),
+ ),
+ FloatingActionButton(
+ onPressed: () {},
+ tooltip: 'Standard',
+ child: const Icon(Icons.add),
+ ),
+ FloatingActionButton.large(
+ onPressed: () {},
+ tooltip: 'Large',
+ child: const Icon(Icons.add),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class Cards extends StatelessWidget {
+ const Cards({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Cards',
+ tooltipMessage: 'Use Card',
+ child: Wrap(
+ alignment: WrapAlignment.spaceEvenly,
+ children: <Widget>[
+ SizedBox(
+ width: cardWidth,
+ child: Card(
+ child: Container(
+ padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
+ child: Column(
+ children: <Widget>[
+ Align(
+ alignment: Alignment.topRight,
+ child: IconButton(
+ icon: const Icon(Icons.more_vert),
+ onPressed: () {},
+ ),
+ ),
+ const SizedBox(height: 20),
+ const Align(
+ alignment: Alignment.bottomLeft,
+ child: Text('Elevated'),
+ )
+ ],
+ ),
+ ),
+ ),
+ ),
+ SizedBox(
+ width: cardWidth,
+ child: Card(
+ color: Theme.of(context).colorScheme.surfaceVariant,
+ elevation: 0,
+ child: Container(
+ padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
+ child: Column(
+ children: <Widget>[
+ Align(
+ alignment: Alignment.topRight,
+ child: IconButton(
+ icon: const Icon(Icons.more_vert),
+ onPressed: () {},
+ ),
+ ),
+ const SizedBox(height: 20),
+ const Align(
+ alignment: Alignment.bottomLeft,
+ child: Text('Filled'),
+ )
+ ],
+ ),
+ ),
+ ),
+ ),
+ SizedBox(
+ width: cardWidth,
+ child: Card(
+ elevation: 0,
+ shape: RoundedRectangleBorder(
+ side: BorderSide(
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ borderRadius: const BorderRadius.all(Radius.circular(12)),
+ ),
+ child: Container(
+ padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
+ child: Column(
+ children: <Widget>[
+ Align(
+ alignment: Alignment.topRight,
+ child: IconButton(
+ icon: const Icon(Icons.more_vert),
+ onPressed: () {},
+ ),
+ ),
+ const SizedBox(height: 20),
+ const Align(
+ alignment: Alignment.bottomLeft,
+ child: Text('Outlined'),
+ )
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _ClearButton extends StatelessWidget {
+ const _ClearButton({required this.controller});
+
+ final TextEditingController controller;
+
+ @override
+ Widget build(BuildContext context) => IconButton(
+ icon: const Icon(Icons.clear),
+ onPressed: () => controller.clear(),
+ );
+}
+
+class TextFields extends StatefulWidget {
+ const TextFields({super.key});
+
+ @override
+ State<TextFields> createState() => _TextFieldsState();
+}
+
+class _TextFieldsState extends State<TextFields> {
+ final TextEditingController _controllerFilled = TextEditingController();
+ final TextEditingController _controllerOutlined = TextEditingController();
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Text fields',
+ tooltipMessage: 'Use TextField with different InputDecoration',
+ child: Column(
+ children: <Widget>[
+ Padding(
+ padding: const EdgeInsets.all(smallSpacing),
+ child: TextField(
+ controller: _controllerFilled,
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.search),
+ suffixIcon: _ClearButton(controller: _controllerFilled),
+ labelText: 'Filled',
+ hintText: 'hint text',
+ helperText: 'supporting text',
+ filled: true,
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(smallSpacing),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: <Widget>[
+ Flexible(
+ child: SizedBox(
+ width: 200,
+ child: TextField(
+ maxLength: 10,
+ maxLengthEnforcement: MaxLengthEnforcement.none,
+ controller: _controllerFilled,
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.search),
+ suffixIcon: _ClearButton(controller: _controllerFilled),
+ labelText: 'Filled',
+ hintText: 'hint text',
+ helperText: 'supporting text',
+ filled: true,
+ errorText: 'error text',
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: smallSpacing),
+ Flexible(
+ child: SizedBox(
+ width: 200,
+ child: TextField(
+ controller: _controllerFilled,
+ enabled: false,
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.search),
+ suffixIcon: _ClearButton(controller: _controllerFilled),
+ labelText: 'Disabled',
+ hintText: 'hint text',
+ helperText: 'supporting text',
+ filled: true,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(smallSpacing),
+ child: TextField(
+ controller: _controllerOutlined,
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.search),
+ suffixIcon: _ClearButton(controller: _controllerOutlined),
+ labelText: 'Outlined',
+ hintText: 'hint text',
+ helperText: 'supporting text',
+ border: const OutlineInputBorder(),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(smallSpacing),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: <Widget>[
+ Flexible(
+ child: SizedBox(
+ width: 200,
+ child: TextField(
+ controller: _controllerOutlined,
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.search),
+ suffixIcon:
+ _ClearButton(controller: _controllerOutlined),
+ labelText: 'Outlined',
+ hintText: 'hint text',
+ helperText: 'supporting text',
+ errorText: 'error text',
+ border: const OutlineInputBorder(),
+ filled: true,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: smallSpacing),
+ Flexible(
+ child: SizedBox(
+ width: 200,
+ child: TextField(
+ controller: _controllerOutlined,
+ enabled: false,
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.search),
+ suffixIcon:
+ _ClearButton(controller: _controllerOutlined),
+ labelText: 'Disabled',
+ hintText: 'hint text',
+ helperText: 'supporting text',
+ border: const OutlineInputBorder(),
+ filled: true,
+ ),
+ ),
+ ),
+ ),
+ ])),
+ ],
+ ),
+ );
+ }
+}
+
+class Dialogs extends StatefulWidget {
+ const Dialogs({super.key});
+
+ @override
+ State<Dialogs> createState() => _DialogsState();
+}
+
+class _DialogsState extends State<Dialogs> {
+ void openDialog(BuildContext context) {
+ showDialog<void>(
+ context: context,
+ builder: (BuildContext context) => AlertDialog(
+ title: const Text('What is a dialog?'),
+ content: const Text(
+ 'A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made.'),
+ actions: <Widget>[
+ TextButton(
+ child: const Text('Okay'),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ FilledButton(
+ child: const Text('Dismiss'),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void openFullscreenDialog(BuildContext context) {
+ showDialog<void>(
+ context: context,
+ builder: (BuildContext context) => Dialog.fullscreen(
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Scaffold(
+ appBar: AppBar(
+ title: const Text('Full-screen dialog'),
+ centerTitle: false,
+ leading: IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ actions: <Widget>[
+ TextButton(
+ child: const Text('Close'),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Dialog',
+ tooltipMessage:
+ 'Use showDialog with Dialog.fullscreen, AlertDialog, or SimpleDialog',
+ child: Wrap(
+ alignment: WrapAlignment.spaceBetween,
+ children: <Widget>[
+ TextButton(
+ child: const Text(
+ 'Show dialog',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ onPressed: () => openDialog(context),
+ ),
+ TextButton(
+ child: const Text(
+ 'Show full-screen dialog',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ onPressed: () => openFullscreenDialog(context),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class Dividers extends StatelessWidget {
+ const Dividers({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentDecoration(
+ label: 'Dividers',
+ tooltipMessage: 'Use Divider or VerticalDivider',
+ child: Column(
+ children: <Widget>[
+ Divider(key: Key('divider')),
+ ],
+ ),
+ );
+ }
+}
+
+class Switches extends StatelessWidget {
+ const Switches({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentDecoration(
+ label: 'Switches',
+ tooltipMessage: 'Use SwitchListTile or Switch',
+ child: Column(
+ children: <Widget>[
+ SwitchRow(isEnabled: true),
+ SwitchRow(isEnabled: false),
+ ],
+ ),
+ );
+ }
+}
+
+class SwitchRow extends StatefulWidget {
+ const SwitchRow({super.key, required this.isEnabled});
+
+ final bool isEnabled;
+
+ @override
+ State<SwitchRow> createState() => _SwitchRowState();
+}
+
+class _SwitchRowState extends State<SwitchRow> {
+ bool value0 = false;
+ bool value1 = true;
+
+ final MaterialStateProperty<Icon?> thumbIcon =
+ MaterialStateProperty.resolveWith<Icon?>((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return const Icon(Icons.check);
+ }
+ return const Icon(Icons.close);
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ Switch(
+ value: value0,
+ onChanged: widget.isEnabled
+ ? (bool value) {
+ setState(() {
+ value0 = value;
+ });
+ }
+ : null,
+ ),
+ Switch(
+ thumbIcon: thumbIcon,
+ value: value1,
+ onChanged: widget.isEnabled
+ ? (bool value) {
+ setState(() {
+ value1 = value;
+ });
+ }
+ : null,
+ ),
+ ],
+ );
+ }
+}
+
+class Checkboxes extends StatefulWidget {
+ const Checkboxes({super.key});
+
+ @override
+ State<Checkboxes> createState() => _CheckboxesState();
+}
+
+class _CheckboxesState extends State<Checkboxes> {
+ bool? isChecked0 = true;
+ bool? isChecked1;
+ bool? isChecked2 = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Checkboxes',
+ tooltipMessage: 'Use CheckboxListTile or Checkbox',
+ child: Column(
+ children: <Widget>[
+ CheckboxListTile(
+ tristate: true,
+ value: isChecked0,
+ title: const Text('Option 1'),
+ onChanged: (bool? value) {
+ setState(() {
+ isChecked0 = value;
+ });
+ },
+ ),
+ CheckboxListTile(
+ tristate: true,
+ value: isChecked1,
+ title: const Text('Option 2'),
+ onChanged: (bool? value) {
+ setState(() {
+ isChecked1 = value;
+ });
+ },
+ ),
+ CheckboxListTile(
+ tristate: true,
+ value: isChecked2,
+ title: const Text('Option 3'),
+ onChanged: (bool? value) {
+ setState(() {
+ isChecked2 = value;
+ });
+ },
+ ),
+ const CheckboxListTile(
+ tristate: true,
+ title: Text('Option 4'),
+ value: true,
+ onChanged: null,
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+enum Value { first, second }
+
+class Radios extends StatefulWidget {
+ const Radios({super.key});
+
+ @override
+ State<Radios> createState() => _RadiosState();
+}
+
+enum Options { option1, option2, option3 }
+
+class _RadiosState extends State<Radios> {
+ Options? _selectedOption = Options.option1;
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Radio buttons',
+ tooltipMessage: 'Use RadioListTile<T> or Radio<T>',
+ child: Column(
+ children: <Widget>[
+ RadioListTile<Options>(
+ title: const Text('Option 1'),
+ value: Options.option1,
+ groupValue: _selectedOption,
+ onChanged: (Options? value) {
+ setState(() {
+ _selectedOption = value;
+ });
+ },
+ ),
+ RadioListTile<Options>(
+ title: const Text('Option 2'),
+ value: Options.option2,
+ groupValue: _selectedOption,
+ onChanged: (Options? value) {
+ setState(() {
+ _selectedOption = value;
+ });
+ },
+ ),
+ RadioListTile<Options>(
+ title: const Text('Option 3'),
+ value: Options.option3,
+ groupValue: _selectedOption,
+ onChanged: null,
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class ProgressIndicators extends StatefulWidget {
+ const ProgressIndicators({super.key});
+
+ @override
+ State<ProgressIndicators> createState() => _ProgressIndicatorsState();
+}
+
+class _ProgressIndicatorsState extends State<ProgressIndicators> {
+ bool playProgressIndicator = false;
+
+ @override
+ Widget build(BuildContext context) {
+ final double? progressValue = playProgressIndicator ? null : 0.7;
+
+ return ComponentDecoration(
+ label: 'Progress indicators',
+ tooltipMessage:
+ 'Use CircularProgressIndicator or LinearProgressIndicator',
+ child: Column(
+ children: <Widget>[
+ Row(
+ children: <Widget>[
+ IconButton(
+ isSelected: playProgressIndicator,
+ selectedIcon: const Icon(Icons.pause),
+ icon: const Icon(Icons.play_arrow),
+ onPressed: () {
+ setState(() {
+ playProgressIndicator = !playProgressIndicator;
+ });
+ },
+ ),
+ Expanded(
+ child: Row(
+ children: <Widget>[
+ rowDivider,
+ CircularProgressIndicator(
+ value: progressValue,
+ ),
+ rowDivider,
+ Expanded(
+ child: LinearProgressIndicator(
+ value: progressValue,
+ ),
+ ),
+ rowDivider,
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+const List<NavigationDestination> appBarDestinations = <NavigationDestination>[
+ NavigationDestination(
+ tooltip: '',
+ icon: Icon(Icons.widgets_outlined),
+ label: 'Components',
+ selectedIcon: Icon(Icons.widgets),
+ ),
+ NavigationDestination(
+ tooltip: '',
+ icon: Icon(Icons.format_paint_outlined),
+ label: 'Color',
+ selectedIcon: Icon(Icons.format_paint),
+ ),
+ NavigationDestination(
+ tooltip: '',
+ icon: Icon(Icons.text_snippet_outlined),
+ label: 'Typography',
+ selectedIcon: Icon(Icons.text_snippet),
+ ),
+ NavigationDestination(
+ tooltip: '',
+ icon: Icon(Icons.invert_colors_on_outlined),
+ label: 'Elevation',
+ selectedIcon: Icon(Icons.opacity),
+ )
+];
+
+const List<Widget> exampleBarDestinations = <Widget>[
+ NavigationDestination(
+ tooltip: '',
+ icon: Icon(Icons.explore_outlined),
+ label: 'Explore',
+ selectedIcon: Icon(Icons.explore),
+ ),
+ NavigationDestination(
+ tooltip: '',
+ icon: Icon(Icons.pets_outlined),
+ label: 'Pets',
+ selectedIcon: Icon(Icons.pets),
+ ),
+ NavigationDestination(
+ tooltip: '',
+ icon: Icon(Icons.account_box_outlined),
+ label: 'Account',
+ selectedIcon: Icon(Icons.account_box),
+ )
+];
+
+List<Widget> barWithBadgeDestinations = <Widget>[
+ NavigationDestination(
+ tooltip: '',
+ icon: Badge.count(count: 1000, child: const Icon(Icons.mail_outlined)),
+ label: 'Mail',
+ selectedIcon: Badge.count(count: 1000, child: const Icon(Icons.mail)),
+ ),
+ const NavigationDestination(
+ tooltip: '',
+ icon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble_outline)),
+ label: 'Chat',
+ selectedIcon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble)),
+ ),
+ const NavigationDestination(
+ tooltip: '',
+ icon: Badge(child: Icon(Icons.group_outlined)),
+ label: 'Rooms',
+ selectedIcon: Badge(child: Icon(Icons.group_rounded)),
+ ),
+ NavigationDestination(
+ tooltip: '',
+ icon: Badge.count(count: 3, child: const Icon(Icons.videocam_outlined)),
+ label: 'Meet',
+ selectedIcon: Badge.count(count: 3, child: const Icon(Icons.videocam)),
+ )
+];
+
+class NavigationBars extends StatefulWidget {
+ const NavigationBars({
+ super.key,
+ this.onSelectItem,
+ required this.selectedIndex,
+ required this.isExampleBar,
+ this.isBadgeExample = false,
+ });
+
+ final void Function(int)? onSelectItem;
+ final int selectedIndex;
+ final bool isExampleBar;
+ final bool isBadgeExample;
+
+ @override
+ State<NavigationBars> createState() => _NavigationBarsState();
+}
+
+class _NavigationBarsState extends State<NavigationBars> {
+ late int selectedIndex;
+
+ @override
+ void initState() {
+ super.initState();
+ selectedIndex = widget.selectedIndex;
+ }
+
+ @override
+ void didUpdateWidget(covariant NavigationBars oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.selectedIndex != oldWidget.selectedIndex) {
+ selectedIndex = widget.selectedIndex;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // App NavigationBar should get first focus.
+ Widget navigationBar = Focus(
+ autofocus: !(widget.isExampleBar || widget.isBadgeExample),
+ child: NavigationBar(
+ selectedIndex: selectedIndex,
+ onDestinationSelected: (int index) {
+ setState(() {
+ selectedIndex = index;
+ });
+ if (!widget.isExampleBar) {
+ widget.onSelectItem!(index);
+ }
+ },
+ destinations: widget.isExampleBar && widget.isBadgeExample
+ ? barWithBadgeDestinations
+ : widget.isExampleBar
+ ? exampleBarDestinations
+ : appBarDestinations,
+ ),
+ );
+
+ if (widget.isExampleBar && widget.isBadgeExample) {
+ navigationBar = ComponentDecoration(
+ label: 'Badges',
+ tooltipMessage: 'Use Badge or Badge.count',
+ child: navigationBar);
+ } else if (widget.isExampleBar) {
+ navigationBar = ComponentDecoration(
+ label: 'Navigation bar',
+ tooltipMessage: 'Use NavigationBar',
+ child: navigationBar);
+ }
+
+ return navigationBar;
+ }
+}
+
+class IconToggleButtons extends StatefulWidget {
+ const IconToggleButtons({super.key});
+
+ @override
+ State<IconToggleButtons> createState() => _IconToggleButtonsState();
+}
+
+class _IconToggleButtonsState extends State<IconToggleButtons> {
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentDecoration(
+ label: 'Icon buttons',
+ tooltipMessage: 'Use IconButton',
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: <Widget>[
+ Column(
+ // Standard IconButton
+ children: <Widget>[
+ IconToggleButton(
+ isEnabled: true,
+ tooltip: 'Standard',
+ ),
+ colDivider,
+ IconToggleButton(
+ isEnabled: false,
+ tooltip: 'Standard (disabled)',
+ ),
+ ],
+ ),
+ Column(
+ children: <Widget>[
+ // Filled IconButton
+ IconToggleButton(
+ isEnabled: true,
+ tooltip: 'Filled',
+ getDefaultStyle: enabledFilledButtonStyle,
+ ),
+ colDivider,
+ IconToggleButton(
+ isEnabled: false,
+ tooltip: 'Filled (disabled)',
+ getDefaultStyle: disabledFilledButtonStyle,
+ ),
+ ],
+ ),
+ Column(
+ children: <Widget>[
+ // Filled Tonal IconButton
+ IconToggleButton(
+ isEnabled: true,
+ tooltip: 'Filled tonal',
+ getDefaultStyle: enabledFilledTonalButtonStyle,
+ ),
+ colDivider,
+ IconToggleButton(
+ isEnabled: false,
+ tooltip: 'Filled tonal (disabled)',
+ getDefaultStyle: disabledFilledTonalButtonStyle,
+ ),
+ ],
+ ),
+ Column(
+ children: <Widget>[
+ // Outlined IconButton
+ IconToggleButton(
+ isEnabled: true,
+ tooltip: 'Outlined',
+ getDefaultStyle: enabledOutlinedButtonStyle,
+ ),
+ colDivider,
+ IconToggleButton(
+ isEnabled: false,
+ tooltip: 'Outlined (disabled)',
+ getDefaultStyle: disabledOutlinedButtonStyle,
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class IconToggleButton extends StatefulWidget {
+ const IconToggleButton({
+ required this.isEnabled,
+ required this.tooltip,
+ this.getDefaultStyle,
+ super.key,
+ });
+
+ final bool isEnabled;
+ final String tooltip;
+ final ButtonStyle? Function(bool, ColorScheme)? getDefaultStyle;
+
+ @override
+ State<IconToggleButton> createState() => _IconToggleButtonState();
+}
+
+class _IconToggleButtonState extends State<IconToggleButton> {
+ bool selected = false;
+
+ @override
+ Widget build(BuildContext context) {
+ final ColorScheme colors = Theme.of(context).colorScheme;
+ final VoidCallback? onPressed = widget.isEnabled
+ ? () {
+ setState(() {
+ selected = !selected;
+ });
+ }
+ : null;
+ final ButtonStyle? style = widget.getDefaultStyle?.call(selected, colors);
+
+ return IconButton(
+ visualDensity: VisualDensity.standard,
+ isSelected: selected,
+ tooltip: widget.tooltip,
+ icon: const Icon(Icons.settings_outlined),
+ selectedIcon: const Icon(Icons.settings),
+ onPressed: onPressed,
+ style: style,
+ );
+ }
+}
+
+ButtonStyle enabledFilledButtonStyle(bool selected, ColorScheme colors) {
+ return IconButton.styleFrom(
+ foregroundColor: selected ? colors.onPrimary : colors.primary,
+ backgroundColor: selected ? colors.primary : colors.surfaceVariant,
+ disabledForegroundColor: colors.onSurface.withOpacity(0.38),
+ disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
+ hoverColor: selected
+ ? colors.onPrimary.withOpacity(0.08)
+ : colors.primary.withOpacity(0.08),
+ focusColor: selected
+ ? colors.onPrimary.withOpacity(0.12)
+ : colors.primary.withOpacity(0.12),
+ highlightColor: selected
+ ? colors.onPrimary.withOpacity(0.12)
+ : colors.primary.withOpacity(0.12),
+ );
+}
+
+ButtonStyle disabledFilledButtonStyle(bool selected, ColorScheme colors) {
+ return IconButton.styleFrom(
+ disabledForegroundColor: colors.onSurface.withOpacity(0.38),
+ disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
+ );
+}
+
+ButtonStyle enabledFilledTonalButtonStyle(bool selected, ColorScheme colors) {
+ return IconButton.styleFrom(
+ foregroundColor:
+ selected ? colors.onSecondaryContainer : colors.onSurfaceVariant,
+ backgroundColor:
+ selected ? colors.secondaryContainer : colors.surfaceVariant,
+ hoverColor: selected
+ ? colors.onSecondaryContainer.withOpacity(0.08)
+ : colors.onSurfaceVariant.withOpacity(0.08),
+ focusColor: selected
+ ? colors.onSecondaryContainer.withOpacity(0.12)
+ : colors.onSurfaceVariant.withOpacity(0.12),
+ highlightColor: selected
+ ? colors.onSecondaryContainer.withOpacity(0.12)
+ : colors.onSurfaceVariant.withOpacity(0.12),
+ );
+}
+
+ButtonStyle disabledFilledTonalButtonStyle(bool selected, ColorScheme colors) {
+ return IconButton.styleFrom(
+ disabledForegroundColor: colors.onSurface.withOpacity(0.38),
+ disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
+ );
+}
+
+ButtonStyle enabledOutlinedButtonStyle(bool selected, ColorScheme colors) {
+ return IconButton.styleFrom(
+ backgroundColor: selected ? colors.inverseSurface : null,
+ hoverColor: selected
+ ? colors.onInverseSurface.withOpacity(0.08)
+ : colors.onSurfaceVariant.withOpacity(0.08),
+ focusColor: selected
+ ? colors.onInverseSurface.withOpacity(0.12)
+ : colors.onSurfaceVariant.withOpacity(0.12),
+ highlightColor: selected
+ ? colors.onInverseSurface.withOpacity(0.12)
+ : colors.onSurface.withOpacity(0.12),
+ side: BorderSide(color: colors.outline),
+ ).copyWith(
+ foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return colors.onInverseSurface;
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return colors.onSurface;
+ }
+ return null;
+ }),
+ );
+}
+
+ButtonStyle disabledOutlinedButtonStyle(bool selected, ColorScheme colors) {
+ return IconButton.styleFrom(
+ disabledForegroundColor: colors.onSurface.withOpacity(0.38),
+ disabledBackgroundColor:
+ selected ? colors.onSurface.withOpacity(0.12) : null,
+ side: selected ? null : BorderSide(color: colors.outline.withOpacity(0.12)),
+ );
+}
+
+class Chips extends StatefulWidget {
+ const Chips({super.key});
+
+ @override
+ State<Chips> createState() => _ChipsState();
+}
+
+class _ChipsState extends State<Chips> {
+ bool isFiltered = true;
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Chips',
+ tooltipMessage:
+ 'Use ActionChip, FilterChip, or InputChip. \nActionChip can also be used for suggestion chip',
+ child: Column(
+ children: <Widget>[
+ Wrap(
+ spacing: smallSpacing,
+ runSpacing: smallSpacing,
+ children: <Widget>[
+ ActionChip(
+ label: const Text('Assist'),
+ avatar: const Icon(Icons.event),
+ onPressed: () {},
+ ),
+ FilterChip(
+ label: const Text('Filter'),
+ selected: isFiltered,
+ onSelected: (bool selected) {
+ setState(() => isFiltered = selected);
+ },
+ ),
+ InputChip(
+ label: const Text('Input'),
+ onPressed: () {},
+ onDeleted: () {},
+ ),
+ ActionChip(
+ label: const Text('Suggestion'),
+ onPressed: () {},
+ ),
+ ],
+ ),
+ colDivider,
+ Wrap(
+ spacing: smallSpacing,
+ runSpacing: smallSpacing,
+ children: <Widget>[
+ const ActionChip(
+ label: Text('Assist'),
+ avatar: Icon(Icons.event),
+ ),
+ FilterChip(
+ label: const Text('Filter'),
+ selected: isFiltered,
+ onSelected: null,
+ ),
+ InputChip(
+ label: const Text('Input'),
+ onDeleted: () {},
+ isEnabled: false,
+ ),
+ const ActionChip(
+ label: Text('Suggestion'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class SegmentedButtons extends StatelessWidget {
+ const SegmentedButtons({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentDecoration(
+ label: 'Segmented buttons',
+ tooltipMessage: 'Use SegmentedButton<T>',
+ child: Column(
+ children: <Widget>[
+ SingleChoice(),
+ colDivider,
+ MultipleChoice(),
+ ],
+ ),
+ );
+ }
+}
+
+enum Calendar { day, week, month, year }
+
+class SingleChoice extends StatefulWidget {
+ const SingleChoice({super.key});
+
+ @override
+ State<SingleChoice> createState() => _SingleChoiceState();
+}
+
+class _SingleChoiceState extends State<SingleChoice> {
+ Calendar calendarView = Calendar.day;
+
+ @override
+ Widget build(BuildContext context) {
+ return SegmentedButton<Calendar>(
+ segments: const <ButtonSegment<Calendar>>[
+ ButtonSegment<Calendar>(
+ value: Calendar.day,
+ label: Text('Day'),
+ icon: Icon(Icons.calendar_view_day)),
+ ButtonSegment<Calendar>(
+ value: Calendar.week,
+ label: Text('Week'),
+ icon: Icon(Icons.calendar_view_week)),
+ ButtonSegment<Calendar>(
+ value: Calendar.month,
+ label: Text('Month'),
+ icon: Icon(Icons.calendar_view_month)),
+ ButtonSegment<Calendar>(
+ value: Calendar.year,
+ label: Text('Year'),
+ icon: Icon(Icons.calendar_today)),
+ ],
+ selected: <Calendar>{calendarView},
+ onSelectionChanged: (Set<Calendar> newSelection) {
+ setState(() {
+ // By default there is only a single segment that can be
+ // selected at one time, so its value is always the first
+ // item in the selected set.
+ calendarView = newSelection.first;
+ });
+ },
+ );
+ }
+}
+
+enum Sizes { extraSmall, small, medium, large, extraLarge }
+
+class MultipleChoice extends StatefulWidget {
+ const MultipleChoice({super.key});
+
+ @override
+ State<MultipleChoice> createState() => _MultipleChoiceState();
+}
+
+class _MultipleChoiceState extends State<MultipleChoice> {
+ Set<Sizes> selection = <Sizes>{Sizes.large, Sizes.extraLarge};
+
+ @override
+ Widget build(BuildContext context) {
+ return SegmentedButton<Sizes>(
+ segments: const <ButtonSegment<Sizes>>[
+ ButtonSegment<Sizes>(value: Sizes.extraSmall, label: Text('XS')),
+ ButtonSegment<Sizes>(value: Sizes.small, label: Text('S')),
+ ButtonSegment<Sizes>(value: Sizes.medium, label: Text('M')),
+ ButtonSegment<Sizes>(
+ value: Sizes.large,
+ label: Text('L'),
+ ),
+ ButtonSegment<Sizes>(value: Sizes.extraLarge, label: Text('XL')),
+ ],
+ selected: selection,
+ onSelectionChanged: (Set<Sizes> newSelection) {
+ setState(() {
+ selection = newSelection;
+ });
+ },
+ multiSelectionEnabled: true,
+ );
+ }
+}
+
+class SnackBarSection extends StatelessWidget {
+ const SnackBarSection({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Snackbar',
+ tooltipMessage:
+ 'Use ScaffoldMessenger.of(context).showSnackBar with SnackBar',
+ child: TextButton(
+ onPressed: () {
+ final SnackBar snackBar = SnackBar(
+ behavior: SnackBarBehavior.floating,
+ width: 400.0,
+ content: const Text('This is a snackbar'),
+ action: SnackBarAction(
+ label: 'Close',
+ onPressed: () {},
+ ),
+ );
+
+ ScaffoldMessenger.of(context).hideCurrentSnackBar();
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ },
+ child: const Text(
+ 'Show snackbar',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ );
+ }
+}
+
+class BottomSheetSection extends StatefulWidget {
+ const BottomSheetSection({super.key});
+
+ @override
+ State<BottomSheetSection> createState() => _BottomSheetSectionState();
+}
+
+class _BottomSheetSectionState extends State<BottomSheetSection> {
+ bool isNonModalBottomSheetOpen = false;
+ PersistentBottomSheetController<void>? _nonModalBottomSheetController;
+
+ @override
+ Widget build(BuildContext context) {
+ List<Widget> buttonList = <Widget>[
+ IconButton(onPressed: () {}, icon: const Icon(Icons.share_outlined)),
+ IconButton(onPressed: () {}, icon: const Icon(Icons.add)),
+ IconButton(onPressed: () {}, icon: const Icon(Icons.delete_outline)),
+ IconButton(onPressed: () {}, icon: const Icon(Icons.archive_outlined)),
+ IconButton(onPressed: () {}, icon: const Icon(Icons.settings_outlined)),
+ IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)),
+ ];
+ const List<Text> labelList = <Text>[
+ Text('Share'),
+ Text('Add to'),
+ Text('Trash'),
+ Text('Archive'),
+ Text('Settings'),
+ Text('Favorite')
+ ];
+
+ buttonList = List<Widget>.generate(
+ buttonList.length,
+ (int index) => Padding(
+ padding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0),
+ child: Column(
+ children: <Widget>[
+ buttonList[index],
+ labelList[index],
+ ],
+ ),
+ ));
+
+ return ComponentDecoration(
+ label: 'Bottom sheet',
+ tooltipMessage: 'Use showModalBottomSheet<T> or showBottomSheet<T>',
+ child: Wrap(
+ alignment: WrapAlignment.spaceEvenly,
+ children: <Widget>[
+ TextButton(
+ child: const Text(
+ 'Show modal bottom sheet',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ onPressed: () {
+ showModalBottomSheet<void>(
+ context: context,
+ constraints: const BoxConstraints(maxWidth: 640),
+ builder: (BuildContext context) {
+ return SizedBox(
+ height: 150,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32.0),
+ child: ListView(
+ shrinkWrap: true,
+ scrollDirection: Axis.horizontal,
+ children: buttonList,
+ ),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ TextButton(
+ child: Text(
+ isNonModalBottomSheetOpen
+ ? 'Hide bottom sheet'
+ : 'Show bottom sheet',
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ ),
+ onPressed: () {
+ if (isNonModalBottomSheetOpen) {
+ _nonModalBottomSheetController?.close();
+ setState(() {
+ isNonModalBottomSheetOpen = false;
+ });
+ return;
+ } else {
+ setState(() {
+ isNonModalBottomSheetOpen = true;
+ });
+ }
+
+ _nonModalBottomSheetController = showBottomSheet<void>(
+ elevation: 8.0,
+ context: context,
+ constraints: const BoxConstraints(maxWidth: 640),
+ builder: (BuildContext context) {
+ return SizedBox(
+ height: 150,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32.0),
+ child: ListView(
+ shrinkWrap: true,
+ scrollDirection: Axis.horizontal,
+ children: buttonList,
+ ),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class BottomAppBars extends StatelessWidget {
+ const BottomAppBars({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Bottom app bar',
+ tooltipMessage: 'Use BottomAppBar',
+ child: Column(
+ children: <Widget>[
+ SizedBox(
+ height: 80,
+ child: Scaffold(
+ floatingActionButton: FloatingActionButton(
+ onPressed: () {},
+ elevation: 0.0,
+ child: const Icon(Icons.add),
+ ),
+ floatingActionButtonLocation:
+ FloatingActionButtonLocation.endContained,
+ bottomNavigationBar: BottomAppBar(
+ child: Row(
+ children: <Widget>[
+ const IconButtonAnchorExample(),
+ IconButton(
+ tooltip: 'Search',
+ icon: const Icon(Icons.search),
+ onPressed: () {},
+ ),
+ IconButton(
+ tooltip: 'Favorite',
+ icon: const Icon(Icons.favorite),
+ onPressed: () {},
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class IconButtonAnchorExample extends StatelessWidget {
+ const IconButtonAnchorExample({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MenuAnchor(
+ builder: (BuildContext context, MenuController controller, Widget? child) {
+ return IconButton(
+ onPressed: () {
+ if (controller.isOpen) {
+ controller.close();
+ } else {
+ controller.open();
+ }
+ },
+ icon: const Icon(Icons.more_vert),
+ );
+ },
+ menuChildren: <Widget>[
+ MenuItemButton(
+ child: const Text('Menu 1'),
+ onPressed: () {},
+ ),
+ MenuItemButton(
+ child: const Text('Menu 2'),
+ onPressed: () {},
+ ),
+ SubmenuButton(
+ menuChildren: <Widget>[
+ MenuItemButton(
+ onPressed: () {},
+ child: const Text('Menu 3.1'),
+ ),
+ MenuItemButton(
+ onPressed: () {},
+ child: const Text('Menu 3.2'),
+ ),
+ MenuItemButton(
+ onPressed: () {},
+ child: const Text('Menu 3.3'),
+ ),
+ ],
+ child: const Text('Menu 3'),
+ ),
+ ],
+ );
+ }
+}
+
+class ButtonAnchorExample extends StatelessWidget {
+ const ButtonAnchorExample({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MenuAnchor(
+ builder: (BuildContext context, MenuController controller, Widget? child) {
+ return FilledButton.tonal(
+ onPressed: () {
+ if (controller.isOpen) {
+ controller.close();
+ } else {
+ controller.open();
+ }
+ },
+ child: const Text('Show menu'),
+ );
+ },
+ menuChildren: <Widget>[
+ MenuItemButton(
+ leadingIcon: const Icon(Icons.people_alt_outlined),
+ child: const Text('Item 1'),
+ onPressed: () {},
+ ),
+ MenuItemButton(
+ leadingIcon: const Icon(Icons.remove_red_eye_outlined),
+ child: const Text('Item 2'),
+ onPressed: () {},
+ ),
+ MenuItemButton(
+ leadingIcon: const Icon(Icons.refresh),
+ onPressed: () {},
+ child: const Text('Item 3'),
+ ),
+ ],
+ );
+ }
+}
+
+class NavigationDrawers extends StatelessWidget {
+ const NavigationDrawers({super.key, required this.scaffoldKey});
+ final GlobalKey<ScaffoldState> scaffoldKey;
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Navigation drawer',
+ tooltipMessage:
+ 'Use NavigationDrawer. For modal navigation drawers, see Scaffold.endDrawer',
+ child: Column(
+ children: <Widget>[
+ const SizedBox(height: 520, child: NavigationDrawerSection()),
+ colDivider,
+ colDivider,
+ TextButton(
+ child: const Text('Show modal navigation drawer',
+ style: TextStyle(fontWeight: FontWeight.bold)),
+ onPressed: () {
+ scaffoldKey.currentState!.openEndDrawer();
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class NavigationDrawerSection extends StatefulWidget {
+ const NavigationDrawerSection({super.key});
+
+ @override
+ State<NavigationDrawerSection> createState() =>
+ _NavigationDrawerSectionState();
+}
+
+class _NavigationDrawerSectionState extends State<NavigationDrawerSection> {
+ int navDrawerIndex = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ return NavigationDrawer(
+ onDestinationSelected: (int selectedIndex) {
+ setState(() {
+ navDrawerIndex = selectedIndex;
+ });
+ },
+ selectedIndex: navDrawerIndex,
+ children: <Widget>[
+ Padding(
+ padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
+ child: Text(
+ 'Mail',
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ ),
+ ...destinations.map((ExampleDestination destination) {
+ return NavigationDrawerDestination(
+ label: Text(destination.label),
+ icon: destination.icon,
+ selectedIcon: destination.selectedIcon,
+ );
+ }),
+ const Divider(indent: 28, endIndent: 28),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
+ child: Text(
+ 'Labels',
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ ),
+ ...labelDestinations.map((ExampleDestination destination) {
+ return NavigationDrawerDestination(
+ label: Text(destination.label),
+ icon: destination.icon,
+ selectedIcon: destination.selectedIcon,
+ );
+ }),
+ ],
+ );
+ }
+}
+
+class ExampleDestination {
+ const ExampleDestination(this.label, this.icon, this.selectedIcon);
+
+ final String label;
+ final Widget icon;
+ final Widget selectedIcon;
+}
+
+const List<ExampleDestination> destinations = <ExampleDestination>[
+ ExampleDestination('Inbox', Icon(Icons.inbox_outlined), Icon(Icons.inbox)),
+ ExampleDestination('Outbox', Icon(Icons.send_outlined), Icon(Icons.send)),
+ ExampleDestination(
+ 'Favorites', Icon(Icons.favorite_outline), Icon(Icons.favorite)),
+ ExampleDestination('Trash', Icon(Icons.delete_outline), Icon(Icons.delete)),
+];
+
+const List<ExampleDestination> labelDestinations = <ExampleDestination>[
+ ExampleDestination(
+ 'Family', Icon(Icons.bookmark_border), Icon(Icons.bookmark)),
+ ExampleDestination(
+ 'School', Icon(Icons.bookmark_border), Icon(Icons.bookmark)),
+ ExampleDestination('Work', Icon(Icons.bookmark_border), Icon(Icons.bookmark)),
+];
+
+class NavigationRails extends StatelessWidget {
+ const NavigationRails({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const ComponentDecoration(
+ label: 'Navigation rail',
+ tooltipMessage: 'Use NavigationRail',
+ child: IntrinsicWidth(
+ child: SizedBox(height: 420, child: NavigationRailSection())),
+ );
+ }
+}
+
+class NavigationRailSection extends StatefulWidget {
+ const NavigationRailSection({super.key});
+
+ @override
+ State<NavigationRailSection> createState() => _NavigationRailSectionState();
+}
+
+class _NavigationRailSectionState extends State<NavigationRailSection> {
+ int navRailIndex = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ return NavigationRail(
+ onDestinationSelected: (int selectedIndex) {
+ setState(() {
+ navRailIndex = selectedIndex;
+ });
+ },
+ elevation: 4,
+ leading: FloatingActionButton(
+ child: const Icon(Icons.create), onPressed: () {}),
+ groupAlignment: 0.0,
+ selectedIndex: navRailIndex,
+ labelType: NavigationRailLabelType.selected,
+ destinations: <NavigationRailDestination>[
+ ...destinations.map((ExampleDestination destination) {
+ return NavigationRailDestination(
+ label: Text(destination.label),
+ icon: destination.icon,
+ selectedIcon: destination.selectedIcon,
+ );
+ }),
+ ],
+ );
+ }
+}
+
+class Tabs extends StatefulWidget {
+ const Tabs({super.key});
+
+ @override
+ State<Tabs> createState() => _TabsState();
+}
+
+class _TabsState extends State<Tabs> with TickerProviderStateMixin {
+ late TabController _tabController;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 3, vsync: this);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Tabs',
+ tooltipMessage: 'Use TabBar',
+ child: SizedBox(
+ height: 80,
+ child: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ controller: _tabController,
+ tabs: const <Widget>[
+ Tab(
+ icon: Icon(Icons.videocam_outlined),
+ text: 'Video',
+ iconMargin: EdgeInsets.zero,
+ ),
+ Tab(
+ icon: Icon(Icons.photo_outlined),
+ text: 'Photos',
+ iconMargin: EdgeInsets.zero,
+ ),
+ Tab(
+ icon: Icon(Icons.audiotrack_sharp),
+ text: 'Audio',
+ iconMargin: EdgeInsets.zero,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class TopAppBars extends StatelessWidget {
+ const TopAppBars({super.key});
+
+ static final List<IconButton> actions = <IconButton>[
+ IconButton(icon: const Icon(Icons.attach_file), onPressed: () {}),
+ IconButton(icon: const Icon(Icons.event), onPressed: () {}),
+ IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Top app bars',
+ tooltipMessage:
+ 'Use AppBar, SliverAppBar, SliverAppBar.medium, or SliverAppBar.large',
+ child: Column(
+ children: <Widget>[
+ AppBar(
+ title: const Text('Center-aligned'),
+ leading: const BackButton(),
+ actions: <Widget>[
+ IconButton(
+ iconSize: 32,
+ icon: const Icon(Icons.account_circle_outlined),
+ onPressed: () {},
+ ),
+ ],
+ centerTitle: true,
+ ),
+ colDivider,
+ AppBar(
+ title: const Text('Small'),
+ leading: const BackButton(),
+ actions: actions,
+ centerTitle: false,
+ ),
+ colDivider,
+ SizedBox(
+ height: 100,
+ child: CustomScrollView(
+ slivers: <Widget>[
+ SliverAppBar.medium(
+ title: const Text('Medium'),
+ leading: const BackButton(),
+ actions: actions,
+ ),
+ const SliverFillRemaining(),
+ ],
+ ),
+ ),
+ colDivider,
+ SizedBox(
+ height: 130,
+ child: CustomScrollView(
+ slivers: <Widget>[
+ SliverAppBar.large(
+ title: const Text('Large'),
+ leading: const BackButton(),
+ actions: actions,
+ ),
+ const SliverFillRemaining(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class Menus extends StatefulWidget {
+ const Menus({super.key});
+
+ @override
+ State<Menus> createState() => _MenusState();
+}
+
+class _MenusState extends State<Menus> {
+ final TextEditingController colorController = TextEditingController();
+ final TextEditingController iconController = TextEditingController();
+ IconLabel? selectedIcon = IconLabel.smile;
+ ColorLabel? selectedColor;
+
+ @override
+ Widget build(BuildContext context) {
+ final List<DropdownMenuEntry<ColorLabel>> colorEntries =
+ <DropdownMenuEntry<ColorLabel>>[];
+ for (final ColorLabel color in ColorLabel.values) {
+ colorEntries.add(DropdownMenuEntry<ColorLabel>(
+ value: color, label: color.label, enabled: color.label != 'Grey'));
+ }
+
+ final List<DropdownMenuEntry<IconLabel>> iconEntries =
+ <DropdownMenuEntry<IconLabel>>[];
+ for (final IconLabel icon in IconLabel.values) {
+ iconEntries
+ .add(DropdownMenuEntry<IconLabel>(value: icon, label: icon.label));
+ }
+
+ return ComponentDecoration(
+ label: 'Menus',
+ tooltipMessage: 'Use MenuAnchor or DropdownMenu<T>',
+ child: Column(
+ children: <Widget>[
+ const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ButtonAnchorExample(),
+ rowDivider,
+ IconButtonAnchorExample(),
+ ],
+ ),
+ colDivider,
+ Wrap(
+ alignment: WrapAlignment.spaceAround,
+ runAlignment: WrapAlignment.center,
+ crossAxisAlignment: WrapCrossAlignment.center,
+ spacing: smallSpacing,
+ runSpacing: smallSpacing,
+ children: <Widget>[
+ DropdownMenu<ColorLabel>(
+ controller: colorController,
+ label: const Text('Color'),
+ enableFilter: true,
+ dropdownMenuEntries: colorEntries,
+ inputDecorationTheme: const InputDecorationTheme(filled: true),
+ onSelected: (ColorLabel? color) {
+ setState(() {
+ selectedColor = color;
+ });
+ },
+ ),
+ DropdownMenu<IconLabel>(
+ initialSelection: IconLabel.smile,
+ controller: iconController,
+ leadingIcon: const Icon(Icons.search),
+ label: const Text('Icon'),
+ dropdownMenuEntries: iconEntries,
+ onSelected: (IconLabel? icon) {
+ setState(() {
+ selectedIcon = icon;
+ });
+ },
+ ),
+ Icon(
+ selectedIcon?.icon,
+ color: selectedColor?.color ?? Colors.grey.withOpacity(0.5),
+ )
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+enum ColorLabel {
+ blue('Blue', Colors.blue),
+ pink('Pink', Colors.pink),
+ green('Green', Colors.green),
+ yellow('Yellow', Colors.yellow),
+ grey('Grey', Colors.grey);
+
+ const ColorLabel(this.label, this.color);
+ final String label;
+ final Color color;
+}
+
+enum IconLabel {
+ smile('Smile', Icons.sentiment_satisfied_outlined),
+ cloud(
+ 'Cloud',
+ Icons.cloud_outlined,
+ ),
+ brush('Brush', Icons.brush_outlined),
+ heart('Heart', Icons.favorite);
+
+ const IconLabel(this.label, this.icon);
+ final String label;
+ final IconData icon;
+}
+
+class Sliders extends StatefulWidget {
+ const Sliders({super.key});
+
+ @override
+ State<Sliders> createState() => _SlidersState();
+}
+
+class _SlidersState extends State<Sliders> {
+ double sliderValue0 = 30.0;
+ double sliderValue1 = 20.0;
+
+ @override
+ Widget build(BuildContext context) {
+ return ComponentDecoration(
+ label: 'Sliders',
+ tooltipMessage: 'Use Slider or RangeSlider',
+ child: Column(
+ children: <Widget>[
+ Slider(
+ max: 100,
+ value: sliderValue0,
+ onChanged: (double value) {
+ setState(() {
+ sliderValue0 = value;
+ });
+ },
+ ),
+ const SizedBox(height: 20),
+ Slider(
+ max: 100,
+ divisions: 5,
+ value: sliderValue1,
+ label: sliderValue1.round().toString(),
+ onChanged: (double value) {
+ setState(() {
+ sliderValue1 = value;
+ });
+ },
+ ),
+ ],
+ ));
+ }
+}
+
+class ComponentDecoration extends StatefulWidget {
+ const ComponentDecoration({
+ super.key,
+ required this.label,
+ required this.child,
+ this.tooltipMessage = '',
+ });
+
+ final String label;
+ final Widget child;
+ final String? tooltipMessage;
+
+ @override
+ State<ComponentDecoration> createState() => _ComponentDecorationState();
+}
+
+class _ComponentDecorationState extends State<ComponentDecoration> {
+ final FocusNode focusNode = FocusNode();
+
+ @override
+ Widget build(BuildContext context) {
+ return RepaintBoundary(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: smallSpacing),
+ child: Column(
+ children: <Widget>[
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ Text(widget.label,
+ style: Theme.of(context).textTheme.titleSmall),
+ Tooltip(
+ message: widget.tooltipMessage,
+ child: const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 5.0),
+ child: Icon(Icons.info_outline, size: 16)),
+ ),
+ ],
+ ),
+ ConstrainedBox(
+ constraints:
+ const BoxConstraints.tightFor(width: widthConstraint),
+ // Tapping within the a component card should request focus
+ // for that component's children.
+ child: Focus(
+ focusNode: focusNode,
+ canRequestFocus: true,
+ child: GestureDetector(
+ onTapDown: (_) {
+ focusNode.requestFocus();
+ },
+ behavior: HitTestBehavior.opaque,
+ child: Card(
+ elevation: 0,
+ shape: RoundedRectangleBorder(
+ side: BorderSide(
+ color: Theme.of(context).colorScheme.outlineVariant,
+ ),
+ borderRadius: const BorderRadius.all(Radius.circular(12)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 5.0, vertical: 20.0),
+ child: Center(
+ child: widget.child,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class ComponentGroupDecoration extends StatelessWidget {
+ const ComponentGroupDecoration(
+ {super.key, required this.label, required this.children});
+
+ final String label;
+ final List<Widget> children;
+
+ @override
+ Widget build(BuildContext context) {
+ // Fully traverse this component group before moving on
+ return FocusTraversalGroup(
+ child: Card(
+ margin: EdgeInsets.zero,
+ elevation: 0,
+ color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 20.0),
+ child: Center(
+ child: Column(
+ children: <Widget>[
+ Text(label, style: Theme.of(context).textTheme.titleLarge),
+ colDivider,
+ ...children
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
index d2c7831..ae583dc 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
@@ -426,12 +426,18 @@
_runCompleter!.completeError(error, stackTrace);
}
+ late final _RecordingWidgetsBinding _binding;
+
+ @override
+ @mustCallSuper
+ Future<void> setUpAll() async {
+ _binding = _RecordingWidgetsBinding.ensureInitialized();
+ }
+
@override
Future<Profile> run() async {
_runCompleter = Completer<void>();
final Profile localProfile = profile = Profile(name: name, useCustomWarmUp: useCustomWarmUp);
- final _RecordingWidgetsBinding binding =
- _RecordingWidgetsBinding.ensureInitialized();
final Widget widget = createWidget();
registerEngineBenchmarkValueListener(kProfilePrerollFrame, (num value) {
@@ -449,7 +455,7 @@
);
});
- binding._beginRecording(this, widget);
+ _binding._beginRecording(this, widget);
try {
await _runCompleter!.future;
@@ -508,6 +514,14 @@
}
}
+ late final _RecordingWidgetsBinding _binding;
+
+ @override
+ @mustCallSuper
+ Future<void> setUpAll() async {
+ _binding = _RecordingWidgetsBinding.ensureInitialized();
+ }
+
@override
@mustCallSuper
void frameWillDraw() {
@@ -546,9 +560,7 @@
Future<Profile> run() async {
_runCompleter = Completer<void>();
final Profile localProfile = profile = Profile(name: name);
- final _RecordingWidgetsBinding binding =
- _RecordingWidgetsBinding.ensureInitialized();
- binding._beginRecording(this, _WidgetBuildRecorderHost(this));
+ _binding._beginRecording(this, _WidgetBuildRecorderHost(this));
try {
await _runCompleter!.future;
@@ -948,6 +960,15 @@
}
}
+ /// A convenience wrapper over [addDataPoint] for adding [AggregatedTimedBlock]
+ /// to the profile.
+ ///
+ /// Uses [AggregatedTimedBlock.name] as the name of the data point, and
+ /// [AggregatedTimedBlock.duration] as the duration.
+ void addTimedBlock(AggregatedTimedBlock timedBlock, { required bool reported }) {
+ addDataPoint(timedBlock.name, Duration(microseconds: timedBlock.duration.toInt()), reported: reported);
+ }
+
/// Checks the samples collected so far and sets the appropriate benchmark phase.
///
/// If enough warm-up samples have been collected, stops the warm-up phase and
diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
index 7820261..544f5bb 100644
--- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
@@ -19,6 +19,7 @@
import 'src/web/bench_dynamic_clip_on_static_picture.dart';
import 'src/web/bench_image_decoding.dart';
import 'src/web/bench_material_3.dart';
+import 'src/web/bench_material_3_semantics.dart';
import 'src/web/bench_mouse_region_grid_hover.dart';
import 'src/web/bench_mouse_region_grid_scroll.dart';
import 'src/web/bench_mouse_region_mixed_grid_hover.dart';
@@ -64,6 +65,8 @@
BenchPlatformViewInfiniteScroll.benchmarkName: () => BenchPlatformViewInfiniteScroll.forward(),
BenchPlatformViewInfiniteScroll.benchmarkNameBackward: () => BenchPlatformViewInfiniteScroll.backward(),
BenchMaterial3Components.benchmarkName: () => BenchMaterial3Components(),
+ BenchMaterial3Semantics.benchmarkName: () => BenchMaterial3Semantics(),
+ BenchMaterial3ScrollSemantics.benchmarkName: () => BenchMaterial3ScrollSemantics(),
// CanvasKit-only benchmarks
if (isCanvasKit) ...<String, RecorderFactory>{
diff --git a/dev/benchmarks/microbenchmarks/lib/foundation/timeline_bench.dart b/dev/benchmarks/microbenchmarks/lib/foundation/timeline_bench.dart
index 339a46c..7c9eb4a 100644
--- a/dev/benchmarks/microbenchmarks/lib/foundation/timeline_bench.dart
+++ b/dev/benchmarks/microbenchmarks/lib/foundation/timeline_bench.dart
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:developer';
+import 'package:flutter/foundation.dart';
import '../common.dart';
@@ -16,8 +16,8 @@
final Stopwatch watch = Stopwatch();
watch.start();
for (int i = 0; i < _kNumIterations; i += 1) {
- Timeline.startSync('foo');
- Timeline.finishSync();
+ FlutterTimeline.startSync('foo');
+ FlutterTimeline.finishSync();
}
watch.stop();
@@ -31,14 +31,14 @@
watch.reset();
watch.start();
for (int i = 0; i < _kNumIterations; i += 1) {
- Timeline.startSync('foo', arguments: <String, dynamic>{
+ FlutterTimeline.startSync('foo', arguments: <String, dynamic>{
'int': 1234,
'double': 0.3,
'list': <int>[1, 2, 3, 4],
'map': <String, dynamic>{'map': true},
'bool': false,
});
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
watch.stop();
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index d128e1d..6a91177 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -1375,6 +1375,14 @@
pos = javaScript.indexOf(word, pos);
}
+ // The following are classes from `timeline.dart` that should be treeshaken
+ // off unless the app (typically a benchmark) uses methods that need them.
+ expect(javaScript.contains('AggregatedTimedBlock'), false);
+ expect(javaScript.contains('AggregatedTimings'), false);
+ expect(javaScript.contains('_BlockBuffer'), false);
+ expect(javaScript.contains('_StringListChain'), false);
+ expect(javaScript.contains('_Float64ListChain'), false);
+
const int kMaxExpectedDebugFillProperties = 11;
if (count > kMaxExpectedDebugFillProperties) {
throw Exception(
diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart
index dff7553..43ac813 100644
--- a/packages/flutter/lib/foundation.dart
+++ b/packages/flutter/lib/foundation.dart
@@ -46,4 +46,5 @@
export 'src/foundation/service_extensions.dart';
export 'src/foundation/stack_frame.dart';
export 'src/foundation/synchronous_future.dart';
+export 'src/foundation/timeline.dart';
export 'src/foundation/unicode.dart';
diff --git a/packages/flutter/lib/src/foundation/_timeline_io.dart b/packages/flutter/lib/src/foundation/_timeline_io.dart
new file mode 100644
index 0000000..8c4886b
--- /dev/null
+++ b/packages/flutter/lib/src/foundation/_timeline_io.dart
@@ -0,0 +1,11 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:developer';
+
+/// Returns the current timestamp in microseconds from a monotonically
+/// increasing clock.
+///
+/// This is the Dart VM implementation.
+double get performanceTimestamp => Timeline.now.toDouble();
diff --git a/packages/flutter/lib/src/foundation/_timeline_web.dart b/packages/flutter/lib/src/foundation/_timeline_web.dart
new file mode 100644
index 0000000..133f096
--- /dev/null
+++ b/packages/flutter/lib/src/foundation/_timeline_web.dart
@@ -0,0 +1,27 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:js_interop';
+
+/// Returns the current timestamp in microseconds from a monotonically
+/// increasing clock.
+///
+/// This is the web implementation, which uses `window.performance.now` as the
+/// source of the timestamp.
+///
+/// See:
+/// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
+double get performanceTimestamp => 1000 * _performance.now();
+
+@JS()
+@staticInterop
+class _DomPerformance {}
+
+@JS('performance')
+external _DomPerformance get _performance;
+
+extension _DomPerformanceExtension on _DomPerformance {
+ @JS()
+ external double now();
+}
diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart
index 8532f6d..bc451ac 100644
--- a/packages/flutter/lib/src/foundation/binding.dart
+++ b/packages/flutter/lib/src/foundation/binding.dart
@@ -20,6 +20,7 @@
import 'platform.dart';
import 'print.dart';
import 'service_extensions.dart';
+import 'timeline.dart';
export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow; // ignore: deprecated_member_use
@@ -141,7 +142,9 @@
/// [initServiceExtensions] to have bindings initialize their
/// VM service extensions, if any.
BindingBase() {
- developer.Timeline.startSync('Framework initialization');
+ if (!kReleaseMode) {
+ FlutterTimeline.startSync('Framework initialization');
+ }
assert(() {
_debugConstructed = true;
return true;
@@ -157,7 +160,9 @@
developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
- developer.Timeline.finishSync();
+ if (!kReleaseMode) {
+ FlutterTimeline.finishSync();
+ }
}
bool _debugConstructed = false;
diff --git a/packages/flutter/lib/src/foundation/timeline.dart b/packages/flutter/lib/src/foundation/timeline.dart
new file mode 100644
index 0000000..b110ea0
--- /dev/null
+++ b/packages/flutter/lib/src/foundation/timeline.dart
@@ -0,0 +1,432 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:developer';
+import 'dart:typed_data';
+
+import 'package:meta/meta.dart';
+
+import '_timeline_io.dart'
+ if (dart.library.js_util) '_timeline_web.dart' as impl;
+import 'constants.dart';
+
+/// Measures how long blocks of code take to run.
+///
+/// This class can be used as a drop-in replacement for [Timeline] as it
+/// provides methods compatible with [Timeline] signature-wise, and it has
+/// minimal overhead.
+///
+/// Provides [debugReset] and [debugCollect] methods that make it convenient to use in
+/// frame-oriented environment where collected metrics can be attributed to a
+/// frame, then aggregated into frame statistics, e.g. frame averages.
+///
+/// Forwards measurements to [Timeline] so they appear in Flutter DevTools.
+abstract final class FlutterTimeline {
+ static _BlockBuffer _buffer = _BlockBuffer();
+
+ /// Whether block timings are collected and can be retrieved using the
+ /// [debugCollect] method.
+ ///
+ /// This is always false in release mode.
+ static bool get debugCollectionEnabled => _collectionEnabled;
+
+ /// Enables metric collection.
+ ///
+ /// Metric collection can only be enabled in non-release modes. It is most
+ /// useful in profile mode where application performance is representative
+ /// of a deployed application.
+ ///
+ /// When disabled, resets collected data by calling [debugReset].
+ ///
+ /// Throws a [StateError] if invoked in release mode.
+ static set debugCollectionEnabled(bool value) {
+ if (kReleaseMode) {
+ throw _createReleaseModeNotSupportedError();
+ }
+ if (value == _collectionEnabled) {
+ return;
+ }
+ _collectionEnabled = value;
+ debugReset();
+ }
+
+ static StateError _createReleaseModeNotSupportedError() {
+ return StateError('FlutterTimeline metric collection not supported in release mode.');
+ }
+
+ static bool _collectionEnabled = false;
+
+ /// Start a synchronous operation labeled `name`.
+ ///
+ /// Optionally takes a map of `arguments`. This slice may also optionally be
+ /// associated with a [Flow] event. This operation must be finished by calling
+ /// [finishSync] before returning to the event queue.
+ ///
+ /// This is a drop-in replacement for [Timeline.startSync].
+ static void startSync(String name, { Map<String, Object?>? arguments, Flow? flow }) {
+ Timeline.startSync(name, arguments: arguments, flow: flow);
+ if (!kReleaseMode && _collectionEnabled) {
+ _buffer.startSync(name, arguments: arguments, flow: flow);
+ }
+ }
+
+ /// Finish the last synchronous operation that was started.
+ ///
+ /// This is a drop-in replacement for [Timeline.finishSync].
+ static void finishSync() {
+ Timeline.finishSync();
+ if (!kReleaseMode && _collectionEnabled) {
+ _buffer.finishSync();
+ }
+ }
+
+ /// Emit an instant event.
+ ///
+ /// This is a drop-in replacement for [Timeline.instantSync].
+ static void instantSync(String name, { Map<String, Object?>? arguments }) {
+ Timeline.instantSync(name, arguments: arguments);
+ }
+
+ /// A utility method to time a synchronous `function`. Internally calls
+ /// `function` bracketed by calls to [startSync] and [finishSync].
+ ///
+ /// This is a drop-in replacement for [Timeline.timeSync].
+ static T timeSync<T>(String name, TimelineSyncFunction<T> function,
+ { Map<String, Object?>? arguments, Flow? flow }) {
+ startSync(name, arguments: arguments, flow: flow);
+ try {
+ return function();
+ } finally {
+ finishSync();
+ }
+ }
+
+ /// The current time stamp from the clock used by the timeline in
+ /// microseconds.
+ ///
+ /// When run on the Dart VM, uses the same monotonic clock as the embedding
+ /// API's `Dart_TimelineGetMicros`.
+ ///
+ /// When run on the web, uses `window.performance.now`.
+ ///
+ /// This is a drop-in replacement for [Timeline.now].
+ static int get now => impl.performanceTimestamp.toInt();
+
+ /// Returns timings collected since [debugCollectionEnabled] was set to true,
+ /// since the previous [debugCollect], or since the previous [debugReset],
+ /// whichever was last.
+ ///
+ /// Resets the collected timings.
+ ///
+ /// This is only meant to be used in non-release modes, typically in profile
+ /// mode that provides timings close to release mode timings.
+ static AggregatedTimings debugCollect() {
+ if (kReleaseMode) {
+ throw _createReleaseModeNotSupportedError();
+ }
+ if (!_collectionEnabled) {
+ throw StateError('Timeline metric collection not enabled.');
+ }
+ final AggregatedTimings result = AggregatedTimings(_buffer.computeTimings());
+ debugReset();
+ return result;
+ }
+
+ /// Forgets all previously collected timing data.
+ ///
+ /// Use this method to scope metrics to a frame, a pointer event, or any
+ /// other event. To do that, call [debugReset] at the start of the event, then
+ /// call [debugCollect] at the end of the event.
+ ///
+ /// This is only meant to be used in non-release modes.
+ static void debugReset() {
+ if (kReleaseMode) {
+ throw _createReleaseModeNotSupportedError();
+ }
+ _buffer = _BlockBuffer();
+ }
+}
+
+/// Provides [start], [end], and [duration] of a named block of code, timed by
+/// [FlutterTimeline].
+@immutable
+final class TimedBlock {
+ /// Creates a timed block of code from a [name], [start], and [end].
+ ///
+ /// The [name] should be sufficiently unique and descriptive for someone to
+ /// easily tell which part of code was measured.
+ const TimedBlock({
+ required this.name,
+ required this.start,
+ required this.end,
+ }) : assert(end >= start, 'The start timestamp must not be greater than the end timestamp.');
+
+ /// A readable label for a block of code that was measured.
+ ///
+ /// This field should be sufficiently unique and descriptive for someone to
+ /// easily tell which part of code was measured.
+ final String name;
+
+ /// The timestamp in microseconds that marks the beginning of the measured
+ /// block of code.
+ final double start;
+
+ /// The timestamp in microseconds that marks the end of the measured block of
+ /// code.
+ final double end;
+
+ /// How long the measured block of code took to execute in microseconds.
+ double get duration => end - start;
+
+ @override
+ String toString() {
+ return 'TimedBlock($name, $start, $end, $duration)';
+ }
+}
+
+/// Provides aggregated results for timings collected by [FlutterTimeline].
+@immutable
+final class AggregatedTimings {
+ /// Creates aggregated timings for the provided timed blocks.
+ AggregatedTimings(this.timedBlocks);
+
+ /// All timed blocks collected between the last reset and [FlutterTimeline.debugCollect].
+ final List<TimedBlock> timedBlocks;
+
+ /// Aggregated timed blocks collected between the last reset and [FlutterTimeline.debugCollect].
+ ///
+ /// Does not guarantee that all code blocks will be reported. Only those that
+ /// executed since the last reset are listed here. Use [getAggregated] for
+ /// graceful handling of missing code blocks.
+ late final List<AggregatedTimedBlock> aggregatedBlocks = _computeAggregatedBlocks();
+
+ List<AggregatedTimedBlock> _computeAggregatedBlocks() {
+ final Map<String, (double, int)> aggregate = <String, (double, int)>{};
+ for (final TimedBlock block in timedBlocks) {
+ final (double, int) previousValue = aggregate.putIfAbsent(block.name, () => (0, 0));
+ aggregate[block.name] = (previousValue.$1 + block.duration, previousValue.$2 + 1);
+ }
+ return aggregate.entries.map<AggregatedTimedBlock>(
+ (MapEntry<String, (double, int)> entry) {
+ return AggregatedTimedBlock(name: entry.key, duration: entry.value.$1, count: entry.value.$2);
+ }
+ ).toList();
+ }
+
+ /// Returns aggregated numbers for a named block of code.
+ ///
+ /// If the block in question never executed since the last reset, returns an
+ /// aggregation with zero duration and count.
+ AggregatedTimedBlock getAggregated(String name) {
+ return aggregatedBlocks.singleWhere(
+ (AggregatedTimedBlock block) => block.name == name,
+ // Handle the case where there are no recorded blocks of the specified
+ // type. In this case, the aggregated duration is simply zero, and so is
+ // the number of occurrences (i.e. count).
+ orElse: () => AggregatedTimedBlock(name: name, duration: 0, count: 0),
+ );
+ }
+}
+
+/// Aggregates multiple [TimedBlock] objects that share a [name].
+///
+/// It is common for the same block of code to be executed multiple times within
+/// a frame. It is useful to combine multiple executions and report the total
+/// amount of time attributed to that block of code.
+@immutable
+final class AggregatedTimedBlock {
+ /// Creates a timed block of code from a [name] and [duration].
+ ///
+ /// The [name] should be sufficiently unique and descriptive for someone to
+ /// easily tell which part of code was measured.
+ const AggregatedTimedBlock({
+ required this.name,
+ required this.duration,
+ required this.count,
+ }) : assert(duration >= 0);
+
+ /// A readable label for a block of code that was measured.
+ ///
+ /// This field should be sufficiently unique and descriptive for someone to
+ /// easily tell which part of code was measured.
+ final String name;
+
+ /// The sum of [TimedBlock.duration] values of aggretaged blocks.
+ final double duration;
+
+ /// The number of [TimedBlock] objects aggregated.
+ final int count;
+
+ @override
+ String toString() {
+ return 'AggregatedTimedBlock($name, $duration, $count)';
+ }
+}
+
+const int _kSliceSize = 500;
+
+/// A growable list of float64 values with predictable [add] performance.
+///
+/// The list is organized into a "chain" of [Float64List]s. The object starts
+/// with a `Float64List` "slice". When [add] is called, the value is added to
+/// the slice. Once the slice is full, it is moved into the chain, and a new
+/// slice is allocated. Slice size is static and therefore its allocation has
+/// predictable cost. This is unlike the default [List] implementation, which,
+/// when full, doubles its buffer size and copies all old elements into the new
+/// buffer, leading to unpredictable performance. This makes it a poor choice
+/// for recording performance because buffer reallocation would affect the
+/// runtime.
+///
+/// The trade-off is that reading values back from the chain is more expensive
+/// compared to [List] because it requires iterating over multiple slices. This
+/// is a reasonable trade-off for performance metrics, because it is more
+/// important to minimize the overhead while recording metrics, than it is when
+/// reading them.
+final class _Float64ListChain {
+ _Float64ListChain();
+
+ final List<Float64List> _chain = <Float64List>[];
+ Float64List _slice = Float64List(_kSliceSize);
+ int _pointer = 0;
+
+ int get length => _length;
+ int _length = 0;
+
+ /// Adds and [element] to this chain.
+ void add(double element) {
+ _slice[_pointer] = element;
+ _pointer += 1;
+ _length += 1;
+ if (_pointer >= _kSliceSize) {
+ _chain.add(_slice);
+ _slice = Float64List(_kSliceSize);
+ _pointer = 0;
+ }
+ }
+
+ /// Returns all elements added to this chain.
+ ///
+ /// This getter is not optimized to be fast. It is assumed that when metrics
+ /// are read back, they do not affect the timings of the work being
+ /// benchmarked.
+ List<double> extractElements() {
+ final List<double> result = <double>[];
+ _chain.forEach(result.addAll);
+ for (int i = 0; i < _pointer; i++) {
+ result.add(_slice[i]);
+ }
+ return result;
+ }
+}
+
+/// Same as [_Float64ListChain] but for recording string values.
+final class _StringListChain {
+ _StringListChain();
+
+ final List<List<String?>> _chain = <List<String?>>[];
+ List<String?> _slice = List<String?>.filled(_kSliceSize, null);
+ int _pointer = 0;
+
+ int get length => _length;
+ int _length = 0;
+
+ /// Adds and [element] to this chain.
+ void add(String element) {
+ _slice[_pointer] = element;
+ _pointer += 1;
+ _length += 1;
+ if (_pointer >= _kSliceSize) {
+ _chain.add(_slice);
+ _slice = List<String?>.filled(_kSliceSize, null);
+ _pointer = 0;
+ }
+ }
+
+ /// Returns all elements added to this chain.
+ ///
+ /// This getter is not optimized to be fast. It is assumed that when metrics
+ /// are read back, they do not affect the timings of the work being
+ /// benchmarked.
+ List<String> extractElements() {
+ final List<String> result = <String>[];
+ for (final List<String?> slice in _chain) {
+ for (final String? element in slice) {
+ result.add(element!);
+ }
+ }
+ for (int i = 0; i < _pointer; i++) {
+ result.add(_slice[i]!);
+ }
+ return result;
+ }
+}
+
+/// A buffer that records starts and ends of code blocks, and their names.
+final class _BlockBuffer {
+ // Start-finish blocks can be nested. Track this nestedness by stacking the
+ // start timestamps. Finish timestamps will pop timings from the stack and
+ // add the (start, finish) tuple to the _block.
+ static const int _stackDepth = 1000;
+ static final Float64List _startStack = Float64List(_stackDepth);
+ static final List<String?> _nameStack = List<String?>.filled(_stackDepth, null);
+ static int _stackPointer = 0;
+
+ final _Float64ListChain _starts = _Float64ListChain();
+ final _Float64ListChain _finishes = _Float64ListChain();
+ final _StringListChain _names = _StringListChain();
+
+ List<TimedBlock> computeTimings() {
+ assert(
+ _stackPointer == 0,
+ 'Invalid sequence of `startSync` and `finishSync`.\n'
+ 'The operation stack was not empty. The following operations are still '
+ 'waiting to be finished via the `finishSync` method:\n'
+ '${List<String>.generate(_stackPointer, (int i) => _nameStack[i]!).join(', ')}'
+ );
+
+ final List<TimedBlock> result = <TimedBlock>[];
+ final int length = _finishes.length;
+ final List<double> starts = _starts.extractElements();
+ final List<double> finishes = _finishes.extractElements();
+ final List<String> names = _names.extractElements();
+
+ assert(starts.length == length);
+ assert(finishes.length == length);
+ assert(names.length == length);
+
+ for (int i = 0; i < length; i++) {
+ result.add(TimedBlock(
+ start: starts[i],
+ end: finishes[i],
+ name: names[i],
+ ));
+ }
+
+ return result;
+ }
+
+ void startSync(String name, { Map<String, Object?>? arguments, Flow? flow }) {
+ _startStack[_stackPointer] = impl.performanceTimestamp;
+ _nameStack[_stackPointer] = name;
+ _stackPointer += 1;
+ }
+
+ void finishSync() {
+ assert(
+ _stackPointer > 0,
+ 'Invalid sequence of `startSync` and `finishSync`.\n'
+ 'Attempted to finish timing a block of code, but there are no pending '
+ '`startSync` calls.'
+ );
+
+ final double finishTime = impl.performanceTimestamp;
+ final double startTime = _startStack[_stackPointer - 1];
+ final String name = _nameStack[_stackPointer - 1]!;
+ _stackPointer -= 1;
+
+ _starts.add(startTime);
+ _finishes.add(finishTime);
+ _names.add(name);
+ }
+}
diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart
index dec407b..a90e062 100644
--- a/packages/flutter/lib/src/rendering/binding.dart
+++ b/packages/flutter/lib/src/rendering/binding.dart
@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:developer';
import 'dart:ui' as ui show SemanticsUpdate;
import 'package:flutter/foundation.dart';
@@ -507,13 +506,13 @@
await super.performReassemble();
if (BindingBase.debugReassembleConfig?.widgetName == null) {
if (!kReleaseMode) {
- Timeline.startSync('Preparing Hot Reload (layout)');
+ FlutterTimeline.startSync('Preparing Hot Reload (layout)');
}
try {
renderView.reassemble();
} finally {
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
}
diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart
index e89ad00..6a9a007 100644
--- a/packages/flutter/lib/src/rendering/box.dart
+++ b/packages/flutter/lib/src/rendering/box.dart
@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:developer' show Timeline;
import 'dart:math' as math;
import 'dart:ui' as ui show lerpDouble;
@@ -1396,7 +1395,7 @@
}());
if (!kReleaseMode) {
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
- Timeline.startSync(
+ FlutterTimeline.startSync(
'$runtimeType intrinsics',
arguments: debugTimelineArguments,
);
@@ -1411,7 +1410,7 @@
if (!kReleaseMode) {
_debugIntrinsicsDepth -= 1;
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
return result;
@@ -1832,7 +1831,7 @@
}());
if (!kReleaseMode) {
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
- Timeline.startSync(
+ FlutterTimeline.startSync(
'$runtimeType.getDryLayout',
arguments: debugTimelineArguments,
);
@@ -1844,7 +1843,7 @@
if (!kReleaseMode) {
_debugIntrinsicsDepth -= 1;
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
return result;
diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart
index b1984b4..3e42a22 100644
--- a/packages/flutter/lib/src/rendering/object.dart
+++ b/packages/flutter/lib/src/rendering/object.dart
@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:developer';
import 'dart:ui' as ui show PictureRecorder;
import 'dart:ui';
@@ -986,7 +985,7 @@
}
return true;
}());
- Timeline.startSync(
+ FlutterTimeline.startSync(
'LAYOUT',
arguments: debugTimelineArguments,
);
@@ -1035,7 +1034,7 @@
return true;
}());
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
}
@@ -1074,7 +1073,7 @@
/// [flushPaint].
void flushCompositingBits() {
if (!kReleaseMode) {
- Timeline.startSync('UPDATING COMPOSITING BITS');
+ FlutterTimeline.startSync('UPDATING COMPOSITING BITS');
}
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
@@ -1088,7 +1087,7 @@
}
assert(_nodesNeedingCompositingBitsUpdate.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.');
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
@@ -1122,7 +1121,7 @@
}
return true;
}());
- Timeline.startSync(
+ FlutterTimeline.startSync(
'PAINT',
arguments: debugTimelineArguments,
);
@@ -1161,7 +1160,7 @@
return true;
}());
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
}
@@ -1250,7 +1249,7 @@
return;
}
if (!kReleaseMode) {
- Timeline.startSync('SEMANTICS');
+ FlutterTimeline.startSync('SEMANTICS');
}
assert(_semanticsOwner != null);
assert(() {
@@ -1277,7 +1276,7 @@
return true;
}());
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
}
@@ -2379,7 +2378,7 @@
}
return true;
}());
- Timeline.startSync(
+ FlutterTimeline.startSync(
'$runtimeType',
arguments: debugTimelineArguments,
);
@@ -2443,7 +2442,7 @@
}
if (!kReleaseMode && debugProfileLayoutsEnabled) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
return;
}
@@ -2510,7 +2509,7 @@
markNeedsPaint();
if (!kReleaseMode && debugProfileLayoutsEnabled) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
@@ -3082,7 +3081,7 @@
}
return true;
}());
- Timeline.startSync(
+ FlutterTimeline.startSync(
'$runtimeType',
arguments: debugTimelineArguments,
);
@@ -3166,7 +3165,7 @@
return true;
}());
if (!kReleaseMode && debugProfilePaintsEnabled) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
@@ -3528,14 +3527,24 @@
// The subtree is probably being kept alive by a viewport but not laid out.
return;
}
+ if (!kReleaseMode) {
+ FlutterTimeline.startSync('Semantics.GetFragment');
+ }
final _SemanticsFragment fragment = _getSemanticsForParent(
mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false,
blockUserActions: _semantics?.areUserActionsBlocked ?? false,
);
+ if (!kReleaseMode) {
+ FlutterTimeline.finishSync();
+ }
assert(fragment is _InterestingSemanticsFragment);
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
final List<SemanticsNode> result = <SemanticsNode>[];
final List<SemanticsNode> siblingNodes = <SemanticsNode>[];
+
+ if (!kReleaseMode) {
+ FlutterTimeline.startSync('Semantics.compileChildren');
+ }
interestingFragment.compileChildren(
parentSemanticsClipRect: _semantics?.parentSemanticsClipRect,
parentPaintClipRect: _semantics?.parentPaintClipRect,
@@ -3543,6 +3552,9 @@
result: result,
siblingNodes: siblingNodes,
);
+ if (!kReleaseMode) {
+ FlutterTimeline.finishSync();
+ }
// Result may contain sibling nodes that are irrelevant for this update.
assert(interestingFragment.config == null && result.any((SemanticsNode node) => node == _semantics));
}
diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart
index ac397ad..906d237 100644
--- a/packages/flutter/lib/src/rendering/view.dart
+++ b/packages/flutter/lib/src/rendering/view.dart
@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:developer';
import 'dart:io' show Platform;
import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate;
@@ -229,7 +228,7 @@
/// Actually causes the output of the rendering pipeline to appear on screen.
void compositeFrame() {
if (!kReleaseMode) {
- Timeline.startSync('COMPOSITING');
+ FlutterTimeline.startSync('COMPOSITING');
}
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
@@ -247,7 +246,7 @@
}());
} finally {
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
}
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index b2f3d96..2c0e3ca 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -4,7 +4,6 @@
import 'dart:async';
import 'dart:collection';
-import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
@@ -2700,7 +2699,7 @@
}
return true;
}());
- Timeline.startSync(
+ FlutterTimeline.startSync(
'BUILD',
arguments: debugTimelineArguments
);
@@ -2771,7 +2770,7 @@
}
return true;
}());
- Timeline.startSync(
+ FlutterTimeline.startSync(
'${element.widget.runtimeType}',
arguments: debugTimelineArguments,
);
@@ -2794,7 +2793,7 @@
);
}
if (isTimelineTracked) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
index += 1;
if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {
@@ -2832,7 +2831,7 @@
_scheduledFlushDirtyElements = false;
_dirtyElementsNeedsResorting = null;
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
assert(_debugBuilding);
assert(() {
@@ -3044,7 +3043,7 @@
@pragma('vm:notify-debugger-on-exception')
void finalizeTree() {
if (!kReleaseMode) {
- Timeline.startSync('FINALIZE TREE');
+ FlutterTimeline.startSync('FINALIZE TREE');
}
try {
lockState(_inactiveElements._unmountAll); // this unregisters the GlobalKeys
@@ -3140,7 +3139,7 @@
_reportException(ErrorSummary('while finalizing the widget tree'), e, stack);
} finally {
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
}
@@ -3153,7 +3152,7 @@
/// This is expensive and should not be called except during development.
void reassemble(Element root, DebugReassembleConfig? reassembleConfig) {
if (!kReleaseMode) {
- Timeline.startSync('Preparing Hot Reload (widgets)');
+ FlutterTimeline.startSync('Preparing Hot Reload (widgets)');
}
try {
assert(root._parent == null);
@@ -3162,7 +3161,7 @@
root.reassemble();
} finally {
if (!kReleaseMode) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
}
@@ -3678,14 +3677,14 @@
}
return true;
}());
- Timeline.startSync(
+ FlutterTimeline.startSync(
'${newWidget.runtimeType}',
arguments: debugTimelineArguments,
);
}
child.update(newWidget);
if (isTimelineTracked) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
assert(child.widget == newWidget);
assert(() {
@@ -4153,7 +4152,7 @@
}
return true;
}());
- Timeline.startSync(
+ FlutterTimeline.startSync(
'${newWidget.runtimeType}',
arguments: debugTimelineArguments,
);
@@ -4186,7 +4185,7 @@
return newChild;
} finally {
if (isTimelineTracked) {
- Timeline.finishSync();
+ FlutterTimeline.finishSync();
}
}
}
diff --git a/packages/flutter/test/foundation/timeline_test.dart b/packages/flutter/test/foundation/timeline_test.dart
new file mode 100644
index 0000000..80c95d1
--- /dev/null
+++ b/packages/flutter/test/foundation/timeline_test.dart
@@ -0,0 +1,143 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+// IMPORTANT: keep this in sync with the same constant defined
+// in foundation/timeline.dart
+const int kSliceSize = 500;
+
+void main() {
+ setUp(() {
+ FlutterTimeline.debugReset();
+ FlutterTimeline.debugCollectionEnabled = false;
+ });
+
+ test('Does not collect when collection not enabled', () {
+ FlutterTimeline.startSync('TEST');
+ FlutterTimeline.finishSync();
+ expect(
+ () => FlutterTimeline.debugCollect(),
+ throwsStateError,
+ );
+ });
+
+ test('Collects when collection is enabled', () {
+ FlutterTimeline.debugCollectionEnabled = true;
+ FlutterTimeline.startSync('TEST');
+ FlutterTimeline.finishSync();
+ final AggregatedTimings data = FlutterTimeline.debugCollect();
+ expect(data.timedBlocks, hasLength(1));
+ expect(data.aggregatedBlocks, hasLength(1));
+
+ final AggregatedTimedBlock block = data.getAggregated('TEST');
+ expect(block.name, 'TEST');
+ expect(block.count, 1);
+
+ // After collection the timeline is reset back to empty.
+ final AggregatedTimings data2 = FlutterTimeline.debugCollect();
+ expect(data2.timedBlocks, isEmpty);
+ expect(data2.aggregatedBlocks, isEmpty);
+ });
+
+ test('Deletes old data when reset', () {
+ FlutterTimeline.debugCollectionEnabled = true;
+ FlutterTimeline.startSync('TEST');
+ FlutterTimeline.finishSync();
+ FlutterTimeline.debugReset();
+
+ final AggregatedTimings data = FlutterTimeline.debugCollect();
+ expect(data.timedBlocks, isEmpty);
+ expect(data.aggregatedBlocks, isEmpty);
+ });
+
+ test('Reports zero aggregation when requested missing block', () {
+ FlutterTimeline.debugCollectionEnabled = true;
+
+ final AggregatedTimings data = FlutterTimeline.debugCollect();
+ final AggregatedTimedBlock block = data.getAggregated('MISSING');
+ expect(block.name, 'MISSING');
+ expect(block.count, 0);
+ expect(block.duration, 0);
+ });
+
+ test('Measures the runtime of a function', () {
+ FlutterTimeline.debugCollectionEnabled = true;
+
+ // The off-by-one values for `start` and `end` are for web's sake where
+ // timer values are reported as float64 and toInt/toDouble conversions
+ // are noops, so there's no value truncation happening, which makes it
+ // a bit inconsistent with Stopwatch.
+ final int start = FlutterTimeline.now - 1;
+ FlutterTimeline.timeSync('TEST', () {
+ final Stopwatch watch = Stopwatch()..start();
+ while (watch.elapsedMilliseconds < 5) {}
+ watch.stop();
+ });
+ final int end = FlutterTimeline.now + 1;
+
+ final AggregatedTimings data = FlutterTimeline.debugCollect();
+ expect(data.timedBlocks, hasLength(1));
+ expect(data.aggregatedBlocks, hasLength(1));
+
+ final TimedBlock block = data.timedBlocks.single;
+ expect(block.name, 'TEST');
+ expect(block.start, greaterThanOrEqualTo(start));
+ expect(block.end, lessThanOrEqualTo(end));
+ expect(block.duration, greaterThan(0));
+
+ final AggregatedTimedBlock aggregated = data.getAggregated('TEST');
+ expect(aggregated.name, 'TEST');
+ expect(aggregated.count, 1);
+ expect(aggregated.duration, block.duration);
+ });
+
+ test('FlutterTimeline.instanceSync does not collect anything', () {
+ FlutterTimeline.debugCollectionEnabled = true;
+ FlutterTimeline.instantSync('TEST');
+
+ final AggregatedTimings data = FlutterTimeline.debugCollect();
+ expect(data.timedBlocks, isEmpty);
+ expect(data.aggregatedBlocks, isEmpty);
+ });
+
+ test('FlutterTimeline.now returns a value', () {
+ FlutterTimeline.debugCollectionEnabled = true;
+ expect(FlutterTimeline.now, isNotNull);
+ });
+
+ test('Can collect more than one slice of data', () {
+ FlutterTimeline.debugCollectionEnabled = true;
+
+ for (int i = 0; i < 10 * kSliceSize; i++) {
+ FlutterTimeline.startSync('TEST');
+ FlutterTimeline.finishSync();
+ }
+ final AggregatedTimings data = FlutterTimeline.debugCollect();
+ expect(data.timedBlocks, hasLength(10 * kSliceSize));
+ expect(data.aggregatedBlocks, hasLength(1));
+
+ final AggregatedTimedBlock block = data.getAggregated('TEST');
+ expect(block.name, 'TEST');
+ expect(block.count, 10 * kSliceSize);
+ });
+
+ test('Collects blocks in a correct order', () {
+ FlutterTimeline.debugCollectionEnabled = true;
+ const int testCount = 7 * kSliceSize ~/ 2;
+
+ for (int i = 0; i < testCount; i++) {
+ FlutterTimeline.startSync('TEST$i');
+ FlutterTimeline.finishSync();
+ }
+
+ final AggregatedTimings data = FlutterTimeline.debugCollect();
+ expect(data.timedBlocks, hasLength(testCount));
+ expect(
+ data.timedBlocks.map<String>((TimedBlock block) => block.name).toList(),
+ List<String>.generate(testCount, (int i) => 'TEST$i'),
+ );
+ });
+}