A touch of class – how to use classes and objects
We saw in Chapter 1, Dart – A Modern Web Programming Language, how a class contains members such as properties, a constructor, and methods (refer to banking_v2.dart
). For those familiar with classes in Java or C#, it's nothing special and we can see already certain simplifications:
- The short constructor notation lets the parameter values flow directly into the properties:
BankAccount(this.owner, this.number, this.balance) { … }
- The keyword
this
is necessary here and refers to the actual object (being constructed), but it is rarely used elsewhere (only when there is a name conflict). Initialization of instance variables can also be done in the so-called initializer list, in this shorter version of the constructor:BankAccount(this.owner, this.number, this.balance): dateCreated = new DateTime.now();
- The variables are initialized after the colon (
:
) and are separated by a comma. You cannot use the keywordthis
in the initializer expression. If nothing else needs to be done, the constructor body can be left out. - The properties have automatic getters to read the value (as in
ba.balance
) and, when they are notfinal
orconstant
, they also have a setter method to change the value (as inbalance += amount
).
Tip
You can start out by using dynamic typing (var
) for properties, especially when you haven't decided what type a property will become. As development progresses, though, you should aim to change dynamic types into strong types that give more meaning to your code and can be validated by the tools.
Properties that are Boolean values are commonly named with is
at the beginning, for example, isOdd
.
A class has a default constructor when there are no other constructors defined. Objects (instances of the class) are made with the keyword new
, and an object is
of the type of the class. We can test this with the is operator, for example if object ba
is of type BankAccount
, then the following is true: ba
is
BankAccount
. Single inheritance between classes is defined by the extends
keyword, the base class of all classes being Object
.
Member access uses the dot (.
) notation, as in ba.balance
or ba.withdraw(100.0)
. A class can contain objects that are instances of other classes: a feature known as composition (aggregation). For example, we could decide at a later stage that the String owner in the BankAccount
class should really be an object of a Person
class, with many other properties and methods.
A neat feature to simplify code is the cascade operator (..
); with it, you can set a number of properties and execute methods on the same object, for example, on the ba
object in the following code (it's not chaining operations!):
ba ..balance = 5000.0 ..withdraw(100.0) ..deposit(250.0);
We'll focus on what makes Dart different and more powerful than common OO languages.
Visibility – getters and setters
What about the visibility or access of class members? They are public by default, but if you name them to begin with an underscore (_
), they become private. However, private in Dart does not mean only visible in its class; a private field (property)—for example, _owner
—is visible in the entire library in which it is defined but not in the client code that uses the library.
For the moment, this means that it is accessible in the code file where it is declared because a code file defines an implicit library. The entire picture will become clear in the coming section on libraries. A good feature that enhances productivity is that you can begin with public properties, as in project_v1.dart
. A Project
object has a name and a description and we use the default constructor:
main() { var p1 = new Project(); p1.name = 'Breeding'; p1.description = 'Managing the breeding of animals'; print('$p1'); // prints: Project name: Breeding - Managing the breeding of animals } class Project { String name, description; toString() => 'Project name: $name - $description'; }
Suppose now that new requirements arrive; the length of a project name must be less than 20 characters and, when printed, the name must be in capital letters. We want the Project
class to be responsible for these changes, so we create a private property, _name
, and the get
and set
methods to implement the requirements (refer to project_v2.dart
):
class Project { String _name; // private variable String description; Stringget
name => _name == null ? "" :_name.toUpperCase();set
name(String prName) { if (prName.length > 20) throw 'Only 20 characters or less in project name'; _name = prName; } toString() => 'Project name: $name - $description'; }
The get
method makes sure that, in case _name
is not yet filled in, an empty string is returned.
The code that already existed in main
(or in general, the client code that uses this property) does not need to change; it now prints Project name: BREEDING - Managing the breeding of animals
and, if a project name that is too long is given, the code generates an exception.
A getter (and a setter) can also be used without a corresponding property instead of a method, again simplifying the code, such as the getters for area, perimeter, and diagonal in the class Square
(square_v1.dart
):
import 'dart:math'; void main() { var s1 = new Square(2); print('${s1.perimeter}'); // 8 print('${s1.area}'); // 4 print('${s1.diagonal}'); // 2.8284271247461903 } class Square { num length; Square(this.length); num get perimeter => 4 * length; num get area => length * length; num get diagonal => length * SQRT2; }
SQRT2
is defined in dart:math
.
The new properties (derived from other properties) cannot be changed because they are getters. Dart doesn't do function overloading because of optional typing, but does allow operator overloading, redefining a number of operators (such as ==
, >=
, >
, <=
, and <
—all arithmetic operators—as well as []
and []=
). For example, examine the operator >
in square_v1.dart
:
bool operator >(Square other) => length > other.length? true : false;
If s1
and s2
are Square
objects, we can now write code like this: if (s2 > s1) { … }
.
Types of constructors
All OO languages have class constructors, but Dart has some special kinds of constructors covered in the following sections.
Because there is no function overloading, there can be only one constructor with the class name (the so-called main constructor). So if we want more, we must use named constructors, which take the form ClassName.constructorName
. If the main constructor does not have any parameters, it is called a default constructor. If the default constructor does not have a body of statements such as BankAccount();
, it can be omitted. If you don't declare a constructor, a default constructor is provided for you. Suppose we want to to create a new bank account for a person by copying data from another of his/her bank accounts, for example, the owner's name. We could do this with the named constructor BankAccount.sameOwner
(refer to banking_v3.dart
):
BankAccount.sameOwner(BankAccount acc) {
owner = acc.owner;
}
We could also do this with the initializer version:
BankAccount.sameOwner(BankAccount acc): owner = acc.owner;
When we make an object via this constructor and print it out, we get:
Bank account from John Gates with number null and balance null
A constructor can also redirect to the main constructor by using the this
keyword like so:
BankAccount.sameOwner2(BankAccount acc): this(acc.owner, "000-0000000-00", 0.0);
We initialize the number and balance to dummy values, because this()
has to provide three arguments for the three parameters of the main constructor.
Sometimes we don't want a constructor to always make a new object of the class; perhaps we want to return an object from a cache or create an object from a subtype instead. The factory
constructor provides this flexibility and is extensively used in the Dart SDK. In factory_singleton.dart
, we use this ability to implement the singleton pattern, in which there can be only one instance of the class:
class SearchEngine { static SearchEngine theOne; (1) String name; factory SearchEngine(name) { (2) if (theOne == null) { theOne = new SearchEngine._internal(name); } return theOne; } // private, named constructor SearchEngine._internal(this.name); (3) // static method: static nameSearchEngine () => theOne.name; (4) } main() { //substitute your favorite search-engine for se1: var se1 = new SearchEngine('Google'); (5) var se2 = new SearchEngine('Bing'); (6) print(se1.name); // 'Google' print(se2.name); // 'Google' print(SearchEngine.theOne.name); // 'Google' (7) print(SearchEngine.nameSearchEngine()); // 'Google' (8) assert(identical(se1, se2)); (9) }
In line (1)
, the static variable theOne
(here of type SearchEngine
itself, but it could also be of a simple type, such as num
or String
) is declared: such a variable is the same for all instances of the class. It is invoked on the class name itself, as in line (7)
; that's why it is also called a class variable. Likewise, you can have static methods (or class methods) such as nameSearchEngine
(line (4)
) called in the same way (line (8)
).
In lines (5)
and (6)
, two SearchEngine
objects se1
and se2
are created through the factory
constructor in line (2)
. This checks whether our static variable theOne
already refers to an object or not. If not, a SearchEngine
object is created through the named constructor SearchEngine._internal
from line (3)
; if it had already been created, nothing is done and the object theOne
is returned in both cases. The two SearchEngine
objects se1
and se2
are in fact the same object, as is proven in line (9)
. Note that the named constructor SearchEngine._internal
is private; a factory invoking a private constructor is also a common pattern.
Two squares created with the same length are different objects in memory. If you want to make a class where each object cannot change, provide it with const
constructors and make sure that every property is const
or final
, for example, class ImmutableSquare
in square_v1.dart
:
class ImmutableSquare { final num length; static final ImmutableSquare ONE = const ImmutableSquare(1); const ImmutableSquare(this.length); }
Objects are created with const
instead of new
, using the const
constructor in the last line of the class to give length
its value:
var s4 = const ImmutableSquare(4); var s5 = const ImmutableSquare(4); assert(identical(s4,s5));
Inheritance
Inheritance in Dart comes with no surprises if you know the concept from Java or .NET. Its main use is to reduce the codebase by factoring common code (properties, methods, and so on) into a common parent class. In square_v2.dart
, the class Square
inherits from the Rectangle
class, indicated with the extends
keyword (line (4)
). A Square
object inherits the properties from its parent class (see line (1)
), and you can refer to constructors or methods from the parent class with the keyword super
(as in line (5)
):
main() { var s1 = new Square(2); print(s1.width); (1) print(s1.height); print('${s1.area()}'); // 4 assert(s1 is Rectangle); (2) } class Rectangle { num width, height; Rectangle(this.width, this.height); num area() => width * height; (3) } class Square extends Rectangle { (4) num length; Square(length): super(length, length) { (5) this.length = length; } num area() => length * length; (6) }
Methods from the parent class can be overridden in the derived class without special annotations, for example, method area()
(lines (3)
and (6)
). An object of a child class is also of the type of the parent class (see line (2)
) and thus can be used whenever a parent class object is needed. This is the basis of what is called the polymorphic behavior of objects. All classes inherit from the class Object
, but a class can have only one direct parent class (single inheritance) and constructors are not inherited. Does author mean an object, the class of this object, its parent class, and so on (until Object
); they are all searched for the method that is called on. A class can have many derived classes, so an application is typically structured as a class hierarchy tree.
In OO programming, the class decomposition (with properties representing components/objects of other classes) and inheritance are used to support a divide-and-conquer approach to problem solving. Class A inherits from class B only when A is a subset of B, for example, a Square is a Rectangle, a Manager is an Employee; basically when class B is more generic and less specific than class A. It is recommended that inheritance be used with caution because an inheritance hierarchy is more rigid in the maintenance of programs than decomposition.
Looking for parent classes is an abstraction process, and this can go so far that the parent class we have decided to work with can no longer be fully implemented; that is, it contains methods that we cannot code at this point, so-called abstract methods. Extending the previous example to square_v3.dart
, we could easily abstract out a parent class, Shape
. This could contain methods for calculating the area and the perimeter, but they would be empty because we can't calculate them without knowing the exact shape. Other classes such as Rectangle
and Square
could inherit from Shape
and provide the implementation for these methods.
main() { var s1 = new Square(2); print('${s1.area()}'); // 4 print('${s1.perimeter()}'); // 8 var r1 = new Rectangle(2, 3); print('${r1.area()}'); // 6 print('${r1.perimeter()}'); // 10 assert(s1 is Shape); assert(r1 is Shape); // warning + exception in checked mode: Cannot instantiate// abstract class Shape // var f = new Shape(); } abstract class Shape { num perimeter(); num area(); } class Rectangle extends Shape { num width, height; Rectangle(this.width, this.height); num perimeter() => 2 * (height + width); num area() => height * width; } class Square extends Shape { num length; Square(this.length); num perimeter() => 4 * length; num area() => length * length; }
Also, making instances of Shape
isn't very useful, so it is rightfully an abstract class. An abstract class can also have properties and implemented methods, but you cannot make objects from an abstract class unless it contains a factory constructor that creates an object from another class. This can be useful as a default object creator for that abstract class. A simple example can be seen in factory_abstract.dart
:
void main() { Animal an1 = new Animal(); (1) print('${an1.makeNoise()}'); // Miauw } abstract class Animal { String makeNoise(); factory Animal() => new Cat(); (2) } class Cat implements Animal { String makeNoise() => "Miauw"; }
Animal
is an abstract class; because we most need cats in our app, we decide to give it a factory constructor to make a cat (line (2)
). Now we can construct an object from the Animal
class (line (1)
) and it will behave like a cat. Note that we must use the keyword implements
here to make the relationship between the class and the abstract class (this is also an interface, as we discuss in the next section). Many of the core types in the Dart SDK are abstract classes (or interfaces), such as num
, int
, String
, List
, and Map
. They often have factory constructors that redirect to a specific implementation class for making an object.
In Java and .NET, an abstract class without any implementation in its methods is called an interface—a description of a collection of fields and methods—and classes can implement interfaces. In Dart, this concept is greatly enhanced and there is no need for an explicit interface concept. Here, every class implicitly defines its own interface (also called API) containing all the public instance members of the class (and of any interfaces it implements). The abstract classes of the previous section are also interfaces in Dart. Interface is not a keyword in the Dart syntax, but both words are used as synonyms. Class B can implement any other class C by providing code for C's public methods. In fact, the previous example, square_v3.dart
, continues to work when we change the keyword extends
to implements
:
class Rectangle implements
Shape {
num width, height;
Rectangle(this.width, this.height);
num perimeter() => 2 * (height + width);
num area() => height * width;
}
This has the additional benefit that class Rectangle
could now inherit from another class if necessary. Every class that implements an interface is also of that type as is proven by the following line of code (when r1
is an object of class Rectangle
):
assert(r1 is Shape);
extends
is much less used than implements
, but it also clearly has a different meaning. The inheritance chain is searched for a called method, not the implemented interfaces.
Implementing an interface is not restricted to one interface. A class can implement many different interfaces, for example, class Cat implements Mammal, Pet { ... }
. In this new vision, where every class defines its own interface, abstract classes (that could be called explicit interfaces) are of much less importance (in fact, the keyword abstract
is optional; leaving it off only gives a warning of unimplemented members). This interface concept is more flexible than in most OO languages and doesn't force us to define our interfaces right from the start of a project. The dynamic
type, which we discussed briefly in the beginning of this chapter, is in fact the base interface that every other class (also Object
) implements. However, it is an interface without properties or methods and cannot be extended.
In summary, interfaces are used to describe functionality that is shared (implemented) by a number of classes. The implementing classes must fulfill the interface requirements. Coding against interfaces is an excellent way to provide more coherence and structure in your class hierarchy.
Polymorphism and the dynamic nature of Dart
Because Dart fully implements all OO principles, we can write polymorphic code, in which an object can be used wherever something of its type, the type of its parent classes, or the type of any of the interfaces it implements is needed. We see this in action in polymorphic.dart
:
main() { var duck1 = new Duck(); var duck2 = new Duck('blue'); var duck3 = new Duck.yellow(); polytest (new Duck()); // Quack I'm gone, quack! (1) polytest (new Person());// human_quack I am a person swimming (2) } polytest(Duck duck) { (3) print('${duck.sayQuack()}'); print('${duck.swimAway()}'); } abstract class Quackable { String sayQuack(); } class Duck implements Quackable { var color; Duck([this.color='red']); Duck.yellow() { this.color = 'yellow';} String sayQuack() => 'Quack'; String swimAway() => "I'm gone, quack!"; } class Person implements Duck { (4) sayQuack() => 'human_quack'; swimAway() => 'I am a person swimming'; (5) noSuchMethod(Invocation invocation) { (6) if (invocation.memberName == new Symbol("swimAway"))print("I'm not really a duck!"); } }
The top-level function polytest
in line (3)
takes anything that is a Duck
as argument. In this case, this is not only a real duck, but also a person because class Person
also implements Duck
(line (4)
). This is polymorphism. This property of a language permits us to write code that is generic in nature; using objects of interface types, our code can be valid for all classes that implement the interface used.
Another property shows that Dart also resembles dynamic languages such as Ruby and Python; when a method is called on an object, its class, parent class, the parent class of the parent class, and so on (until the class Object
), are searched for the method called. If it is found nowhere, Dart searches the class tree from the class to Object
again for a method called noSuchMethod()
.
Object
has this method, and its effect is to throw a noSuchMethodError
. We can use this to our advantage by implementing this method in our class itself; see line (6)
in class Person
(the argument mirror is of type Invocation
, its property memberName
is the name of the method called, and its property namedArguments
supplies a Map with the method's arguments). If we now remove line (5)
so that Person
no longer implements the method swimAway()
, the Editor gives us a warning:
Concrete class Person has unimplemented members fromDuck: String swimAway().
But if we now execute the code, the message I'm not really a duck!
is printed when print('${duck.swimAway()}')
is called for the Person
object. Because swimAway()
didn't exist for class Person
or any of its parent classes, noSuchMethod
is then searched, found in the class itself, and then executed. noSuchMethod
can be used to do what is generally called metaprogramming in the dynamic languages arena, giving our applications greater flexibility to efficiently handle new situations.
Collection types and generics
In the Built-in types and their methods section in Chapter 2, Getting to Work with Dart, we saw that very powerful data structures such as List and Map are core to Dart and not something added afterwards in a separate library as in Java or .NET.
How can we check the type of the items in a List or Map? A List created either as a literal or with the default constructor can contain items of any type, as the following code shows (refer to generics.dart
):
var date = new DateTime.now(); // untyped List (or a List of type dynamic): var lst1 = [7, "lucky number", 56.2, date]; print('$lst1'); // [7, lucky number, 56.2,// 2013-02-22 10:08:20.074] var lst2 = new List(); lst2.add(7); lst2.add("lucky number"); lst2.add(56.2); lst2.add(date); print('$lst2'); // [7, lucky number, 56.2,// 2013-02-22 10:08:20.074]
While this makes for very versatile Lists most of the time, you know that the items will be of a certain type, such as int
or String
or BankAccount
or even List
, themselves. In this case, you can indicate type E
between <
and >
in this way: <E>
. An example is shown in the following code:
var langs = <String>["Python","Ruby", "Dart"]; var langs2 = new List<String>(); (1) langs2.add("Python"); langs2.add("Ruby"); langs2.add("Dart"); var lstOfString = new List<List<String>>(); (2)
(Don't forget the ()
at the end of lines (1)
and (2)
because this calls the constructor!
With this, Dart can control the items for us; langs2.add(42);
gives us a warning and a TypeErrorImplementation
exception when run in checked mode:
type 'int' is not a subtype of type 'String' of 'value'
Here, value
means 42. However, when we run in production mode, this code runs just fine. Again, indicating the type helps us to prevent possible errors and at the same time documents your code.
Why is the special notation <>
also used as List<E>
in the API documents for List? This is because all of the properties and methods of List work for any type E
. That's why the List<E>
type is called generic (or parameterized). The formal type parameter E
stands for any possible type.
The same goes for Maps; a Map is in fact a generic type Map<K,V>
, where K
and V
are formal type parameters for the types of the keys and values respectively, giving us the same benefits as the following code demonstrates:
var map = new Map<int, String>(); map[1] = 'Dart'; map[2] = 'JavaScript'; map[3] = 'Java'; map[4] = 'C#'; print('$map'); // {1: Dart, 2: JavaScript, 3: Java, 4: C#} map['five'] = 'Perl'; // String is not assignable to int (3)
Again, line (3)
gives us a TypeError
exception in checked mode, not in production mode. We can test the generic types like this:
print('${langs2 is List}'); // true print('${langs2 is List<String>}'); // true (4) print('${langs2 is List<double>}'); // false (5)
We see that, in line (5)
, the type of the List is checked; this check works even in production mode! (Uncheck the Run in Checked Mode checkbox in Run | Manage Launches and click on Apply to see this in action.) This is because generic types in Dart (unlike in Java) are reified; their type info is preserved at runtime, so you can test the type of a collection even in production mode. Note, however, that this is the type of the collection only. When adding the statement langs2.add(42);
(which executes fine in production mode), the check in line (4)
still gives us the value true
. If you want to check the types of all the elements in a collection in production mode, you have to do this for each element individually, as shown in the following code:
for (var s in langs2) { if (s is String) print('$s is a String'); else print ('$s is not a String!'); } // output: // Python is a String // Ruby is a String // Dart is a String // 42 is not a String!
Checking the types of generic Lists gives mostly expected results:
print(new List <String>() is List<Object>); // true (1) print(new List<Object>() is List<String>); // false (2) print(new List<String>() is List<int>); // false (3) print(new List<String>() is List); // true (4) print(new List() is List<String>); // true (5)
Line (1)
is true because Strings (as everything) are Objects. (2)
is false because not every Object is a String. (3)
is false because Strings are not of type int (4)
is true because Strings are also of the general type dynamic
. Line (5)
can be a surprise: dynamic
is String
. This is because generic types without type parameters are considered substitutable (subtypes of) for any other version of that generic type.
Apart from List
and Map
, there are other important collection classes, such as Queue
and Set
, among others specified in the dart:collection
library; most of them are generic. We can't review them all here but the most important ones have the following relations (an arrow is UML notation for "is a subclass of" (extends
in Dart):
List
and Queue
are classes that inherit from Iterable
, and Set
inherits from IterableBase
; all these are abstract classes. The Map
class is also abstract and forms on its own the root of a whole series of classes that implement containers of values associated with keys, sometimes also called dictionaries. Put simply, the Iterable
interface allows you to enumerate (or iterate, that is, read but not change) all items of a collection one-by-one using what is called an Iterator. As an example, you can make a collection of the numbers 0 to 9 by making an Iterator with:
var digits = new Iterable.generate(10, (i) => i);
The iteration can be performed with the for (
item in
collection )
statement:
for
(var noin
digits) { print(no); } // prints 0 1 2 3 4 5 6 7 8 9 on successive lines
This prints all the numbers from 0 to 9 successively. Members such as isEmpty
, length
, and contains()
, which we saw in action with List (refer to lists.dart
) are already defined at this level, but there is a lot more. Iterable
also defines very useful methods for filtering, searching, transforming, reducing, chaining, and so on. This shows that Dart has a lot of the characteristics of a functional language: we see lots of functions taking functions as parameters or returning functions. Let us look at some examples applied to a list by applying toList()
to our Iterable
object digits:
var digList = digits.toList();
An even shorter and more functional version than for...in
is forEach
, which takes as parameter a function that is applied to every item i
of the collection in turn. In the following example, an anonymous function that simply prints the item is shown:
digList.forEach((i) => print('$i'));
Use forEach
whenever you don't need the index of the item in the loop. This also works for Maps, for example, to print out all the keys in the following map:
Map webLinks = { 'Dart': 'http://www.dartlang.org/','HTML5': 'http://www.html5rocks.com/' }; webLinks.forEach((k,v) => print('$k')); // prints: Dart HTML5
If we want the first or last element of a List, use the corresponding functions.
If you want to skip the first n items use skip(
n)
, or skip by testing on a condition with skipWhile(
condition)
:
var skipL1 = digList.skip(4).toList(); print('$skipL1'); // [4, 5, 6, 7, 8, 9] var skipL2 = digList.skipWhile((i) => i <= 6).toList(); print('$skipL2'); // [7, 8, 9]
The functions take
and takeWhile
do the opposite; they take the given number of items or the items that fulfill the condition:
var takeL1 = digList.take(4).toList(); print('$takeL1'); // [0, 1, 2, 3] var takeL2 = digList.takeWhile((i) => i <= 6).toList(); print('$takeL2'); // [0, 1, 2, 3, 4, 5, 6]
If you want to test whether any of the items fulfill a condition, use any
; to test whether all of the items do so, use every
:
var test = digList.any((i) => i > 10); print('$test'); // false var test2 = digList.every((i) => i < 10); print('$test2'); // true
Suppose you have a List and you want to filter out only these items that fulfill a certain condition (this is a function that returns a Boolean, called a predicate), in our case the even digits; here is how it's done:
var even = (i) => i.isEven; (1) var evens = digList.where(even).toList(); (2) print('$evens'); // [0, 2, 4, 6, 8] (3) evens = digList.where((i) => i.isEven).toList(); (4) print('$evens'); // [0, 2, 4, 6, 8]
We use the isEven
property of int
to construct an anonymous function in line (1)
. It takes the parameter i
to test its evenness, and we assign the anonymous function to a function variable called even
. We pass this function as a parameter to where
, and we make a list of the result in line (2)
. The output in line (3)
is what we expect.
It is important to note that where
takes a function that for each item tests a certain condition and thus returns true
or false
. In line (4)
, we write it more tersely in one line, appropriate and elegant for short predicate functions. Why do we need the call toList()
in this and the previous functions? Because where
(and the other Iterable
methods) return a so-called lazy Iterable
. Calling where
alone does nothing; it is toList()
that actually performs the iteration and stuffs the results in a List (try it out: if you leave out toList()
, in line (4
), then the right-hand side is an instance of WhereIterable
).
If you want to apply a function to every item and form a new List with the results, you can use the map
function; in the following example, we triple each number:
var triples = digList.map((i) => 3 * i).toList(); print('$triples'); // [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
Another useful utility is to apply a given operation with each item in succession, combined with a previously calculated value. Concretely, say we want to sum all elements of our List. We can of course do this in a for
loop, accumulating the sum in a temporary variable:
var sum = 0; for (var i in digList) { sum += i; } print('$sum'); // 45
Dart provides a more succinct and functional way to do this kind of manipulation with the reduce
function (eliminating the need for a temporary variable):
var sum2 = digList.reduce((prev, i) => prev + i); print('$sum2'); // 45
We can apply reduce
to obtain the minimum and maximum of a numeric List as follows:
var min = digList.reduce(Math.min); print('minimum: $min'); // 0 var max = digList.reduce(Math.max); print('maximum: $max'); // 9
For this to work, we need to import the math library:
import 'dart:math' as Math;
We could do this because min
and max
are defined for numbers, but what about other types? For this, we need to be able to compare two List
items: i1
and i2
. If i2
is greater than i1
, we know the min
and max
of the two and we can sort them. Dart has this intrinsically defined for the basic types int
, num
, String
, Duration
, and Date
. So in our example, with types int
we can simply write:
var lst = [17, 3, -7, 42, 1000, 90];
lst.sort();
print('$lst'); // [-7, 3, 17, 42, 90, 1000]
If you look up the definition of sort()
, you will see that it takes as optional argument a function of type int
, compare(E a, E b)
, belonging to the Comparable
interface. Generally, this is implemented as follows:
- if
a < b
return-1
- if
a > b
return1
- if
a == b
return0
In the following code, we use the preceding logic to obtain the minimum and maximum of a List of Strings:
var lstS = ['heg', 'wyf', 'abc']; var minS = lstS.reduce((s1,s2) => s1.compareTo(s2) < 0 ? s1 : s2); print('Minimum String: $minS'); // abc
In a general case, we need to implement compareTo
ourselves for the element type of the list, and it turns out that the preceding code lines can then be used to obtain the minimum and maximum of a List of a general type! To illustrate this, we will construct a List of persons; these are objects of a very simple Person
class:
class Person { String name; Person(this.name); }
We make a List of four Person
objects and try to sort it as shown in the following code:
var p1 = new Person('Peeters Kris'); var p2 = new Person('Obama Barak'); var p3 = new Person('Poetin Vladimir'); var p4 = new Person('Lincoln Abraham'); var pList = [p1, p2, p3, p4]; pList.sort();
We then get the following exception:
type 'Person' is not a subtype of type 'Comparable'.
This means that class Person
must implement the Comparable
interface by providing code for the method compareTo
. Because String
already implements this interface, we can use the compareTo
method for the person's names:
lass Person implements Comparable{ String name; Person(this.name); // many other properties and methods compareTo(Person p) => name.compareTo(p.name); }
Then we can get the minimum and maximum and sort our Person
List in place simply by:
var minP = pList.reduce((s1,s2) => s1.compareTo(s2) < 0 ? s1 : s2); print('Minimum Person: ${minP.name}'); // Lincoln Abraham var maxP = pList.reduce((s1,s2) => s1.compareTo(s2) < 0 ? s2 : s1); print('Maximum Person: ${maxP.name}'); // Poetin Vladimir pList.sort(); pList.forEach((p) => print('${p.name}'));
The preceding code prints the following output (on successive lines):
Lincoln Abraham Obama Barak Peeters Kris Poetin Vladimir
For using Queue, your code must import the collection library by using import 'dart:collection';
because that's the library this class is defined in. It is another collection type, differing from a List in that the first (head) or the last item (tail) are important here. You can add an item to the head with addFirst
or to the tail with add
or addLast
; or you can remove an item with removeFirst
or removeLast
:
var langsQ = new Queue(); langsQ.addFirst('Dart'); langsQ.addFirst('JavaScript'); print('${langsQ.elementAt(1)}'); // Dart var lng = langsQ.removeFirst(); assert(lng=='JavaScript'); langsQ.addLast('C#'); langsQ.removeLast(); print('$langsQ'); // {Dart}
You have access to the items in a Queue by index with elementAt(index)
, and forEach
is also available. For this reason, Queues are ideal when you need a first-in first-out data structure (FIFO), or a last-in first-out data structure (LIFO, called a stack in most languages).
Lists and Queues allow duplicate items. If you don't need ordering and your requirement is to only have unique items in a collection, use a Set
type:
var langsS = new Set(); langsS.add('Java'); langsS.add('Dart'); langsS.add('Java'); langsS.length == 2; print('$langsS'); // {Dart, Java}
Again, Sets allow for the same methods as List and Queue from their place in the collection hierarchy (see the The collection hierarchy figure). They also have the specific intersection method that returns the common elements between a Set and another collection.
Here is a handy flowchart to decide which data structure to use:
Tip
Maps have unique keys (but not values) and Sets have unique items, while Lists and Queues do not. Lists are ideal for arbitrary access to items anywhere in the collection (by index), but changing their size can be costly. Queues are the type to use if you mainly want to operate on the head or tail of the collection.
Structuring your code using libraries
Using classes, extending them, and implementing interfaces are the way to go to structure your Dart code. But how do we group together a number of classes, interfaces, and top-level functions that are coupled together? To package an application or to create a shareable code base, we use a library. The Dart SDK already provides us with some 30 utility libraries, such as dart:core
, dart:math
, and dart:io
. You can look them up in your Editor by going to Help | API Reference or via the URL http://api.dartlang.org. All built-in libraries have the dart:
prefix. We have seen them in use a few times and know that we have to import them in our code as import 'dart:math';
in prorabbits_v7.dart
. Web applications will always import dart:html
(dart:core
is the most fundamental library and so is imported automatically).
Likewise, we can create our own libraries and let other apps import them to use their functionality. To illustrate this, let us do so for our rabbit-breeding application (perhaps there is a market for this app after all). For an app this simple, this is not needed, of course. However, every Dart app that contains a main()
function is also a library even when not indicated. We make a new app called breeding
that could contain all kinds of breeding calculations. We group together all the constants that we will need in a file called constants.dart
, and we move the function that calculates the rabbit breeding to a file named rabbits.dart
in a subfolder called rabbits
. All files now have to declare how they are part of the library. There is one code file (the library file in the bin
subfolder; its file icon in the Editor is shown in bold) that contains the library
keyword; in our example, this is breeding.dart
in line (1)
:
library breeding; (1) import 'dart:math'; (2) part 'constants.dart'; (3) part 'rabbits/rabbits.dart'; void main() { (4) print("The number of rabbits increases as:\n"); for (int years = 0; years <= NO_YEARS; years++) { print("${calculateRabbits(years)}"); } }
A library needs a name; here it is breeding
(all lowercase, and not in quotes); other apps can import our library through this name. This file also contains all necessary import
statements (line (2)
) and then sums up (in no particular order) all source files that together constitute the library. This is done with the part
keyword, followed by the quoted (relative) pathname to the source file. For example, when rabbits.dart
resides in a subfolder called rabbits
, this will be written as:
part 'rabbits/rabbits.dart';
But everything is simpler if all files of a library reside in one folder. So, the library file presents an overview of all the part files in which it is split; if needed, we can structure our library with subfolders, but Dart sees all this code as a single file. Furthermore, all library source files need to indicate that they are part of the library (we show only rabbits.dart
here); again, the library name is not quoted (line (1)
):
part of breeding; (1) String calculateRabbits(int years) { calc() => (2 * pow(E, log(GROWTH_FACTOR) * years)).round().toInt(); var out = "After $years years:\t ${calc()} rabbits"; return out; }
All these statements (library
, import
, part
, and part of
) need to appear at the top before any other code. The Dart compiler will import a specific source file only once even when it is mentioned several times. If there is a main entry function in our library, it must be in the library file (line (4)
); start the app to verify that we obtain the same breeding results as in our previous versions. A library that contains main()
is also a runnable app in itself but, in general, a library does not need to contain a main()
function. The part of
annotation enforces that a file can only be part of one library. Is this a restriction? No, because it strengthens the principle that code must not be duplicated. If you have a collection of business classes in an app, group them in their own library and import them into your app; that way, these classes are reusable.
To show how we can use our newly made library in another app, create a new application app_breeding
; in its startup file (app_breeding.dart
), we can call our library as shown in the following code:
import '../../breeding/bin/breeding.dart'; (1)
int years;
void main() {
years = 5;
print("The number of rabbits has attained:");
print("${calculateRabbits(years)}");
}
// Output:
//The number of rabbits has attained:
//After 5 years: 1518750 rabbits
The import statement in line (1)
points to the main file of our library, relative in the file system to the .dart
file we are in (two folder levels up with two periods (..
) and then into subfolder bin
of breeding
). As long as your libraries retain the same relative position to your client app (while deploying it in production), this works. You can also import a library from a (remote) website using a URL in this manner:
import 'http://www.breeding.org/breeding.dart';
Absolute file paths in import
are not recommended because they break too easily when deploying. In the next section, we discuss the best way of importing a library by using the package manager called pub.
If you only want one or a few items (variables, functions, or classes) from a library, you have the option of only importing these by enumerating them after show
:
import 'library1.dart' show var1, func1, Class1;
The inverse can also be done; if you want to import everything from the library excluding these items, use hide
:
import 'library1.dart' hide var1, func1, Class1;
We know that everything in a Dart app must have a unique name; or, to put it another way, there can be no name conflicts in the app's namespace. What if we have to import into our app two libraries that have the same names for some of their objects? If you only need one of them, you can use show
and/or hide
. But what if you need both? In such a case, you can give one of the libraries an alias and differentiate between both by using this alias as a prefix. Suppose library1
and library2
both have an object A
; you can use this as follows:
import 'library1.dart'; // contains class A import 'library2.dart' as libr2; // contains class A var obj1 = new A(); // Use A from library1. var obj2 = new libr2.A(); // Use A from library2.
Use this feature only when you really have to, for example, to solve name conflicts or aid in readability. Finally, the export
command (possibly combined with show
or hide
) gives you the ability to combine (parts of) libraries. Refer to the app export
.
Suppose liba.dart
contains the following code:
library liba; abc() => 'abc from liba'; xyz() => 'xyz from liba';
Additionally, suppose libb.dart
contains the following code:
library libb; import 'liba.dart'; export 'liba.dart' show abc;
Then, if export.dart
imports libb
, it knows method abc
but not method xyz
:
import 'libb.dart'; void main() { print('${abc()}'); // abc from liba // xyz(); // cannot resolve method 'xyz' }
In the A touch of class – how to use classes and objects section, we mentioned that starting a name with _
makes it private at library level (so it is only known in the library itself not outside of it). This is the case for all objects: variables, functions, classes, methods, and so on. Now we will illustrate this in our breeding
library.
Suppose breeding.dart
now contains two top-level variables:
String s1 = 'the breeding of cats'; (1) var _s2 = 'the breeding of dogs'; (2)
We can use them both in main()
but also anywhere else in the library, for example, in rabbits.dart
:
String calculateRabbits(int years) { print('$s1 and $_s2'); //… return out; }
But if we try to use them in the app breeding.dart
, which imports breeding
, we get a warning in line (3)
of the following code in the Editor; it says cannot resolve _s2; s1 is visible but _s2 is not.
void main() { years = 5; // … print('$s1 and $_s2'); (3) }
An exception occurs when the code is run (both in checked and production mode). Note that, in lines (1)
and (2)
, we typed the public variable s1
as String
, while the private variable _s2
was left untyped. This is a general rule: give the publicly visible area of your library strong types and signatures. Privacy is an enhancement for developers used to JavaScript but people coming from the OO arena will certainly ask why there is no class privacy. There are probably a number of reasons: classes are not as primordial in Dart as in OO languages, Dart has to compile to JavaScript, and so on. Class privacy is not needed to the extent usually imagined, and if you really want to have it in Dart you can do it. Let the library only contain the class that has some private variables; these are visible only in this class because other classes or functions are outside this library.
Managing library dependencies with pub
Often your app depends on libraries (put in a package) that are installed in the cloud (in the pub repository, or the GitHub repository, and so on). In this section, we discuss how to install such packages and make them available to your code.
In the web version of our rabbits program (prorabbits_v3.dart
) in Chapter 1, Dart – A Modern Web Programming Language, we discussed the use of the pubspec.yaml
file. This file is present in every Dart project and contains the dependencies of our app on external packages. The pub tool takes care of installing (or updating) the necessary packages: right-click on the selected pubspec.yaml
file and choose Pub Get (or Upgrade, in case you need a more recent version of the packages). Alternatively, you can double-click on the pubspec.yaml
; then, a screen called Pubspec Details appears that lets you change the contents of the file itself. This screen contains a section called Pub Actions where you will find a link to Run Pub Get). It even automatically installs so-called transitive dependencies: if the package to install needs other packages, they will also be installed.
Let's prepare for the next section on unit testing by installing the unittest
package with the pub tool. Create a new command-line application and call it unittest_v1
. When you open the pubspec screen, you see no dependencies; however, at the bottom there is a tab called Source to go to the text file itself. This shows us:
name: unittest_v1 description: A sample command-line application #dependencies: # unittest: any
The lines preceded with #
are commented out in a .yaml
file; remove these to make our app dependent on the unittest
package. If we now run Pub Get, we see that a folder called packages
appears, containing in a folder called unittest
the complete source of the requested package. The same subfolders appear under the bin
folder. If needed, the command pub get
can also be run outside the Editor from the command line. The unittest
package belongs to the Dart SDK. In the Dart Editor installation, you can find it at D:\dart\dart-sdk\pkg
(substitute D:\dart
with the name of the folder where your Dart installation resides). However pub installs it from its central repository pub.dartlang.org, as you can see in the following screenshot. Another file pubspec.lock
is also created (or updated); this file is used by the pub tool and contains the version info of the installed packages (don't change anything in here). In our example, this contains:
# Generated by pub. See: http://pub.dartlang.org/doc/glossary.html#lockfile packages: dartlero: description: ref: null resolved-ref: c1c36b4c5e7267e2e77067375e2a69405f9b59ce url: "https://github.com/dzenanr/dartlero" source: git version: "1.0.2" path: description: path source: hosted version: "0.9.0" stack_trace: description: stack_trace source: hosted version: "0.9.0" unittest: description: unittest source: hosted version:
The following screenshot shows the configuration information for pubspec.yaml
:
The pubspec screen, as you can see in the preceding screenshot, also gives you the ability to change or fill in complementary app info, such as Name, Author, Version, Homepage, SDK version, and Description. The Version field is of particular importance; with it, you can indicate that your app needs a specific version of a package (such as 2.1.0) or a major version number of 1 (>= 1.0.0 < 2.0.0); it locks your app to these versions of the dependencies. To use the installed unittest
package, write the following code line at the top of unittest_v1.dart
:
import 'package:unittest/unittest.dart';
The path to a Dart source file after package:
is searched for in the packages
folder. As a second example and in preparation for the next chapter, we will install the dartlero
package from Pub (although the unittest_v1.dart
program will not use its specific functionality). Add a dependency called dartlero
via the pubspec screen; any version is good. Take the default value hosted
from the Source drop-down list and fill in https://github.com/dzenanr/dartlero for the path. Save this and then run Pub Get. Pub will install the project from GitHub, install it in the packages
folder, and update the pubspec.lock
file. To make it known to your app, use the following import
statement:
import 'package:dartlero/dartlero.dart';
The command pub publish
checks whether your package conforms to certain conditions and then uploads it to pub's central repository at pub.dartlang.org.
Tip
Dart Editor stores links to the installed packages for each app; these get invalid when you move or rename your code folders. If Editor gives you the error Cannot find referenced source: package: somepkg/pkg.dart
, do this: close the app in the editor and restart the editor. In most cases, the problem is solved; if not, clean out the Editor cache by deleting everything in C:\users\yourname\DartEditor
. When you reopen the app in the Editor the problem is solved.
Here is a summary of how to install packages:
- Change
pubspec.yaml
and add dependencies through the Details screen - Run the
pub get
command - Add an import statement to your code for every installed package
Unit testing in Dart
Dart has a built-in unit-test framework. We learned how to import it in our app in the previous section. Every real app, and certainly the ones that you're going to deploy somewhere, should contain a sufficient amount of unit tests. Test programs will normally be separated from the main app code, residing in their own directory called test
. Unit testing offers quite a lot of features; we will apply them in the forthcoming projects. Here we want to show you the basics, and we will do so by creating a BankAccount
object, making some transactions on it, and verifying the results so that we can trust our BankAccount
methods are doing fine (we continue to work in unittest_v1.dart
). Let's create a BankAccount
constructor and do some transactions:
var ba1 = new BankAccount("John Gates","075-0623456-72", 1000.0); ba1.deposit(500.0); ba1.withdraw(300.0); ba1.deposit(136.0);
After this, ba1.balance
is equal to 1336.0
(because 1000 + 500 – 300 + 136 = 1336). We can test whether our program calculated this correctly with the following statement:
test('Account Balance after deposit and withdrawal', () { expect(ba1.balance, equals(1336.0)); });
Or we can use a shorter statement as follows:
test
('Account Balance after deposit and withdrawal', () =>expect
(ba1.balance,equals
(1336.0)));
The function test
from unittest
takes two parameters:
- A test name (
String
); here, this isAccount Balance after deposit and withdrawal
- A function (here anonymous) that calls the
expect
function; this function also takes two parameters:- The value as given by the program
- The expected value, here given by
equals(
expected value)
Now running the program gives this output:
unittest-suite-wait-for-done PASS: Account Balance after deposit and withdrawal All 1 tests passed. unittest-suite-success
Of course, here PASS
indicates that our program tested successfully. If this were not the case (suppose the balance had to be 1335.0 but the program produced 1336.0) we would get an exception with the message Some tests failed
:
unittest-suite-wait-for-done FAIL: Account Balance after deposit and withdrawal Expected: <1335.0> but: was <1336.0> 0 PASSED, 1 FAILED, 0 ERRORS
There would also be screen output showing you which test went wrong, the expected (correct) value, and the program value (it is important to note that the tests run after all other statements in the method have been executed). Usually, you will have more than one test, and then you can group them as follows using the same syntax as test
:
group('Bank Account tests', () {
test('Account Balance after deposit and withdrawal', () => expect(ba1.balance, equals(1336.0)));
test('Owner is correct', () => expect(ba1.owner, equals("John Gates")));
test('Account Number is correct', () => expect(ba1.number, equals("075-0623456-72")));
});
We can even prepare the tests in a setUp
function (in this case, that would be creating the account and doing the transactions, setUp is run before each test) and clean up after each test executes in a tearDown
function (indicating that the test objects are no longer needed):
group('Bank Account tests', () { setUp(() { ba1 = new BankAccount("John Gates","075-0623456-72", 1000.0); ba1.deposit(500.0); ba1.withdraw(300.0); ba1.deposit(136.0); }); tearDown(() { ba1 = null; }); test('Account Balance after deposit and withdrawal', () =>expect(ba1.balance, equals(1336.0))); test('Owner is correct', () => expect(ba1.owner, equals("John Gates"))); test('Account Number is correct', () => expect(ba1.number, equals("075-0623456-72"))); });
The preceding code produces the following output:
unittest-suite-wait-for-done PASS: Bank Account tests Account Balance afterdeposit and withdrawal PASS: Bank Account tests Owner is correct PASS: Bank Account tests Account Number is correct All 3 tests passed. unittest-suite-success
In general, the second parameter of expect is a so-called matcher that tests whether the value satisfies some constraint. Here are some matcher possibilities: isNull
, isNotNull
, isTrue
, isFalse
, isEmpty
, isPositive
, hasLength(m)
, greaterThan(v)
, closeTo(value, delta)
, inInclusiveRange(low, high)
and their variants. For a more detailed discussion of their use, see the documentation at http://www.dartlang.org/articles/dart-unit-tests/#basic-synchronous-tests. We'll apply unit testing in the coming projects, notably in the example that illustrates Dartlero in the next chapter.
Project – word frequency
We will now develop systematically a small but useful web app that takes as input ordinary text and produces as output an alphabetical listing of all the words appearing in the text, together with the number of times they appear (their frequency). For an idea of the typical output, see the following screenshot (word_frequency.dart
):
The user interface is easy: the text is taken from the textarea
tag with id
text in the top half. Clicking on the frequency button sets the processing in motion, and the result is shown in the bottom half with id
words. Here is the markup from word_frequency.html
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Word frequency</title> <link rel="stylesheet" href="word_frequency.css"> </head> <body> <h1>Word frequency</h1> <section> <textarea id="text" rows=10 cols=80></textarea> <br/> <button id="frequency">Frequency</button> <button id="clear">Clear</button> <br/> <textarea id="words" rows=40 cols=80></textarea> </section> <script type="application/dart"src="word_frequency.dart"></script> <script src="packages/browser/dart.js"></script> </body> </html>
In the last line, we see that the special script dart.js
(which checks for the existence of the Dart VM and starts the JavaScript version if that is not found) is also installed by pub. In Chapter 1, Dart – A Modern Web Programming Language, we learned how to connect variables with the HTML elements through the querySelector
function:
variable = querySelector('#id')
So that's what we will do first in main()
:
// binding to the user interface: var textArea = querySelector('#text'); var wordsArea = querySelector('#words'); var wordsBtn = querySelector('#frequency'); var clearBtn = querySelector('#clear');
Our buttons listen to click events with the mouse; this is translated into Dart as:
wordsBtn.onClick.listen((MouseEvent e) { ... }
Here is the processing we need to do in this click-event handler:
- The input text is a String; we need to clean it up (remove spaces and special characters).
- Then we must translate the text to a list of words. This will be programmed in the following function:
List fromTextToWords(String text)
- Then, we traverse through the List and count for each word the number of times it occurs; this effectively constructs a map. We'll do this in the following function:
Map analyzeWordFreq(List wordList)
- From the map, we will then produce a sorted list for the output area:
List sortWords(Map wordFreqMap)
With this design in mind, our event handler becomes:
wordsBtn.onClick.listen((MouseEvent e) { wordsArea.value = 'Word: frequency \n'; var text = textArea.value.trim(); if (text != '') { var wordsList = fromTextToWords(text); var wordsMap = analyzeWordFreq(wordsList); var sortedWordsList = sortWords(wordsMap); sortedWordsList.skip(1).forEach((word) => wordsArea.value = '${wordsArea.value} \n${word}'); } });
In the last line, we append the output for each word to wordsArea
.
Now we fill in the details. Removing unwanted characters can be done by chaining replaceAll()
for each character like this:
var textWithout = text.replaceAll(',', '').replaceAll(';', '').replaceAll('.', '').replaceAll('\n', ' ');
This is very ugly code! We can do better by defining a regular expression that assembles all these characters. We can do this with the expression \W
that represents all noncharacters (letters, digits, or underscores), and then we only have to apply replaceAll
once:
List fromTextToWords(String text) {
var regexp = new RegExp(r"(\W\s?)"); (1)
var textWithout = text.replaceAll(regexp, '');
return textWithout.split(' '); (2)
}
We use the class RegExp
in line (1)
, which is more often used to detect pattern matches in a String
. Then we apply the split()
method of String
in line (2)
to produce a list of words wordsList
. This list is transformed into a Map with the following function:
Map analyzeWordFreq(List wordList) {
var wordFreqMap = new Map();
for (var w in wordList) {
var word = w.trim();
wordFreqMap.putIfAbsent(word, () => 0); (3)
wordFreqMap[word] += 1;
}
return wordFreqMap;
}
Note the use of putIfAbsent
instead of if...else
in line (3)
.
Then we use the generated Map to produce the desired output in the method sortWords
:
List sortWords(Map wordFreqMap) {
var temp = new List<String>();
wordFreqMap.forEach((k, v) => temp.add('${k}:${v.toString()}'));
temp.sort();
return temp;
}
The resulting list is shown in the bottom text area. You can find the complete listing in the file word_frequency.dart
.