A tour of the Dart language

Dart 2.3 introduced the spread operator (...) and the null-aware spread operator (...?), which provide a concise way to insert multiple elements into a collection.

Dart 2.3 also introduced collection if and collection for, which you can use to build collections using conditionals (if) and repetition (for).

Although the Set type has always been a core part of Dart, set literals were introduced in Dart 2.2.

To create an empty set, use {} preceded by a type argument, or assign {} to a variable of type Set:

var names = <String>{};
// Set<String> names = {}; // This works, too.
// var names = {}; // Creates a map, not a set.

As of Dart 2, the new keyword is optional.

In Dart, runes are the UTF-32 code points of a string.Unicode defines a unique numeric value for each letter, digit, and symbol used in all of the world’s writing systems. Because a Dart string is a sequence of UTF-16 code units, expressing 32-bit Unicode values within a string requires special syntax.

A Symbol object represents an operator or identifier declared in a Dart program. You might never need to use symbols, but they’re invaluable for APIs that refer to identifiers by name, because minification changes identifier names but not identifier symbols.To get the symbol for an identifier, use a symbol literal, which is just # followed by the identifier:#radix.Symbol literals are compile-time constants.

The => *expr* syntax is a shorthand for { return *expr*; }. The => notation is sometimes referred to as arrow syntax.

Required is defined in the meta package. Either import package:meta/meta.dart directly, or import another package that exports meta, such as Flutter’s package:flutter/material.dart.

Old code might use a colon (:) instead of = to set default values of named parameters. The reason is that originally, only : was supported for named parameters. That support is likely to be deprecated, so we recommend that you use = to specify default values.

Every app must have a top-level main() function, which serves as the entrypoint to the app. The main()function returns void and has an optional List<String> parameter for arguments.

The .. syntax in the preceding code is called a cascade. With cascades, you can perform multiple operations on the members of a single object.

For operators that work on two operands, the leftmost operand determines which version of the operator is used. For example, if you have a Vector object and a Point object, aVector + aPoint uses the Vector version of +.

// Assign value to b if b is null; otherwise, b stays the same
b ??= value;
expr1 ?? expr2  //If expr1 is non-null, returns its value; otherwise, evaluates and returns the value of expr2.
String playerName(String name) => name ?? 'Guest';

Cascades (..) allow you to make a sequence of operations on the same object. In addition to function calls, you can also access fields on that same object. This often saves you the step of creating a temporary variable and allows you to write more fluid code.Strictly speaking, the “double dot” notation for cascades is not an operator. It’s just part of the Dart syntax.

Switch statements in Dart compare integer, string, or compile-time constants using ==. The compared objects must all be instances of the same class (and not of any of its subtypes), and the class must not override ==.Enumerated types work well in switch statements.

Assert statements have no effect in production code; they’re for development only. Flutter enables asserts in debug mode. Development-only tools such as dartdevc typically support asserts by default. Some tools, such as dart and dart2js, support asserts through a command-line flag: --enable-asserts.

Your Dart code can throw and catch exceptions. Exceptions are errors indicating that something unexpected happened. If the exception isn’t caught, the isolate that raised the exception is suspended, and typically the isolate and its program are terminated.

In contrast to Java, all of Dart’s exceptions are unchecked exceptions. Methods do not declare which exceptions they might throw, and you are not required to catch any exceptions.

Dart provides Exception and Error types, as well as numerous predefined subtypes. You can, of course, define your own exceptions. However, Dart programs can throw any non-null object—not just Exception and Error objects - as an exception.

Production-quality code usually throws types that implement Error or Exception.

To handle code that can throw more than one type of exception, you can specify multiple catch clauses. The first catch clause that matches the thrown object’s type handles the exception. If the catch clause does not specify a type, that clause can handle any type of thrown object:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

You can specify one or two parameters to catch(). The first is the exception that was thrown, and the second is the stack trace (a StackTrace object).

try {
  // ···
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}

To ensure that some code runs whether or not an exception is thrown, use a finally clause. If no catchclause matches the exception, the exception is propagated after the finally clause runs:

try {
  breedMoreLlamas();
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

Dart is an object-oriented language with classes and mixin-based inheritance. Every object is an instance of a class, and all classes descend from Object.Mixin-based inheritance means that although every class (except for Object) has exactly one superclass, a class body can be reused in multiple class hierarchies.

Use ?. instead of . to avoid an exception when the leftmost operand is null.

You can create an object using a constructor. Constructor names can be either *ClassName* or*ClassName*.*identifier*. For example, the following code creates Point objects using the Point() and Point.fromJson() constructors:

var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});

Some classes provide constant constructors. To create a compile-time constant using a constant constructor, put the const keyword before the constructor name.

Constructing two identical compile-time constants results in a single, canonical instance:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // They are the same instance!

The const keyword became optional within a constant context in Dart 2.

All instance variables generate an implicit getter method. Non-final instance variables also generate an implicit setter method.

If you don’t declare a constructor, a default constructor is provided for you. The default constructor has no arguments and invokes the no-argument constructor in the superclass.

Subclasses don’t inherit constructors from their superclass. A subclass that declares no constructors has only the default (no argument, no name) constructor.

Use a named constructor to implement multiple constructors for a class or to provide extra clarity.

class Point {
  num x, y;

  Point(this.x, this.y);

  // Named constructor
  Point.origin() {
    x = 0;
    y = 0;
  }
}

Remember that constructors are not inherited, which means that a superclass’s named constructor is not inherited by a subclass. If you want a subclass to be created with a named constructor defined in the superclass, you must implement that constructor in the subclass.

By default, a constructor in a subclass calls the superclass’s unnamed, no-argument constructor. The superclass’s constructor is called at the beginning of the constructor body. If an initializer list is also being used, it executes before the superclass is called. In summary, the order of execution is as follows:

  1. initializer list
  2. superclass’s no-arg constructor
  3. main class’s no-arg constructor

If the superclass doesn’t have an unnamed, no-argument constructor, then you must manually call one of the constructors in the superclass. Specify the superclass constructor after a colon (:), just before the constructor body (if any).Because the arguments to the superclass constructor are evaluated before invoking the constructor, an argument can be an expression such as a function call.Arguments to the superclass constructor do not have access to this. For example, arguments can call static methods but not instance methods.

Besides invoking a superclass constructor, you can also initialize instance variables before the constructor body runs. Separate initializers with commas.

Sometimes a constructor’s only purpose is to redirect to another constructor in the same class. A redirecting constructor’s body is empty, with the constructor call appearing after a colon (:).

If your class produces objects that never change, you can make these objects compile-time constants. To do this, define a const constructor and make sure that all instance variables are final.

Use the factory keyword when implementing a constructor that doesn’t always create a new instance of its class. For example, a factory constructor might return an instance from a cache, or it might return an instance of a subtype.Factory constructors have no access to this.

Invoke a factory constructor just like you would any other constructor.

Getters and setters are special methods that provide read and write access to an object’s properties. Recall that each instance variable has an implicit getter, plus a setter if appropriate. You can create additional properties by implementing getters and setters, using the get and set keywords.

To make a method abstract, use a semicolon (;) instead of a method body.

Use the abstract modifier to define an abstract class—a class that can’t be instantiated. Abstract classes are useful for defining interfaces, often with some implementation. If you want your abstract class to appear to be instantiable, define a factory constructor.

Every class implicitly defines an interface containing all the instance members of the class and of any interfaces it implements. If you want to create a class A that supports class B’s API without inheriting B’s implementation, class A should implement the B interface.

A class implements one or more interfaces by declaring them in an implements clause and then providing the APIs required by the interfaces.

Use extends to create a subclass, and super to refer to the superclass.

Subclasses can override instance methods, getters, and setters. You can use the @override annotation to indicate that you are intentionally overriding a member.To narrow the type of a method parameter or instance variable in code that is type safe, you can use the covariant keyword.

You can override the operators shown in the following table. For example, if you define a Vector class, you might define a + method to add two vectors.

< + ` ` []
> / ^ []=
<= ~/ & ~
>= * << ==
% >>

If you override ==, you should also override Object’s hashCode getter.

Enumerated types, often called enumerations or enums, are a special kind of class used to represent a fixed number of constant values.

Each value in an enum has an index getter, which returns the zero-based position of the value in the enum declaration. To get a list of all of the values in the enum, use the enum’s values constant.

Enumerated types have the following limits:

  • You can’t subclass, mix in, or implement an enum.
  • You can’t explicitly instantiate an enum.

Mixins are a way of reusing a class’s code in multiple class hierarchies.

To use a mixin, use the with keyword followed by one or more mixin names.

To implement a mixin, create a class that extends Object and declares no constructors. Unless you want your mixin to be usable as a regular class, use the mixin keyword instead of class.To specify that only certain types can use the mixin — for example, so your mixin can invoke a method that it doesn’t define — use on to specify the required superclass.Support for the mixin keyword was introduced in Dart 2.1. Code in earlier releases usually used abstract class instead.

Use the static keyword to implement class-wide variables and methods.Static variables (class variables) are useful for class-wide state and constants.Static variables aren’t initialized until they’re used.

Consider using top-level functions, instead of static methods, for common or widely used utilities and functionality.

The <…> notation marks List as a generic (or parameterized) type—a type that has formal type parameters. By convention, most type variables have single-letter names, such as E, T, S, K, and V.

