Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

dart.dev uses cookies from Google to deliver and enhance the quality of its services and to analyze traffic.

Learn more
Effective Dart: Usage

Dart 3.10 is taking off with dot shorthands, stable build hooks, nuanced deprecation annotations, and more!Learn more

You can use these guidelines every day in the bodies of your Dart code.Users of your library may not be able to tell that you've internalized the ideas here, butmaintainers of it sure will.

Libraries

#

These guidelines help you compose your program out of multiple files in a consistent, maintainable way. To keep these guidelines brief, they use "import" to coverimport andexport directives. The guidelines apply equally to both.

DO use strings inpart of directives

#

Linter rule:use_string_in_part_of_directives

Many Dart developers avoid usingpart entirely. They find it easier to reason about their code when each library is a single file. If you do choose to usepart to split part of a library out into another file, Dart requires the other file to in turn indicate which library it's a part of.

Dart allows thepart of directive to use thename of a library. Naming libraries is a legacy feature that is nowdiscouraged. Library names can introduce ambiguity when determining which library a part belongs to.

The preferred syntax is to use a URI string that points directly to the library file. If you have some library,my_library.dart, that contains:

my_library.dart
dart
librarymy_library;part'some/other/file.dart';

Then the part file should use the library file's URI string:

gooddart
partof'../../my_library.dart';

Not the library name:

baddart
partofmy_library;

DON'T import libraries that are inside thesrc directory of another package

#

Linter rule:implementation_imports

Thesrc directory underlibis specified to contain libraries private to the package's own implementation. The way package maintainers version their package takes this convention into account. They are free to make sweeping changes to code undersrc without it being a breaking change to the package.

That means that if you import some other package's private library, a minor, theoretically non-breaking point release of that package could break your code.

DON'T allow an import path to reach into or out oflib

#

Linter rule:avoid_relative_lib_imports

Apackage: import lets you access a library inside a package'slib directory without having to worry about where the package is stored on your computer. For this to work, you cannot have imports that require thelib to be in some location on disk relative to other files. In other words, a relative import path in a file insidelib can't reach out and access a file outside of thelib directory, and a library outside oflib can't use a relative path to reach into thelib directory. Doing either leads to confusing errors and broken programs.

For example, say your directory structure looks like this:

  • my_package/
    • lib/
      • api.dart
    • test/
      • api_test.dart

And sayapi_test.dart importsapi.dart in two ways:

api_test.dart
baddart
import'package:my_package/api.dart';import'../lib/api.dart';

Dart thinks those are imports of two completely unrelated libraries. To avoid confusing Dart and yourself, follow these two rules:

  • Don't use/lib/ in import paths.
  • Don't use../ to escape thelib directory.

Instead, when you need to reach into a package'slib directory (even from the same package'stest directory or any other top-level directory), use apackage: import.

api_test.dart
gooddart
import'package:my_package/api.dart';

A package should never reachout of itslib directory and import libraries from other places in the package.

PREFER relative import paths

#

Linter rule:prefer_relative_imports

Whenever the previous rule doesn't come into play, follow this one. When an import doesnot reach acrosslib, prefer using relative imports. They're shorter. For example, say your directory structure looks like this:

  • my_package/
    • lib/
      • src/
        • stuff.dart
        • utils.dart
      • api.dart
    • test/
      • api_test.dart
      • test_utils.dart

Here is how the various libraries should import each other:

lib/api.dart
gooddart
import'src/stuff.dart';import'src/utils.dart';
lib/src/utils.dart
gooddart
import'../api.dart';import'stuff.dart';
test/api_test.dart
gooddart
import'package:my_package/api.dart';// Don't reach into 'lib'.import'test_utils.dart';// Relative within 'test' is fine.

Null

#

DON'T explicitly initialize variables tonull

#

Linter rule:avoid_init_to_null

If a variable has a non-nullable type, Dart reports a compile error if you try to use it before it has been definitely initialized. If the variable is nullable, then it is implicitly initialized tonull for you. There's no concept of "uninitialized memory" in Dart and no need to explicitly initialize a variable tonull to be "safe".

gooddart
Item?bestDeal(List<Item>cart){Item?bestItem;for(finalitemincart){if(bestItem==null||item.price<bestItem.price){bestItem=item;}}returnbestItem;}
baddart
Item?bestDeal(List<Item>cart){Item?bestItem=null;for(finalitemincart){if(bestItem==null||item.price<bestItem.price){bestItem=item;}}returnbestItem;}

DON'T use an explicit default value ofnull

#

Linter rule:avoid_init_to_null

