Skip to content

Commit

Permalink
feat: shrink_wrapped_scroll_view (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
ronnnnn authored Oct 7, 2023
1 parent e1a67b1 commit 0ae46f2
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 0 deletions.
32 changes: 32 additions & 0 deletions packages/nilts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Some of lint rules support quick fixes on IDE.
| [defined\_void\_callback\_type](#defined_void_callback_type) | Checks `void Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [fixed\_text\_scale\_factor\_rich\_text](#fixed_text_scale_factor_rich_text) | Checks usage of `textScaleFactor` in `RichText` constructor. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [flaky\_tests\_with\_set\_up\_all](#flaky_tests_with_set_up_all) | Checks `setUpAll` usages. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [shrink\_wrapped\_scroll\_view](#shrink_wrapped_scroll_view) | Checks the content of the scroll view is shrink wrapped. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [unnecessary\_rebuilds\_from\_media\_query](#unnecessary_rebuilds_from_media_query) | Checks `MediaQuery.xxxOf(context)` or `MediaQuery.maybeXxxOf(context)` usages. | >= Flutter 3.10.0 (Dart 3.0.0) | Practice | Experimental | ✅️ |

### Details
Expand Down Expand Up @@ -181,6 +182,37 @@ See also:
- [setUpAll function - flutter_test library - Dart API](https://api.flutter.dev/flutter/flutter_test/setUpAll.html)
- [setUp function - flutter_test library - Dart API](https://api.flutter.dev/flutter/flutter_test/setUp.html)

#### shrink_wrapped_scroll_view

- Target SDK: Any versions nilts supports
- Rule type: Practice
- Maturity level: Experimental
- Quick fix:

**Consider** removing `shrinkWrap` argument and update the Widget not to shrink wrap.
Shrink wrapping the content of the scroll view is significantly more expensive than expanding to the maximum allowed size because the content can expand and contract during scrolling, which means the size of the scroll view needs to be recomputed whenever the scroll position changes.

You can avoid shrink wrap with 3 steps below in case of your scroll view is nested.

1. Replace the parent scroll view with `CustomScrollView`.
2. Replace the child scroll view with `SliverListView` or `SliverGridView`.
3. Set `SliverChildBuilderDelegate` to `delegate` argument of the `SliverListView` or `SliverGridView`.

**BAD:**
```dart
ListView(shrinkWrap: true)
```

**GOOD:**
```dart
ListView(shrinkWrap: false)
```

See also:

- [shrinkWrap property - ScrollView class - widgets library - Dart API](https://api.flutter.dev/flutter/widgets/ScrollView/shrinkWrap.html)
- [ShrinkWrap vs Slivers | Decoding Flutter - YouTube](https://youtu.be/LUqDNnv_dh0)

#### unnecessary_rebuilds_from_media_query

- Target SDK: >= Flutter 3.10.0 (Dart 3.0.0)
Expand Down
2 changes: 2 additions & 0 deletions packages/nilts/lib/nilts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:nilts/src/lints/defined_void_callback_type.dart';
import 'package:nilts/src/lints/fixed_text_scale_factor_rich_text.dart';
import 'package:nilts/src/lints/flaky_tests_with_set_up_all.dart';
import 'package:nilts/src/lints/shrink_wrapped_scroll_view.dart';
import 'package:nilts/src/lints/unnecessary_rebuilds_from_media_query.dart';

/// custom_lint integrates the nilts's plugin from this method on
Expand All @@ -15,6 +16,7 @@ class _NiltsLint extends PluginBase {
const DefinedVoidCallbackType(),
const FixedTextScaleFactorRichText(),
const FlakyTestsWithSetUpAll(),
const ShrinkWrappedScrollView(),
const UnnecessaryRebuildsFromMediaQuery(),
];
}
3 changes: 3 additions & 0 deletions packages/nilts/lib/src/change_priority.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class ChangePriority {
/// The priority for [_AddTextScaleFactor].
static const int addTextScaleFactor = 100;

/// The priority for [_RemoveShrinkWrap].
static const int removeShrinkWrap = 100;

/// The priority for [_ReplaceWithMediaQueryXxxOf].
static const int replaceWithMediaQueryXxxOf = 100;

Expand Down
148 changes: 148 additions & 0 deletions packages/nilts/lib/src/lints/shrink_wrapped_scroll_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// ignore_for_file: comment_references

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:nilts/src/change_priority.dart';
import 'package:nilts/src/utils/library_element_ext.dart';

/// A class for `shrink_wrapped_scroll_view` rule.
///
/// This rule checks if the content of the scroll view is shrink wrapped.
///
/// - Target SDK: Any versions nilts supports
/// - Rule type: Practice
/// - Maturity level: Experimental
/// - Quick fix: ✅
///
/// **Consider** removing `shrinkWrap` argument and update the Widget not to
/// shrink wrap.
/// Shrink wrapping the content of the scroll view is
/// significantly more expensive than expanding to the maximum allowed size
/// because the content can expand and contract during scrolling,
/// which means the size of the scroll view needs to be recomputed
/// whenever the scroll position changes.
///
/// You can avoid shrink wrap with 3 steps below
/// in case of your scroll view is nested.
///
/// 1. Replace the parent scroll view with [CustomScrollView].
/// 2. Replace the child scroll view with [SliverListView] or [SliverGridView].
/// 3. Set [SliverChildBuilderDelegate] to `delegate` argument of
/// [SliverListView] or [SliverGridView].
///
/// **BAD:**
/// ```dart
/// ListView(shrinkWrap: true)
/// ```
///
/// **GOOD:**
/// ```dart
/// ListView(shrinkWrap: false)
/// ```
///
/// See also:
///
/// - [shrinkWrap property - ScrollView class - widgets library - Dart API](https://api.flutter.dev/flutter/widgets/ScrollView/shrinkWrap.html)
/// - [ShrinkWrap vs Slivers | Decoding Flutter - YouTube](https://youtu.be/LUqDNnv_dh0)
class ShrinkWrappedScrollView extends DartLintRule {
/// Create a new instance of [ShrinkWrappedScrollView].
const ShrinkWrappedScrollView() : super(code: _code);

static const _code = LintCode(
name: 'shrink_wrapped_scroll_view',
problemMessage: 'Shrink wrapping the content of the scroll view is '
'significantly more expensive than '
'expanding to the maximum allowed size.',
url: 'https://github.com/ronnnnn/nilts#shrink_wrapped_scroll_view',
);

@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addInstanceCreationExpression((node) {
// Do nothing if the package of constructor is not `flutter`.
final constructorName = node.constructorName;
final library = constructorName.staticElement?.library;
if (library == null) return;
if (!library.isFlutter) return;

// Do nothing if the constructor is not sub class of `ScrollView`.
if (!_scrollViewSubClasses.contains(constructorName.type.element?.name)) {
return;
}

// Do nothing if the constructor doesn't have `shrinkWrap` argument.
final arguments = node.argumentList.arguments;
final isShrinkWrapSet = arguments.any(
(argument) =>
argument is NamedExpression &&
argument.name.label.name == 'shrinkWrap',
);
if (!isShrinkWrapSet) return;

// Do nothing if `shrinkWrap: true` is not set.
final isShrinkWrapped = arguments.any(
(argument) =>
argument is NamedExpression &&
argument.name.label.name == 'shrinkWrap' &&
argument.expression is BooleanLiteral &&
(argument.expression as BooleanLiteral).value,
);
if (!isShrinkWrapped) return;

reporter.reportErrorForNode(_code, node);
});
}

@override
List<Fix> getFixes() => [
_RemoveShrinkWrapArgument(),
];
}

class _RemoveShrinkWrapArgument extends DartFix {
@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
AnalysisError analysisError,
List<AnalysisError> others,
) {
context.registry.addInstanceCreationExpression((node) {
if (!node.sourceRange.intersects(analysisError.sourceRange)) return;

// Do nothing if the constructor is not sub class of `ScrollView`.
final constructorName = node.constructorName;
if (!_scrollViewSubClasses.contains(constructorName.type.element?.name)) {
return;
}

reporter
.createChangeBuilder(
message: 'Remove shrinkWrap',
priority: ChangePriority.removeShrinkWrap,
)
.addDartFileEdit((builder) {
final arguments = node.argumentList.arguments;
final argument = arguments.firstWhere(
(argument) =>
argument is NamedExpression &&
argument.name.label.name == 'shrinkWrap',
);
builder.addDeletion(argument.sourceRange);
});
});
}
}

const _scrollViewSubClasses = [
'ListView',
'GridView',
'CustomScrollView',
];
79 changes: 79 additions & 0 deletions packages/nilts_test/test/lints/shrink_wrapped_scroll_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// ignore_for_file: avoid_redundant_argument_values

import 'package:flutter/material.dart';

void main() {
runApp(const MainApp());
}

class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
ListView(),
ListView(shrinkWrap: false),
// expect_lint: shrink_wrapped_scroll_view
ListView(shrinkWrap: true),
// expect_lint: shrink_wrapped_scroll_view
ListView.builder(
itemBuilder: (_, __) => null,
shrinkWrap: true,
),
// expect_lint: shrink_wrapped_scroll_view
ListView.custom(
childrenDelegate: SliverChildListDelegate([]),
shrinkWrap: true,
),
// expect_lint: shrink_wrapped_scroll_view
ListView.separated(
itemBuilder: (_, __) => null,
separatorBuilder: (_, __) => const SizedBox.shrink(),
itemCount: 0,
shrinkWrap: true,
),
// expect_lint: shrink_wrapped_scroll_view
GridView(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
shrinkWrap: true,
),
// expect_lint: shrink_wrapped_scroll_view
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (_, __) => null,
shrinkWrap: true,
),
// expect_lint: shrink_wrapped_scroll_view
GridView.custom(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
childrenDelegate: SliverChildListDelegate([]),
shrinkWrap: true,
),
// expect_lint: shrink_wrapped_scroll_view
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
),
// expect_lint: shrink_wrapped_scroll_view
GridView.extent(
maxCrossAxisExtent: 2,
shrinkWrap: true,
),
// expect_lint: shrink_wrapped_scroll_view
const CustomScrollView(shrinkWrap: true),
],
),
),
);
}
}

0 comments on commit 0ae46f2

Please sign in to comment.