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);
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-2String 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-3String 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-4Multi-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);
}
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);
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);
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-9Enumerated 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-10Use 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-11Function 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-12Anonymous functions
3.5 Using Typedefs
Problem
You want to have an alias of a function type.
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);
}
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'
..address = (Address()
..street = 'my street'
..suburb = 'my suburb'
..zipCode = '1000'
..log())
..sayHi();
}
Listing 3-14Using 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-15Overriding 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-17Facto+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());
}
3.10 Adding Features to a Class
Problem
You want to reuse a class’s code but are limited by single inheritance of Dart.
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;
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();
}
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();
}
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-21Generic 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 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-22Generic 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-23Generic 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-24Import 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;
Listing 3-25Rename 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-26Show 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;
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-27Use 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.