If you make a nullable parameter optional but don't give it a default value, the language implicitly usesnull as the default, so there's no need to write it.

gooddart
voiderror([String?message]){stderr.write(message??'\n');}
baddart
voiderror([String?message=null]){stderr.write(message??'\n');}

DON'T usetrue orfalse in equality operations

#

Using the equality operator to evaluate anon-nullable boolean expression against a boolean literal is redundant. It's always simpler to eliminate the equality operator, and use the unary negation operator! if necessary:

gooddart
if(nonNullableBool){...}if(!nonNullableBool){...}
baddart
if(nonNullableBool==true){...}if(nonNullableBool==false){...}

To evaluate a boolean expression thatis nullable, you should use?? or an explicit!= null check.

gooddart
// If you want null to result in false:if(nullableBool??false){...}// If you want null to result in false// and you want the variable to type promote:if(nullableBool!=null&&nullableBool){...}
baddart
// Static error if null:if(nullableBool){...}// If you want null to be false:if(nullableBool==true){...}

nullableBool == true is a viable expression, but shouldn't be used for several reasons:

  • It doesn't indicate the code has anything to do withnull.

  • Because it's not evidentlynull related, it can easily be mistaken for the non-nullable case, where the equality operator is redundant and can be removed. That's only true when the boolean expression on the left has no chance of producing null, but not when it can.

  • The boolean logic is confusing. IfnullableBool is null, thennullableBool == true means the condition evaluates tofalse.

The?? operator makes it clear that something to do with null is happening, so it won't be mistaken for a redundant operation. The logic is much clearer too; the result of the expression beingnull is the same as the boolean literal.

Using a null-aware operator such as?? on a variable inside a condition doesn't promote the variable to a non-nullable type. If you want the variable to be promoted inside the body of theif statement, it's better to use an explicit!= null check instead of??.

AVOIDlate variables if you need to check whether they are initialized

#

Dart offers no way to tell if alate variable has been initialized or assigned to. If you access it, it either immediately runs the initializer (if it has one) or throws an exception. Sometimes you have some state that's lazily initialized wherelate might be a good fit, but you also need to be able totell if the initialization has happened yet.

Although you could detect initialization by storing the state in alate variable and having a separate boolean field that tracks whether the variable has been set, that's redundant because Dartinternally maintains the initialized status of thelate variable. Instead, it's usually clearer to make the variable non-late and nullable. Then you can see if the variable has been initialized by checking fornull.

Of course, ifnull is a valid initialized value for the variable, then it probably does make sense to have a separate boolean field.

CONSIDER type promotion or null-check patterns for using nullable types

#

Checking that a nullable variable is not equal tonull promotes the variable to a non-nullable type. That lets you access members on the variable and pass it to functions expecting a non-nullable type.

Type promotion is only supported, however, for local variables, parameters, and private final fields. Values that are open to manipulationcan't be type promoted.

Declaring membersprivate andfinal, as we generally recommend, is often enough to bypass these limitations. But, that's not always an option.

One pattern to work around type promotion limitations is to use anull-check pattern. This simultaneously confirms the member's value is not null, and binds that value to a new non-nullable variable of the same base type.

gooddart
classUploadException{finalResponse?response;UploadException([this.response]);@overrideStringtoString(){if(this.responsecasevarresponse?){return'Could not complete upload to${response.url}''(error code${response.errorCode}):${response.reason}.';}return'Could not upload (no response).';}}

Another work around is to assign the field's value to a local variable. Null checks on that variable will promote, so you can safely treat it as non-nullable.

gooddart
classUploadException{finalResponse?response;UploadException([this.response]);@overrideStringtoString(){finalresponse=this.response;if(response!=null){return'Could not complete upload to${response.url}''(error code${response.errorCode}):${response.reason}.';}return'Could not upload (no response).';}}

Be careful when using a local variable. If you need to write back to the field, make sure that you don't write back to the local variable instead. (Making the local variablefinal can prevent such mistakes.) Also, if the field might change while the local is still in scope, then the local might have a stale value.

Sometimes it's best to simplyuse! on the field. In some cases, though, using either a local variable or a null-check pattern can be cleaner and safer than using! every time you need to treat the value as non-null:

baddart
classUploadException{finalResponse?response;UploadException([this.response]);@overrideStringtoString(){if(response!=null){return'Could not complete upload to${response!.url}''(error code${response!.errorCode}):${response!.reason}.';}return'Could not upload (no response).';}}

Strings

#

Here are some best practices to keep in mind when composing strings in Dart.

DO use adjacent strings to concatenate string literals

#

