© Fu Cheng 2019
F. ChengFlutter Recipeshttps://doi.org/10.1007/978-1-4842-4982-6_3

3. Essential Dart

Fu Cheng1 
(1)
Sandringham, Auckland, New Zealand
 

Flutter projects can have cross-platform code and platform-specific code. Cross-platform code is written in Dart. Sufficient knowledge of Dart is a prerequisite for building Flutter apps. Details of Dart language is out of the scope of this book. You can find plenty of online resources related to Dart. However, it’s still very helpful to cover essential part of Dart for building Flutter apps. Recipes in this chapter cover different aspects of Dart. You can skip this chapter if you are confident about your knowledge of Dart.

3.1 Understanding Built-In Types

Problem

You want to know the built-in types of Dart.

Solution

Dart has built-in types of numbers, strings, booleans, lists, maps, runes, and symbols.

Discussion

Dart has several built-in types, including numbers, strings, booleans, lists, maps, runes, and symbols.

Numbers

Numbers in Dart can be integer values no larger than 64 bits or 64-bit double-precision floating-point number specified by the IEEE 754 standard. Types int and double represent these two types of numbers, respectively. Type num is the supertype of int and double. Unlike primitive types in Java, numbers in Dart are also objects. They have methods to work with them.

In Listing 3-1, the type of x is int, while the type of y is double. The method toRadixString() returns a string value by converting the value to the specified radix. The method toStringAsFixed() makes sure that the given number of fraction digits is kept in the string representation. The static method tryParse() of double tries to parse a string as a double literal.
var x = 10;
var y = 1.5;
assert(x.toRadixString(8) == '12');
assert(y.toStringAsFixed(2) == '1.50');
var z = double.tryParse('3.14');
assert(z == 3.14);
Listing 3-1

Numbers

Strings

Dart strings are sequences of UTF-16 code units. Either single or double quotes can be used to create strings. It doesn’t matter which quote is used. The key point is to be consistent across the whole code base. Dart has built-in support for string interpolation. Expressions can be embedded into strings using the form ${expression}. Values of embedded expressions are evaluated when strings are used. If the expression is an identifier, then {} can be omitted. In Listing 3-2, name is an identifier, so we can use $name in the string.
var name = 'Alex';
assert('The length of $name is ${name.length}' == 'The length of Alex is 4');
Listing 3-2

String interpolation

If you want to concatenate strings, you can simply place these string literals next to each other without the + operator; see Listing 3-3.
var longString = 'This is a long'
  'long'
  'long'
  'string';
Listing 3-3

String concatenation

Another way to create a multi-line string is to use a triple quote with either single or double quotes; see Listing 3-4.
var longString2 = "'
This is also a long
  long
  long
  string
"';
Listing 3-4

Multi-line string

Booleans

Boolean values are represented using the type bool. bool type has only two objects: true and false. It’s worth noting that only bool values can be used in if, while, and assert as conditions to check. JavaScript has a broader concept of truthy and falsy values, while Dart follows a stricter rule. For example, if ('abc') is valid in JavaScript, but not in Dart.

In Listing 3-5, name is an empty string. To use it in if, we need to invoke the getter isEmpty. We also need explicit check for null and 0.
var name = ";
if (name.isEmpty) {
  print('name is emtpy');
}
var value;
assert(value == null);
var count = 5;
while(count-- != 0) {
  print(count);
}
Listing 3-5

Booleans

Lists and Maps

Lists and maps are commonly used collection types. In Dart, arrays are List objects. Lists and maps can be created using literals or constructors. It’s recommended to use collection literals when possible. Listing 3-6 shows how to create lists and maps using literals and constructors.
var list1 = [1, 2, 3];
var list2 = List<int>(3);
var map1 = {'a': 'A', 'b': 'B'};
var map2 = Map<String, String>();
Listing 3-6

Lists and maps

Runes

Runes are UTF-32 code points of a string. To express 32-bit Unicode values in a string, we can use the form uXXXX, where XXXX is the four-digit hexadecimal value of the code point. If the code point cannot be expressed as four-digit hexadecimal value, then {} is required to wrap those digits, for example, u{XXXXX}. In Listing 3-7, the string value contains two emojis.
var value = 'u{1F686} u{1F6B4}';
print(value);
Listing 3-7

Runes

Symbols

