Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: shrink_wrapped_scroll_view #41

Merged
merged 1 commit into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
],
),
),
);
}
}