1, Variable
For local variables, the consistent rules of var and final must be followed.
Most local variables should not have type annotations, but should only be declared with var or final. When to use another rule there are two widely used rules:
For unallocated local variables, use final; For those reallocating local variables, use var.
Use var for all local variables, even if there is no reallocation. Do not use final on local variables. (of course, final is still recommended for fields and top-level variables.)
Any rule is acceptable, but select a rule and apply it consistently throughout the code. In this way, when readers see var, they will know whether this means that the variable will be allocated later in the function.
Avoid storing what you can calculate.
When designing classes, you usually want to expose multiple views to the same underlying state. Typically, you will see the code that evaluates all these views in the constructor and then stores them:
Poor writing
class Circle { double radius; double area; double circumference; Circle(double radius) : radius = radius, area = pi * radius * radius, circumference = pi * 2.0 * radius; }
There are two errors in this code. First, this is likely to waste memory. Strictly speaking, area and perimeter are caches. They are stored calculations that we can recalculate from other data we already have. They are swapping increased memory to reduce CPU usage. Do we know we have a performance issue worth weighing?
Worse, the code is wrong. The problem with caching is invalidity - how do you know when the cache expires and needs to be recalculated? Here, even if the radius is variable, we will never do so. You can assign a different value. The area and perimeter will retain the previous value, which is now an incorrect value.
In order to properly handle cache invalidation, we need to do this:
Poor writing
class Circle { double _radius; double get radius => _radius; set radius(double value) { _radius = value; _recalculate(); } double _area = 0.0; double get area => _area; double _circumference = 0.0; double get circumference => _circumference; Circle(this._radius) { _recalculate(); } void _recalculate() { _area = pi * _radius * _radius; _circumference = pi * 2.0 * _radius; } }
Write, maintain, debug and read a lot of code. Instead, your first implementation should
Good writing
class Circle { double radius; Circle(this.radius); double get area => pi * radius * radius; double get circumference => pi * 2.0 * radius; }
This code is shorter, uses less memory, and has fewer errors. It stores the minimum amount of data needed to represent a circle. No fields are out of sync because there is only one fact source.
In some cases, you may need to cache the results of slow calculations, but do so only after you know the performance problem. Please handle it carefully and explain the optimization content in the comments.
2, Members
In Dart, objects have members that can be functions (Methods) or data (instance variables). The following best practices apply to members of objects.
Don't wrap fields unnecessarily in getter s and setter s.
In Java and C, all fields are usually hidden behind getters and setter s (or properties in C), even if the implementation just forwards to the field. This way, if you need to do more work among these members, you don't need to touch call sites. This is because calling getter method is different from accessing fields in Java, and accessing properties is not binary compatible with accessing original fields in C.
Dart does not have this restriction. Fields and getters/setters are completely indistinguishable. You can expose a field in a class and wrap it in a getter and setter without touching any code that uses the field.
Good writing
class Box { var contents; }
Poor writing
class Box { var _contents; get contents => _contents; set contents(value) { _contents = value; } }
Use the final field first to create read-only attributes.
If you have a field that external code should see but cannot assign to it, a simple solution that can be used in many cases is to simply mark it final.
Good writing
class Box { final contents = []; }
Poor writing
class Box { var _contents; get contents => _contents; }
Consider = > for simple members.
In addition to = > for function expressions, Dart allows you to use it to define members. This style is ideal for simple members that only evaluate and return values.
Good writing
double get area => (right - left) * (bottom - top); String capitalize(String name) => '${name[0].toUpperCase()}${name.substring(1)}';
You can also use = > on members that do not return a value. When the setter is small and has a corresponding getter to use, this is customary = >.
Good writing
num get x => center.x; set x(num value) => center = Point(value, center.y);
Do not use this In addition to redirecting to named constructors or avoiding masking.
JavaScript needs to be explicit. Refers to the member on the object whose method is currently executing, but Dart (such as C + +, Java and C) does not have this restriction.
You just need to use this Once, when a local variable with the same name masks the member you want to access:
Poor writing
class Box { var value; void clear() { this.update(null); } void update(value) { this.value = value; } }
Good writing
class Box { var value; void clear() { update(null); } void update(value) { this.value = value; } }
Another use time is this When redirecting to a named constructor:
Poor writing
class ShadeOfGray { final int brightness; ShadeOfGray(int val) : brightness = val; ShadeOfGray.black() : this(0); // This won't parse or compile! // ShadeOfGray.alsoBlack() : black(); }
Good writing
class ShadeOfGray { final int brightness; ShadeOfGray(int val) : brightness = val; ShadeOfGray.black() : this(0); // But now it will! ShadeOfGray.alsoBlack() : this.black(); }
Note that constructor parameters never mask fields in the constructor initializer list:
Good writing
class Box extends BaseBox { var value; Box(value) : value = value, super(value); }
This may seem surprising, but it works the way you want. Fortunately, such code is relatively rare due to the initialized form.
Initialize fields at declaration time whenever possible.
If a field does not depend on any constructor parameters, it can and should be initialized in its declaration. When a class has multiple constructors, it will cost less code and avoid duplication
Poor writing
class ProfileMark { final String name; final DateTime start; ProfileMark(this.name) : start = DateTime.now(); ProfileMark.unnamed() : name = '', start = DateTime.now(); }
Good writing
class ProfileMark { final String name; final DateTime start = DateTime.now(); ProfileMark(this.name); ProfileMark.unnamed() : name = ''; }
Some fields cannot be initialized in their declarations because they need to reference} This - for example, to use other fields or call methods. However, if the field late is marked, the initializer can access this.
Of course, this guideline does not apply if the field depends on constructor parameters or is initialized differently by different constructors.
3, Constructor
Use initialization whenever possible.
Many fields are initialized directly from constructor parameters, for example:
Poor writing
class Point { double x, y; Point(double x, double y) : x = x, y = y; }
Good writing
class Point { double x, y; Point(this.x, this.y); }
Do not use the late constructor initializer list when it will be used.
Because the security of null requires Dart to ensure that non nullable fields must be initialized before reading. Since fields can be read inside the constructor body, you will receive an error message if you do not initialize non nullable fields before the body runs.
You can eliminate this error by marking the field late. If you access a field before initializing it, a compile time error is converted to a run-time error. In some cases, this is what you need, but usually the correct solution is to initialize the fields in the constructor initializer list:
Good writing
class Point { double x, y; Point.polar(double theta, double radius) : x = cos(theta) * radius, y = sin(theta) * radius; }
Poor writing
class Point { late double x, y; Point.polar(double theta, double radius) { x = cos(theta) * radius; y = sin(theta) * radius; } }
The initializer list gives you access to constructor parameters and initializes fields before they are read. Therefore, if you can use the initialization list, it is better than delaying field work and losing some static security and performance.
To be used; Replace {} empty constructor body
In Dart, constructors with empty bodies can be terminated with semicolons only. (in fact, it is necessary for const constructor.)
Good writing
class Point { double x, y; Point(this.x, this.y); }
Poor writing
class Point { double x, y; Point(this.x, this.y) {} }
Do not use new.
Dart 2 makes the new keyword optional. Even in Dart 1, the meaning is never clear, because the factory constructor means that the new call may not actually return a new object.
The language still allows for ease of new migration, but consider not using it and removing it from your code.
Good writing
Widget build(BuildContext context) { return Row( children: [ RaisedButton( child: Text('Increment'), ), Text('Click!'), ], ); }
Poor writing
Widget build(BuildContext context) { return new Row( children: [ new RaisedButton( child: new Text('Increment'), ), new Text('Click!'), ], ); }
Do not use const for redundancy.
When the expression must be constant, the const keyword is implicit and does not need to be written. These contexts are any of the expressions:
const set text.
const constructor call
Metadata annotation.
Initializer for const variable declaration.
Convert case expression - the case is followed by the part immediately before: instead of the body of the case.
(default values are not included in this list because future versions of Dart may support non const defaults.)
Basically, Dart 2 allows you to omit const anywhere you write new instead of const.
Good writing
const primaryColors = [ Color("red", [255, 0, 0]), Color("green", [0, 255, 0]), Color("blue", [0, 0, 255]), ];
Poor writing
const primaryColors = const [ const Color("red", const [255, 0, 0]), const Color("green", const [0, 255, 0]), const Color("blue", const [0, 0, 255]), ];
4, Error handling
Avoid catching without an on clause.
A catch clause without an on qualifier captures anything thrown by the code in the try block. "Magic Baby exception (terminology)" handling is probably not what you want. Does your code handle StackOverflowError or OutOfMemoryError correctly? If you mistakenly pass the wrong parameter to the method in the try block, do you want the debugger to point you to the error, or do you want to swallow the useful ArgumentError? Do you want to capture the assert () statement of all AssertionErrors that throw assertions in your code?
The answer may be "no", in which case you should filter the type of capture. In most cases, there should be an on clause that limits you to the types of runtime failures that you know and handle correctly.
In rare cases, you may want to catch any runtime errors. This is usually in frameworks or low-level code that try to isolate arbitrary application code to avoid causing problems. Even here, it's usually better to catch exceptions than all types. Exception s are the base class for all runtime errors and exclude errors that indicate programming errors in the code.
Do not discard captured errors without an on clause.
If you really need to capture everything that might be thrown from the code area, process the captured content. Record it, show it to the user or throw it away again, but don't silently discard it.
Don't just throw an object that implements Error for program errors.
The Error class is the base class for program errors. When an object of this type or one of its sub interfaces (such as ArgumentError) is thrown, it means that there is an Error in the code. When your API wants to report the wrong use of the API to the caller, throwing an Error will clearly send the signal.
On the contrary, if the Exception is some kind of run-time fault and does not mean that there is an error in the code, throwing an error will be misleading. Instead, throw one of the core Exception classes or some other type.
Do not explicitly capture an Error or the type that implements it.
This is derived from the above. Since "error" indicates an error in the code, it should expand the entire call stack, pause the program, and print the stack trace so that you can find and fix the error.
Capturing these types of errors interrupts the process and masks the error. After the fact occurs, instead of adding error handling code to handle this exception, go back and fix the code that caused the exception to be thrown first.
Use rethrow to re throw the caught exception.
It is better to use the rethrow statement instead of throwing the same exception. Re throw the original stack trace that retains the exception. Throw, on the other hand, resets the stack trace to where it was last thrown.
Poor writing
try { somethingRisky(); } catch (e) { if (!canHandle(e)) throw e; handle(e); }
Good writing
try { somethingRisky(); } catch (e) { if (!canHandle(e)) rethrow; handle(e); }
5, Asynchronous
The original feature async/await is preferred.
As we all know, even with beautiful abstract methods such as Future, asynchronous code is difficult to read and debug. async/await syntax improves readability and allows you to use all Dart control flow structures in asynchronous code.
Good writing
Future<int> countActivePlayers(String teamName) async { try { var team = await downloadTeam(teamName); if (team == null) return 0; var players = await team.roster; return players.where((player) => player.isActive).length; } catch (e) { log.error(e); return 0; } }
Poor writing
Future<int> countActivePlayers(String teamName) { return downloadTeam(teamName).then((team) { if (team == null) return Future.value(0); return team.roster.then((players) { return players.where((player) => player.isActive).length; }); }).catchError((e) { log.error(e); return 0; }); }
async should not be used when it has no useful effect.
It's easy to get into the habit of using asynchrony on any function related to asynchrony. But in some cases, this is redundant. Do this if you can ignore asynchrony without changing the behavior of the feature.
Good writing
Future<int> fastestBranch( Future<int> left, Future<int> right) { return Future.any([left, right]); }
Poor writing
Future<int> fastestBranch(Future<int> left, Future<int> right) async { return Future.any([left, right]); }
Asynchronous is useful when:
You are using wait. (this is obvious.)
You are returning an error asynchronously. Asynchronously, and then throw the ratio to return future error(...) Short.
You are returning a value and want to wrap it implicitly in the future. Asynchronous ratio future value(...) Short.
Good writing
Future<void> usesAwait(Future<String> later) async { print(await later); } Future<void> asyncError() async { throw 'Error!'; } Future<void> asyncValue() async => 'value';
Consider using a higher-order method to convert the flow.
This is similar to the above recommendations for iteratable objects. Streaming supports many of the same methods, and also correctly handles transmission errors, shutdown and other problems.
Avoid using Completer directly.
Many novice asynchronous programmers want to write code that can create the Future. The constructor in Future didn't seem to meet their needs, so they finally found and used the Completer class.
Poor writing
Future<bool> fileContainsBear(String path) { var completer = Completer<bool>(); File(path).readAsString().then((contents) { completer.complete(contents.contains('bear')); }); return completer.future; }
Two types of low-level code require the use of Completer: new asynchronous primitives and interfaces with asynchronous code that does not use features. Most other code should use async / await or future Then (), because they are clearer and make error handling easier.
Good writing
Future<bool> fileContainsBear(String path) { return File(path).readAsString().then((contents) { return contents.contains('bear'); }); }
Future<bool> fileContainsBear(String path) async { var contents = await File(path).readAsString(); return contents.contains('bear'); }
When the parameter type of futureor < T > may be Object, please test future < T >.
Before doing anything useful for futureor < T >, you usually need to check whether there is future < T > or bare t. If the type parameter is some specific type, such as futureor < int >, neither int nor future < int > matters no matter which test you use. It works because the two types are disjoint.
However, if the value type is object or a type parameter that may be instantiated with object, the two branches overlap. Future < Object > implements object itself, so object is still T, where t is some type parameters that can be instantiated with object. Even if the object is future, it returns true. Instead, explicitly test the future case:
Good writing
Future<T> logValue<T>(FutureOr<T> value) async { if (value is Future<T>) { var result = await value; print(result); return result; } else { print(value); return value; } }
Poor writing
Future<T> logValue<T>(FutureOr<T> value) async { if (value is T) { print(value); return value; } else { var result = await value; print(result); return result; } }
In the poor writing example, if you pass future < Object > to it, it will be mistakenly regarded as a bare synchronization value.