Dart generic types are reified, which means that they carry their type information around at runtime.In contrast, generics in Java use erasure, which means that generic type parameters are removed at runtime. In Java, you can test whether an object is a List, but you can’t test whether it’s a List<String>.

When implementing a generic type, you might want to limit the types of its parameters. You can do this using extends.

Initially, Dart’s generic support was limited to classes. A newer syntax, called generic methods, allows type arguments on methods and functions.

The import and library directives can help you create a modular and shareable code base. Libraries not only provide APIs, but are a unit of privacy: identifiers that start with an underscore (_) are visible only inside the library. Every Dart app is a library, even if it doesn’t use a library directive.

Libraries can be distributed using packages.

The only required argument to import is a URI specifying the library. For built-in libraries, the URI has the special dart: scheme. For other libraries, you can use a file system path or the package: scheme. The package: scheme specifies libraries provided by a package manager such as the pub tool. URI stands for uniform resource identifier. URLs (uniform resource locators) are a common kind of URI.

Deferred loading (also called lazy loading) allows an application to load a library on demand, if and when it’s needed. Here are some cases when you might use deferred loading:

  • To reduce an app’s initial startup time.
  • To perform A/B testing—trying out alternative implementations of an algorithm, for example.
  • To load rarely used functionality, such as optional screens and dialogs.

To lazily load a library, you must first import it using deferred as.

When you need the library, invoke loadLibrary() using the library’s identifier.

You can invoke loadLibrary() multiple times on a library without problems. The library is loaded only once.

Keep in mind the following when you use deferred loading:

  • A deferred library’s constants aren’t constants in the importing file. Remember, these constants don’t exist until the deferred library is loaded.
  • You can’t use types from a deferred library in the importing file. Instead, consider moving interface types to a library imported by both the deferred library and the importing file.
  • Dart implicitly inserts loadLibrary() into the namespace that you define using deferred as*namespace*. The loadLibrary() function returns a Future.

The Dart VM allows access to members of deferred libraries even before the call to loadLibrary(). This behavior might change, so don’t depend on the current VM behavior.

Dart libraries are full of functions that return Future or Stream objects. These functions are asynchronous: they return after setting up a possibly time-consuming operation (such as I/O), without waiting for that operation to complete.

Although an async function might perform time-consuming operations, it doesn’t wait for those operations. Instead, the async function executes only until it encounters its first awaitexpression (details). Then it returns a Future object, resuming execution only after the awaitexpression completes.

In await *expression*, the value of *expression* is usually a Future; if it isn’t, then the value is automatically wrapped in a Future. This Future object indicates a promise to return an object. The value of await*expression* is that returned object. The await expression makes execution pause until that object is available.

An async function is a function whose body is marked with the async modifier.

Note that the function’s body doesn’t need to use the Future API. Dart creates the Future object if necessary.

If your function doesn’t return a useful value, make its return type Future<void>.

When you need to get values from a Stream, you have two options:

  • Use async and an asynchronous for loop (await for).
  • Use the Stream API, as described in the library tour.

When you need to lazily produce a sequence of values, consider using a generator function. Dart has built-in support for two kinds of generator functions:

  • Synchronous generator: Returns an Iterable object.
  • Asynchronous generator: Returns a Stream object.

To implement a synchronous generator function, mark the function body as sync*, and use yieldstatements to deliver values:

Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

To implement an asynchronous generator function, mark the function body as async*, and use yieldstatements to deliver values:

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

If your generator is recursive, you can improve its performance by using yield*:

Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

To allow an instance of your Dart class to be called like a function, implement the call() method.

Most computers, even on mobile platforms, have multi-core CPUs. To take advantage of all those cores, developers traditionally use shared-memory threads running concurrently. However, shared-state concurrency is error prone and can lead to complicated code.

Instead of threads, all Dart code runs inside of isolates. Each isolate has its own memory heap, ensuring that no isolate’s state is accessible from any other isolate.

In Dart, functions are objects, just like strings and numbers are objects. A typedef, or function-type alias, gives a function type a name that you can use when declaring fields and return types. A typedef retains type information when a function type is assigned to a variable.

typedef Compare = int Function(Object a, Object b);

class SortedCollection {
  Compare compare;

  SortedCollection(this.compare);
}

// Initial, broken implementation.
int sort(Object a, Object b) => 0;

void main() {
  SortedCollection coll = SortedCollection(sort);
  assert(coll.compare is Function);
  assert(coll.compare is Compare);
}

Metadata can appear before a library, class, typedef, type parameter, constructor, factory, function, field, parameter, or variable declaration and before an import or export directive. You can retrieve metadata at runtime using reflection.

results matching ""

    No results matching ""