Tomáš Repčík - 24. 8. 2025

Essential Flutter Lint Rules: A Categorized Guide

A concise guide to the Flutter lint rules you need to know

Flutter Preview

Flutter comes in with a lot of built-in tooling, and one of them is its linter. It is highly opinionated and helps enforce best practices across your codebase. You can use your own set and add even more custom ones coming from packages, or even build your own.

In my opinion, existing linters cover all the essential areas and only if you use some package which is accompanied by its own lint rules, you might need to add more specific ones.

Here are categories and list of lint rules, which I find useful:

Code Consistency

linter:
  rules:
    - always_put_control_body_on_new_line
    - avoid_positional_boolean_parameters
    - avoid_field_initializers_in_const_classes
    - flutter_style_todos
    - matching_super_parameters
    - leading_newlines_in_multiline_strings
    - sort_pub_dependencies
    - unnecessary_breaks
    - unnecessary_raw_strings
    - unnecessary_underscores
    - use_named_constants
    - use_to_and_as_if_applicable
// avoid_positional_boolean_parameters
Widget setUser({required bool isEnabled}) {} // avoid using build setUser(true)

// matching_super_parameters
class MyWidget extends StatelessWidget {
  const MyWidget({super.key}); // super.key is key for StatelessWidget, not other name
}

// flutter_style_todos
// TODO(github_username): Implement the widget's build method

// use_named_constants
Duration.zero; // some classes offer named constants, use them

// unnecessary_breaks
switch (value) {
  case 1:
    doSomething(); // break is redundant here
  case 2:
    doSomethingElse();
}

// use_to_and_as_if_applicable
Foo toFoo() => Foo(); // `to` for copying object state / `as` to convert it to another type

These rules ensure that code is more readable without duplication and explicit intent.

You would be amazed at how much I have almost mixed up super parameters, because the naming from one object to another has changed.

Most of them are cosmetic changes, but they add up to a more consistent and cleaner codebase.

Type Safety

linter:
  rules:
    - always_declare_return_types
    - avoid_dynamic_calls
    - no_runtimeType_toString
    - omit_obvious_local_variable_types
    - omit_obvious_property_types
    - only_throw_errors
    - prefer_void_to_null
    - use_enums
    - use_null_aware_elements
// always_declare_return_types
String getUserName() => 'John Doe';  // Explicit return type, no dynamic return 

// avoid_dynamic_calls - this should not happen, define the type
Future<void> processData(dynamic data) async {}

// avoid_void_async
Future<void> saveData() async {}  // every async should return Future<T>

// only_throw_errors
throw ArgumentError('Input cannot be empty');  // Throw Error types, not random classes

// use_enums
enum ConnectionStatus { connected, disconnected, connecting }  // limit your options by enums/sealed classes

// omit_obvious_local_variable_types
var controller = TextEditingController();  // Type is obvious
final widgets = <Widget>[];  // Generic type is explicit where needed

Any mismatched types can lead to runtime errors, which you will not be aware of, only if you actually run the code path. Let the compiler help you catch these issues early by defining all the types as they should be.

Prefer to use as specific types as possible. Limit your options with sealed classes or enums instead of strings or integers.

Moreover, defined types will increase code readability and maintainability. It is clear and explicit what each variable represents, making it easier for others (and your future self) to understand the code.

📧 Get more content like this in your inbox

Avoiding Issues with Futures

linter:
  rules:
    - avoid_slow_async_io
    - avoid_void_async
    - unawaited_futures
// avoid_slow_async_io
String readConfigSync() {
  return File('config.txt').existsSync();  // is much faster than async equivalent
}

// avoid_void_async
Future<void> saveData() async {}  // Use Future<void> instead of void

// unawaited_futures
Future<void> saveAndNavigate() async {
  await saveData(); // Explicit await ensures completion
  unawaited(Navigator.push(context, route)); // Explicit unawaited usage
}

In my opinion, these are unsung heroes of Flutter development because they can help you reduce bottlenecks and missing await statements. I run into multiple issues when I just forgot to await something and it caused weird bugs.

This way, everything is explicit and clear.

Immutability Rules

linter:
  rules:
    - prefer_const_constructors
    - prefer_constructors_over_static_methods
    - prefer_final_in_for_each
    - prefer_final_locals
// prefer_const_constructors
const widget = Text('Hello');

// prefer_final_in_for_each
final items = [1, 2, 3];
for (final item in items) {}

// prefer_final_locals
final name = 'Hello';

I prefer to use final variables whenever possible for a simple reason: most of the time, the variable does not need to change. Final ensures that the variable cannot be reassigned, providing better safety and clarity.

Even though the usage of const has minimal performance impact, it is still a good practice to use it whenever possible. If it is possible, why not take advantage of it?

Conclusion

I have not mentioned all the small lint rules, which are useful, but here is a full list:

linter:
  rules:
    - always_declare_return_types
    - always_put_control_body_on_new_line
    - always_put_required_named_parameters_first
    - avoid_catching_errors
    - avoid_dynamic_calls
    - avoid_field_initializers_in_const_classes
    - avoid_positional_boolean_parameters
    - avoid_slow_async_io
    - avoid_void_async
    - do_not_use_environment
    - flutter_style_todos
    - leading_newlines_in_multiline_strings
    - matching_super_parameters
    - no_runtimeType_toString
    - omit_obvious_local_variable_types
    - omit_obvious_property_types
    - only_throw_errors
    - prefer_const_constructors
    - prefer_constructors_over_static_methods
    - prefer_final_in_for_each
    - prefer_final_locals
    - prefer_int_literals
    - prefer_mixin
    - prefer_null_aware_method_calls
    - prefer_void_to_null
    - sort_pub_dependencies
    - throw_in_finally
    - unawaited_futures
    - unnecessary_breaks
    - unnecessary_raw_strings
    - unnecessary_underscores
    - use_colored_box
    - use_decorated_box
    - use_enums
    - use_named_constants
    - use_null_aware_elements
    - use_truncating_division
    - use_to_and_as_if_applicable

These lint rules work together to create a more maintainable, readable and bug-free Flutter codebase.

They enforce better practices, but they are not silver bullets.

Introduce them gradually, so you don’t have to rework your entire codebase at once.

Socials

Thanks for reading this article!

For more content like this, follow me here or on X or LinkedIn.

Subscribe for more
LinkedIn GitHub Medium X