Unverified Commit 07772a3d authored by Yegor's avatar Yegor Committed by GitHub

[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.
parent 22005779
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'material3.dart';
import 'recorder.dart'; import 'recorder.dart';
/// Measures how expensive it is to construct the material 3 components screen. /// Measures how expensive it is to construct the material 3 components screen.
...@@ -15,2330 +15,6 @@ class BenchMaterial3Components extends WidgetBuildRecorder { ...@@ -15,2330 +15,6 @@ class BenchMaterial3Components extends WidgetBuildRecorder {
@override @override
Widget createWidget() { Widget createWidget() {
return const Material3Components(); return const TwoColumnMaterial3Components();
}
}
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
],
),
),
),
),
);
} }
} }
// 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,
);
}
}
...@@ -203,6 +203,7 @@ class BenchBuildColorsGrid extends WidgetBuildRecorder { ...@@ -203,6 +203,7 @@ class BenchBuildColorsGrid extends WidgetBuildRecorder {
@override @override
Future<void> setUpAll() async { Future<void> setUpAll() async {
super.setUpAll();
registerEngineBenchmarkValueListener('text_layout', (num value) { registerEngineBenchmarkValueListener('text_layout', (num value) {
_textLayoutMicros += value; _textLayoutMicros += value;
}); });
......
// 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
],
),
),
),
),
);
}
}
...@@ -426,12 +426,18 @@ abstract class WidgetRecorder extends Recorder implements FrameRecorder { ...@@ -426,12 +426,18 @@ abstract class WidgetRecorder extends Recorder implements FrameRecorder {
_runCompleter!.completeError(error, stackTrace); _runCompleter!.completeError(error, stackTrace);
} }
late final _RecordingWidgetsBinding _binding;
@override
@mustCallSuper
Future<void> setUpAll() async {
_binding = _RecordingWidgetsBinding.ensureInitialized();
}
@override @override
Future<Profile> run() async { Future<Profile> run() async {
_runCompleter = Completer<void>(); _runCompleter = Completer<void>();
final Profile localProfile = profile = Profile(name: name, useCustomWarmUp: useCustomWarmUp); final Profile localProfile = profile = Profile(name: name, useCustomWarmUp: useCustomWarmUp);
final _RecordingWidgetsBinding binding =
_RecordingWidgetsBinding.ensureInitialized();
final Widget widget = createWidget(); final Widget widget = createWidget();
registerEngineBenchmarkValueListener(kProfilePrerollFrame, (num value) { registerEngineBenchmarkValueListener(kProfilePrerollFrame, (num value) {
...@@ -449,7 +455,7 @@ abstract class WidgetRecorder extends Recorder implements FrameRecorder { ...@@ -449,7 +455,7 @@ abstract class WidgetRecorder extends Recorder implements FrameRecorder {
); );
}); });
binding._beginRecording(this, widget); _binding._beginRecording(this, widget);
try { try {
await _runCompleter!.future; await _runCompleter!.future;
...@@ -508,6 +514,14 @@ abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder { ...@@ -508,6 +514,14 @@ abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder {
} }
} }
late final _RecordingWidgetsBinding _binding;
@override
@mustCallSuper
Future<void> setUpAll() async {
_binding = _RecordingWidgetsBinding.ensureInitialized();
}
@override @override
@mustCallSuper @mustCallSuper
void frameWillDraw() { void frameWillDraw() {
...@@ -546,9 +560,7 @@ abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder { ...@@ -546,9 +560,7 @@ abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder {
Future<Profile> run() async { Future<Profile> run() async {
_runCompleter = Completer<void>(); _runCompleter = Completer<void>();
final Profile localProfile = profile = Profile(name: name); final Profile localProfile = profile = Profile(name: name);
final _RecordingWidgetsBinding binding = _binding._beginRecording(this, _WidgetBuildRecorderHost(this));
_RecordingWidgetsBinding.ensureInitialized();
binding._beginRecording(this, _WidgetBuildRecorderHost(this));
try { try {
await _runCompleter!.future; await _runCompleter!.future;
...@@ -948,6 +960,15 @@ class Profile { ...@@ -948,6 +960,15 @@ class Profile {
} }
} }
/// 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. /// 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 /// If enough warm-up samples have been collected, stops the warm-up phase and
......
...@@ -19,6 +19,7 @@ import 'src/web/bench_draw_rect.dart'; ...@@ -19,6 +19,7 @@ import 'src/web/bench_draw_rect.dart';
import 'src/web/bench_dynamic_clip_on_static_picture.dart'; import 'src/web/bench_dynamic_clip_on_static_picture.dart';
import 'src/web/bench_image_decoding.dart'; import 'src/web/bench_image_decoding.dart';
import 'src/web/bench_material_3.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_hover.dart';
import 'src/web/bench_mouse_region_grid_scroll.dart'; import 'src/web/bench_mouse_region_grid_scroll.dart';
import 'src/web/bench_mouse_region_mixed_grid_hover.dart'; import 'src/web/bench_mouse_region_mixed_grid_hover.dart';
...@@ -64,6 +65,8 @@ final Map<String, RecorderFactory> benchmarks = <String, RecorderFactory>{ ...@@ -64,6 +65,8 @@ final Map<String, RecorderFactory> benchmarks = <String, RecorderFactory>{
BenchPlatformViewInfiniteScroll.benchmarkName: () => BenchPlatformViewInfiniteScroll.forward(), BenchPlatformViewInfiniteScroll.benchmarkName: () => BenchPlatformViewInfiniteScroll.forward(),
BenchPlatformViewInfiniteScroll.benchmarkNameBackward: () => BenchPlatformViewInfiniteScroll.backward(), BenchPlatformViewInfiniteScroll.benchmarkNameBackward: () => BenchPlatformViewInfiniteScroll.backward(),
BenchMaterial3Components.benchmarkName: () => BenchMaterial3Components(), BenchMaterial3Components.benchmarkName: () => BenchMaterial3Components(),
BenchMaterial3Semantics.benchmarkName: () => BenchMaterial3Semantics(),
BenchMaterial3ScrollSemantics.benchmarkName: () => BenchMaterial3ScrollSemantics(),
// CanvasKit-only benchmarks // CanvasKit-only benchmarks
if (isCanvasKit) ...<String, RecorderFactory>{ if (isCanvasKit) ...<String, RecorderFactory>{
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:developer'; import 'package:flutter/foundation.dart';
import '../common.dart'; import '../common.dart';
...@@ -16,8 +16,8 @@ void main() { ...@@ -16,8 +16,8 @@ void main() {
final Stopwatch watch = Stopwatch(); final Stopwatch watch = Stopwatch();
watch.start(); watch.start();
for (int i = 0; i < _kNumIterations; i += 1) { for (int i = 0; i < _kNumIterations; i += 1) {
Timeline.startSync('foo'); FlutterTimeline.startSync('foo');
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
watch.stop(); watch.stop();
...@@ -31,14 +31,14 @@ void main() { ...@@ -31,14 +31,14 @@ void main() {
watch.reset(); watch.reset();
watch.start(); watch.start();
for (int i = 0; i < _kNumIterations; i += 1) { for (int i = 0; i < _kNumIterations; i += 1) {
Timeline.startSync('foo', arguments: <String, dynamic>{ FlutterTimeline.startSync('foo', arguments: <String, dynamic>{
'int': 1234, 'int': 1234,
'double': 0.3, 'double': 0.3,
'list': <int>[1, 2, 3, 4], 'list': <int>[1, 2, 3, 4],
'map': <String, dynamic>{'map': true}, 'map': <String, dynamic>{'map': true},
'bool': false, 'bool': false,
}); });
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
watch.stop(); watch.stop();
......
...@@ -1375,6 +1375,14 @@ Future<void> _runWebTreeshakeTest() async { ...@@ -1375,6 +1375,14 @@ Future<void> _runWebTreeshakeTest() async {
pos = javaScript.indexOf(word, pos); 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; const int kMaxExpectedDebugFillProperties = 11;
if (count > kMaxExpectedDebugFillProperties) { if (count > kMaxExpectedDebugFillProperties) {
throw Exception( throw Exception(
......
...@@ -46,4 +46,5 @@ export 'src/foundation/serialization.dart'; ...@@ -46,4 +46,5 @@ export 'src/foundation/serialization.dart';
export 'src/foundation/service_extensions.dart'; export 'src/foundation/service_extensions.dart';
export 'src/foundation/stack_frame.dart'; export 'src/foundation/stack_frame.dart';
export 'src/foundation/synchronous_future.dart'; export 'src/foundation/synchronous_future.dart';
export 'src/foundation/timeline.dart';
export 'src/foundation/unicode.dart'; export 'src/foundation/unicode.dart';
// 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();
// 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();
}
...@@ -20,6 +20,7 @@ import 'object.dart'; ...@@ -20,6 +20,7 @@ import 'object.dart';
import 'platform.dart'; import 'platform.dart';
import 'print.dart'; import 'print.dart';
import 'service_extensions.dart'; import 'service_extensions.dart';
import 'timeline.dart';
export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow; // ignore: deprecated_member_use export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow; // ignore: deprecated_member_use
...@@ -141,7 +142,9 @@ abstract class BindingBase { ...@@ -141,7 +142,9 @@ abstract class BindingBase {
/// [initServiceExtensions] to have bindings initialize their /// [initServiceExtensions] to have bindings initialize their
/// VM service extensions, if any. /// VM service extensions, if any.
BindingBase() { BindingBase() {
developer.Timeline.startSync('Framework initialization'); if (!kReleaseMode) {
FlutterTimeline.startSync('Framework initialization');
}
assert(() { assert(() {
_debugConstructed = true; _debugConstructed = true;
return true; return true;
...@@ -157,7 +160,9 @@ abstract class BindingBase { ...@@ -157,7 +160,9 @@ abstract class BindingBase {
developer.postEvent('Flutter.FrameworkInitialization', <String, String>{}); developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
developer.Timeline.finishSync(); if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
} }
bool _debugConstructed = false; bool _debugConstructed = false;
......
// 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);
}
}
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:developer';
import 'dart:ui' as ui show SemanticsUpdate; import 'dart:ui' as ui show SemanticsUpdate;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -507,13 +506,13 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture ...@@ -507,13 +506,13 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
await super.performReassemble(); await super.performReassemble();
if (BindingBase.debugReassembleConfig?.widgetName == null) { if (BindingBase.debugReassembleConfig?.widgetName == null) {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.startSync('Preparing Hot Reload (layout)'); FlutterTimeline.startSync('Preparing Hot Reload (layout)');
} }
try { try {
renderView.reassemble(); renderView.reassemble();
} finally { } finally {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
} }
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:developer' show Timeline;
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui show lerpDouble; import 'dart:ui' as ui show lerpDouble;
...@@ -1396,7 +1395,7 @@ abstract class RenderBox extends RenderObject { ...@@ -1396,7 +1395,7 @@ abstract class RenderBox extends RenderObject {
}()); }());
if (!kReleaseMode) { if (!kReleaseMode) {
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
Timeline.startSync( FlutterTimeline.startSync(
'$runtimeType intrinsics', '$runtimeType intrinsics',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
...@@ -1411,7 +1410,7 @@ abstract class RenderBox extends RenderObject { ...@@ -1411,7 +1410,7 @@ abstract class RenderBox extends RenderObject {
if (!kReleaseMode) { if (!kReleaseMode) {
_debugIntrinsicsDepth -= 1; _debugIntrinsicsDepth -= 1;
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
return result; return result;
...@@ -1832,7 +1831,7 @@ abstract class RenderBox extends RenderObject { ...@@ -1832,7 +1831,7 @@ abstract class RenderBox extends RenderObject {
}()); }());
if (!kReleaseMode) { if (!kReleaseMode) {
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
Timeline.startSync( FlutterTimeline.startSync(
'$runtimeType.getDryLayout', '$runtimeType.getDryLayout',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
...@@ -1844,7 +1843,7 @@ abstract class RenderBox extends RenderObject { ...@@ -1844,7 +1843,7 @@ abstract class RenderBox extends RenderObject {
if (!kReleaseMode) { if (!kReleaseMode) {
_debugIntrinsicsDepth -= 1; _debugIntrinsicsDepth -= 1;
if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
return result; return result;
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:developer';
import 'dart:ui' as ui show PictureRecorder; import 'dart:ui' as ui show PictureRecorder;
import 'dart:ui'; import 'dart:ui';
...@@ -986,7 +985,7 @@ class PipelineOwner { ...@@ -986,7 +985,7 @@ class PipelineOwner {
} }
return true; return true;
}()); }());
Timeline.startSync( FlutterTimeline.startSync(
'LAYOUT', 'LAYOUT',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
...@@ -1035,7 +1034,7 @@ class PipelineOwner { ...@@ -1035,7 +1034,7 @@ class PipelineOwner {
return true; return true;
}()); }());
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
} }
...@@ -1074,7 +1073,7 @@ class PipelineOwner { ...@@ -1074,7 +1073,7 @@ class PipelineOwner {
/// [flushPaint]. /// [flushPaint].
void flushCompositingBits() { void flushCompositingBits() {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.startSync('UPDATING COMPOSITING BITS'); FlutterTimeline.startSync('UPDATING COMPOSITING BITS');
} }
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth); _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) { for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
...@@ -1088,7 +1087,7 @@ class PipelineOwner { ...@@ -1088,7 +1087,7 @@ class PipelineOwner {
} }
assert(_nodesNeedingCompositingBitsUpdate.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.'); assert(_nodesNeedingCompositingBitsUpdate.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.');
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
...@@ -1122,7 +1121,7 @@ class PipelineOwner { ...@@ -1122,7 +1121,7 @@ class PipelineOwner {
} }
return true; return true;
}()); }());
Timeline.startSync( FlutterTimeline.startSync(
'PAINT', 'PAINT',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
...@@ -1161,7 +1160,7 @@ class PipelineOwner { ...@@ -1161,7 +1160,7 @@ class PipelineOwner {
return true; return true;
}()); }());
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
} }
...@@ -1250,7 +1249,7 @@ class PipelineOwner { ...@@ -1250,7 +1249,7 @@ class PipelineOwner {
return; return;
} }
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.startSync('SEMANTICS'); FlutterTimeline.startSync('SEMANTICS');
} }
assert(_semanticsOwner != null); assert(_semanticsOwner != null);
assert(() { assert(() {
...@@ -1277,7 +1276,7 @@ class PipelineOwner { ...@@ -1277,7 +1276,7 @@ class PipelineOwner {
return true; return true;
}()); }());
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
} }
...@@ -2379,7 +2378,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge ...@@ -2379,7 +2378,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
} }
return true; return true;
}()); }());
Timeline.startSync( FlutterTimeline.startSync(
'$runtimeType', '$runtimeType',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
...@@ -2443,7 +2442,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge ...@@ -2443,7 +2442,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
} }
if (!kReleaseMode && debugProfileLayoutsEnabled) { if (!kReleaseMode && debugProfileLayoutsEnabled) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
return; return;
} }
...@@ -2510,7 +2509,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge ...@@ -2510,7 +2509,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
markNeedsPaint(); markNeedsPaint();
if (!kReleaseMode && debugProfileLayoutsEnabled) { if (!kReleaseMode && debugProfileLayoutsEnabled) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
...@@ -3082,7 +3081,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge ...@@ -3082,7 +3081,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
} }
return true; return true;
}()); }());
Timeline.startSync( FlutterTimeline.startSync(
'$runtimeType', '$runtimeType',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
...@@ -3166,7 +3165,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge ...@@ -3166,7 +3165,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
return true; return true;
}()); }());
if (!kReleaseMode && debugProfilePaintsEnabled) { if (!kReleaseMode && debugProfilePaintsEnabled) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
...@@ -3528,14 +3527,24 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge ...@@ -3528,14 +3527,24 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
// The subtree is probably being kept alive by a viewport but not laid out. // The subtree is probably being kept alive by a viewport but not laid out.
return; return;
} }
if (!kReleaseMode) {
FlutterTimeline.startSync('Semantics.GetFragment');
}
final _SemanticsFragment fragment = _getSemanticsForParent( final _SemanticsFragment fragment = _getSemanticsForParent(
mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false, mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false,
blockUserActions: _semantics?.areUserActionsBlocked ?? false, blockUserActions: _semantics?.areUserActionsBlocked ?? false,
); );
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
assert(fragment is _InterestingSemanticsFragment); assert(fragment is _InterestingSemanticsFragment);
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment; final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
final List<SemanticsNode> result = <SemanticsNode>[]; final List<SemanticsNode> result = <SemanticsNode>[];
final List<SemanticsNode> siblingNodes = <SemanticsNode>[]; final List<SemanticsNode> siblingNodes = <SemanticsNode>[];
if (!kReleaseMode) {
FlutterTimeline.startSync('Semantics.compileChildren');
}
interestingFragment.compileChildren( interestingFragment.compileChildren(
parentSemanticsClipRect: _semantics?.parentSemanticsClipRect, parentSemanticsClipRect: _semantics?.parentSemanticsClipRect,
parentPaintClipRect: _semantics?.parentPaintClipRect, parentPaintClipRect: _semantics?.parentPaintClipRect,
...@@ -3543,6 +3552,9 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge ...@@ -3543,6 +3552,9 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
result: result, result: result,
siblingNodes: siblingNodes, siblingNodes: siblingNodes,
); );
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
// Result may contain sibling nodes that are irrelevant for this update. // Result may contain sibling nodes that are irrelevant for this update.
assert(interestingFragment.config == null && result.any((SemanticsNode node) => node == _semantics)); assert(interestingFragment.config == null && result.any((SemanticsNode node) => node == _semantics));
} }
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:developer';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate; import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate;
...@@ -229,7 +228,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -229,7 +228,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// Actually causes the output of the rendering pipeline to appear on screen. /// Actually causes the output of the rendering pipeline to appear on screen.
void compositeFrame() { void compositeFrame() {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.startSync('COMPOSITING'); FlutterTimeline.startSync('COMPOSITING');
} }
try { try {
final ui.SceneBuilder builder = ui.SceneBuilder(); final ui.SceneBuilder builder = ui.SceneBuilder();
...@@ -247,7 +246,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -247,7 +246,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
}()); }());
} finally { } finally {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
} }
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:developer';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -2700,7 +2699,7 @@ class BuildOwner { ...@@ -2700,7 +2699,7 @@ class BuildOwner {
} }
return true; return true;
}()); }());
Timeline.startSync( FlutterTimeline.startSync(
'BUILD', 'BUILD',
arguments: debugTimelineArguments arguments: debugTimelineArguments
); );
...@@ -2771,7 +2770,7 @@ class BuildOwner { ...@@ -2771,7 +2770,7 @@ class BuildOwner {
} }
return true; return true;
}()); }());
Timeline.startSync( FlutterTimeline.startSync(
'${element.widget.runtimeType}', '${element.widget.runtimeType}',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
...@@ -2794,7 +2793,7 @@ class BuildOwner { ...@@ -2794,7 +2793,7 @@ class BuildOwner {
); );
} }
if (isTimelineTracked) { if (isTimelineTracked) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
index += 1; index += 1;
if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) { if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {
...@@ -2832,7 +2831,7 @@ class BuildOwner { ...@@ -2832,7 +2831,7 @@ class BuildOwner {
_scheduledFlushDirtyElements = false; _scheduledFlushDirtyElements = false;
_dirtyElementsNeedsResorting = null; _dirtyElementsNeedsResorting = null;
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
assert(_debugBuilding); assert(_debugBuilding);
assert(() { assert(() {
...@@ -3044,7 +3043,7 @@ class BuildOwner { ...@@ -3044,7 +3043,7 @@ class BuildOwner {
@pragma('vm:notify-debugger-on-exception') @pragma('vm:notify-debugger-on-exception')
void finalizeTree() { void finalizeTree() {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.startSync('FINALIZE TREE'); FlutterTimeline.startSync('FINALIZE TREE');
} }
try { try {
lockState(_inactiveElements._unmountAll); // this unregisters the GlobalKeys lockState(_inactiveElements._unmountAll); // this unregisters the GlobalKeys
...@@ -3140,7 +3139,7 @@ class BuildOwner { ...@@ -3140,7 +3139,7 @@ class BuildOwner {
_reportException(ErrorSummary('while finalizing the widget tree'), e, stack); _reportException(ErrorSummary('while finalizing the widget tree'), e, stack);
} finally { } finally {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
} }
...@@ -3153,7 +3152,7 @@ class BuildOwner { ...@@ -3153,7 +3152,7 @@ class BuildOwner {
/// This is expensive and should not be called except during development. /// This is expensive and should not be called except during development.
void reassemble(Element root, DebugReassembleConfig? reassembleConfig) { void reassemble(Element root, DebugReassembleConfig? reassembleConfig) {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.startSync('Preparing Hot Reload (widgets)'); FlutterTimeline.startSync('Preparing Hot Reload (widgets)');
} }
try { try {
assert(root._parent == null); assert(root._parent == null);
...@@ -3162,7 +3161,7 @@ class BuildOwner { ...@@ -3162,7 +3161,7 @@ class BuildOwner {
root.reassemble(); root.reassemble();
} finally { } finally {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
} }
...@@ -3678,14 +3677,14 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3678,14 +3677,14 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
} }
return true; return true;
}()); }());
Timeline.startSync( FlutterTimeline.startSync(
'${newWidget.runtimeType}', '${newWidget.runtimeType}',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
} }
child.update(newWidget); child.update(newWidget);
if (isTimelineTracked) { if (isTimelineTracked) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
assert(child.widget == newWidget); assert(child.widget == newWidget);
assert(() { assert(() {
...@@ -4153,7 +4152,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -4153,7 +4152,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
} }
return true; return true;
}()); }());
Timeline.startSync( FlutterTimeline.startSync(
'${newWidget.runtimeType}', '${newWidget.runtimeType}',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
...@@ -4186,7 +4185,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -4186,7 +4185,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return newChild; return newChild;
} finally { } finally {
if (isTimelineTracked) { if (isTimelineTracked) {
Timeline.finishSync(); FlutterTimeline.finishSync();
} }
} }
} }
......
// 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'),
);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment