Sangria Lints

Custom lints wanted for personal developments.

Table of content

Getting started

Add Sangria_lints

Add sangria_lints to your pubspec.yaml:

dev_dependencies:
  sangria_lints:

Enable custom_lint

sangria_lints comes bundled with its own rules using custom_lints.

  • Add both sangria_lints and custom_lint to your pubspec.yaml:

    dev_dependencies:
      sangria_lints:
      custom_lint: # <- add this
    
  • Enable custom_lint's plugin in your analysis_options.yaml:

    analyzer:
      plugins:
        - sangria_lints
    

Disabling lint rules

By default when installing sangria_lints, all the lints will be enabled. To change this, you have a few options.

analyzer:
  plugins:
    - custom_lint

custom_lint:
  rules:
    # Explicitly disable one custom-lint rule.
    - use_setstate_synchronously: false

All custom-lint rules in sangria_lints

use_setstate_synchronously

A use_setstate_synchronously rule that discourages the use of setState across asynchronous gaps within subclasses of State.

In async functions, the state of a widget may have been disposed across asynchronous gaps in a case when the user moves to a different screen. This leads to setState() called after dispose() error. Since widgets can be unmounted before a Future gets resolved, seeing if widgets are mounted is necessary before calling setState.

❌ BAD

class _MyWidgetState extends State<MyWidget> {
  String message;

  @override
  Widget build(BuildContext context) {
    return Button(
      onPressed: () async {
        String fromSharedPreference = await getFromSharedPreference();

        // LINT: Avoid calling 'setState' across asynchronous gaps without seeing if the widget is mounted.
        setState(() {
          message = fromSharedPreference;
        });
      },
      child: Text(message),
    );
  }
}

✅ GOOD

class _MyWidgetState extends State<MyWidget> {
  String message;

  @override
  Widget build(BuildContext context) {
    return Button(
      onPressed: () async {
        String fromSharedPreference = await getFromSharedPreference();
        if (mounted) {
          setState(() {
            message = fromSharedPreference;
          });
        }
      },
      child: Text(message),
    );
  }
}

avoid_empty_container

A avoid_empty_container rule that discourages the use of empty container.

Since Container generates many properties like padding, margin, decoration, and constraints, it is perfered to use SizedBox instead.

❌ BAD

class CustomText extends StatelessWidget {
  bool canDisplay;

  @override
  Widget build(BuildContext context) {
    return canDisplay ? Text('canDisplay') : Container();
  }
}

✅ GOOD

class CustomText extends StatelessWidget {
  bool canDisplay;

  @override
  Widget build(BuildContext context) {
    return canDisplay ? Text('canDisplay') : SizedBox.shrink();
  }
}

no_disabled_tests

A no_disabled_tests rule that raises a warning about disabled tests inspired by no-disabled-tests rule in plugins for Jest and Vitest.

Test and flutter_test package have a feature that allows you to temporarily mark tests as disabled. This feature is often helpful, however before committing changes we may want to check that all tests are running.

❌ BAD

void main() {
  group('test group', () {
    test('test', () {}, skip: true);

    testWidgets('test widgets', (tester) async {}, skip: true);
  }, skip: true)
}

✅ GOOD

void main() {
  group('test group', () {
    test('test', () {});

    testWidgets('test widgets', (tester) async {});
  })
}

use_widget_ref_synchronously

A use_widget_ref_synchronously rule that discourages the use of WidgetRef across asynchronous gaps within Consumer widgets provided by Riverpod.

In async functions, a widget may have been disposed across asynchronous gaps in a case when the user got back to previous screen. This leads to Cannot use ref after the widget was disposed called error. Since widgets can be unmounted before a Future gets resolved, seeing if widgets are mounted is necessary before using WidgetRef.

Riverpod recommends seeing if widgets are mounted to fix the error as well.

❌ BAD

ElevatedButton(
  onPressed: () async {
    await future;
    ref.read(...); // May throw "Cannot use "ref" after the widget was disposed"
  }
)

✅ GOOD

ElevatedButton(
  onPressed: () async {
    await future;
    if (!context.mounted) return;
    ref.read(...); // No longer throws
  }
)

Libraries

sangria_lints