xlints

xlints is a Flutter lint package focused on widget and logic performance.

Available Rules

xlints_prefer_const_constructors

Detects widgets that can be const but are not marked const.

BAD:

Widget build(BuildContext context) {
  return Padding(
    padding: EdgeInsets.all(8),
    child: Icon(Icons.star),
  );
}

GOOD:

Widget build(BuildContext context) {
  return const Padding(
    padding: EdgeInsets.all(8),
    child: Icon(Icons.star),
  );
}
xlints_prefer_listview_builder

Detects long lists that should use .builder.

BAD:

ListView(
  children: List.generate(1000, (i) => Text('Item $i')),
)

GOOD:

ListView.builder(
  itemCount: 1000,
  itemBuilder: (_, i) => Text('Item $i'),
)
xlints_avoid_listview_with_children

Detects ListView/GridView(children: ...) usage that builds all items eagerly.

BAD:

GridView(
  children: List.generate(200, (i) => Card(child: Text('$i'))),
)

GOOD:

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
  itemCount: 200,
  itemBuilder: (_, i) => Card(child: Text('$i')),
)
xlints_avoid_opacity_widget

Detects Opacity usage in cases where a cheaper alternative is preferred.

BAD:

Opacity(
  opacity: 0.5,
  child: Image.network(url),
)

GOOD:

AnimatedOpacity(
  opacity: 0.5,
  duration: const Duration(milliseconds: 200),
  child: Image.network(url),
)
xlints_avoid_padding_wrapping_margin_widget

Detects Padding wrapping a child that already uses margin.

BAD:

Padding(
  padding: const EdgeInsets.all(16),
  child: Container(
    margin: const EdgeInsets.all(8),
    child: const Text('Hello'),
  ),
)

GOOD:

Container(
  margin: const EdgeInsets.all(24),
  child: const Text('Hello'),
)
xlints_avoid_shrink_wrap_true

Detects shrinkWrap: true on list/grid widgets, which can increase layout cost.

BAD:

ListView(
  shrinkWrap: true,
  children: items.map((e) => Text(e)).toList(),
)

GOOD:

Expanded(
  child: ListView.builder(
    itemCount: items.length,
    itemBuilder: (_, i) => Text(items[i]),
  ),
)
xlints_avoid_intrinsic_widgets

Detects expensive IntrinsicHeight/IntrinsicWidth usage.

BAD:

IntrinsicHeight(
  child: Row(children: children),
)

GOOD:

SizedBox(
  height: 72,
  child: Row(children: children),
)
xlints_avoid_controller_in_build

Detects controller/node creation inside build().

BAD:

@override
Widget build(BuildContext context) {
  final controller = ScrollController();
  return ListView(controller: controller);
}

BAD:

@override
Widget build(BuildContext context) {
  final focusNode = FocusNode();
  return TextField(focusNode: focusNode);
}

GOOD:

class _MyState extends State<MyWidget> {
  late final ScrollController controller;

  @override
  void initState() {
    super.initState();
    controller = ScrollController();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView(controller: controller);
  }
}
xlints_avoid_widget_operator_equals

Detects operator == override on Widget subclasses.

BAD:

class MyCard extends StatelessWidget {
  @override
  bool operator ==(Object other) => identical(this, other);

  @override
  int get hashCode => 0;

  @override
  Widget build(BuildContext context) => const SizedBox();
}

GOOD:

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

  @override
  Widget build(BuildContext context) => const SizedBox();
}
xlints_avoid_set_state_in_build

Detects setState() calls inside build().

BAD:

@override
Widget build(BuildContext context) {
  setState(() => counter++);
  return Text('$counter');
}

GOOD:

void _increment() {
  setState(() => counter++);
}
xlints_avoid_json_decode_in_build

Detects jsonDecode usage inside build().

BAD:

@override
Widget build(BuildContext context) {
  final data = jsonDecode(rawJson);
  return Text('${data['title']}');
}

GOOD:

late final Map<String, dynamic> data;

@override
void initState() {
  super.initState();
  data = jsonDecode(rawJson) as Map<String, dynamic>;
}
xlints_avoid_heavy_sync_work_in_build

Detects heavy synchronous collection work in build().

BAD:

@override
Widget build(BuildContext context) {
  final values = items.toList()..sort();
  return Text('${values.first}');
}

GOOD:

late final List<int> sorted;

@override
void initState() {
  super.initState();
  sorted = items.toList()..sort();
}
xlints_prefer_final_locals

Detects local var variables that are never reassigned.

BAD:

void logValue() {
  var message = 'ready';
  print(message);
}

GOOD:

void logValue() {
  final message = 'ready';
  print(message);
}
xlints_avoid_recreating_regexp

Detects repeated RegExp(...) creation in loops/build.

BAD:

for (final item in items) {
  final r = RegExp(r'\d+');
  if (r.hasMatch(item)) {}
}

GOOD:

final r = RegExp(r'\d+');
for (final item in items) {
  if (r.hasMatch(item)) {}
}
xlints_avoid_list_contains_in_large_loops

Detects list.contains(...) calls inside loops.

BAD:

for (final id in ids) {
  if (selectedIds.contains(id)) {}
}

GOOD:

final selectedSet = selectedIds.toSet();
for (final id in ids) {
  if (selectedSet.contains(id)) {}
}
xlints_avoid_repeated_datetime_now_in_loop

Detects repeated DateTime.now() calls inside loops.

BAD:

for (var i = 0; i < 100; i++) {
  final now = DateTime.now();
  consume(now);
}

GOOD:

final now = DateTime.now();
for (var i = 0; i < 100; i++) {
  consume(now);
}
xlints_prefer_collection_if_spread_over_temp_lists

Detects temporary list accumulation patterns better expressed with collection literals.

BAD:

final widgets = <Widget>[];
if (showHeader) widgets.add(const Text('Header'));
if (items.isNotEmpty) widgets.addAll(items.map(Text.new));

GOOD:

final widgets = <Widget>[
  if (showHeader) const Text('Header'),
  if (items.isNotEmpty) ...items.map(Text.new),
];
xlints_prefer_string_buffer

Detects string concatenation with + inside loops.

BAD:

var result = '';
for (var i = 0; i < 1000; i++) {
  result = result + values[i];
}

GOOD:

final buffer = StringBuffer();
for (var i = 0; i < 1000; i++) {
  buffer.write(values[i]);
}
final result = buffer.toString();

Prerequisites

  • Flutter SDK available in your environment.
  • Dart SDK compatible with this package (sdk: ^3.10.4).

Installation

Option A: from pub.dev

Add this to dev_dependencies:

dev_dependencies:
  xlints: ^1.0.0

Run:

flutter pub get

Option B: from a local path (for local development)

dev_dependencies:
  xlints:
    path: ../path-to-xlints

Run:

flutter pub get

Configure analysis_options.yaml

Option 1: Fresh project

Use the provided xlints config:

include: package:xlints/analysis_options_xlints.yaml

This enables the custom_lint plugin.

If your project already uses flutter_lints or another lint config, keep your current include: and add plugins: [custom_lint] under the analyzer section.

Before (your current config):

include: package:flutter_lints/flutter.yaml

linter:
  rules:

analyzer:
  errors:
    invalid_annotation_target: ignore
  enable-experiment:
    - dot-shorthands

After (add xlints):

include: package:flutter_lints/flutter.yaml

linter:
  rules:

analyzer:
  errors:
    invalid_annotation_target: ignore
  enable-experiment:
    - dot-shorthands
  plugins:
    - custom_lint

The only change is adding plugins: [custom_lint] under analyzer. Your existing rules, errors, and experiments stay as-is.

Notes for both options

  • You do not need to add custom_lint manually to pubspec.yaml; xlints bundles it.
  • You do not need to add analyzer.plugins manually if you use Option 1 (include xlints config).

Run Lints

Run from your Flutter app root:

dart run custom_lint

Alternative:

flutter pub run custom_lint

To auto-apply available fixes:

dart run custom_lint --fix

IDE Integration

  • Make sure analysis_options.yaml is configured correctly.
  • Reopen the project if warnings do not appear.
  • Use lightbulb Quick Fix for rules that provide fixes.

Rules With Quick Fix (Current)

  • xlints_prefer_const_constructors: adds the const keyword.
  • xlints_avoid_padding_wrapping_margin_widget: removes the outer Padding.

Rule Configuration

Disable specific rules by adding a custom_lint section to your analysis_options.yaml:

custom_lint:
  rules:
    - xlints_prefer_const_constructors: false
    - xlints_avoid_opacity_widget: false

Works with both Option 1 (xlints-only) and Option 2 (flutter_lints + xlints).

Example Usage

An example project is in the example/ directory in the GitHub repo (it is not published to pub.dev so that pub.dev scoring does not hit analyzer plugin errors).

Run from the repo:

cd example
dart run custom_lint

The example intentionally contains bad patterns to trigger all xlints rules.

Troubleshooting

include_file_not_found for package:xlints/analysis_options_xlints.yaml

Common causes:

  • flutter pub get has not been run.
  • xlints is not present in your dependency graph.
  • Incorrect local path dependency.

Checklist:

  1. Ensure pubspec.yaml includes xlints in dev_dependencies.
  2. Run flutter pub get again.
  3. Ensure the include line is exactly: include: package:xlints/analysis_options_xlints.yaml
  4. Run dart run custom_lint again.

Rules do not appear in IDE, but appear in CLI

  1. Reload the IDE window or restart the analysis server.
  2. Make sure the opened project root contains analysis_options.yaml.

Internal Dependencies

  • analyzer: ^8.4.0
  • custom_lint: ^0.8.1
  • custom_lint_builder: ^0.8.1

Libraries

xlints