diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart index a531281c6d..882ae9266f 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart @@ -3,8 +3,8 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'material3.dart'; import 'recorder.dart'; /// Measures how expensive it is to construct the material 3 components screen. @@ -15,2330 +15,6 @@ class BenchMaterial3Components extends WidgetBuildRecorder { @override Widget createWidget() { - return const Material3Components(); - } -} - -const SizedBox rowDivider = SizedBox(width: 20); -const SizedBox colDivider = SizedBox(height: 10); -const double smallSpacing = 10.0; -const double cardWidth = 115; -const double widthConstraint = 450; - -class Material3Components extends StatefulWidget { - const Material3Components({super.key}); - - @override - State createState() => _Material3ComponentsState(); -} - -class _Material3ComponentsState extends State { - final GlobalKey scaffoldKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - key: scaffoldKey, - body: Row( - children: [ - 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 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: [ - const Actions(), - colDivider, - const Communication(), - colDivider, - const Containment(), - if (!showSecondList) ...[ - colDivider, - Navigation(scaffoldKey: scaffoldKey), - colDivider, - const Selection(), - colDivider, - const TextInputs() - ], - ], - ), - ); - } -} - -class SecondComponentList extends StatelessWidget { - const SecondComponentList({ - super.key, - required this.scaffoldKey, - }); - - final GlobalKey scaffoldKey; - - @override - Widget build(BuildContext context) { - // Fully traverse this list before moving on. - return FocusTraversalGroup( - child: ListView( - padding: const EdgeInsetsDirectional.only(end: smallSpacing), - children: [ - 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: [ - Buttons(), - FloatingActionButtons(), - IconToggleButtons(), - SegmentedButtons(), - ]); - } -} - -class Communication extends StatelessWidget { - const Communication({super.key}); - - @override - Widget build(BuildContext context) { - return const ComponentGroupDecoration(label: 'Communication', children: [ - 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: [ - BottomSheetSection(), - Cards(), - Dialogs(), - Dividers(), - ]); - } -} - -class Navigation extends StatelessWidget { - const Navigation({super.key, required this.scaffoldKey}); - - final GlobalKey scaffoldKey; - - @override - Widget build(BuildContext context) { - return ComponentGroupDecoration(label: 'Navigation', children: [ - 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: [ - 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: [TextFields()], - ); - } -} - -class Buttons extends StatefulWidget { - const Buttons({super.key}); - - @override - State createState() => _ButtonsState(); -} - -class _ButtonsState extends State { - @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: [ - 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: [ - 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: [ - 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: [ - 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: [ - SizedBox( - width: cardWidth, - child: Card( - child: Container( - padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), - child: Column( - children: [ - 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: [ - 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: [ - 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 createState() => _TextFieldsState(); -} - -class _TextFieldsState extends State { - 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: [ - 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: [ - 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: [ - 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 createState() => _DialogsState(); -} - -class _DialogsState extends State { - void openDialog(BuildContext context) { - showDialog( - 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: [ - 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( - 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: [ - 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: [ - 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: [ - 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: [ - SwitchRow(isEnabled: true), - SwitchRow(isEnabled: false), - ], - ), - ); - } -} - -class SwitchRow extends StatefulWidget { - const SwitchRow({super.key, required this.isEnabled}); - - final bool isEnabled; - - @override - State createState() => _SwitchRowState(); -} - -class _SwitchRowState extends State { - bool value0 = false; - bool value1 = true; - - final MaterialStateProperty thumbIcon = - MaterialStateProperty.resolveWith((Set 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: [ - 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 createState() => _CheckboxesState(); -} - -class _CheckboxesState extends State { - 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: [ - 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 createState() => _RadiosState(); -} - -enum Options { option1, option2, option3 } - -class _RadiosState extends State { - Options? _selectedOption = Options.option1; - - @override - Widget build(BuildContext context) { - return ComponentDecoration( - label: 'Radio buttons', - tooltipMessage: 'Use RadioListTile or Radio', - child: Column( - children: [ - RadioListTile( - title: const Text('Option 1'), - value: Options.option1, - groupValue: _selectedOption, - onChanged: (Options? value) { - setState(() { - _selectedOption = value; - }); - }, - ), - RadioListTile( - title: const Text('Option 2'), - value: Options.option2, - groupValue: _selectedOption, - onChanged: (Options? value) { - setState(() { - _selectedOption = value; - }); - }, - ), - RadioListTile( - title: const Text('Option 3'), - value: Options.option3, - groupValue: _selectedOption, - onChanged: null, - ), - ], - ), - ); - } -} - -class ProgressIndicators extends StatefulWidget { - const ProgressIndicators({super.key}); - - @override - State createState() => _ProgressIndicatorsState(); -} - -class _ProgressIndicatorsState extends State { - 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: [ - Row( - children: [ - IconButton( - isSelected: playProgressIndicator, - selectedIcon: const Icon(Icons.pause), - icon: const Icon(Icons.play_arrow), - onPressed: () { - setState(() { - playProgressIndicator = !playProgressIndicator; - }); - }, - ), - Expanded( - child: Row( - children: [ - rowDivider, - CircularProgressIndicator( - value: progressValue, - ), - rowDivider, - Expanded( - child: LinearProgressIndicator( - value: progressValue, - ), - ), - rowDivider, - ], - ), - ), - ], - ), - ], - ), - ); - } -} - -const List appBarDestinations = [ - 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 exampleBarDestinations = [ - 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 barWithBadgeDestinations = [ - 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 createState() => _NavigationBarsState(); -} - -class _NavigationBarsState extends State { - 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 createState() => _IconToggleButtonsState(); -} - -class _IconToggleButtonsState extends State { - @override - Widget build(BuildContext context) { - return const ComponentDecoration( - label: 'Icon buttons', - tooltipMessage: 'Use IconButton', - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Column( - // Standard IconButton - children: [ - IconToggleButton( - isEnabled: true, - tooltip: 'Standard', - ), - colDivider, - IconToggleButton( - isEnabled: false, - tooltip: 'Standard (disabled)', - ), - ], - ), - Column( - children: [ - // Filled IconButton - IconToggleButton( - isEnabled: true, - tooltip: 'Filled', - getDefaultStyle: enabledFilledButtonStyle, - ), - colDivider, - IconToggleButton( - isEnabled: false, - tooltip: 'Filled (disabled)', - getDefaultStyle: disabledFilledButtonStyle, - ), - ], - ), - Column( - children: [ - // Filled Tonal IconButton - IconToggleButton( - isEnabled: true, - tooltip: 'Filled tonal', - getDefaultStyle: enabledFilledTonalButtonStyle, - ), - colDivider, - IconToggleButton( - isEnabled: false, - tooltip: 'Filled tonal (disabled)', - getDefaultStyle: disabledFilledTonalButtonStyle, - ), - ], - ), - Column( - children: [ - // 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 createState() => _IconToggleButtonState(); -} - -class _IconToggleButtonState extends State { - 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 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 createState() => _ChipsState(); -} - -class _ChipsState extends State { - 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: [ - Wrap( - spacing: smallSpacing, - runSpacing: smallSpacing, - children: [ - 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: [ - 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', - child: Column( - children: [ - SingleChoice(), - colDivider, - MultipleChoice(), - ], - ), - ); - } -} - -enum Calendar { day, week, month, year } - -class SingleChoice extends StatefulWidget { - const SingleChoice({super.key}); - - @override - State createState() => _SingleChoiceState(); -} - -class _SingleChoiceState extends State { - Calendar calendarView = Calendar.day; - - @override - Widget build(BuildContext context) { - return SegmentedButton( - segments: const >[ - ButtonSegment( - value: Calendar.day, - label: Text('Day'), - icon: Icon(Icons.calendar_view_day)), - ButtonSegment( - value: Calendar.week, - label: Text('Week'), - icon: Icon(Icons.calendar_view_week)), - ButtonSegment( - value: Calendar.month, - label: Text('Month'), - icon: Icon(Icons.calendar_view_month)), - ButtonSegment( - value: Calendar.year, - label: Text('Year'), - icon: Icon(Icons.calendar_today)), - ], - selected: {calendarView}, - onSelectionChanged: (Set 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 createState() => _MultipleChoiceState(); -} - -class _MultipleChoiceState extends State { - Set selection = {Sizes.large, Sizes.extraLarge}; - - @override - Widget build(BuildContext context) { - return SegmentedButton( - segments: const >[ - ButtonSegment(value: Sizes.extraSmall, label: Text('XS')), - ButtonSegment(value: Sizes.small, label: Text('S')), - ButtonSegment(value: Sizes.medium, label: Text('M')), - ButtonSegment( - value: Sizes.large, - label: Text('L'), - ), - ButtonSegment(value: Sizes.extraLarge, label: Text('XL')), - ], - selected: selection, - onSelectionChanged: (Set 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 createState() => _BottomSheetSectionState(); -} - -class _BottomSheetSectionState extends State { - bool isNonModalBottomSheetOpen = false; - PersistentBottomSheetController? _nonModalBottomSheetController; - - @override - Widget build(BuildContext context) { - List buttonList = [ - 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 labelList = [ - Text('Share'), - Text('Add to'), - Text('Trash'), - Text('Archive'), - Text('Settings'), - Text('Favorite') - ]; - - buttonList = List.generate( - buttonList.length, - (int index) => Padding( - padding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0), - child: Column( - children: [ - buttonList[index], - labelList[index], - ], - ), - )); - - return ComponentDecoration( - label: 'Bottom sheet', - tooltipMessage: 'Use showModalBottomSheet or showBottomSheet', - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - children: [ - TextButton( - child: const Text( - 'Show modal bottom sheet', - style: TextStyle(fontWeight: FontWeight.bold), - ), - onPressed: () { - showModalBottomSheet( - 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( - 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: [ - SizedBox( - height: 80, - child: Scaffold( - floatingActionButton: FloatingActionButton( - onPressed: () {}, - elevation: 0.0, - child: const Icon(Icons.add), - ), - floatingActionButtonLocation: - FloatingActionButtonLocation.endContained, - bottomNavigationBar: BottomAppBar( - child: Row( - children: [ - 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: [ - MenuItemButton( - child: const Text('Menu 1'), - onPressed: () {}, - ), - MenuItemButton( - child: const Text('Menu 2'), - onPressed: () {}, - ), - SubmenuButton( - menuChildren: [ - 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: [ - 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 scaffoldKey; - - @override - Widget build(BuildContext context) { - return ComponentDecoration( - label: 'Navigation drawer', - tooltipMessage: - 'Use NavigationDrawer. For modal navigation drawers, see Scaffold.endDrawer', - child: Column( - children: [ - 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 createState() => - _NavigationDrawerSectionState(); -} - -class _NavigationDrawerSectionState extends State { - int navDrawerIndex = 0; - - @override - Widget build(BuildContext context) { - return NavigationDrawer( - onDestinationSelected: (int selectedIndex) { - setState(() { - navDrawerIndex = selectedIndex; - }); - }, - selectedIndex: navDrawerIndex, - children: [ - 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 destinations = [ - 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 labelDestinations = [ - 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 createState() => _NavigationRailSectionState(); -} - -class _NavigationRailSectionState extends State { - 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: [ - ...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 createState() => _TabsState(); -} - -class _TabsState extends State 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 [ - 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 actions = [ - 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: [ - AppBar( - title: const Text('Center-aligned'), - leading: const BackButton(), - actions: [ - 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: [ - SliverAppBar.medium( - title: const Text('Medium'), - leading: const BackButton(), - actions: actions, - ), - const SliverFillRemaining(), - ], - ), - ), - colDivider, - SizedBox( - height: 130, - child: CustomScrollView( - slivers: [ - SliverAppBar.large( - title: const Text('Large'), - leading: const BackButton(), - actions: actions, - ), - const SliverFillRemaining(), - ], - ), - ), - ], - ), - ); - } -} - -class Menus extends StatefulWidget { - const Menus({super.key}); - - @override - State createState() => _MenusState(); -} - -class _MenusState extends State { - final TextEditingController colorController = TextEditingController(); - final TextEditingController iconController = TextEditingController(); - IconLabel? selectedIcon = IconLabel.smile; - ColorLabel? selectedColor; - - @override - Widget build(BuildContext context) { - final List> colorEntries = - >[]; - for (final ColorLabel color in ColorLabel.values) { - colorEntries.add(DropdownMenuEntry( - value: color, label: color.label, enabled: color.label != 'Grey')); - } - - final List> iconEntries = - >[]; - for (final IconLabel icon in IconLabel.values) { - iconEntries - .add(DropdownMenuEntry(value: icon, label: icon.label)); - } - - return ComponentDecoration( - label: 'Menus', - tooltipMessage: 'Use MenuAnchor or DropdownMenu', - child: Column( - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ButtonAnchorExample(), - rowDivider, - IconButtonAnchorExample(), - ], - ), - colDivider, - Wrap( - alignment: WrapAlignment.spaceAround, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: smallSpacing, - runSpacing: smallSpacing, - children: [ - DropdownMenu( - controller: colorController, - label: const Text('Color'), - enableFilter: true, - dropdownMenuEntries: colorEntries, - inputDecorationTheme: const InputDecorationTheme(filled: true), - onSelected: (ColorLabel? color) { - setState(() { - selectedColor = color; - }); - }, - ), - DropdownMenu( - 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 createState() => _SlidersState(); -} - -class _SlidersState extends State { - 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: [ - 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 createState() => _ComponentDecorationState(); -} - -class _ComponentDecorationState extends State { - final FocusNode focusNode = FocusNode(); - - @override - Widget build(BuildContext context) { - return RepaintBoundary( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: smallSpacing), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - 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 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: [ - Text(label, style: Theme.of(context).textTheme.titleLarge), - colDivider, - ...children - ], - ), - ), - ), - ), - ); + return const TwoColumnMaterial3Components(); } } diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3_semantics.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3_semantics.dart new file mode 100644 index 0000000000..8b230803c8 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3_semantics.dart @@ -0,0 +1,147 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/semantics.dart'; + +import 'material3.dart'; +import 'recorder.dart'; + +/// Measures the cost of semantics when constructing screens containing +/// Material 3 widgets. +class BenchMaterial3Semantics extends WidgetBuildRecorder { + BenchMaterial3Semantics() : super(name: benchmarkName); + + static const String benchmarkName = 'bench_material3_semantics'; + + @override + Future setUpAll() async { + FlutterTimeline.debugCollectionEnabled = true; + super.setUpAll(); + SemanticsBinding.instance.ensureSemantics(); + } + + @override + Future 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 setUpAll() async { + FlutterTimeline.debugCollectionEnabled = true; + super.setUpAll(); + SemanticsBinding.instance.ensureSemantics(); + } + + @override + Future tearDownAll() async { + FlutterTimeline.debugReset(); + } + + @override + void frameDidDraw() { + final AggregatedTimings timings = FlutterTimeline.debugCollect(); + final AggregatedTimedBlock semanticsBlock = timings.getAggregated('SEMANTICS'); + final AggregatedTimedBlock getFragmentBlock = timings.getAggregated('Semantics.GetFragment'); + final AggregatedTimedBlock compileChildrenBlock = timings.getAggregated('Semantics.compileChildren'); + profile!.addTimedBlock(semanticsBlock, reported: true); + profile!.addTimedBlock(getFragmentBlock, reported: true); + profile!.addTimedBlock(compileChildrenBlock, reported: true); + + super.frameDidDraw(); + FlutterTimeline.debugReset(); + } + + @override + Widget createWidget() => _ScrollTest(); +} + +class _ScrollTest extends StatefulWidget { + @override + State<_ScrollTest> createState() => _ScrollTestState(); +} + +class _ScrollTestState extends State<_ScrollTest> with SingleTickerProviderStateMixin { + late final Ticker ticker; + late final ScrollController scrollController; + + @override + void initState() { + super.initState(); + + scrollController = ScrollController(); + + bool forward = true; + + // A one-off timer is necessary to allow the framework to measure the + // available scroll extents before the scroll controller can be exercised + // to change the scroll position. + Timer.run(() { + ticker = createTicker((_) { + scrollController.jumpTo(forward ? 1 : 0); + forward = !forward; + }); + ticker.start(); + }); + } + + @override + void dispose() { + ticker.dispose(); + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleColumnMaterial3Components( + scrollController: scrollController, + ); + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart index e1156dd383..6788ec8b71 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart @@ -203,6 +203,7 @@ class BenchBuildColorsGrid extends WidgetBuildRecorder { @override Future setUpAll() async { + super.setUpAll(); registerEngineBenchmarkValueListener('text_layout', (num value) { _textLayoutMicros += value; }); diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/material3.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/material3.dart new file mode 100644 index 0000000000..1ec8ae15bf --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/material3.dart @@ -0,0 +1,2369 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +const SizedBox rowDivider = SizedBox(width: 20); +const SizedBox colDivider = SizedBox(height: 10); +const double smallSpacing = 10.0; +const double cardWidth = 115; +const double widthConstraint = 450; +final GlobalKey scaffoldKey = GlobalKey(); + +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: [ + 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 createState() => _TwoColumnMaterial3ComponentsState(); +} + +class _TwoColumnMaterial3ComponentsState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + key: scaffoldKey, + body: Row( + children: [ + 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 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: [ + const Actions(), + colDivider, + const Communication(), + colDivider, + const Containment(), + if (!showSecondList) ...[ + colDivider, + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs() + ], + ], + ), + ); + } +} + +class SecondComponentList extends StatelessWidget { + const SecondComponentList({ + super.key, + required this.scaffoldKey, + }); + + final GlobalKey scaffoldKey; + + @override + Widget build(BuildContext context) { + // Fully traverse this list before moving on. + return FocusTraversalGroup( + child: ListView( + padding: const EdgeInsetsDirectional.only(end: smallSpacing), + children: [ + 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: [ + Buttons(), + FloatingActionButtons(), + IconToggleButtons(), + SegmentedButtons(), + ]); + } +} + +class Communication extends StatelessWidget { + const Communication({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Communication', children: [ + 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: [ + BottomSheetSection(), + Cards(), + Dialogs(), + Dividers(), + ]); + } +} + +class Navigation extends StatelessWidget { + const Navigation({super.key, required this.scaffoldKey}); + + final GlobalKey scaffoldKey; + + @override + Widget build(BuildContext context) { + return ComponentGroupDecoration(label: 'Navigation', children: [ + 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: [ + 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: [TextFields()], + ); + } +} + +class Buttons extends StatefulWidget { + const Buttons({super.key}); + + @override + State createState() => _ButtonsState(); +} + +class _ButtonsState extends State { + @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: [ + 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: [ + 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: [ + 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: [ + 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: [ + SizedBox( + width: cardWidth, + child: Card( + child: Container( + padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), + child: Column( + children: [ + 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: [ + 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: [ + 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 createState() => _TextFieldsState(); +} + +class _TextFieldsState extends State { + 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: [ + 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: [ + 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: [ + 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 createState() => _DialogsState(); +} + +class _DialogsState extends State { + void openDialog(BuildContext context) { + showDialog( + 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: [ + 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( + 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: [ + 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: [ + 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: [ + 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: [ + SwitchRow(isEnabled: true), + SwitchRow(isEnabled: false), + ], + ), + ); + } +} + +class SwitchRow extends StatefulWidget { + const SwitchRow({super.key, required this.isEnabled}); + + final bool isEnabled; + + @override + State createState() => _SwitchRowState(); +} + +class _SwitchRowState extends State { + bool value0 = false; + bool value1 = true; + + final MaterialStateProperty thumbIcon = + MaterialStateProperty.resolveWith((Set 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: [ + 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 createState() => _CheckboxesState(); +} + +class _CheckboxesState extends State { + 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: [ + 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 createState() => _RadiosState(); +} + +enum Options { option1, option2, option3 } + +class _RadiosState extends State { + Options? _selectedOption = Options.option1; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Radio buttons', + tooltipMessage: 'Use RadioListTile or Radio', + child: Column( + children: [ + RadioListTile( + title: const Text('Option 1'), + value: Options.option1, + groupValue: _selectedOption, + onChanged: (Options? value) { + setState(() { + _selectedOption = value; + }); + }, + ), + RadioListTile( + title: const Text('Option 2'), + value: Options.option2, + groupValue: _selectedOption, + onChanged: (Options? value) { + setState(() { + _selectedOption = value; + }); + }, + ), + RadioListTile( + title: const Text('Option 3'), + value: Options.option3, + groupValue: _selectedOption, + onChanged: null, + ), + ], + ), + ); + } +} + +class ProgressIndicators extends StatefulWidget { + const ProgressIndicators({super.key}); + + @override + State createState() => _ProgressIndicatorsState(); +} + +class _ProgressIndicatorsState extends State { + 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: [ + Row( + children: [ + IconButton( + isSelected: playProgressIndicator, + selectedIcon: const Icon(Icons.pause), + icon: const Icon(Icons.play_arrow), + onPressed: () { + setState(() { + playProgressIndicator = !playProgressIndicator; + }); + }, + ), + Expanded( + child: Row( + children: [ + rowDivider, + CircularProgressIndicator( + value: progressValue, + ), + rowDivider, + Expanded( + child: LinearProgressIndicator( + value: progressValue, + ), + ), + rowDivider, + ], + ), + ), + ], + ), + ], + ), + ); + } +} + +const List appBarDestinations = [ + 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 exampleBarDestinations = [ + 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 barWithBadgeDestinations = [ + 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 createState() => _NavigationBarsState(); +} + +class _NavigationBarsState extends State { + 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 createState() => _IconToggleButtonsState(); +} + +class _IconToggleButtonsState extends State { + @override + Widget build(BuildContext context) { + return const ComponentDecoration( + label: 'Icon buttons', + tooltipMessage: 'Use IconButton', + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + // Standard IconButton + children: [ + IconToggleButton( + isEnabled: true, + tooltip: 'Standard', + ), + colDivider, + IconToggleButton( + isEnabled: false, + tooltip: 'Standard (disabled)', + ), + ], + ), + Column( + children: [ + // Filled IconButton + IconToggleButton( + isEnabled: true, + tooltip: 'Filled', + getDefaultStyle: enabledFilledButtonStyle, + ), + colDivider, + IconToggleButton( + isEnabled: false, + tooltip: 'Filled (disabled)', + getDefaultStyle: disabledFilledButtonStyle, + ), + ], + ), + Column( + children: [ + // Filled Tonal IconButton + IconToggleButton( + isEnabled: true, + tooltip: 'Filled tonal', + getDefaultStyle: enabledFilledTonalButtonStyle, + ), + colDivider, + IconToggleButton( + isEnabled: false, + tooltip: 'Filled tonal (disabled)', + getDefaultStyle: disabledFilledTonalButtonStyle, + ), + ], + ), + Column( + children: [ + // 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 createState() => _IconToggleButtonState(); +} + +class _IconToggleButtonState extends State { + 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 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 createState() => _ChipsState(); +} + +class _ChipsState extends State { + 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: [ + Wrap( + spacing: smallSpacing, + runSpacing: smallSpacing, + children: [ + 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: [ + 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', + child: Column( + children: [ + SingleChoice(), + colDivider, + MultipleChoice(), + ], + ), + ); + } +} + +enum Calendar { day, week, month, year } + +class SingleChoice extends StatefulWidget { + const SingleChoice({super.key}); + + @override + State createState() => _SingleChoiceState(); +} + +class _SingleChoiceState extends State { + Calendar calendarView = Calendar.day; + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: const >[ + ButtonSegment( + value: Calendar.day, + label: Text('Day'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Calendar.week, + label: Text('Week'), + icon: Icon(Icons.calendar_view_week)), + ButtonSegment( + value: Calendar.month, + label: Text('Month'), + icon: Icon(Icons.calendar_view_month)), + ButtonSegment( + value: Calendar.year, + label: Text('Year'), + icon: Icon(Icons.calendar_today)), + ], + selected: {calendarView}, + onSelectionChanged: (Set 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 createState() => _MultipleChoiceState(); +} + +class _MultipleChoiceState extends State { + Set selection = {Sizes.large, Sizes.extraLarge}; + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: const >[ + ButtonSegment(value: Sizes.extraSmall, label: Text('XS')), + ButtonSegment(value: Sizes.small, label: Text('S')), + ButtonSegment(value: Sizes.medium, label: Text('M')), + ButtonSegment( + value: Sizes.large, + label: Text('L'), + ), + ButtonSegment(value: Sizes.extraLarge, label: Text('XL')), + ], + selected: selection, + onSelectionChanged: (Set 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 createState() => _BottomSheetSectionState(); +} + +class _BottomSheetSectionState extends State { + bool isNonModalBottomSheetOpen = false; + PersistentBottomSheetController? _nonModalBottomSheetController; + + @override + Widget build(BuildContext context) { + List buttonList = [ + 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 labelList = [ + Text('Share'), + Text('Add to'), + Text('Trash'), + Text('Archive'), + Text('Settings'), + Text('Favorite') + ]; + + buttonList = List.generate( + buttonList.length, + (int index) => Padding( + padding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0), + child: Column( + children: [ + buttonList[index], + labelList[index], + ], + ), + )); + + return ComponentDecoration( + label: 'Bottom sheet', + tooltipMessage: 'Use showModalBottomSheet or showBottomSheet', + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + child: const Text( + 'Show modal bottom sheet', + style: TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () { + showModalBottomSheet( + 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( + 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: [ + SizedBox( + height: 80, + child: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () {}, + elevation: 0.0, + child: const Icon(Icons.add), + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.endContained, + bottomNavigationBar: BottomAppBar( + child: Row( + children: [ + 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: [ + MenuItemButton( + child: const Text('Menu 1'), + onPressed: () {}, + ), + MenuItemButton( + child: const Text('Menu 2'), + onPressed: () {}, + ), + SubmenuButton( + menuChildren: [ + 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: [ + 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 scaffoldKey; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Navigation drawer', + tooltipMessage: + 'Use NavigationDrawer. For modal navigation drawers, see Scaffold.endDrawer', + child: Column( + children: [ + 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 createState() => + _NavigationDrawerSectionState(); +} + +class _NavigationDrawerSectionState extends State { + int navDrawerIndex = 0; + + @override + Widget build(BuildContext context) { + return NavigationDrawer( + onDestinationSelected: (int selectedIndex) { + setState(() { + navDrawerIndex = selectedIndex; + }); + }, + selectedIndex: navDrawerIndex, + children: [ + 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 destinations = [ + 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 labelDestinations = [ + 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 createState() => _NavigationRailSectionState(); +} + +class _NavigationRailSectionState extends State { + 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: [ + ...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 createState() => _TabsState(); +} + +class _TabsState extends State 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 [ + 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 actions = [ + 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: [ + AppBar( + title: const Text('Center-aligned'), + leading: const BackButton(), + actions: [ + 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: [ + SliverAppBar.medium( + title: const Text('Medium'), + leading: const BackButton(), + actions: actions, + ), + const SliverFillRemaining(), + ], + ), + ), + colDivider, + SizedBox( + height: 130, + child: CustomScrollView( + slivers: [ + SliverAppBar.large( + title: const Text('Large'), + leading: const BackButton(), + actions: actions, + ), + const SliverFillRemaining(), + ], + ), + ), + ], + ), + ); + } +} + +class Menus extends StatefulWidget { + const Menus({super.key}); + + @override + State createState() => _MenusState(); +} + +class _MenusState extends State { + final TextEditingController colorController = TextEditingController(); + final TextEditingController iconController = TextEditingController(); + IconLabel? selectedIcon = IconLabel.smile; + ColorLabel? selectedColor; + + @override + Widget build(BuildContext context) { + final List> colorEntries = + >[]; + for (final ColorLabel color in ColorLabel.values) { + colorEntries.add(DropdownMenuEntry( + value: color, label: color.label, enabled: color.label != 'Grey')); + } + + final List> iconEntries = + >[]; + for (final IconLabel icon in IconLabel.values) { + iconEntries + .add(DropdownMenuEntry(value: icon, label: icon.label)); + } + + return ComponentDecoration( + label: 'Menus', + tooltipMessage: 'Use MenuAnchor or DropdownMenu', + child: Column( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ButtonAnchorExample(), + rowDivider, + IconButtonAnchorExample(), + ], + ), + colDivider, + Wrap( + alignment: WrapAlignment.spaceAround, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: smallSpacing, + runSpacing: smallSpacing, + children: [ + DropdownMenu( + controller: colorController, + label: const Text('Color'), + enableFilter: true, + dropdownMenuEntries: colorEntries, + inputDecorationTheme: const InputDecorationTheme(filled: true), + onSelected: (ColorLabel? color) { + setState(() { + selectedColor = color; + }); + }, + ), + DropdownMenu( + 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 createState() => _SlidersState(); +} + +class _SlidersState extends State { + 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: [ + 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 createState() => _ComponentDecorationState(); +} + +class _ComponentDecorationState extends State { + final FocusNode focusNode = FocusNode(); + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: smallSpacing), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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 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: [ + Text(label, style: Theme.of(context).textTheme.titleLarge), + colDivider, + ...children + ], + ), + ), + ), + ), + ); + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart index d2c7831673..ae583dc8ba 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart @@ -426,12 +426,18 @@ abstract class WidgetRecorder extends Recorder implements FrameRecorder { _runCompleter!.completeError(error, stackTrace); } + late final _RecordingWidgetsBinding _binding; + + @override + @mustCallSuper + Future setUpAll() async { + _binding = _RecordingWidgetsBinding.ensureInitialized(); + } + @override Future run() async { _runCompleter = Completer(); final Profile localProfile = profile = Profile(name: name, useCustomWarmUp: useCustomWarmUp); - final _RecordingWidgetsBinding binding = - _RecordingWidgetsBinding.ensureInitialized(); final Widget widget = createWidget(); registerEngineBenchmarkValueListener(kProfilePrerollFrame, (num value) { @@ -449,7 +455,7 @@ abstract class WidgetRecorder extends Recorder implements FrameRecorder { ); }); - binding._beginRecording(this, widget); + _binding._beginRecording(this, widget); try { await _runCompleter!.future; @@ -508,6 +514,14 @@ abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder { } } + late final _RecordingWidgetsBinding _binding; + + @override + @mustCallSuper + Future setUpAll() async { + _binding = _RecordingWidgetsBinding.ensureInitialized(); + } + @override @mustCallSuper void frameWillDraw() { @@ -546,9 +560,7 @@ abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder { Future run() async { _runCompleter = Completer(); final Profile localProfile = profile = Profile(name: name); - final _RecordingWidgetsBinding binding = - _RecordingWidgetsBinding.ensureInitialized(); - binding._beginRecording(this, _WidgetBuildRecorderHost(this)); + _binding._beginRecording(this, _WidgetBuildRecorderHost(this)); try { await _runCompleter!.future; @@ -948,6 +960,15 @@ 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. /// /// If enough warm-up samples have been collected, stops the warm-up phase and diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart index 7820261513..544f5bb1cf 100644 --- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart +++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart @@ -19,6 +19,7 @@ import 'src/web/bench_draw_rect.dart'; import 'src/web/bench_dynamic_clip_on_static_picture.dart'; import 'src/web/bench_image_decoding.dart'; import 'src/web/bench_material_3.dart'; +import 'src/web/bench_material_3_semantics.dart'; import 'src/web/bench_mouse_region_grid_hover.dart'; import 'src/web/bench_mouse_region_grid_scroll.dart'; import 'src/web/bench_mouse_region_mixed_grid_hover.dart'; @@ -64,6 +65,8 @@ final Map benchmarks = { BenchPlatformViewInfiniteScroll.benchmarkName: () => BenchPlatformViewInfiniteScroll.forward(), BenchPlatformViewInfiniteScroll.benchmarkNameBackward: () => BenchPlatformViewInfiniteScroll.backward(), BenchMaterial3Components.benchmarkName: () => BenchMaterial3Components(), + BenchMaterial3Semantics.benchmarkName: () => BenchMaterial3Semantics(), + BenchMaterial3ScrollSemantics.benchmarkName: () => BenchMaterial3ScrollSemantics(), // CanvasKit-only benchmarks if (isCanvasKit) ...{ diff --git a/dev/benchmarks/microbenchmarks/lib/foundation/timeline_bench.dart b/dev/benchmarks/microbenchmarks/lib/foundation/timeline_bench.dart index 339a46ce15..7c9eb4a396 100644 --- a/dev/benchmarks/microbenchmarks/lib/foundation/timeline_bench.dart +++ b/dev/benchmarks/microbenchmarks/lib/foundation/timeline_bench.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:developer'; +import 'package:flutter/foundation.dart'; import '../common.dart'; @@ -16,8 +16,8 @@ void main() { final Stopwatch watch = Stopwatch(); watch.start(); for (int i = 0; i < _kNumIterations; i += 1) { - Timeline.startSync('foo'); - Timeline.finishSync(); + FlutterTimeline.startSync('foo'); + FlutterTimeline.finishSync(); } watch.stop(); @@ -31,14 +31,14 @@ void main() { watch.reset(); watch.start(); for (int i = 0; i < _kNumIterations; i += 1) { - Timeline.startSync('foo', arguments: { + FlutterTimeline.startSync('foo', arguments: { 'int': 1234, 'double': 0.3, 'list': [1, 2, 3, 4], 'map': {'map': true}, 'bool': false, }); - Timeline.finishSync(); + FlutterTimeline.finishSync(); } watch.stop(); diff --git a/dev/bots/test.dart b/dev/bots/test.dart index d128e1d8c9..6a911772c2 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -1375,6 +1375,14 @@ Future _runWebTreeshakeTest() async { pos = javaScript.indexOf(word, pos); } + // The following are classes from `timeline.dart` that should be treeshaken + // off unless the app (typically a benchmark) uses methods that need them. + expect(javaScript.contains('AggregatedTimedBlock'), false); + expect(javaScript.contains('AggregatedTimings'), false); + expect(javaScript.contains('_BlockBuffer'), false); + expect(javaScript.contains('_StringListChain'), false); + expect(javaScript.contains('_Float64ListChain'), false); + const int kMaxExpectedDebugFillProperties = 11; if (count > kMaxExpectedDebugFillProperties) { throw Exception( diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index dff7553436..43ac813387 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -46,4 +46,5 @@ export 'src/foundation/serialization.dart'; export 'src/foundation/service_extensions.dart'; export 'src/foundation/stack_frame.dart'; export 'src/foundation/synchronous_future.dart'; +export 'src/foundation/timeline.dart'; export 'src/foundation/unicode.dart'; diff --git a/packages/flutter/lib/src/foundation/_timeline_io.dart b/packages/flutter/lib/src/foundation/_timeline_io.dart new file mode 100644 index 0000000000..8c4886b382 --- /dev/null +++ b/packages/flutter/lib/src/foundation/_timeline_io.dart @@ -0,0 +1,11 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:developer'; + +/// Returns the current timestamp in microseconds from a monotonically +/// increasing clock. +/// +/// This is the Dart VM implementation. +double get performanceTimestamp => Timeline.now.toDouble(); diff --git a/packages/flutter/lib/src/foundation/_timeline_web.dart b/packages/flutter/lib/src/foundation/_timeline_web.dart new file mode 100644 index 0000000000..133f096cbe --- /dev/null +++ b/packages/flutter/lib/src/foundation/_timeline_web.dart @@ -0,0 +1,27 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:js_interop'; + +/// Returns the current timestamp in microseconds from a monotonically +/// increasing clock. +/// +/// This is the web implementation, which uses `window.performance.now` as the +/// source of the timestamp. +/// +/// See: +/// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now +double get performanceTimestamp => 1000 * _performance.now(); + +@JS() +@staticInterop +class _DomPerformance {} + +@JS('performance') +external _DomPerformance get _performance; + +extension _DomPerformanceExtension on _DomPerformance { + @JS() + external double now(); +} diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart index 8532f6d64c..bc451ac718 100644 --- a/packages/flutter/lib/src/foundation/binding.dart +++ b/packages/flutter/lib/src/foundation/binding.dart @@ -20,6 +20,7 @@ import 'object.dart'; import 'platform.dart'; import 'print.dart'; import 'service_extensions.dart'; +import 'timeline.dart'; export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow; // ignore: deprecated_member_use @@ -141,7 +142,9 @@ abstract class BindingBase { /// [initServiceExtensions] to have bindings initialize their /// VM service extensions, if any. BindingBase() { - developer.Timeline.startSync('Framework initialization'); + if (!kReleaseMode) { + FlutterTimeline.startSync('Framework initialization'); + } assert(() { _debugConstructed = true; return true; @@ -157,7 +160,9 @@ abstract class BindingBase { developer.postEvent('Flutter.FrameworkInitialization', {}); - developer.Timeline.finishSync(); + if (!kReleaseMode) { + FlutterTimeline.finishSync(); + } } bool _debugConstructed = false; diff --git a/packages/flutter/lib/src/foundation/timeline.dart b/packages/flutter/lib/src/foundation/timeline.dart new file mode 100644 index 0000000000..b110ea0cb0 --- /dev/null +++ b/packages/flutter/lib/src/foundation/timeline.dart @@ -0,0 +1,432 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:developer'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import '_timeline_io.dart' + if (dart.library.js_util) '_timeline_web.dart' as impl; +import 'constants.dart'; + +/// Measures how long blocks of code take to run. +/// +/// This class can be used as a drop-in replacement for [Timeline] as it +/// provides methods compatible with [Timeline] signature-wise, and it has +/// minimal overhead. +/// +/// Provides [debugReset] and [debugCollect] methods that make it convenient to use in +/// frame-oriented environment where collected metrics can be attributed to a +/// frame, then aggregated into frame statistics, e.g. frame averages. +/// +/// Forwards measurements to [Timeline] so they appear in Flutter DevTools. +abstract final class FlutterTimeline { + static _BlockBuffer _buffer = _BlockBuffer(); + + /// Whether block timings are collected and can be retrieved using the + /// [debugCollect] method. + /// + /// This is always false in release mode. + static bool get debugCollectionEnabled => _collectionEnabled; + + /// Enables metric collection. + /// + /// Metric collection can only be enabled in non-release modes. It is most + /// useful in profile mode where application performance is representative + /// of a deployed application. + /// + /// When disabled, resets collected data by calling [debugReset]. + /// + /// Throws a [StateError] if invoked in release mode. + static set debugCollectionEnabled(bool value) { + if (kReleaseMode) { + throw _createReleaseModeNotSupportedError(); + } + if (value == _collectionEnabled) { + return; + } + _collectionEnabled = value; + debugReset(); + } + + static StateError _createReleaseModeNotSupportedError() { + return StateError('FlutterTimeline metric collection not supported in release mode.'); + } + + static bool _collectionEnabled = false; + + /// Start a synchronous operation labeled `name`. + /// + /// Optionally takes a map of `arguments`. This slice may also optionally be + /// associated with a [Flow] event. This operation must be finished by calling + /// [finishSync] before returning to the event queue. + /// + /// This is a drop-in replacement for [Timeline.startSync]. + static void startSync(String name, { Map? 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? 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(String name, TimelineSyncFunction function, + { Map? 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 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 aggregatedBlocks = _computeAggregatedBlocks(); + + List _computeAggregatedBlocks() { + final Map aggregate = {}; + 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( + (MapEntry 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 _chain = []; + 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 extractElements() { + final List result = []; + _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> _chain = >[]; + List _slice = List.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.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 extractElements() { + final List result = []; + for (final List 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 _nameStack = List.filled(_stackDepth, null); + static int _stackPointer = 0; + + final _Float64ListChain _starts = _Float64ListChain(); + final _Float64ListChain _finishes = _Float64ListChain(); + final _StringListChain _names = _StringListChain(); + + List 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.generate(_stackPointer, (int i) => _nameStack[i]!).join(', ')}' + ); + + final List result = []; + final int length = _finishes.length; + final List starts = _starts.extractElements(); + final List finishes = _finishes.extractElements(); + final List 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? arguments, Flow? flow }) { + _startStack[_stackPointer] = impl.performanceTimestamp; + _nameStack[_stackPointer] = name; + _stackPointer += 1; + } + + void finishSync() { + assert( + _stackPointer > 0, + 'Invalid sequence of `startSync` and `finishSync`.\n' + 'Attempted to finish timing a block of code, but there are no pending ' + '`startSync` calls.' + ); + + final double finishTime = impl.performanceTimestamp; + final double startTime = _startStack[_stackPointer - 1]; + final String name = _nameStack[_stackPointer - 1]!; + _stackPointer -= 1; + + _starts.add(startTime); + _finishes.add(finishTime); + _names.add(name); + } +} diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index dec407bd5f..a90e062c42 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:developer'; import 'dart:ui' as ui show SemanticsUpdate; import 'package:flutter/foundation.dart'; @@ -507,13 +506,13 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture await super.performReassemble(); if (BindingBase.debugReassembleConfig?.widgetName == null) { if (!kReleaseMode) { - Timeline.startSync('Preparing Hot Reload (layout)'); + FlutterTimeline.startSync('Preparing Hot Reload (layout)'); } try { renderView.reassemble(); } finally { if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } } diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index e89ad006c7..6a9a00791e 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:developer' show Timeline; import 'dart:math' as math; import 'dart:ui' as ui show lerpDouble; @@ -1396,7 +1395,7 @@ abstract class RenderBox extends RenderObject { }()); if (!kReleaseMode) { if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { - Timeline.startSync( + FlutterTimeline.startSync( '$runtimeType intrinsics', arguments: debugTimelineArguments, ); @@ -1411,7 +1410,7 @@ abstract class RenderBox extends RenderObject { if (!kReleaseMode) { _debugIntrinsicsDepth -= 1; if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } return result; @@ -1832,7 +1831,7 @@ abstract class RenderBox extends RenderObject { }()); if (!kReleaseMode) { if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { - Timeline.startSync( + FlutterTimeline.startSync( '$runtimeType.getDryLayout', arguments: debugTimelineArguments, ); @@ -1844,7 +1843,7 @@ abstract class RenderBox extends RenderObject { if (!kReleaseMode) { _debugIntrinsicsDepth -= 1; if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } return result; diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index b1984b48ae..3e42a22ca2 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:developer'; import 'dart:ui' as ui show PictureRecorder; import 'dart:ui'; @@ -986,7 +985,7 @@ class PipelineOwner { } return true; }()); - Timeline.startSync( + FlutterTimeline.startSync( 'LAYOUT', arguments: debugTimelineArguments, ); @@ -1035,7 +1034,7 @@ class PipelineOwner { return true; }()); if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } } @@ -1074,7 +1073,7 @@ class PipelineOwner { /// [flushPaint]. void flushCompositingBits() { if (!kReleaseMode) { - Timeline.startSync('UPDATING COMPOSITING BITS'); + FlutterTimeline.startSync('UPDATING COMPOSITING BITS'); } _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth); for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) { @@ -1088,7 +1087,7 @@ class PipelineOwner { } assert(_nodesNeedingCompositingBitsUpdate.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.'); if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } @@ -1122,7 +1121,7 @@ class PipelineOwner { } return true; }()); - Timeline.startSync( + FlutterTimeline.startSync( 'PAINT', arguments: debugTimelineArguments, ); @@ -1161,7 +1160,7 @@ class PipelineOwner { return true; }()); if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } } @@ -1250,7 +1249,7 @@ class PipelineOwner { return; } if (!kReleaseMode) { - Timeline.startSync('SEMANTICS'); + FlutterTimeline.startSync('SEMANTICS'); } assert(_semanticsOwner != null); assert(() { @@ -1277,7 +1276,7 @@ class PipelineOwner { return true; }()); if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } } @@ -2379,7 +2378,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge } return true; }()); - Timeline.startSync( + FlutterTimeline.startSync( '$runtimeType', arguments: debugTimelineArguments, ); @@ -2443,7 +2442,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge } if (!kReleaseMode && debugProfileLayoutsEnabled) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } return; } @@ -2510,7 +2509,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge markNeedsPaint(); if (!kReleaseMode && debugProfileLayoutsEnabled) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } @@ -3082,7 +3081,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge } return true; }()); - Timeline.startSync( + FlutterTimeline.startSync( '$runtimeType', arguments: debugTimelineArguments, ); @@ -3166,7 +3165,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge return true; }()); if (!kReleaseMode && debugProfilePaintsEnabled) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } @@ -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. return; } + if (!kReleaseMode) { + FlutterTimeline.startSync('Semantics.GetFragment'); + } final _SemanticsFragment fragment = _getSemanticsForParent( mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false, blockUserActions: _semantics?.areUserActionsBlocked ?? false, ); + if (!kReleaseMode) { + FlutterTimeline.finishSync(); + } assert(fragment is _InterestingSemanticsFragment); final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment; final List result = []; final List siblingNodes = []; + + if (!kReleaseMode) { + FlutterTimeline.startSync('Semantics.compileChildren'); + } interestingFragment.compileChildren( parentSemanticsClipRect: _semantics?.parentSemanticsClipRect, parentPaintClipRect: _semantics?.parentPaintClipRect, @@ -3543,6 +3552,9 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge result: result, siblingNodes: siblingNodes, ); + if (!kReleaseMode) { + FlutterTimeline.finishSync(); + } // Result may contain sibling nodes that are irrelevant for this update. assert(interestingFragment.config == null && result.any((SemanticsNode node) => node == _semantics)); } diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index ac397ad76b..906d237c1d 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:developer'; import 'dart:io' show Platform; import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate; @@ -229,7 +228,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin /// Actually causes the output of the rendering pipeline to appear on screen. void compositeFrame() { if (!kReleaseMode) { - Timeline.startSync('COMPOSITING'); + FlutterTimeline.startSync('COMPOSITING'); } try { final ui.SceneBuilder builder = ui.SceneBuilder(); @@ -247,7 +246,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin }()); } finally { if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index b2f3d96dbf..2c0e3ca01b 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -2700,7 +2699,7 @@ class BuildOwner { } return true; }()); - Timeline.startSync( + FlutterTimeline.startSync( 'BUILD', arguments: debugTimelineArguments ); @@ -2771,7 +2770,7 @@ class BuildOwner { } return true; }()); - Timeline.startSync( + FlutterTimeline.startSync( '${element.widget.runtimeType}', arguments: debugTimelineArguments, ); @@ -2794,7 +2793,7 @@ class BuildOwner { ); } if (isTimelineTracked) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } index += 1; if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) { @@ -2832,7 +2831,7 @@ class BuildOwner { _scheduledFlushDirtyElements = false; _dirtyElementsNeedsResorting = null; if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } assert(_debugBuilding); assert(() { @@ -3044,7 +3043,7 @@ class BuildOwner { @pragma('vm:notify-debugger-on-exception') void finalizeTree() { if (!kReleaseMode) { - Timeline.startSync('FINALIZE TREE'); + FlutterTimeline.startSync('FINALIZE TREE'); } try { lockState(_inactiveElements._unmountAll); // this unregisters the GlobalKeys @@ -3140,7 +3139,7 @@ class BuildOwner { _reportException(ErrorSummary('while finalizing the widget tree'), e, stack); } finally { if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } } @@ -3153,7 +3152,7 @@ class BuildOwner { /// This is expensive and should not be called except during development. void reassemble(Element root, DebugReassembleConfig? reassembleConfig) { if (!kReleaseMode) { - Timeline.startSync('Preparing Hot Reload (widgets)'); + FlutterTimeline.startSync('Preparing Hot Reload (widgets)'); } try { assert(root._parent == null); @@ -3162,7 +3161,7 @@ class BuildOwner { root.reassemble(); } finally { if (!kReleaseMode) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } } @@ -3678,14 +3677,14 @@ abstract class Element extends DiagnosticableTree implements BuildContext { } return true; }()); - Timeline.startSync( + FlutterTimeline.startSync( '${newWidget.runtimeType}', arguments: debugTimelineArguments, ); } child.update(newWidget); if (isTimelineTracked) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } assert(child.widget == newWidget); assert(() { @@ -4153,7 +4152,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { } return true; }()); - Timeline.startSync( + FlutterTimeline.startSync( '${newWidget.runtimeType}', arguments: debugTimelineArguments, ); @@ -4186,7 +4185,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { return newChild; } finally { if (isTimelineTracked) { - Timeline.finishSync(); + FlutterTimeline.finishSync(); } } } diff --git a/packages/flutter/test/foundation/timeline_test.dart b/packages/flutter/test/foundation/timeline_test.dart new file mode 100644 index 0000000000..80c95d1a8b --- /dev/null +++ b/packages/flutter/test/foundation/timeline_test.dart @@ -0,0 +1,143 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// IMPORTANT: keep this in sync with the same constant defined +// in foundation/timeline.dart +const int kSliceSize = 500; + +void main() { + setUp(() { + FlutterTimeline.debugReset(); + FlutterTimeline.debugCollectionEnabled = false; + }); + + test('Does not collect when collection not enabled', () { + FlutterTimeline.startSync('TEST'); + FlutterTimeline.finishSync(); + expect( + () => FlutterTimeline.debugCollect(), + throwsStateError, + ); + }); + + test('Collects when collection is enabled', () { + FlutterTimeline.debugCollectionEnabled = true; + FlutterTimeline.startSync('TEST'); + FlutterTimeline.finishSync(); + final AggregatedTimings data = FlutterTimeline.debugCollect(); + expect(data.timedBlocks, hasLength(1)); + expect(data.aggregatedBlocks, hasLength(1)); + + final AggregatedTimedBlock block = data.getAggregated('TEST'); + expect(block.name, 'TEST'); + expect(block.count, 1); + + // After collection the timeline is reset back to empty. + final AggregatedTimings data2 = FlutterTimeline.debugCollect(); + expect(data2.timedBlocks, isEmpty); + expect(data2.aggregatedBlocks, isEmpty); + }); + + test('Deletes old data when reset', () { + FlutterTimeline.debugCollectionEnabled = true; + FlutterTimeline.startSync('TEST'); + FlutterTimeline.finishSync(); + FlutterTimeline.debugReset(); + + final AggregatedTimings data = FlutterTimeline.debugCollect(); + expect(data.timedBlocks, isEmpty); + expect(data.aggregatedBlocks, isEmpty); + }); + + test('Reports zero aggregation when requested missing block', () { + FlutterTimeline.debugCollectionEnabled = true; + + final AggregatedTimings data = FlutterTimeline.debugCollect(); + final AggregatedTimedBlock block = data.getAggregated('MISSING'); + expect(block.name, 'MISSING'); + expect(block.count, 0); + expect(block.duration, 0); + }); + + test('Measures the runtime of a function', () { + FlutterTimeline.debugCollectionEnabled = true; + + // The off-by-one values for `start` and `end` are for web's sake where + // timer values are reported as float64 and toInt/toDouble conversions + // are noops, so there's no value truncation happening, which makes it + // a bit inconsistent with Stopwatch. + final int start = FlutterTimeline.now - 1; + FlutterTimeline.timeSync('TEST', () { + final Stopwatch watch = Stopwatch()..start(); + while (watch.elapsedMilliseconds < 5) {} + watch.stop(); + }); + final int end = FlutterTimeline.now + 1; + + final AggregatedTimings data = FlutterTimeline.debugCollect(); + expect(data.timedBlocks, hasLength(1)); + expect(data.aggregatedBlocks, hasLength(1)); + + final TimedBlock block = data.timedBlocks.single; + expect(block.name, 'TEST'); + expect(block.start, greaterThanOrEqualTo(start)); + expect(block.end, lessThanOrEqualTo(end)); + expect(block.duration, greaterThan(0)); + + final AggregatedTimedBlock aggregated = data.getAggregated('TEST'); + expect(aggregated.name, 'TEST'); + expect(aggregated.count, 1); + expect(aggregated.duration, block.duration); + }); + + test('FlutterTimeline.instanceSync does not collect anything', () { + FlutterTimeline.debugCollectionEnabled = true; + FlutterTimeline.instantSync('TEST'); + + final AggregatedTimings data = FlutterTimeline.debugCollect(); + expect(data.timedBlocks, isEmpty); + expect(data.aggregatedBlocks, isEmpty); + }); + + test('FlutterTimeline.now returns a value', () { + FlutterTimeline.debugCollectionEnabled = true; + expect(FlutterTimeline.now, isNotNull); + }); + + test('Can collect more than one slice of data', () { + FlutterTimeline.debugCollectionEnabled = true; + + for (int i = 0; i < 10 * kSliceSize; i++) { + FlutterTimeline.startSync('TEST'); + FlutterTimeline.finishSync(); + } + final AggregatedTimings data = FlutterTimeline.debugCollect(); + expect(data.timedBlocks, hasLength(10 * kSliceSize)); + expect(data.aggregatedBlocks, hasLength(1)); + + final AggregatedTimedBlock block = data.getAggregated('TEST'); + expect(block.name, 'TEST'); + expect(block.count, 10 * kSliceSize); + }); + + test('Collects blocks in a correct order', () { + FlutterTimeline.debugCollectionEnabled = true; + const int testCount = 7 * kSliceSize ~/ 2; + + for (int i = 0; i < testCount; i++) { + FlutterTimeline.startSync('TEST$i'); + FlutterTimeline.finishSync(); + } + + final AggregatedTimings data = FlutterTimeline.debugCollect(); + expect(data.timedBlocks, hasLength(testCount)); + expect( + data.timedBlocks.map((TimedBlock block) => block.name).toList(), + List.generate(testCount, (int i) => 'TEST$i'), + ); + }); +}