Linter rule:prefer_adjacent_string_concatenation

If you have two string literals—not values, but the actual quoted literal form—you do not need to use+ to concatenate them. Just like in C and C++, simply placing them next to each other does it. This is a good way to make a single long string that doesn't fit on one line.

gooddart
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other''parts are overrun by martians. Unclear which are which.',);
baddart
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other'+'parts are overrun by martians. Unclear which are which.',);

PREFER using interpolation to compose strings and values

#

Linter rule:prefer_interpolation_to_compose_strings

If you're coming from other languages, you're used to using long chains of+ to build a string out of literals and other values. That does work in Dart, but it's almost always cleaner and shorter to use interpolation:

gooddart
'Hello,$name! You are${year - birth} years old.';
baddart
'Hello,'+name+'! You are'+(year-birth).toString()+' y...';

Note that this guideline applies to combiningmultiple literals and values. It's fine to use.toString() when converting only a single object to a string.

AVOID using curly braces in interpolation when not needed

#

Linter rule:unnecessary_brace_in_string_interps

If you're interpolating a simple identifier not immediately followed by more alphanumeric text, the{} should be omitted.

gooddart
vargreeting='Hi,$name! I love your${decade}s costume.';
baddart
vargreeting='Hi,${name}! I love your${decade}s costume.';

Collections

#

Out of the box, Dart supports four collection types: lists, maps, queues, and sets. The following best practices apply to collections.

DO use collection literals when possible

#

Linter rule:prefer_collection_literals

Dart has three core collection types: List, Map, and Set. The Map and Set classes have unnamed constructors like most classes do. But because these collections are used so frequently, Dart has nicer built-in syntax for creating them:

gooddart
varpoints=<Point>[];varaddresses=<String,Address>{};varcounts=<int>{};
baddart
varaddresses=Map<String,Address>();varcounts=Set<int>();

Note that this guideline doesn't apply to thenamed constructors for those classes.List.from(),Map.fromIterable(), and friends all have their uses. (The List class also has an unnamed constructor, but it is prohibited in null safe Dart.)

Collection literals are particularly powerful in Dart because they give you access to thespread operator for including the contents of other collections, andif andfor for performing control flow while building the contents:

gooddart
vararguments=[...options,command,...?modeFlags,for(varpathinfilePaths)if(path.endsWith('.dart'))path.replaceAll('.dart','.js'),];
baddart
vararguments=<String>[];arguments.addAll(options);arguments.add(command);if(modeFlags!=null)arguments.addAll(modeFlags);arguments.addAll(filePaths.where((path)=>path.endsWith('.dart')).map((path)=>path.replaceAll('.dart','.js')),);

DON'T use.length to see if a collection is empty

#

Linter rules:prefer_is_empty,prefer_is_not_empty

TheIterable contract does not require that a collection know its length or be able to provide it in constant time. Calling.length just to see if the collection containsanything can be painfully slow.

Instead, there are faster and more readable getters:.isEmpty and.isNotEmpty. Use the one that doesn't require you to negate the result.

gooddart
if(lunchBox.isEmpty)return'so hungry...';if(words.isNotEmpty)returnwords.join('');
baddart
if(lunchBox.length==0)return'so hungry...';if(!words.isEmpty)returnwords.join('');

AVOID usingIterable.forEach() with a function literal

#

Linter rule:avoid_function_literals_in_foreach_calls

forEach() functions are widely used in JavaScript because the built infor-in loop doesn't do what you usually want. In Dart, if you want to iterate over a sequence, the idiomatic way to do that is using a loop.

gooddart
for(finalpersoninpeople){...}
baddart
people.forEach((person){...});

Note that this guideline specifically says "functionliteral". If you want to invoke somealready existing function on each element,forEach() is fine.

gooddart
people.forEach(print);

Also note that it's always OK to useMap.forEach(). Maps aren't iterable, so this guideline doesn't apply.

DON'T useList.from() unless you intend to change the type of the result

#

Given an Iterable, there are two obvious ways to produce a new List that contains the same elements:

dart
varcopy1=iterable.toList();varcopy2=List.from(iterable);

The obvious difference is that the first one is shorter. Theimportant difference is that the first one preserves the type argument of the original object:

gooddart
// Creates a List<int>:variterable=[1,2,3];// Prints "List<int>":print(iterable.toList().runtimeType);
baddart
// Creates a List<int>:variterable=[1,2,3];// Prints "List<dynamic>":print(List.from(iterable).runtimeType);

If youwant to change the type, then callingList.from() is useful:

gooddart
varnumbers=[1,2.3,4];// List<num>.numbers.removeAt(1);// Now it only contains integers.varints=List<int>.from(numbers);

But if your goal is just to copy the iterable and preserve its original type, or you don't care about the type, then usetoList().

DO usewhereType() to filter a collection by type

#

Linter rule:prefer_iterable_whereType

Let's say you have a list containing a mixture of objects, and you want to get just the integers out of it. You could usewhere() like this:

baddart
varobjects=[1,'a',2,'b',3];varints=objects.where((e)=>eisint);

This is verbose, but, worse, it returns an iterable whose type probably isn't what you want. In the example here, it returns anIterable<Object> even though you likely want anIterable<int> since that's the type you're filtering it to.

Sometimes you see code that "corrects" the above error by addingcast():

baddart
varobjects=[1,'a',2,'b',3];varints=objects.where((e)=>eisint).cast<int>();

That's verbose and causes two wrappers to be created, with two layers of indirection and redundant runtime checking. Fortunately, the core library has thewhereType() method for this exact use case:

gooddart
varobjects=[1,'a',2,'b',3];varints=objects.whereType<int>();

UsingwhereType() is concise, produces anIterable of the desired type, and has no unnecessary levels of wrapping.

DON'T usecast() when a nearby operation will do

#

Often when you're dealing with an iterable or stream, you perform several transformations on it. At the end, you want to produce an object with a certain type argument. Instead of tacking on a call tocast(), see if one of the existing transformations can change the type.

If you're already callingtoList(), replace that with a call toList<T>.from() whereT is the type of resulting list you want.

gooddart
varstuff=<dynamic>[1,2];varints=List<int>.from(stuff);
baddart
varstuff=<dynamic>[1,2];varints=stuff.toList().cast<int>();

If you are callingmap(), give it an explicit type argument so that it produces an iterable of the desired type. Type inference often picks the correct type for you based on the function you pass tomap(), but sometimes you need to be explicit.

gooddart
varstuff=<dynamic>[1,2];varreciprocals=stuff.map<double>((n)=>n*2);
baddart
varstuff=<dynamic>[1,2];varreciprocals=stuff.map((n)=>n*2).cast<double>();

AVOID usingcast()

#

This is the softer generalization of the previous rule. Sometimes there is no nearby operation you can use to fix the type of some object. Even then, when possible avoid usingcast() to "change" a collection's type.

Prefer any of these options instead:

  • Create it with the right type. Change the code where the collection is first created so that it has the right type.

  • Cast the elements on access. If you immediately iterate over the collection, cast each element inside the iteration.

  • Eagerly cast usingList.from(). If you'll eventually access most of the elements in the collection, and you don't need the object to be backed by the original live object, convert it usingList.from().

    Thecast() method returns a lazy collection that checks the element type onevery operation. If you perform only a few operations on only a few elements, that laziness can be good. But in many cases, the overhead of lazy validation and of wrapping outweighs the benefits.

Here is an example ofcreating it with the right type:

