-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: shrink_wrapped_scroll_view (#41)
- Loading branch information
Showing
5 changed files
with
264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
packages/nilts/lib/src/lints/shrink_wrapped_scroll_view.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
79
packages/nilts_test/test/lints/shrink_wrapped_scroll_view.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
], | ||
), | ||
), | ||
); | ||
} | ||
} |