Skip to content
This repository was archived by the owner on Nov 20, 2024. It is now read-only.

feat: lint when widgets are being returned outside the build method #2582

Closed
wants to merge 5 commits into from
Closed

feat: lint when widgets are being returned outside the build method #2582

wants to merge 5 commits into from

Conversation

jorgecoca
Copy link

@jorgecoca jorgecoca commented Apr 13, 2021

Description

One of the most common questions I have received is why it is preferred to use classes to create widgets in Flutter, as opposed to returning and reusing widgets from fields and methods.

This SO issue explains really well the issue:

https://stackoverflow.com/questions/53234825/what-is-the-difference-between-functions-and-classes-to-create-reusable-widgets

With this new lint rule, I attempt to highlight cases where a widget is not created properly.

Fixes

dart-lang/sdk#58303

@google-cla google-cla bot added the cla: yes label Apr 13, 2021
@jorgecoca jorgecoca changed the title feat: lint when widgets being returned outside the build method feat: lint when widgets are being returned outside the build method Apr 13, 2021
@coveralls
Copy link

coveralls commented Apr 13, 2021

Coverage Status

Coverage increased (+0.03%) to 94.383% when pulling 2826653 on jorgecoca:functions-widget into dda93df on dart-lang:master.

@pq
Copy link
Contributor

pq commented Apr 13, 2021

/fyi @Hixie @goderbauer @jacob314 @a14n for input on semantics.

Copy link
Contributor

@pq pq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few nits... Hoping for some feedback on semantics from Flutter folks.

Thanks!

@jorgecoca jorgecoca requested review from pq and bwilkerson April 14, 2021 11:18
Copy link
Contributor

@bwilkerson bwilkerson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll probably wait a bit for feedback from the Flutter team before committing this, but the implementation looks good to me.

bool _isBuildMethod(ExecutableElement? element) => element?.name == 'build';

