Have you ever wondered "hey, what's that analysis_options.yaml file all about".
Rest assured that most people will never have to ever even open this file, never mind understand or have opinions about it. But that is all about to change, for you.
TLDR: Scroll to the bottom and copy my lint rules liamdoka flavored Flutter.
analysis_options.yaml
This file specifies the lint rules that your IDE and CLI will attempt to enforce while you're writing your code. By default, Flutter and Dart have recommended lint rules that will automatically be applied, and are in place to provide a consistent code-style and avoid many common foot-guns.
E.g: the
annotate_overrides
rule enforces that any time you override a member or method of a parent class,
it must be annotated with @override.
This is to prevent confusion about the ownership of methods.
A lot of the lint rules in the recommended set relate to naming conventions.
camel_case_extensions, camel_case_types, constant_identifier_names, file_names, library_prefixes, package_names, non_constant_identifier_names...
These all assist in keeping the code style consistent between projects.
Outside of these common cases, there exists an entire sea of undiscovered lint rules that help enforce your rigid programming opinions upon the whole code-base. Doesn't that sound positively utopian.
This is a tale about the pursuit of a perfect codebase.
The Journey
To begin, I copied and pasted the snippet found here into my analysis options file.
If you're in a capable IDE with an analysis server, the linter will begin to crawl the file and reveal that some rules do not play nice with others. You may call these, conflicting lints.
Conflicts
I'm going to list these conflicts and my proposed resolution. It's going to be in alphabetical order, not based on how interesting it is unfortunately.
This lint rule is great if you like very verbose code and you want treat the language server like it's an absolute fool.
By using this rule, you lose any sort of inference or "memory" of types that you've already specified.
1// BAD 2final foo = Foo(); 3final list = <Bar>[Bar(), Bar()]; 4list.map((bar) => bar.baz); 5 6// GOOD 7final Foo foo = Foo(); 8final List<Bar> list = [Bar(), Bar()]; 9list.map((Bar bar) => bar.baz);
This lint rule conflicts with a lot of other rules that preach "if a type can be inferred by the compiler, you don't need to waste keystrokes repeating it".
avoid_types_on_closure_parameters, omit_local_variable_types, omit_obvious_local_variable_types, and omit_obvious_property_types,
I tend to agree that if I can safely assume a type, I don't need to specify it every time.
final bar = Bar(); makes perfect sense to me, so I tend to give this lint rule the AXE.
Even the concept that this rule preaches was unknown to me at the time I discovered it.
It states, "by default, function parameters are variable so if they are not reassigned within the function
you must declare it with the final keyword."
1// BAD 2void badParameter(String label) { 3 print(label); 4} 5 6// GOOD 7void goodParameter(final String label) { 8 print(label); 9} 10 11void mutableParameter(String label) { 12 print(label); 13 label = "something else"; 14 print(label); 15}
The concept of being able to modify a parameter of a function from within the function feels evil to me. It begs the question "are parameters passed by reference or copied?" Just a disaster waiting to happen.
The concept is so repugnant that I delete this lint rule. It's too verbose to declare each parameter as final, and the value provided by doing so is preserved by the parameter_assignments rule.
Keep its counterpart, the avoid_final_parameters rule, to prevent unnecessary verbosity.
This rule states that for imports within the /lib directory,
it's preferable to use a relative path to import files.
1// BAD 2import 'package:my_package/bar.dart'; 3 4// GOOD 5import 'bar.dart'; 6import '../../../utils/some_utility.dart';
Given that the import code is typically generated for you by the IDE, the backtracking present in the "Good" example just increases the amount of confusion in where a file actually lives.
I don't particularly "care" either way, I tend to remove this rule in favor of its counterpart always_use_package_imports
prefer_single_quotes and prefer_double_quotes
Ah, a tale as old as time. "Thing", or 'Thing'.
The real answer is "OMG WHO CARES". Delete both of these rules because it actually doesn't matter.
If a type can be simply inferred, prefer to remove it!
1// BAD 2final int myInt = 7; 3final String myString = "Hello"; 4 5// GOOD 6final myInt = 7; 7final myString = "Hello";
While this one is nice in theory, it gets you into the habit of omitting perhaps vital information about a variable.
For instance, final myDouble = 1; would end up resolving to an integer, and may cause further errors down the track.
I tend to remove this rule, and use good old common sense as to when I want more specificity within a definition.
There are two idiomatic patterns for declaring local variables in dart:
- •Use
finalfor local variables that are not reassigned andvarfor those that are. - •Use
varfor all local variables, even ones that aren't reassigned. Never usefinalfor locals. (Usingfinalfor fields and top-level variables is still encouraged, of course.)
This rule aims to enforce the second one.
Coming from a TypeScript background myself, I'm already well versed in the "always use const" mindset that comes with effective TypeScript code.
This rule goes against everything I know about declaring your intent for a variable, so I remove it in favor of the prefer_final_locals rule.
This also concludes the long list of conflicting rules.
Pain Points
These are some lint rules that don't conflict with any other rules, but conflict with my brain and my eyes and my convictions about what "good code" is.
- •public_member_api_docs
- •Annotate every single public class, method, and member with a doc comment. No exceptions.
- •I have exceptions.
- •sort_constructors_first
- •In a class, constructors come before the member definitions.
- •It's fine, I just prefer the other way.
- •always_put_control_body_on_new_line
- •This one prevents inline bodies.
- •I like to do
if (x == null) return null;so I disable it.
- •always_put_required_named_parameters_first
- •When defining parameters, put all required parameters first.
- •I actually like this one, but I'd also prefer
superparameters to be before therequiredones, and my IDE does not allow for that with this rule enabled.
- •prefer_expression_function_bodies
- •If a function rule fits on one line, then use an expression
=>to define it. - •Again, I actually really like this one. My issue with it is that in Flutter, a lot of functions simply return
a single widget. In active development, you don't want to be wasting time converting between classic function
bodies and expression function bodies every time you remove other calculations from a
build()method.
- •If a function rule fits on one line, then use an expression
- •unawaited_futures
- •If you want to call an async function and return from the caller before it finishes, you must annotate it with
unawaited(func) - •Overly verbose when I dont want to think about these futures.
Also runs into the active development issue where you don't want to have to change a function signature or wrap a
call in
unawaitedevery time you change something.
- •If you want to call an async function and return from the caller before it finishes, you must annotate it with
Favourites
There was actually some good to come out of this experiment. It allowed me to find rules, not yet in the Flutter recommended lints package, that aligned with my personal code-style which is an amalgamation, born from years of jumping between different languages and their conventions.
Looking through the lint rules even taught me about some of the language features that I had no idea even existed;
namely the use_raw_strings rule.
- •always_use_package_imports
- •Package imports keep imports looking streamlined, without backtracking.
- •parameter_assignments
- •Prevent parameters from being re-assigned within the function bodies.
- •prefer_const_declarations
- •If a final variable is pointing to a const Class, it's not really a variable, make it const.
- •For instance
final o = const <int>[];vs.const o = <int>[];
- •prefer_const_constructors
- •If something can be const, make it const. There will be so few exceptions that annotating it every time is worth it.
- •prefer_final_locals
- •All local variables should be final unless they're explicitly mutated.
- •prefer_final_in_for_each.
- •In for-each, the variables should be set as final.
- •
for (final word in words), for instance.
- •unnecessary_lambdas
- •Prefer to use tear-offs if possible.
- •
values.forEach((value) => print(value)); // BAD - •
values.forEach(print); // GOOD
- •use_colored_box
- •If you have a
Container()widget with only acolorproperty, remove the bulky container and replace it with a slimconst ColoredBox.
- •If you have a
- •use_decorated_box
- •If you have a
Container()widget with only adecorationproperty, remove the bulky container and replace it with a slimconst DecoratedBox.
- •If you have a
- •use_enums
- •If your class looks suspiciously like an enum, make it a rich enum instead.
- •use_raw_strings
- •Instead of faffing about trying to escape characters in strings, just use raw strings.
- •i.e.
r'$12.34', instead of'\$12.34'.
- •use_named_constants
- •If something has a pre-defined const factory or constructor, use that instead of making it yourself.
- •E.g
Duration.zeroinstead ofconst Duration(seconds: 0)or equivalent.
- •use_null_aware_elements
- •Dart 3.8 introduces the idea of "null-aware elements", which makes it trivial to exclude items from a collection if it is null.
- •Instead of
if (nullable != null) nullable, you can simply write?nullable.
LiamDoka-flavoured Flutter
After this treacherous, yet thorough, expedition into rules, I've come away with a nice minimal set of rules that keep my code structured, concise, and light-weight.
To effectively add these to your project, copy the snippet below into your analysis_options.yaml file.
1include: package:flutter_lints/flutter.yaml 2 3linter: 4 rules: 5 - always_use_package_imports 6 - parameter_assignments 7 - prefer_const_declarations 8 - prefer_const_constructors 9 - constant_identifier_names 10 - prefer_final_locals 11 - prefer_final_in_for_each 12 - unnecessary_lambdas 13 - use_colored_box 14 - use_decorated_box 15 - use_enums 16 - use_raw_strings 17 - use_named_constants 18 - use_null_aware_elements
Then open up your terminal and run a dart fix --apply
and throw in a dart format . while you're there.