A Symbol object represents an operator or identifier. Symbols can be created using constructor Symbol(<name>) or symbol literal #<name>. Symbols created with the same name are equal; see Listing 3-8. Symbols should be used when you want to reference identifiers by name.
assert(Symbol('a') == #a);
Listing 3-8

Symbols

3.2 Using Enumerated Types

Problem

You want to have a type-safe way to declare a set of constant values.

Solution

Use enumerated type.

Discussion

Like other programming languages, Dart has enumerated types. To declare an enumerated type, use the enum keyword. Each value in an enum has an index getter to get the zero-based position of the value. Use values to get a list of all values in an enum. Enums are usually used in switch statements. In Listing 3-9, the enum type TrafficColor has three values. The index of first value red is 0.
enum TrafficColor { red, green, yellow }
void main() {
  assert(TrafficColor.red.index == 0);
  assert(TrafficColor.values.length == 3);
  var color = TrafficColor.red;
  switch (color) {
    case TrafficColor.red:
      print('stop');
      break;
    case TrafficColor.green:
      print('go');
      break;
    case TrafficColor.yellow:
      print('be careful');
  }
}
Listing 3-9

Enumerated type

3.3 Using Dynamic Type

Problem

You don’t know the type of an object or you don’t care about the type.

Solution

Use the dynamic type.

Discussion

Dart is a strong-typed language. Most of the time, we want an object to have a defined type. However, sometimes we may not know or don’t care about the actual type; we can use dynamic as the type. The dynamic type is often confused with the Object type. Both Object and dynamic permit all values. Object should be used if you want to state that all objects are accepted. If the type is dynamic, we can use is operator to check whether it’s the desired type. The actual type can be retrieved using runtimeType. In Listing 3-10, the actual type of value is int, then the type is changed to String.
dynamic value = 1;
print(value.runtimeType);
value = 'test';
if (value is String) {
  print('string');
}
Listing 3-10

Use dynamic type

3.4 Understanding Functions

Problem

You want to understand functions in Dart.

Solution

Functions in Dart are very powerful and flexible.

Discussion

Functions in Dart are objects and have the type Function. Functions can be assigned to values, passed in function arguments, and used as function return values. It’s very easy to create high-order functions in Dart. A function may have zero or many parameters. Some parameters are required, while some are optional. Required arguments come first in the parameters list, followed by optional parameters. Optional positional parameters are wrapped in [].

When a function has a long list of parameters, it’s hard to remember the position and meaning of these parameters. It’s better to use named parameters. Named parameters can be marked as required using the @required annotation. Parameters can have default values specified using =. If no default value is provided, the default value is null.

In Listing 3-11, the function sum() has an optional positional argument initial with the default value 0. The function joinToString() has a required named argument separator and two optional named arguments prefix and suffix. The arrow syntax used in joinToString() is a shorthand for function body with only one expression. The syntax => expr is the same as { return expr; }. Using arrow syntax makes code shorter and easier to read.
import 'package:meta/meta.dart';
int sum(List<int> list, [int initial = 0]) {
  var total = initial;
  list.forEach((v) => total += v);
  return total;
}
String joinToString(List<String> list,
        {@required String separator, String prefix = ", String suffix = "}) =>
    '$prefix${list.join(separator)}$suffix';
void main() {
  assert(sum([1, 2, 3]) == 6);
  assert(sum([1, 2, 3], 10) == 16);
  assert(joinToString(['a', 'b', 'c'], separator: ',') == 'a,b,c');
  assert(
      joinToString(['a', 'b', 'c'], separator: '-', prefix: '*', suffix: '?') ==
          '*a-b-c?');
}
Listing 3-11

Function parameters

Sometimes you may not need a name for a function. These anonymous functions are useful when providing callbacks. In Listing 3-12, an anonymous function is passed to the method forEach().
var list = [1, 2, 3];
list.forEach((v) => print(v * 10));
Listing 3-12

Anonymous functions

3.5 Using Typedefs

Problem

You want to have an alias of a function type.

Solution

Use typedefs.

Discussion

In Dart, functions are objects. Functions are instances of the type Function. But the actual type of a function is defined by the types of its parameters and the type of its return value. What matters is the actual function type when a function is used as a parameter or return value. typedef in Dart allows us to create an alias of a function type. The type alias can be used just like other types. In Listing 3-13, Processor<T> is an alias of the function type which has a parameter of type T and a return type of void. This type is used as the parameter type in the function process().
typedef Processor<T> = void Function(T value);
void process<T>(List<T> list, Processor<T> processor) {
  list.forEach((item) {
    print('processing $item');
    processor(item);
    print('processed $item');
  });
}
void main() {
  process([1, 2, 3], print);
}
Listing 3-13

typedef

3.6 Using Cascade Operator

Problem

You want to make a sequence of operations on the same object.

Solution

Use the cascade operator (..) in Dart.

Discussion

Dart has a special cascade operator (..) which allows us to make a sequence of operations on the same object. To chain operations on the same object in other programming languages, we usually need to create a fluent API in which each method returns the current object. The cascade operator in Dart makes this requirement unnecessary. Methods can still be chained even though they don’t return the current object. The cascade operator also supports field access. In Listing 3-14, cascade operator is used to access the fields and method in classes User and Address.
class User {
  String name, email;
  Address address;
  void sayHi() => print('hi, $name');
}
class Address {
  String street, suburb, zipCode;
  void log() => print('Address: $street');
}
void main() {
  User()
    ..name = 'Alex'
    ..email = '[email protected]'
    ..address = (Address()
      ..street = 'my street'
      ..suburb = 'my suburb'
      ..zipCode = '1000'
      ..log())
    ..sayHi();
}
Listing 3-14

Using cascade operator

3.7 Overriding Operators

Problem

You want to override operators in Dart.

Solution

Define overriding methods in class for operators.

Discussion

Dart has many operators. Only a subset of these operators can be overridden. These overridable operators are <, +, |, [], >, /, ^, []=, <=, ~/, &, ~, >=, *, <<, ==, -, %, and >>. For some classes, using operators is more concise than using methods. For example, the List class overrides the + operator for list concatenation. The code [1] + [2] is very easy to understand. In Listing 3-15, the class Rectangle overrides operators < and > to compare instances by area.
class Rectangle {
  int width, height;
  Rectangle(this.width, this.height);
  get area => width * height;
  bool operator <(Rectangle rect) => area < rect.area;
  bool operator >(Rectangle rect) => area > rect.area;
}
void main() {
  var rect1 = Rectangle(100, 100);
  var rect2 = Rectangle(200, 150);
  assert(rect1 < rect2);
  assert(rect2 > rect1);
}
Listing 3-15

Overriding operators

3.8 Using Constructors

Problem

You want to create new instances of Dart classes.

Solution

Use constructors.

Discussion

Like other programming languages, objects in Dart are created by constructors. Usually, constructors are created by declaring functions with the same name as their classes. Constructors can have arguments to provide necessary values to initialize new objects. If no constructor is declared for a class, a default constructor with no arguments is provided. This default constructor simply invokes the no-argument constructor in the superclass. However, if a constructor is declared, this default constructor doesn’t exist.

A class may have multiple constructors. You can name these constructors in the form ClassName.identifier to better clarify the meanings.

In Listing 3-16, the class Rectangle has a regular constructor that takes four arguments. It also has a named constructor Rectangle.fromPosition.
class Rectangle {
  final num top, left, width, height;
  Rectangle(this.top, this.left, this.width, this.height);
Rectangle.fromPosition(this.top, this.left, num bottom, num right)
      : assert(right > left),
        assert(bottom > top),
        width = right - left,
        height = bottom - top;
  @override
  String toString() {
    return 'Rectangle{top: $top, left: $left, width: $width, height: $height}';
  }
}
void main(List<String> args) {
  var rect1 = Rectangle(100, 100, 300, 200);
  var rect2 = Rectangle.fromPosition(100, 100, 300, 200);
  print(rect1);
  print(rect2);
}
Listing 3-16

Constructors

It’s common to use factories to create objects. Dart has a special kind of factory constructors that implements this pattern. A factory constructor doesn’t always return a new instance of a class. It may return a cached instance, or an instance of a subtype. In Listing 3-17, the class ExpensiveObject has a named constructor ExpensiveObject._create() to actually create a new instance. The factory constructor only invokes ExpensiveObject._create() when _instance is null. When running the code, you can see that the message “created” is only printed once.
class ExpensiveObject {
  static ExpensiveObject _instance;
  ExpensiveObject._create() {
    print('created');
  }
  factory ExpensiveObject() {
    if (_instance == null) {
      _instance = ExpensiveObject._create();
    }
    return _instance;
  }
}
void main() {
  ExpensiveObject();
  ExpensiveObject();
}
Listing 3-17

Facto+ry constructor

3.9 Extending a Class

Problem

You want to inherit behavior from an existing class.

Solution

Extend from the existing class to create a subclass.

Discussion

Dart is an object-oriented programming language. It provides support for inheritance. A class can extend from a superclass using the keyword extends. The superclass can be referred as super in the subclass. Subclasses can override instance methods, getters, and setters of superclasses. Overriding members should be annotated with the @override annotation.

Abstract classes are defined using the abstract modifier. Abstract classes cannot be instantiated. Abstract methods in abstract classes don’t have implementations and must be implemented by non-abstract subclasses.

In Listing 3-18, the class Shape is abstract with an abstract method area(). Classes Rectangle and Circle both extend from Shape and implement the abstract method area().
import 'dart:math' show pi;
abstract class Shape {
  double area();
}
class Rectangle extends Shape {
  double width, height;
  Rectangle(this.width, this.height);
  @override
  double area() {
    return width * height;
  }
}
class Square extends Rectangle {
  Square(double width) : super(width, width);
}
class Circle extends Shape {
  double radius;
  Circle(this.radius);
  @override
  double area() {
    return pi * radius * radius;
  }
}
void main() {
  var rect = Rectangle(100, 50);
  var square = Square(50);
  var circle = Circle(50);
  print(rect.area());
  print(square.area());
  print(circle.area());
}
Listing 3-18

Inheritance

3.10 Adding Features to a Class

Problem

You want to reuse a class’s code but are limited by single inheritance of Dart.

Solution

Use mixins.

Discussion

Inheritance is a common way to reuse code. Dart only supports single inheritance, that is, a class can have at most one superclass. If you want to reuse code from multiple classes, mixins should be used. A class can declare multiple mixins using the keyword with. A mixin is a class that extends from Object and declares on constructors. A mixin can be declared as a regular class using class or as a dedicated mixin using mixin. In Listing 3-19, CardHolder and SystemUser are mixins. The class Assistant extends from Student and has the mixin SystemUser, so we can use the useSystem() method of Assistant instances.
class Person {
  String name;
  Person(this.name);
}
class Student extends Person with CardHolder {
  Student(String name) : super('Student: $name') {
    holder = this;
  }
}
class Teacher extends Person with CardHolder {
  Teacher(String name) : super('Teacher: $name') {
    holder = this;
  }
}
mixin CardHolder {
  Person holder;
  void swipeCard() {
    print('${holder.name} swiped the card');
  }
}
mixin SystemUser {
  Person user;
  void useSystem() {
    print('${user.name} used the system.');
  }
}
class Assistant extends Student with SystemUser {
  Assistant(String name) : super(name) {
    user = this;
  }
}
void main() {
  var assistant = Assistant('Alex');
  assistant.swipeCard();
  assistant.useSystem();
}
Listing 3-19

Mixins

3.11 Using Interfaces

Problem

You want to have a contract for classes to follow.

Solution

Use implicit interface of a class.

Discussion

You should be familiar with interfaces as the contract of classes. Unlike other object-oriented programming languages, Dart has no concept of interfaces. Every class has an implicit interface that contains all the instance members of this class and the interfaces it implements. You can use implements to declare that a class implements the API of another class. In Listing 3-20, class CachedDataLoader implements the implicit interface of class DataLoader.
class DataLoader {
  void load() {
    print('load data');
  }
}
class CachedDataLoader implements DataLoader {
  @override
  void load() {
    print('load from cache');
  }
}
void main() {
  var loader = CachedDataLoader();
  loader.load();
}
Listing 3-20

Interfaces

3.12 Using Generics

Problem

You want to have type safety when your code is designed to work with different types.

Solution

Use generic classes and generic methods.

Discussion

Generics are not a strange concept to developers, especially for Java and C# developers. With generics, we can add type parameters to classes and methods. Generics are usually used in collections to create type-safe collections. Listing 3-21 shows the usage of generic collections in Dart. Dart generic types are reified, which means type information are available at runtime. That’s why the type of names is List<String>.
var names = <String>['a', 'b', 'c'];
print(names is List<String>);
var values = <String, int>{'a': 1, 'b': 2, 'c': 3};
print(values.values.toList());
Listing 3-21

Generic collections

We can use generics to create classes that deal with different types. In Listing 3-22, Pair<F, S> is a generic class with two type parameters F and S. Use extends to specify the upper bound of a generic type parameter. The type parameter P in CardHolder has an upper bound of type Person, so that CardHolder<Student> is valid.
class Pair<F, S> {
  F first;
  S second;
  Pair(this.first, this.second);
}
class Person {}
class Teacher extends Person {}
class Student extends Person {}
class CardHolder<P extends Person> {
  P holder;
  CardHolder(this.holder);
}
void main() {
  var pair = Pair('a', 1);
  print(pair.first);
  var student = Student();
  var cardHolder = CardHolder(student);
  print(cardHolder is CardHolder<Student>);
  print(cardHolder);
}
Listing 3-22

Generic types

Generic methods can be added to regular classes. In Listing 3-23, the regular class Calculator has two generic methods add and subtract.
class Calculator {
  T add<T extends num>(T v1, T v2) => v1 + v2;
  T subtract<T extends num>(T v1, T v2) => v1 - v2;
}
void main() {
  var calculator = Calculator();
  int r1 = calculator.add(1, 2);
  double r2 = calculator.subtract(0.1, 0.2);
  print(r1);
  print(r2);
}
Listing 3-23

Generic methods

3.13 Using Libraries

Problem

You want to reuse libraries from Dart SDK or the community.

Solution

Use import to import libraries to use them in your app.

Discussion

When developing non-trivial Dart apps, it’s inevitable to use libraries. These can be built-in libraries in Dart SDK or libraries contributed by the community. To use these libraries, we need to import them with import first. import has only one argument to specify the URI of the library. Built-in libraries have the URI scheme dart:, for example, dart:html and dart:convert. Community packages have the URI scheme package: and are managed by the Dart pub tool. Listing 3-24 shows examples of importing libraries.
import 'dart:html';
import 'package:meta/meta.dart';
Listing 3-24

Import libraries

It’s possible that two libraries export the same identifiers. To avoid conflicts, we can use as to provide prefixes for one of the libraries or both. In Listing 3-25, both lib1.dart and lib2.dart export the class Counter. After assigning different prefixes to these two libraries, we can use the prefix to access the class Counter.
import 'lib1.dart' as lib1;
import 'lib2.dart' as lib2;
lib1.Counter counter;
Listing 3-25

Rename libraries

You don’t need to import all members of a library. Use show to explicitly include members. Use hide to explicitly exclude members. In Listing 3-26, when importing the library dart:math, only Random is imported; when importing the library dart:html, only Element is excluded.
import 'dart:math' show Random;
import 'dart:html' hide Element;
Listing 3-26

Show and hide members

3.14 Using Exceptions

Problem

You want to deal with failures in Dart apps.

Solution

Report failures using throw. Handle exceptions using try-catch-finally .

Discussion

Code fails. It’s natural for code to report failures and handle them. Dart has a similar exception mechanism as Java, except that all exceptions in Dart are unchecked exceptions. Methods in Dart don’t declare exceptions they may throw, so it’s not required to catch exceptions. However, uncaught exceptions cause the isolate to suspend and may result in program termination. Proper failure handing is also a key characteristic of robust apps.

Report Failures

We can use throw to throw exceptions. In fact, all non-null objects can be thrown, not only types that implement types Error or Exception. It’s recommended to only throw objects of types Error and Exception.

An Error object represents a bug in the code that should not happen. For example, if a list only contains three elements, trying to access the fourth element causes a RangeError to be thrown. Unlike Exceptions, Errors are not intended to be caught. When an error occurred, the safest way is to terminate the program. Errors carry clear information about why they happen.

Comparing to Errors, Exceptions are designed to be caught and handled programmatically. For example, sending HTTP requests may not succeed, so we need to handle exceptions in the code to deal with failures. Exceptions usually carry useful data about the failures. We should create custom types that extend from Exception to encapsulate necessary data.

Catch Exceptions

When an exception is thrown, you can catch it to stop it from propagating, unless you rethrow it. The goal to catch an exception is to handle it. You shouldn’t catch an exception if you don’t want to handle it. Exceptions are caught using try, catch, and on. If you don’t need to access the exception object, using on is enough. With catch, you can access the exception object and the stack trace. Use on to specify the type of exception to be caught.

When you catch an exception, you should handle it. However, sometimes you may only want to partially handle it. In this case, you should use rethrow to rethrow the exception. It’s a bad practice to catch an exception but not handle it completely.

If you want some code to run whether or not an exception is thrown, you can put the code in a finally clause. If no exception is thrown, finally clause runs after the try block. If an exception is thrown, finally clause runs after the matching catch clause.

In Listing 3-27, the function getNumber() throws a custom exception type ValueTooLargeException. In the function main(), the exception is caught and rethrown.
import 'dart:math' show Random;
var random = Random();
class ValueTooLargeException implements Exception {
  int value;
  ValueTooLargeException(this.value);
  @override
  String toString() {
    return 'ValueTooLargeException{value: $value}';
  }
}
int getNumber() {
  var value = random.nextInt(10);
  if (value > 5) {
    throw ValueTooLargeException(value);
  }
  return value;
}
void main() {
  try {
    print(getNumber());
  } on ValueTooLargeException catch (e) {
    print(e);
    rethrow;
  } finally {
    print('in finally');
  }
}
Listing 3-27

Use exceptions

3.15 Summary

Learning a new programming language is not an easy task. Even though Dart looks similar with other programming languages, there are still some unique features in Dart. This chapter only provides a brief introduction of important features in Dart.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.27.244