bool _isBuildMethodOfWidget(MethodDeclaration node) {
if (_isBuildMethod(node.declaredElement)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine as-is, but I'll point out that you don't need to access the ExecutableElement in order to get the name of the method. You can access it as node.name.name and doing so avoids the need for a null-aware operator in _isBuildMethod.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha! Things I've learned today! Is it just improving the syntax, or are there performance improvements as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check takes such a small amount of time that I doubt it would be noticable. The fact that the information is always available is the only advantage, and even that's negligible because I don't think the declaredElement can every be null in practice in the linter code. Mostly just passing along information in case it's useful some day.

@jacob314
Copy link

I really like the overall idea of using lints to guide users to using Widgets rather than functions where possible.
Having a quick fix that turned the method into a Widget class would be fantastic. I do worry this will have a lot of false positives and I'm not sure how to avoid them. For example, here are some possible false positives to consider from the Fluttter Gallery and package:flutter found with a naive grep command.
To reduce the number of false positives it would help if this lint did not trigger for builder methods and did not trigger on tests. Avoiding tests is easy, detecting whether a method is a used as a "builder" is more ambiguous.

This could be a good candidate for a hypothetical level of analyzer hints that only showed up when you authored code. That way a users could be aware of the best practice but you wouldn't have to worry as much about false positives. Fyi @davidmorgan.

jacobr-macbookpro18% git grep " Widget [^b]\\w*[(]"                      
lib/demos/cupertino/cupertino_picker_demo.dart:  Widget _buildDatePicker(BuildContext context) {
lib/demos/cupertino/cupertino_picker_demo.dart:  Widget _buildTimePicker(BuildContext context) {
lib/demos/cupertino/cupertino_picker_demo.dart:  Widget _buildDateAndTimePicker(BuildContext context) {
lib/demos/cupertino/cupertino_picker_demo.dart:  Widget _buildCountdownTimerPicker(BuildContext context) {
lib/demos/material/bottom_sheet_demo.dart:  Widget _bottomSheetDemo(BuildContext context) {
lib/demos/material/progress_indicator_demo.dart:  Widget _buildIndicators(BuildContext context, Widget child) {
lib/demos/reference/motion_demo_fade_scale_transition.dart:  Widget _showExampleAlertDialog() {
lib/feature_discovery/overlay.dart:  Widget _buildTitle(TextTheme theme) {
lib/feature_discovery/overlay.dart:  Widget _buildDescription(TextTheme theme) {
lib/pages/backdrop.dart:  Widget _buildStack(BuildContext context, BoxConstraints constraints) {
lib/pages/category_list_item.dart:  Widget _buildHeaderWithChildren(BuildContext context, Widget child) {
lib/pages/home.dart:  Widget _builder(int index) {
lib/pages/settings_list_item.dart:  Widget _buildHeaderWithChildren(BuildContext context, Widget child) {
lib/routes.dart:typedef PathWidgetBuilder = Widget Function(BuildContext, String);
lib/studies/crane/backdrop.dart:  Widget _header() {
lib/studies/reply/adaptive_nav.dart:  Widget _buildStack(BuildContext context, BoxConstraints constraints) {
lib/studies/shrine/app.dart:  Widget mobileBackdrop() {
lib/studies/shrine/app.dart:  Widget desktopBackdrop() {
lib/studies/shrine/backdrop.dart:  Widget _buildStack(BuildContext context, BoxConstraints constraints) {
lib/studies/shrine/category_menu_page.dart:  Widget _buttonText(String caption, TextStyle style) {
lib/studies/shrine/category_menu_page.dart:  Widget _divider({BuildContext context}) {
lib/studies/shrine/category_menu_page.dart:  Widget _buildCategory(Category category, BuildContext context) {
lib/studies/shrine/expanding_bottom_sheet.dart:  Widget _buildThumbnails(BuildContext context, int numProducts) {
lib/studies/shrine/expanding_bottom_sheet.dart:  Widget _buildShoppingCartPage() {
lib/studies/shrine/expanding_bottom_sheet.dart:  Widget _buildCart(BuildContext context) {
lib/studies/shrine/expanding_bottom_sheet.dart:  Widget _buildSlideAnimation(BuildContext context, Widget child) {
lib/studies/shrine/expanding_bottom_sheet.dart:  Widget _buildRemovedThumbnail(
lib/studies/shrine/expanding_bottom_sheet.dart:  Widget _buildThumbnail(
lib/studies/shrine/expanding_bottom_sheet.dart:  Widget _buildAnimatedList(BuildContext context) {
lib/studies/shrine/expanding_bottom_sheet.dart:  Widget _buildOverflow(AppStateModel model, BuildContext context) {
lib/studies/shrine/expanding_bottom_sheet.dart:  final Widget Function(int, BuildContext, Animation<double>)
test/benchmarks/gallery_automator.dart:  Widget createWidget() {
test/benchmarks/gallery_recorder.dart:  Widget createWidget() {

And false positives from Flutter.

lib/src/cupertino/app.dart:  Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
lib/src/cupertino/bottom_tab_bar.dart:  Widget _wrapActiveItem(BuildContext context, Widget item, { required bool active }) {
lib/src/cupertino/context_menu.dart:typedef ContextMenuPreviewBuilder = Widget Function(
lib/src/cupertino/context_menu.dart:typedef _ContextMenuPreviewBuilderChildless = Widget Function(
lib/src/cupertino/context_menu.dart:  Widget _buildAnimation(BuildContext context, Widget? child) {
lib/src/cupertino/context_menu.dart:  Widget _buildSheetAnimation(BuildContext context, Widget? child) {
lib/src/cupertino/context_menu.dart:  Widget _buildChildAnimation(BuildContext context, Widget? child) {
lib/src/cupertino/context_menu.dart:  Widget _buildAnimation(BuildContext context, Widget? child) {
lib/src/cupertino/date_picker.dart:typedef _ColumnBuilder = Widget Function(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay);
lib/src/cupertino/date_picker.dart:  Widget _buildMediumDatePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildHourPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildMinutePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildAmPmPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildDayPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildMonthPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildYearPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildLabel(String text, EdgeInsetsDirectional pickerPadding) {
lib/src/cupertino/date_picker.dart:  Widget _buildPickerNumberLabel(String text, EdgeInsetsDirectional padding) {
lib/src/cupertino/date_picker.dart:  Widget _buildHourPicker(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildHourColumn(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildMinutePicker(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildMinuteColumn(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildSecondPicker(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
lib/src/cupertino/date_picker.dart:  Widget _buildSecondColumn(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
lib/src/cupertino/desktop_text_selection.dart:  static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
lib/src/cupertino/dialog.dart:  Widget _buildContent(BuildContext context) {
lib/src/cupertino/dialog.dart:  Widget _buildActions() {
lib/src/cupertino/dialog.dart:  Widget _buildContent(BuildContext context) {
lib/src/cupertino/dialog.dart:  Widget _buildActions() {
lib/src/cupertino/dialog.dart:  Widget _buildCancelButton() {
lib/src/cupertino/dialog.dart:  Widget _buildContentWithRegularSizingPolicy({
lib/src/cupertino/dialog.dart:  Widget _buildContentWithAccessibilitySizingPolicy({
lib/src/cupertino/nav_bar.dart:  Widget _buildPreviousTitleWidget(BuildContext context, String? previousTitle, Widget? child) {
lib/src/cupertino/picker.dart:  Widget _buildSelectionOverlay(Widget selectionOverlay) {
lib/src/cupertino/refresh.dart:typedef RefreshControlIndicatorBuilder = Widget Function(
lib/src/cupertino/refresh.dart:  static Widget _buildIndicatorForRefreshState(RefreshIndicatorMode refreshState, double radius, double percentageComplete) {
lib/src/cupertino/text_field.dart:  Widget _addTextDependentAttachments(Widget editableText, TextStyle textStyle, TextStyle placeholderStyle) {
lib/src/cupertino/text_selection_toolbar.dart:typedef CupertinoToolbarBuilder = Widget Function(
lib/src/cupertino/text_selection_toolbar.dart:  static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) {
lib/src/material/about.dart:  Widget _packageLicensePage(BuildContext _, Object? args, ScrollController? scrollController) {
lib/src/material/about.dart:  Widget _packagesView(final BuildContext _, final bool isLateral) {
lib/src/material/about.dart:  Widget _packagesList(
lib/src/material/about.dart:typedef _MasterViewBuilder = Widget Function(BuildContext context, bool isLateralUI);
lib/src/material/about.dart:typedef _DetailPageBuilder = Widget Function(BuildContext context, Object? arguments, ScrollController? scrollController);
lib/src/material/about.dart:  Widget _nestedUI(BuildContext context) {
lib/src/material/about.dart:  Widget _lateralUI(BuildContext context) {
lib/src/material/app.dart:  Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
lib/src/material/app.dart:  Widget _materialBuilder(BuildContext context, Widget? child) {
lib/src/material/app.dart:  Widget _buildWidgetApp(BuildContext context) {
lib/src/material/autocomplete.dart:  static Widget _defaultFieldViewBuilder(BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted) {
lib/src/material/banner_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/bottom_navigation_bar.dart:  Widget _createContainer(List<Widget> tiles) {
lib/src/material/button_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/calendar_date_picker.dart:  Widget _buildPicker() {
lib/src/material/calendar_date_picker.dart:  Widget _buildItems(BuildContext context, int index) {
lib/src/material/calendar_date_picker.dart:  Widget _buildYearItem(BuildContext context, int index) {
lib/src/material/chip_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/data_table.dart:  Widget _buildCheckbox({
lib/src/material/data_table.dart:  Widget _buildHeadingCell({
lib/src/material/data_table.dart:  Widget _buildDataCell({
lib/src/material/date_picker.dart:  Widget _buildMonthItem(BuildContext context, int index, bool beforeInitialMonth) {
lib/src/material/date_picker.dart:  Widget _buildDayItem(BuildContext context, DateTime dayToBuild, int firstDayOffset, int daysInMonth) {
lib/src/material/date_picker.dart:  Widget _buildEdgeContainer(BuildContext context, bool isHighlighted) {
lib/src/material/date_picker_deprecated.dart:  Widget _buildItems(BuildContext context, int index) {
lib/src/material/desktop_text_selection.dart:  static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
lib/src/material/divider_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/drawer.dart:  Widget _buildDrawer(BuildContext context) {
lib/src/material/elevated_button_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/expansion_panel.dart:typedef ExpansionPanelHeaderBuilder = Widget Function(BuildContext context, bool isExpanded);
lib/src/material/expansion_panel.dart:/// Widget _buildPanel() {
lib/src/material/expansion_panel.dart:  /// Widget _buildPanel() {
lib/src/material/expansion_tile.dart:  Widget _buildChildren(BuildContext context, Widget? child) {
lib/src/material/flexible_space_bar.dart:  static Widget createSettings({
lib/src/material/ink_decoration.dart:  Widget _build(BuildContext context) {
lib/src/material/input_decorator.dart:  Widget _buildHelper() {
lib/src/material/input_decorator.dart:  Widget _buildError() {
lib/src/material/list_tile.dart:  static Widget merge({
lib/src/material/list_tile.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/material.dart:  static Widget _transparentInterior({
lib/src/material/navigation_rail_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/outlined_button_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/popup_menu_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/progress_indicator.dart:  Widget _buildSemanticsWrapper({
lib/src/material/progress_indicator.dart:  Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) {
lib/src/material/progress_indicator.dart:  Widget _buildCupertinoIndicator(BuildContext context) {
lib/src/material/progress_indicator.dart:  Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
lib/src/material/progress_indicator.dart:  Widget _buildAnimation() {
lib/src/material/progress_indicator.dart:  Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
lib/src/material/reorderable_list.dart:  Widget _wrapWithSemantics(Widget child, int index) {
lib/src/material/reorderable_list.dart:  Widget _itemBuilder(BuildContext context, int index) {
lib/src/material/reorderable_list.dart:  Widget _proxyDecorator(Widget child, int index, Animation<double> animation) {
lib/src/material/scaffold.dart:  Widget _wrapBottomSheet(Widget bottomSheet) {
lib/src/material/slider.dart:  Widget _buildMaterialSlider(BuildContext context) {
lib/src/material/slider.dart:  Widget _buildCupertinoSlider(BuildContext context) {
lib/src/material/slider_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/stepper.dart:  Widget _buildLine(bool visible) {
lib/src/material/stepper.dart:  Widget _buildCircleChild(int index, bool oldState) {
lib/src/material/stepper.dart:  Widget _buildCircle(int index, bool oldState) {
lib/src/material/stepper.dart:  Widget _buildTriangle(int index, bool oldState) {
lib/src/material/stepper.dart:  Widget _buildIcon(int index) {
lib/src/material/stepper.dart:  Widget _buildVerticalControls() {
lib/src/material/stepper.dart:  Widget _buildHeaderText(int index) {
lib/src/material/stepper.dart:  Widget _buildVerticalHeader(int index) {
lib/src/material/stepper.dart:  Widget _buildVerticalBody(int index) {
lib/src/material/stepper.dart:  Widget _buildVertical() {
lib/src/material/stepper.dart:  Widget _buildHorizontal() {
lib/src/material/switch.dart:  Widget _buildCupertinoSwitch(BuildContext context) {
lib/src/material/switch.dart:  Widget _buildMaterialSwitch(BuildContext context) {
lib/src/material/tabs.dart:  Widget _buildLabelText() {
lib/src/material/tabs.dart:  Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) {
lib/src/material/tabs.dart:  Widget _buildTabIndicator(
lib/src/material/text_button_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/text_field.dart:  /// Widget counter(
lib/src/material/text_selection_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/text_selection_toolbar.dart:  static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
lib/src/material/theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/time_picker_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/toggle_buttons_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/material/tooltip_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/widgets/animated_cross_fade.dart:/// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
lib/src/widgets/animated_cross_fade.dart:typedef AnimatedCrossFadeBuilder = Widget Function(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey);
lib/src/widgets/animated_cross_fade.dart:  static Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
lib/src/widgets/animated_list.dart:typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation);
lib/src/widgets/animated_list.dart:typedef AnimatedListRemovedItemBuilder = Widget Function(BuildContext context, Animation<double> animation);
lib/src/widgets/animated_list.dart:///   Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
lib/src/widgets/animated_list.dart:///   Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
lib/src/widgets/animated_list.dart:/// typedef RemovedItemBuilder = Widget Function(int item, BuildContext context, Animation<double> animation);
lib/src/widgets/animated_list.dart:///   Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
lib/src/widgets/animated_list.dart:///   Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
lib/src/widgets/animated_list.dart:/// typedef RemovedItemBuilder = Widget Function(int item, BuildContext context, Animation<double> animation);
lib/src/widgets/animated_list.dart:  Widget _itemBuilder(BuildContext context, int itemIndex) {
lib/src/widgets/animated_switcher.dart:typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);
lib/src/widgets/animated_switcher.dart:typedef AnimatedSwitcherLayoutBuilder = Widget Function(Widget? currentChild, List<Widget> previousChildren);
lib/src/widgets/animated_switcher.dart:  static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
lib/src/widgets/animated_switcher.dart:  static Widget defaultLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) {
lib/src/widgets/async.dart:typedef AsyncWidgetBuilder<T> = Widget Function(BuildContext context, AsyncSnapshot<T> snapshot);
lib/src/widgets/autocomplete.dart:typedef AutocompleteOptionsViewBuilder<T extends Object> = Widget Function(
lib/src/widgets/autocomplete.dart:typedef AutocompleteFieldViewBuilder = Widget Function(
lib/src/widgets/basic.dart:  static Widget shape({
lib/src/widgets/basic.dart:///   Widget flowMenuItem(IconData icon) {
lib/src/widgets/basic.dart:typedef StatefulWidgetBuilder = Widget Function(BuildContext context, StateSetter setState);
lib/src/widgets/drag_target.dart:typedef DragTargetBuilder<T> = Widget Function(BuildContext context, List<T?> candidateData, List<dynamic> rejectedData);
lib/src/widgets/drag_target.dart:  Widget _build(BuildContext context) {
lib/src/widgets/draggable_scrollable_sheet.dart:typedef ScrollableWidgetBuilder = Widget Function(
lib/src/widgets/dual_transition_builder.dart:typedef AnimatedTransitionBuilder = Widget Function(
lib/src/widgets/focus_scope.dart:///   Widget _buildStack(BuildContext context, BoxConstraints constraints) {
lib/src/widgets/form.dart:typedef FormFieldBuilder<T> = Widget Function(FormFieldState<T> field);
lib/src/widgets/framework.dart:typedef ErrorWidgetBuilder = Widget Function(FlutterErrorDetails details);
lib/src/widgets/framework.dart:  static Widget _defaultErrorWidgetBuilder(FlutterErrorDetails details) {
lib/src/widgets/framework.dart:typedef WidgetBuilder = Widget Function(BuildContext context);
lib/src/widgets/framework.dart:typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
lib/src/widgets/framework.dart:typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child);
lib/src/widgets/framework.dart:typedef ControlsWidgetBuilder = Widget Function(BuildContext context, { VoidCallback? onStepContinue, VoidCallback? onStepCancel });
lib/src/widgets/heroes.dart:typedef HeroPlaceholderBuilder = Widget Function(
lib/src/widgets/heroes.dart:typedef HeroFlightShuttleBuilder = Widget Function(
lib/src/widgets/heroes.dart:///  Widget _blueRectangle(Size size) {
lib/src/widgets/heroes.dart:  Widget _buildOverlay(BuildContext context) {
lib/src/widgets/heroes.dart:  Widget _defaultHeroFlightShuttleBuilder(
lib/src/widgets/icon_theme.dart:  static Widget merge({
lib/src/widgets/icon_theme.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/widgets/image.dart:typedef ImageFrameBuilder = Widget Function(
lib/src/widgets/image.dart:typedef ImageLoadingBuilder = Widget Function(
lib/src/widgets/image.dart:typedef ImageErrorWidgetBuilder = Widget Function(
lib/src/widgets/image.dart:  Widget _debugBuildErrorWidget(BuildContext context, Object error) {
lib/src/widgets/inherited_theme.dart:  /// Widget wrap(BuildContext context, Widget child) {
lib/src/widgets/inherited_theme.dart:  Widget wrap(BuildContext context, Widget child);
lib/src/widgets/inherited_theme.dart:  static Widget captureAll(BuildContext context, Widget child, {BuildContext? to}) {
lib/src/widgets/inherited_theme.dart:  Widget wrap(Widget child) {
lib/src/widgets/interactive_viewer.dart:typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Quad viewport);
lib/src/widgets/interactive_viewer.dart:  /// typedef _CellBuilder = Widget Function(BuildContext context, int row, int column);
lib/src/widgets/layout_builder.dart:typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstraints constraints);
lib/src/widgets/layout_builder.dart:  final Widget Function(BuildContext, ConstraintType) builder;
lib/src/widgets/layout_builder.dart:/// Widget _buildNormalContainer() {
lib/src/widgets/layout_builder.dart:/// Widget _buildWideContainers() {
lib/src/widgets/orientation_builder.dart:typedef OrientationWidgetBuilder = Widget Function(BuildContext context, Orientation orientation);
lib/src/widgets/orientation_builder.dart:  Widget _buildWithConstraints(BuildContext context, BoxConstraints constraints) {
lib/src/widgets/platform_view.dart:typedef PlatformViewSurfaceFactory = Widget Function(BuildContext context, PlatformViewController controller);
lib/src/widgets/reorderable_list.dart:typedef ReorderItemProxyDecorator = Widget Function(Widget child, int index, Animation<double> animation);
lib/src/widgets/reorderable_list.dart:  Widget _itemBuilder(BuildContext context, int index) {
lib/src/widgets/reorderable_list.dart:  Widget createProxy(BuildContext context) {
lib/src/widgets/routes.dart:  Widget _buildModalBarrier(BuildContext context) {
lib/src/widgets/routes.dart:  Widget _buildModalScope(BuildContext context) {
lib/src/widgets/routes.dart:typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
lib/src/widgets/routes.dart:typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
lib/src/widgets/scroll_view.dart:/// Widget myWidget(BuildContext context) {
lib/src/widgets/scrollable.dart:typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset position);
lib/src/widgets/sliver_layout_builder.dart:typedef SliverLayoutWidgetBuilder = Widget Function(BuildContext context, SliverConstraints constraints);
lib/src/widgets/text.dart:  static Widget merge({
lib/src/widgets/text.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/widgets/text.dart:  Widget wrap(BuildContext context, Widget child) {
lib/src/widgets/text_selection.dart:typedef ToolbarBuilder = Widget Function(BuildContext context, Widget child);
lib/src/widgets/text_selection.dart:  Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
lib/src/widgets/text_selection.dart:  Widget _buildToolbar(BuildContext context) {
lib/src/widgets/value_listenable_builder.dart:typedef ValueWidgetBuilder<T> = Widget Function(BuildContext context, T value, Widget? child);
lib/src/widgets/widget_inspector.dart:typedef InspectorSelectButtonBuilder = Widget Function(BuildContext context, VoidCallback onPressed);
test/cupertino/app_test.dart:typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
test/cupertino/context_menu_action_test.dart:  Widget _getApp({VoidCallback? onPressed, bool isDestructiveAction = false, bool isDefaultAction = false}) {
test/cupertino/context_menu_test.dart:  Widget _getChild() {
test/cupertino/context_menu_test.dart:  Widget _getContextMenu({
test/cupertino/date_picker_test.dart:      Widget _buildApp(CupertinoDatePickerMode mode) {
test/cupertino/route_test.dart:  Widget _buildHome(BuildContext context) {
test/cupertino/route_test.dart:  Widget _buildSub(BuildContext context) {
test/cupertino/scaffold_test.dart:    Widget scaffoldWithBrightness(Brightness brightness) {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/scrollbar_test.dart:    Widget viewWithScroll() {
test/cupertino/slider_test.dart:    Widget withTraits(Brightness brightness, CupertinoUserInterfaceLevelData level, bool highContrast) {
test/cupertino/text_selection_test.dart:    Widget createEditableText({
test/material/app_bar_theme_test.dart:    Widget _buildWithBackwardsCompatibility([bool? enabled]) => MaterialApp(
test/material/app_test.dart:typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
test/material/bottom_navigation_bar_test.dart:    Widget runTest() {
test/material/bottom_navigation_bar_test.dart:    Widget feedbackBoilerplate({bool? enableFeedback, bool? enableFeedbackTheme}) {
test/material/calendar_date_picker_test.dart:  Widget calendarDatePicker({
test/material/calendar_date_picker_test.dart:  Widget yearPicker({
test/material/chip_test.dart:    Widget chipWidget({ bool enabled = true, bool selected = false }) {
test/material/chip_test.dart:    Widget chipWidget({ bool enabled = true, bool selected = false }) {
test/material/chip_test.dart:    Widget chipWidget({ bool enabled = true, bool selected = false }) {
test/material/chip_test.dart:    Widget chipWidget({ bool enabled = true, bool selected = false }) {
test/material/chip_test.dart:    Widget chipWidget({ bool enabled = true, bool selected = false }) {
test/material/chip_test.dart:    Widget chipWidget({ bool enabled = true, bool selected = false }) {
test/material/chip_theme_test.dart:    Widget chipWidget({ bool enabled = true, bool selected = false }) {
test/material/chip_theme_test.dart:    Widget chipWidget({ bool selected = false }) {
test/material/chip_theme_test.dart:    Widget chipWidget({ bool selected = false }) {
test/material/chip_theme_test.dart:    Widget chipWidget({ bool selected = false }) {
test/material/floating_action_button_location_test.dart:    Widget _buildTest(
test/material/ink_well_test.dart:    Widget paddedInkWell({Key? key, Widget? child}) {
test/material/ink_well_test.dart:    Widget paddedInkWell({Key? key, Widget? child}) {
test/material/ink_well_test.dart:    Widget paddedInkWell({Key? key, Widget? child}) {
test/material/ink_well_test.dart:    Widget doubleInkWellRow({
test/material/input_date_picker_form_field_test.dart:  Widget _inputDatePickerField({
test/material/input_decorator_test.dart:    Widget getLabeledInputDecorator(bool showFix) {
test/material/input_decorator_test.dart:    Widget getLabeledInputDecorator(FloatingLabelBehavior floatingLabelBehavior) => MaterialApp(
test/material/input_decorator_test.dart:    Widget getInputDecorator(VisualDensity visualDensity) {
test/material/range_slider_test.dart:  Widget _buildThemedApp({
test/material/refresh_indicator_test.dart:    Widget layoutCallback(BuildContext context, BoxConstraints constraints) {
test/material/reorderable_list_test.dart:    Widget listItemToWidget(String listItem) {
test/material/scaffold_test.dart:  Widget _buildStatusBarTestApp(TargetPlatform? platform) {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll() {
test/material/scrollbar_test.dart:    Widget viewWithScroll({Radius? radius}) {
test/material/scrollbar_test.dart:    Widget viewWithScroll(TargetPlatform platform) {
test/material/scrollbar_test.dart:    Widget viewWithScroll(TargetPlatform? platform) {
test/material/scrollbar_test.dart:    Widget _getTabContent({ ScrollController? scrollController }) {
test/material/scrollbar_test.dart:    Widget _buildApp({ ScrollController? scrollController }) {
test/material/slider_test.dart:      Widget createParents(int parents, StateSetter setState) {
test/material/snack_bar_test.dart:    Widget _buildApp() {
test/material/tabs_test.dart:typedef TabControllerFrameBuilder = Widget Function(BuildContext context, TabController controller);
test/material/text_field_focus_test.dart:    Widget makeTest(String? prefix) {
test/material/text_field_test.dart:  Widget textFieldBuilder({
test/material/text_field_test.dart:    Widget expandedTextFieldBuilder({
test/material/text_field_test.dart:    Widget intrinsicTextFieldBuilder(bool wrapInIntrinsic) {
test/material/text_field_test.dart:    Widget textFormFieldBuilder(String? errorText) {
test/material/text_field_test.dart:    Widget containedTextFieldBuilder({
test/material/text_field_test.dart:      Widget _buildTest({ required bool isDense }) {
test/material/text_field_test.dart:    Widget textFieldBuilder({ FloatingLabelBehavior behavior = FloatingLabelBehavior.auto }) {
test/material/text_selection_test.dart:    Widget createEditableText({
test/widgets/animated_cross_fade_test.dart:  Widget crossFadeWithWatcher({ bool towardsSecond = false }) {
test/widgets/animated_switcher_test.dart:    Widget newLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) {
test/widgets/animated_switcher_test.dart:    Widget newLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) {
test/widgets/animated_switcher_test.dart:    Widget newTransitionBuilder(Widget child, Animation<double> animation) {
test/widgets/animated_switcher_test.dart:    Widget newLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) {
test/widgets/animated_switcher_test.dart:    Widget newTransitionBuilder(Widget child, Animation<double> animation) {
test/widgets/app_test.dart:typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
test/widgets/async_test.dart:  Widget snapshotText(BuildContext context, AsyncSnapshot<String> snapshot) {
test/widgets/basic_test.dart:    Widget target({required bool ignoring}) => Align(
test/widgets/basic_test.dart:    Widget target({required bool absorbing}) => Align(
test/widgets/draggable_scrollable_sheet_test.dart:  Widget _boilerplate(VoidCallback? onButtonPressed, {
test/widgets/focus_traversal_test.dart:      Widget generateTestWidgets(bool ignoreTextFields) {
test/widgets/heroes_test.dart:    Widget shuttleBuilder(
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget();
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/implicit_animations_test.dart:  Widget getAnimatedWidget() {
test/widgets/list_view_builder_test.dart:    Widget itemBuilder(BuildContext context, int index) {
test/widgets/list_view_builder_test.dart:    Widget itemBuilder(BuildContext context, int index) {
test/widgets/list_view_builder_test.dart:    Widget itemBuilder(BuildContext context, int index) {
test/widgets/list_view_viewporting_test.dart:    Widget itemBuilder(BuildContext context, int index) {
test/widgets/list_view_viewporting_test.dart:    Widget itemBuilder(BuildContext context, int index) {
test/widgets/list_view_viewporting_test.dart:    Widget itemBuilder(BuildContext context, int index) {
test/widgets/list_view_viewporting_test.dart:    Widget itemBuilder(BuildContext context, int index) {
test/widgets/list_view_viewporting_test.dart:    Widget itemBuilder(BuildContext context, int index) {
test/widgets/mouse_region_test.dart:    Widget hoverableContainer({
test/widgets/mouse_region_test.dart:    Widget tripleRegions({bool? opaqueC, required void Function(String) addLog}) {
test/widgets/mouse_region_test.dart:      Widget mouseRegionWithOptionalOpaque({
test/widgets/navigator_test.dart:    Widget widgetUnderTest({required bool enabled}) {
test/widgets/navigator_test.dart:    Widget _buildFrame(String action) {
test/widgets/nested_scroll_view_test.dart:    Widget _buildBallisticTest(ScrollController controller) {
test/widgets/page_transitions_test.dart:  Widget _build(BuildContext context) => const Text('Overlay');
test/widgets/platform_view_test.dart:    Widget scaffold(Widget target) {
test/widgets/render_object_element_test.dart:    Widget widget() {
test/widgets/render_object_element_test.dart:    Widget widget() {
test/widgets/render_object_element_test.dart:    Widget widget() {
test/widgets/route_notification_messages_test.dart:typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
test/widgets/router_test.dart:typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation?);
test/widgets/selectable_text_test.dart:  Widget selectableTextBuilder({
test/widgets/sliver_visibility_test.dart:    Widget _boilerPlate(Widget sliver) {
test/widgets/slivers_padding_test.dart:    Widget listBuilder(IndexedWidgetBuilder sliverChildBuilder) {
test/widgets/slivers_test.dart:      Widget _buildItem(BuildContext context, int index) {
test/widgets/slivers_test.dart:  Widget _boilerPlate(Widget sliver) {
test/widgets/ticker_mode_test.dart:    Widget nestedTickerModes({required bool innerEnabled, required bool outerEnabled}) {
test/widgets/transform_test.dart:  Widget _generateTransform(bool needsCompositing, double angle) {
test/widgets/widget_inspector_test.dart:  Widget makeClock(String label, int utcOffset) {
test/widgets/widget_inspector_test.dart:      Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
test/widgets/widget_inspector_test.dart:      Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
test/widgets/widget_inspector_test.dart:      Widget createSubtree({ double? width, Key? key }) {

@bwilkerson
Copy link
Contributor

Even without the test cases, that's a concerningly long list of false positives.

I noticed that some of them are because Widget is being used as a return type in a function type. Seem like that ought to be allowed.

I also noticed that many of them are private methods/functions. I wonder whether the lint should ignore those (and local functions). It seems like the advice to use a class is more important when you're trying to share the widget structure in multiple places that when you just trying to break up a large build method.

@jacob314
Copy link

Even without the test cases, that's a concerningly long list of false positives.

I noticed that some of them are because Widget is being used as a return type in a function type. Seem like that ought to be allowed.

I also noticed that many of them are private methods/functions. I wonder whether the lint should ignore those (and local functions). It seems like the advice to use a class is more important when you're trying to share the widget structure in multiple places that when you just trying to break up a large build method.

To be clear. I used a regexp instead of the lint to get the quick list of false positives. I assume the actual lint wouldn't be tripped up by the function type cases. However, each of the function types does however give an example where users need to write a function instead of a Widget class.

@goderbauer
Copy link
Contributor

I am not sure if banning these outright is a good idea. There are legit use cases where you may want to return a widget from a non-build method, as Jacob also pointed out above. The most prominent ones are builder methods. Simple example:

import 'package:flutter/widgets.dart';

class Foo extends StatelessWidget {
  Widget _buildMyWidget(BuildContext context) { // Should not lint.
    return Container();
  }
  
  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: _buildMyWidget,
    );
  }
}

Not sure how one would differentiate this from other methods?

@jorgecoca
Copy link
Author

I am not sure if banning these outright is a good idea. There are legit use cases where you may want to return a widget from a non-build method, as Jacob also pointed out above. The most prominent ones are builder methods. Simple example:

import 'package:flutter/widgets.dart';

class Foo extends StatelessWidget {
  Widget _buildMyWidget(BuildContext context) { // Should not lint.
    return Container();
  }
  
  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: _buildMyWidget,
    );
  }
}

Not sure how one would differentiate this from other methods?

Oh yeah! That's a great example, and I did not cover that in tests.

Let me see if there's anything it could be done.

Thank you for the feedback!

@jacob314
Copy link

Here is how you can solve the builder case robustly. You need to change where you are showing the lint error. Rather that showing a lint error when you define a method that returns a Widget, show a lint error when you invoke a method that returns a Widget directly in a build method.
That way

class Foo extends StatelessWidget {
  Widget _buildMyWidget(BuildContext context) { // Should not lint.
    return Container();
  }
  
  @override
  Widget build(BuildContext context) {
    return _buildMyWidget(context); // LINT ERROR
  }
}

while

class Foo extends StatelessWidget {
  Widget _buildMyWidget(BuildContext context) { // Should not lint.
    return Container();
  }
  
  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: _buildMyWidget, // OK
    );
  }
}

@Hixie
Copy link

Hixie commented Apr 14, 2021

I agree with @goderbauer that banning these outright is not a good idea. Certainly it's usually better to use a widget rather than a method to build widgets, but that's more of a guideline than a hard-and-fast rule. Like @jacob314 says, it might be better as a clippy-style suggestion during development ("would you like to extract this out into a widget class?") than a lint.

@jorgecoca
Copy link
Author

Closing for now. I will try to figure out a better way to handle these cases.

Thank you all so much for the feedback!

@jorgecoca jorgecoca closed this Apr 15, 2021
@pq
Copy link
Contributor

pq commented Apr 15, 2021

Thanks so much for motivating such a thoughtful conversation. I'm sure I speak for everyone when I see I look forward to seeing what you come up with!

@NatoBoram
Copy link

NatoBoram commented Jul 21, 2021

Allowing them when they're declared as anonymous functions could solve the builder issue, forcing the functional widget to become a class widget or be part of the code tree. This one goes against @goderbauer's comment, but I think that forcing it into the build tree or in its own Widget are both better solutions than making a functional widget.

class FunctionalBuilder extends StatelessWidget {
  const FunctionalBuilder({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: _myWidget,
    );
  }

  /// Bad
  Widget _myWidget(BuildContext context) {
    return Container();
  }
}
class AnonymousBuilder extends StatelessWidget {
  const AnonymousBuilder({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: (BuildContext context) {
        return const MyWidget();
      },
    );
  }
}

/// Good
class MyWidget extends StatelessWidget {
  const MyWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Another notable exception is when they're defined but not declared/instantiated.

/// Good
typedef WidgetBuilder = Widget Function(BuildContext context);

class Builder extends StatelessWidget {
  const Builder({
    Key key,
    @required this.builder,
  })  : assert(builder != null),
        super(key: key);

  /// Good
  final WidgetBuilder builder;

  /// Exception because of `@override`
  @override
  Widget build(BuildContext context) {
    return builder(context);
  }
}

These two exceptions, combined with an exception on @override, seem to be reasonable enough to me.

It's important to point out that @jacob314's list of false positives is misleading; many cases are actual positives that should actually be disallowed. For example, have a look at lib/src/cupertino/date_picker.dart.

I think @jacob314's solution is a great example of out-of-the-box thinking, it's more flexible but still satisfying.

After reading this discussion, I edited dart-lang/sdk#58303 to add a few more allow-cases / disallow-cases. I don't have the expertise that many of you have, so I hope you can point out use cases that I missed.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Development

Successfully merging this pull request may close these issues.