gooddart
List<int>singletonList(intvalue){varlist=<int>[];list.add(value);returnlist;}
baddart
List<int>singletonList(intvalue){varlist=[];// List<dynamic>.list.add(value);returnlist.cast<int>();}

Here iscasting each element on access:

gooddart
voidprintEvens(List<Object>objects){// We happen to know the list only contains ints.for(finalninobjects){if((nasint).isEven)print(n);}}
baddart
voidprintEvens(List<Object>objects){// We happen to know the list only contains ints.for(finalninobjects.cast<int>()){if(n.isEven)print(n);}}

Here iscasting eagerly usingList.from():

gooddart
intmedian(List<Object>objects){// We happen to know the list only contains ints.varints=List<int>.from(objects);ints.sort();returnints[ints.length~/2];}
baddart
intmedian(List<Object>objects){// We happen to know the list only contains ints.varints=objects.cast<int>();ints.sort();returnints[ints.length~/2];}

These alternatives don't always work, of course, and sometimescast() is the right answer. But consider that method a little risky and undesirable—it can be slow and may fail at runtime if you aren't careful.

Functions

#

In Dart, even functions are objects. Here are some best practices involving functions.

DO use a function declaration to bind a function to a name

#

Linter rule:prefer_function_declarations_over_variables

Modern languages have realized how useful local nested functions and closures are. It's common to have a function defined inside another one. In many cases, this function is used as a callback immediately and doesn't need a name. A function expression is great for that.

But, if you do need to give it a name, use a function declaration statement instead of binding a lambda to a variable.

gooddart
voidmain(){voidlocalFunction(){...}}
baddart
voidmain(){varlocalFunction=(){...};}

DON'T create a lambda when a tear-off will do

#

Linter rule:unnecessary_lambdas

When you refer to a function, method, or named constructor without parentheses, Dart creates atear-off. This is a closure that takes the same parameters as the function and invokes the underlying function when you call it. If your code needs a closure that invokes a named function with the same parameters as the closure accepts, don't wrap the call in a lambda. Use a tear-off.

gooddart
varcharCodes=[68,97,114,116];varbuffer=StringBuffer();// Function:charCodes.forEach(print);// Method:charCodes.forEach(buffer.write);// Named constructor:varstrings=charCodes.map(String.fromCharCode);// Unnamed constructor:varbuffers=charCodes.map(StringBuffer.new);
baddart
varcharCodes=[68,97,114,116];varbuffer=StringBuffer();// Function:charCodes.forEach((code){print(code);});// Method:charCodes.forEach((code){buffer.write(code);});// Named constructor:varstrings=charCodes.map((code)=>String.fromCharCode(code));// Unnamed constructor:varbuffers=charCodes.map((code)=>StringBuffer(code));

Variables

#

The following best practices describe how to best use variables in Dart.

DO follow a consistent rule forvar andfinal on local variables

#

Most local variables shouldn't have type annotations and should be declared using justvar orfinal. There are two rules in wide use for when to use one or the other:

  • Usefinal for local variables that are not reassigned andvar for those that are.

  • Usevar for all local variables, even ones that aren't reassigned. Never usefinal for locals. (Usingfinal for fields and top-level variables is still encouraged, of course.)

Either rule is acceptable, but pickone and apply it consistently throughout your code. That way when a reader seesvar, they know whether it means that the variable is assigned later in the function.

AVOID storing what you can calculate

#

When designing a class, you often want to expose multiple views into the same underlying state. Often you see code that calculates all of those views in the constructor and then stores them:

baddart
classCircle{doubleradius;doublearea;doublecircumference;Circle(doubleradius):radius=radius,area=pi*radius*radius,circumference=pi*2.0*radius;}

This code has two things wrong with it. First, it's likely wasting memory. The area and circumference, strictly speaking, arecaches. They are stored calculations that we could recalculate from other data we already have. They are trading increased memory for reduced CPU usage. Do we know we have a performance problem that merits that trade-off?

Worse, the code iswrong. The problem with caches isinvalidation—how do you know when the cache is out of date and needs to be recalculated? Here, we never do, even thoughradius is mutable. You can assign a different value and thearea andcircumference will retain their previous, now incorrect values.

To correctly handle cache invalidation, we would need to do this:

baddart
classCircle{double_radius;doublegetradius=>_radius;setradius(doublevalue){_radius=value;_recalculate();}double_area=0.0;doublegetarea=>_area;double_circumference=0.0;doublegetcircumference=>_circumference;Circle(this._radius){_recalculate();}void_recalculate(){_area=pi*_radius*_radius;_circumference=pi*2.0*_radius;}}

That's an awful lot of code to write, maintain, debug, and read. Instead, your first implementation should be:

gooddart
classCircle{doubleradius;Circle(this.radius);doublegetarea=>pi*radius*radius;doublegetcircumference=>pi*2.0*radius;}

This code is shorter, uses less memory, and is less error-prone. It stores the minimal amount of data needed to represent the circle. There are no fields to get out of sync because there is only a single source of truth.

In some cases, you may need to cache the result of a slow calculation, but only do that after you know you have a performance problem, do it carefully, and leave a comment explaining the optimization.

Members

#

In Dart, objects have members which can be functions (methods) or data (instance variables). The following best practices apply to an object's members.

DON'T wrap a field in a getter and setter unnecessarily

#

Linter rule:unnecessary_getters_setters

In Java and C#, it's common to hide all fields behind getters and setters (or properties in C#), even if the implementation just forwards to the field. That way, if you ever need to do more work in those members, you can without needing to touch the call sites. This is because calling a getter method is different than accessing a field in Java, and accessing a property isn't binary-compatible with accessing a raw field in C#.

Dart doesn't have this limitation. Fields and getters/setters are completely indistinguishable. You can expose a field in a class and later wrap it in a getter and setter without having to touch any code that uses that field.

gooddart
classBox{Object?contents;}
baddart
classBox{Object?_contents;Object?getcontents=>_contents;setcontents(Object?value){_contents=value;}}

PREFER using afinal field to make a read-only property

#

If you have a field that outside code should be able to see but not assign to, a simple solution that works in many cases is to simply mark itfinal.

gooddart
classBox{finalcontents=[];}
baddart
classBox{Object?_contents;Object?getcontents=>_contents;}

Of course, if you need to internally assign to the field outside of the constructor, you may need to do the "private field, public getter" pattern, but don't reach for that until you need to.

CONSIDER using=> for simple members

#

Linter rule:prefer_expression_function_bodies

In addition to using=> for function expressions, Dart also lets you define members with it. That style is a good fit for simple members that just calculate and return a value.

gooddart
doublegetarea=>(right-left)*(bottom-top);Stringcapitalize(Stringname)=>'${name[0].toUpperCase()}${name.substring(1)}';

Peoplewriting code seem to love=>, but it's very easy to abuse it and end up with code that's hard toread. If your declaration is more than a couple of lines or contains deeply nested expressions—cascades and conditional operators are common offenders—do yourself and everyone who has to read your code a favor and use a block body and some statements.

gooddart
Treasure?openChest(Chestchest,Pointwhere){if(_opened.containsKey(chest))returnnull;vartreasure=Treasure(where);treasure.addAll(chest.contents);_opened[chest]=treasure;returntreasure;}
baddart
Treasure?openChest(Chestchest,Pointwhere)=>_opened.containsKey(chest)?null:_opened[chest]=(Treasure(where)..addAll(chest.contents));

You can also use=> on members that don't return a value. This is idiomatic when a setter is small and has a corresponding getter that uses=>.

gooddart
numgetx=>center.x;setx(numvalue)=>center=Point(value,center.y);

DON'T usethis. except to redirect to a named constructor or to avoid shadowing

#

Linter rule:unnecessary_this

JavaScript requires an explicitthis. to refer to members on the object whose method is currently being executed, but Dart—like C++, Java, and C#—doesn't have that limitation.

There are only two times you need to usethis.. One is when a local variable with the same name shadows the member you want to access:

baddart
classBox{Object?value;voidclear(){this.update(null);}voidupdate(Object?value){this.value=value;}}
gooddart
classBox{Object?value;voidclear(){update(null);}voidupdate(Object?value){this.value=value;}}

The other time to usethis. is when redirecting to a named constructor:

baddart
classShadeOfGray{finalintbrightness;ShadeOfGray(intval):brightness=val;ShadeOfGray.black():this(0);// This won't parse or compile!// ShadeOfGray.alsoBlack() : black();}
gooddart
classShadeOfGray{finalintbrightness;ShadeOfGray(intval):brightness=val;ShadeOfGray.black():this(0);// But now it will!ShadeOfGray.alsoBlack():this.black();}

Note that constructor parameters never shadow fields in constructor initializer lists:

gooddart
classBoxextendsBaseBox{Object?value;Box(Object?value):value=value,super(value);}

This looks surprising, but works like you want. Fortunately, code like this is relatively rare thanks to initializing formals and super initializers.

DO initialize fields at their declaration when possible

#

If a field doesn't depend on any constructor parameters, it can and should be initialized at its declaration. It takes less code and avoids duplication when the class has multiple constructors.

baddart
classProfileMark{finalStringname;finalDateTimestart;ProfileMark(this.name):start=DateTime.now();ProfileMark.unnamed():name='',start=DateTime.now();}
gooddart
classProfileMark{finalStringname;finalDateTimestart=DateTime.now();ProfileMark(this.name);ProfileMark.unnamed():name='';}

Some fields can't be initialized at their declarations because they need to referencethis—to use other fields or call methods, for example. However, if the field is markedlate, then the initializercan accessthis.

Of course, if a field depends on constructor parameters, or is initialized differently by different constructors, then this guideline does not apply.

Constructors

#

The following best practices apply to declaring constructors for a class.

DO use initializing formals when possible

#

Linter rule:prefer_initializing_formals

Many fields are initialized directly from a constructor parameter, like:

baddart
classPoint{doublex,y;Point(doublex,doubley):x=x,y=y;}

We've got to typexfour times here to define a field. We can do better:

gooddart
classPoint{doublex,y;Point(this.x,this.y);}

Thisthis. syntax before a constructor parameter is called an "initializing formal". You can't always take advantage of it. Sometimes you want to have a named parameter whose name doesn't match the name of the field you are initializing. But when youcan use initializing formals, youshould.

DON'T uselate when a constructor initializer list will do

#

Dart requires you to initialize non-nullable fields before they can be read. Since fields can be read inside the constructor body, this means you get an error if you don't initialize a non-nullable field before the body runs.

You can make this error go away by marking the fieldlate. That turns the compile-time error into aruntime error if you access the field before it is initialized. That's what you need in some cases, but often the right fix is to initialize the field in the constructor initializer list:

gooddart
classPoint{doublex,y;Point.polar(doubletheta,doubleradius):x=cos(theta)*radius,y=sin(theta)*radius;}
baddart
classPoint{latedoublex,y;Point.polar(doubletheta,doubleradius){x=cos(theta)*radius;y=sin(theta)*radius;}}

The initializer list gives you access to constructor parameters and lets you initialize fields before they can be read. So, if it's possible to use an initializer list, that's better than making the fieldlate and losing some static safety and performance.

DO use; instead of{} for empty constructor bodies

#

Linter rule:empty_constructor_bodies

In Dart, a constructor with an empty body can be terminated with just a semicolon. (In fact, it's required for const constructors.)

gooddart
classPoint{doublex,y;Point(this.x,this.y);}
baddart
classPoint{doublex,y;Point(this.x,this.y){}}

DON'T usenew

#

Linter rule:unnecessary_new

Thenew keyword is optional when calling a constructor. Its meaning is not clear because factory constructors mean anew invocation may not actually return a new object.

The language still permitsnew, but consider it deprecated and avoid using it in your code.

gooddart
Widgetbuild(BuildContextcontext){returnRow(children:[RaisedButton(child:Text('Increment')),Text('Click!'),],);}
baddart
Widgetbuild(BuildContextcontext){returnnewRow(children:[newRaisedButton(child:newText('Increment')),newText('Click!'),],);}

DON'T useconst redundantly

#

Linter rule:unnecessary_const

In contexts where an expressionmust be constant, theconst keyword is implicit, doesn't need to be written, and shouldn't. Those contexts are any expression inside:

  • A const collection literal.
  • A const constructor call
  • A metadata annotation.
  • The initializer for a const variable declaration.
  • A switch case expression—the part right aftercase before the:, not the body of the case.

(Default values are not included in this list because future versions of Dart may support non-const default values.)

Basically, any place where it would be an error to writenew instead ofconst, Dart allows you to omit theconst.

gooddart
constprimaryColors=[Color('red',[255,0,0]),Color('green',[0,255,0]),Color('blue',[0,0,255]),];
baddart
constprimaryColors=const[constColor('red',const[255,0,0]),constColor('green',const[0,255,0]),constColor('blue',const[0,0,255]),];

Error handling

#

Dart uses exceptions when an error occurs in your program. The following best practices apply to catching and throwing exceptions.

AVOID catches withouton clauses

#

Linter rule:avoid_catches_without_on_clauses

A catch clause with noon qualifier catchesanything thrown by the code in the try block.Pokémon exception handling is very likely not what you want. Does your code correctly handleStackOverflowError orOutOfMemoryError? If you incorrectly pass the wrong argument to a method in that try block do you want to have your debugger point you to the mistake or would you rather that helpfulArgumentError get swallowed? Do you want anyassert() statements inside that code to effectively vanish since you're catching the thrownAssertionErrors?

The answer is probably "no", in which case you should filter the types you catch. In most cases, you should have anon clause that limits you to the kinds of runtime failures you are aware of and are correctly handling.

In rare cases, you may wish to catch any runtime error. This is usually in framework or low-level code that tries to insulate arbitrary application code from causing problems. Even here, it is usually better to catchException than to catch all types. Exception is the base class for allruntime errors and excludes errors that indicateprogrammatic bugs in the code.

DON'T discard errors from catches withouton clauses

#

If you really do feel you need to catcheverything that can be thrown from a region of code,do something with what you catch. Log it, display it to the user or rethrow it, but do not silently discard it.

DO throw objects that implementError only for programmatic errors

#

TheError class is the base class forprogrammatic errors. When an object of that type or one of its subinterfaces likeArgumentError is thrown, it means there is abug in your code. When your API wants to report to a caller that it is being used incorrectly throwing an Error sends that signal clearly.

Conversely, if the exception is some kind of runtime failure that doesn't indicate a bug in the code, then throwing an Error is misleading. Instead, throw one of the core Exception classes or some other type.

DON'T explicitly catchError or types that implement it

#

Linter rule:avoid_catching_errors

This follows from the above. Since an Error indicates a bug in your code, it should unwind the entire callstack, halt the program, and print a stack trace so you can locate and fix the bug.

Catching errors of these types breaks that process and masks the bug. Instead ofadding error-handling code to deal with this exception after the fact, go back and fix the code that is causing it to be thrown in the first place.

DO userethrow to rethrow a caught exception

#

Linter rule:use_rethrow_when_possible

If you decide to rethrow an exception, prefer using therethrow statement instead of throwing the same exception object usingthrow.rethrow preserves the original stack trace of the exception.throw on the other hand resets the stack trace to the last thrown position.

baddart
try{somethingRisky();}catch(e){if(!canHandle(e))throwe;handle(e);}
gooddart
try{somethingRisky();}catch(e){if(!canHandle(e))rethrow;handle(e);}

Asynchrony

#

Dart has several language features to support asynchronous programming. The following best practices apply to asynchronous coding.

PREFER async/await over using raw futures

#

Asynchronous code is notoriously hard to read and debug, even when using a nice abstraction like futures. Theasync/await syntax improves readability and lets you use all of the Dart control flow structures within your async code.

gooddart
Future<int>countActivePlayers(StringteamName)async{try{varteam=awaitdownloadTeam(teamName);if(team==null)return0;varplayers=awaitteam.roster;returnplayers.where((player)=>player.isActive).length;}onDownloadExceptioncatch(e,_){log.error(e);return0;}}
baddart
Future<int>countActivePlayers(StringteamName){returndownloadTeam(teamName).then((team){if(team==null)returnFuture.value(0);returnteam.roster.then((players){returnplayers.where((player)=>player.isActive).length;});}).onError<DownloadException>((e,_){log.error(e);return0;});}

DON'T useasync when it has no useful effect

#

It's easy to get in the habit of usingasync on any function that does anything related to asynchrony. But in some cases, it's extraneous. If you can omit theasync without changing the behavior of the function, do so.

gooddart
Future<int>fastestBranch(Future<int>left,Future<int>right){returnFuture.any([left,right]);}
baddart
Future<int>fastestBranch(Future<int>left,Future<int>right)async{returnFuture.any([left,right]);}

Cases whereasyncis useful include:

  • You are usingawait. (This is the obvious one.)

  • You are returning an error asynchronously.async and thenthrow is shorter thanreturn Future.error(...).

  • You are returning a value and you want it implicitly wrapped in a future.async is shorter thanFuture.value(...).

gooddart
Future<void>usesAwait(Future<String>later)async{print(awaitlater);}Future<void>asyncError()async{throw'Error!';}Future<String>asyncValue()async=>'value';

CONSIDER using higher-order methods to transform a stream

#

This parallels the above suggestion on iterables. Streams support many of the same methods and also handle things like transmitting errors, closing, etc. correctly.

AVOID using Completer directly

#

Many people new to asynchronous programming want to write code that produces a future. The constructors in Future don't seem to fit their need so they eventually find the Completer class and use that.

baddart
Future<bool>fileContainsBear(Stringpath){varcompleter=Completer<bool>();File(path).readAsString().then((contents){completer.complete(contents.contains('bear'));});returncompleter.future;}

Completer is needed for two kinds of low-level code: new asynchronous primitives, and interfacing with asynchronous code that doesn't use futures. Most other code should use async/await orFuture.then(), because they're clearer and make error handling easier.

gooddart
Future<bool>fileContainsBear(Stringpath){returnFile(path).readAsString().then((contents){returncontents.contains('bear');});}
gooddart
Future<bool>fileContainsBear(Stringpath)async{varcontents=awaitFile(path).readAsString();returncontents.contains('bear');}

DO test forFuture<T> when disambiguating aFutureOr<T> whose type argument could beObject

#

Before you can do anything useful with aFutureOr<T>, you typically need to do anis check to see if you have aFuture<T> or a bareT. If the type argument is some specific type as inFutureOr<int>, it doesn't matter which test you use,is int oris Future<int>. Either works because those two types are disjoint.

However, if the value type isObject or a type parameter that could possibly be instantiated withObject, then the two branches overlap.Future<Object> itself implementsObject, sois Object oris T whereT is some type parameter that could be instantiated withObject returns true even when the object is a future. Instead, explicitly test for theFuture case:

gooddart
Future<T>logValue<T>(FutureOr<T>value)async{if(valueisFuture<T>){varresult=awaitvalue;print(result);returnresult;}else{print(value);returnvalue;}}
baddart
Future<T>logValue<T>(FutureOr<T>value)async{if(valueisT){print(value);returnvalue;}else{varresult=awaitvalue;print(result);returnresult;}}

In the bad example, if you pass it aFuture<Object>, it incorrectly treats it like a bare, synchronous value.

Was this page's content helpful?

Unless stated otherwise, the documentation on this site reflects Dart 3.10.0. Page last updated on 2025-11-12.View source orreport an issue.


[8]ページ先頭

©2009-2025 Movatter.jp