In this chapter, we will look at the object-oriented nature of Dart. If you have prior knowledge of an OO language, most of this chapter will feel familiar. Although, coding classes is more succinct while introducing some nice new features such as factory constructors, and generalizing the use of interfaces. If you come from the JavaScript world, you will start to realize that classes can really structure your application.
Data mostly comes in collections. Dart has some neat classes to work with collections and they can be used for any type of collections. This is why they are called generic. As soon as you get a few code files in your project, structuring them by making libraries would become essential for code maintainability. Also, your code would probably use existing libraries written by other developers; to make it easy, Dart has its own package manager called pub. Automating the testing of code on a functional level will be done with a built-in unit test library.
We will look at the following topics:
We will wrap it all up in a small but useful project to calculate word frequencies in an extract of text.
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 the classes in Java or C#, it's nothing special and we can already see certain simplifications:
BankAccount(this.owner, this.number, this.balance) { … }
this
keyword 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, like in this shorter version of the constructor:BankAccount(this.owner, this.number, this.balance): dateCreated = new DateTime.now();
:
) and are separated by a comma. You cannot use the this
keyword in the initializer expression. If nothing else needs to be done, the constructor body can be left out.ba.balance
) and, when they are not final or constant, they also have a setter method to change the value (as in balance += amount
).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 the 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 new
keyword and an is
object of the type of the class. We can test this with the is
operator; for example, if ba
has the BankAccount
type, then the following will be true: ba is BankAccount
. Single inheritance between the classes is defined by the extends
keyword; the base class of all the 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 the 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 the 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.
What about the visibility or access of class members? They are public by default, but if you name them beginning with an underscore (_
), they will become private. However, private in Dart does not mean that it will only be 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 the 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 will 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 will 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
. If a project name that is too long is given, the code will generate an exception.
A getter (and a setter) can also be used without a corresponding property instead of simplifying the code again, such as the getters for area, perimeter, and diagonal in the Square
(square_v1.dart
) class:
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 cannot be changed, because this is only a getter (they are properties derived from other properties). Dart doesn't do function overloading because of optional typing, but allows operator overloading, redefining a number of operators (such as ==
, >=
, >
, <=
, and <
—all the 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 the code like this: if (s2 > s1) { … }
.
All OO languages have class constructors, but Dart has only few 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 ClassName.constructorName
form. 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 will be 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 BankAccount.sameOwner
named constructor (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, as follows:
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, which is extensively used in the Dart SDK. In factory_singleton.dart
, we use this ability to implement the singleton design pattern (for a general intro to design patterns, see https://en.wikipedia.org/wiki/Software_design_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 theOne
static variable (here, of the SearchEngine
type 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 the instances of the class. It is invoked on the class name itself, as in line (7)
; this is 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 theOne
static variable already refers to an object or not. If not, a SearchEngine
object is created through the SearchEngine._internal
named constructor from line (3)
; if it was already created, nothing is done and the theOne
object 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 SearchEngine._internal
named constructor 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 the const
constructors and ensure that every property is const
or final
, for example, the ImmutableSquare
class 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 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 Square
class inherits from the Rectangle
class indicated by the extends
keyword (line (4)
). A Square
object inherits the properties from its parent class (see line (1)
), and you can refer to the constructors or methods from the parent class with the super
keyword (like 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, the area()
method (lines (3)
and (6)
). An object of a child class is also of the type of the parent class (see line (2)
) and thus it 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 Object
, but a class can have only one direct parent class (single inheritance). Constructors are not inherited. An object, the class of this object, its parent class, and so on (until Object
) 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 composition (with properties representing components / objects of other classes) and inheritance are used to support a divide-and-conquer approach toward 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 a composition.
Looking for parent classes is an abstraction process and it can go so far that the parent class we have decided to work with can no longer be fully implemented. That is, it can contain methods that we cannot code at this point by the so-called abstract methods. Extending the previous example to square_v3.dart
, we could easily abstract out a Shape
parent class. This could contain methods to calculate 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 this abstract class. A simple example can be seen in the factory_abstract.dart
file:
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 and, because we 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 implements
keyword here to make the relationship between the class and the abstract class (which is also an interface, as we will 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 to make 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. Dart's interfaces work differently from Java / C# 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 the words are used as synonyms. Class B can implement any other class C by providing the code for C's public methods. In fact, the previous example, square_v3.dart
, continues to work when we change the extends
keyword 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 the Rectangle
class 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);
The extends
keyword is much less used than implements
, but it clearly has a different meaning too. While using extends
, the inheritance chain is searched for a called method, not the implemented interfaces, as is the case while using implements
.
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 (which could be called explicit interfaces) are of much less importance (in fact, the abstract
keyword is optional; leaving it off only gives a warning of its unimplemented members). This interface concept is more flexible than the one in most OO languages, and it 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 the base interface that every other class (also Object
) implements. However, it is an interface without properties or methods and cannot be extended.
To summarize it, 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.
Because Dart fully implements all the OO principles, we are able to 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 will 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 polytest
function in line (3)
takes anything that is in Duck
as an argument. In this case, this is not only a real duck, but also a person, because the Person
class 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 the 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 the Object
class again for a method called noSuchMethod()
.
The Object
has this method and its effect is to throw noSuchMethodError
. We can use it to our advantage by implementing the method in our class itself; see line (6)
in the Person
class (the argument mirror is of the Invocation
type; its memberName
property is the name of the method called and its namedArguments
property supplies a Map with the method's arguments). If we now remove line (5)
so that Person
no longer implements the swimAway()
method, the Editor will give us a warning:
Concrete class Person has unimplemented members fromDuck: String swimAway().
However, if we now execute the code, the I'm not really a duck!
message would be printed when print('${duck.swimAway()}')
is called for the Person
object. Because swimAway()
didn't exist for the Person
class or any of its parent classes, noSuchMethod
is searched, found in the class itself, and 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.
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, not something that were added afterwards in a separate library like 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, like 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
, String
, BankAccount
or even List
, themselves. In this case, you can indicate the E
type 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)
With this, Dart can control the items for us; langs2.add(42);
gives us a warning and a TypeErrorImplementation
exception when it is run in the checked mode:
type 'int' is not a subtype of type 'String' of 'value'
Here, value means 42
. However, when we run in the production mode, this code runs just fine. Again, indicating the type helps us to prevent possible errors and, at the same time, document the 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 E
type. This is why the List<E>
type is called generic (or parameterized). The E
formal type parameter stands for any possible type.
The same goes for Maps; a Map is, in fact, a generic Map<K,V>
type, 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 will demonstrate:
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 the checked mode, not in the 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 can see that, in line (5)
, the type of the List is checked; this check works even in the production mode! (Uncheck the Run in Checked Mode checkbox in Run | Manage Launches and click on Apply to see it in action.) This is because generic types in Dart (unlike in Java) are reified; their type information is preserved during runtime, so you can test the type of collection even in the production mode. Note, however, that this is the type of the collection only. While adding the langs2.add(42);
statement (which executes fine in the production mode), the check in line (4)
still gives us the true
value. If you want to check the types of all the elements in a collection in the production mode, you will have to do it 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 expected results mostly:
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. Line (2)
is false
, because not every Object is a String. Line (3)
is false
, because Strings are not integers. Line (4)
is true
, because Strings are also of the dynamic
general type. Line (5)
can be a surprise: dynamic
is String
. This is because those generic types without type parameters are considered to be substitutable (subtypes of) for any other version of the generic type.
Apart from List
and Map
, there are other important collection classes, such as Queue
and Set
, among the 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 the 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 the 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 the lists.dart
file), 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 characteristics of a functional language: we see lots of functions taking functions as parameters or returning functions. Let's look at some of the 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 a function as a parameter that is applied to every i
item of the collection in turn. In the following example, a function that simply prints the item is shown:
digList.forEach((i) => print('$i'));
It is called an anonymous function, because it has no name.
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 you 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 take
and takeWhile
functions 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 the 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 i
parameter to test its evenness, and we assign the anonymous function to a function variable called even
. We then pass this function as a parameter to where
and 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, which makes it appropriate and elegant for short predicate functions. Why do we need the toList()
function to be called in this and the previous functions? Because where
(and the other Iterable
methods) returns a so-called lazy Iterable
method. 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()
, you will get 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 will 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 the 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 the other types? For this, we need to be able to compare the two List
items: i1
and i2
. If i2
is greater than i1
, we will know the min
and max
values of the two and be able to sort them. Dart has this intrinsically defined for the basic int
, num
, String
, Duration
, and Date
types. So, in our example with the int
types, 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 an optional argument, a function of the int
type, compare(E a, E b)
, belonging to the Comparable
interface. Generally, this will be implemented as follows:
a < b
, return -1
a > b
, return 1
a == b
, return 0
In the following code, we will 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. 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 will make a List of the 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 will then get the following exception:
type 'Person' is not a subtype of type 'Comparable'.
This means that the Person
class must implement the Comparable
interface by providing the code for the compareTo
method. Because String
already implements this interface, we can use the compareTo
method for the person's name:
class 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 min
and max
values 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 will print the following output (on successive lines):
Lincoln Abraham Obama Barak Peeters Kris Poetin Vladimir
To use Queue, your code must import the collection library by using import 'dart:collection';
, because this is the library the class is defined in. It is another collection type differing from a List in a way that the first (head) or the last item (tail) are important. 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 the index with elementAt(index)
and forEach
is also available. For this reason, Queues are ideal when you need a first-in first-out (FIFO) data structure or a last-in first-out (LIFO, which is called a stack in most languages) data structure.
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 following diagram). 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:
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.
Using classes, extending them, and implementing interfaces are the way to go to structure your Dart code. However, 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 http://api.dartlang.org URL. All the 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's make use of 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 this is 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 move the function that calculates the rabbit breeding to a file named rabbits.dart
in a subfolder called rabbits
. All the files now have to declare how they are a 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, it 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: "); for (int years = 0; years <= NO_YEARS; years++) { print("${calculateRabbits(years)}"); } }
A library needs a name; here, it is breeding
(all in lowercase and not in quotes). Other apps can import our library through this name. This file also contains all the necessary import
statements (line (2)
) and sums up (in no particular order) all the source files that together constitute of 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';
However, everything is simple if all the files of a library reside in one folder. So, the library file presents an overview of all the part files it is split in; if needed, we can structure our library with subfolders, but Dart sees all this code as a single file. Furthermore, all the library source files need to indicate that they are a 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: ${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 obtained 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 a part of one library. Is this a restriction? No, because it strengthens the principle that the 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; this way, these classes will be reusable.
You can start coding your app (library) in a single file. Gradually, you will begin to discover units of the functionality of classes and/or the functions that belong together; then, you can move these into part files, while the library file, which contains the part statements, structures the whole.
To show how we can use our newly made library in another app, create a new app_breeding
application. 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 folders level up with two periods (..
) and then into the bin
subfolder of breeding
). As long as your libraries retain the same relative position to your client app (while deploying it in production), it will work. You can also import a library from a (remote) website using a URL in the following manner:
import 'http://www.breeding.org/breeding.dart';
Absolute file paths in import
are not recommended, because they break too easily while deploying. In the next section, we will 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, and classes) from a library, you will 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
. However, what if you need both? In such a case, you can give one of the libraries an alias and differentiate between the two using this alias as a prefix. Suppose library1
and library2
have an object A
; you can differentiate between the two 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 readability. Finally, the export
command (possibly combined with show
or hide
) gives you the ability to combine (parts of) libraries, refer to the export
app.
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 will know the abc
method but not the xyz
method:
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 file name with _
makes it private for the library (only known in the library itself). 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
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; }
However, if we try to use them in the breeding.dart
app, which imports breeding
, we will get a warning in line (3)
of the following code in the Editor; it will say 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 the checked and production modes). Note that, in lines (1)
and (2)
, we typed the s1
public variable as String
, while the _s2
private variable 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 that 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 of this library.
Often, your app depends on libraries (packages) that are stored in the cloud (in the pub repository, or the GitHub repository, and so on). In this section, we will 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). Alternatives are to select the folder name of the app, go to Tools | Pub Get, and double-click on the .yaml
file (a screen called Pubspec Details appears and 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 the 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 test
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: test: any
We see that our app depends on the test
package. If we now run Pub Get, we will see that a folder called packages
will appear, containing in it a folder called test
of the complete source of the requested package. The same subfolders appear under the bin
folder. If needed, the pub install
command can also be run outside the Editor from the command line. Pub installs the test
package from its https://pub.dartlang.org/ central repository, as you can see in the following screenshot. Another pubspec.lock
file 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":{"test":{"version":"0.12.0","source":"hosted", "description":"test"},"meta":{"version":"0.12.0","source": "hosted","description":"meta"}}}
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 the 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 test
package, write the following code line at the top of test_v1.dart
:
import 'package:test/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 GitHub (although the unittest_v1.dart
program will not use its specific functionality). We will then add a dependency called dartlero
via the pubspec screen; any version would be good. Simply choose git
from the source list and fill in https://github.com/dzenanr/dartlero
for the path. Save this and then run Pub Install. Pub will clone 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 pub publish
command checks whether your package conforms to certain conditions and uploads it to the pub's central repository at pub.dartlang.org.
Dart Editor stores links to the installed packages for each app; these become invalid when you move or rename your code folders. If the Editor gives you the Cannot find referenced source: package: somepkg/pkg.dart
error, close the app in the Editor and restart the Editor. In most cases, the problem would be solved. However, if not, clean out the Editor cache by deleting everything in C:usersyournameDartEditor
. When you reopen the app in the Editor, the problem will be solved.
Here is a summary of how to install the packages:
pubspec.yaml
and add dependencies through the Details screen.pub get
command.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 we can trust that our BankAccount
methods are doing fine (we will 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 test
function from the test
package takes two parameters:
String
); here, this is Account Balance after deposit and withdrawal
expect
function; this function also takes two parameters:equals(
expected value)
Now, running the program will give the following output:
test-suite-wait-for-done PASS: Account Balance after deposit and withdrawal All 1 tests passed. test-suite-success
Of course, here PASS
indicates that our program is 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 Some tests failed
message:
test-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 a screen output showing you which tests went wrong, the expected (correct) value, and the program value (it is important to note that the tests run after all the other statements in the method have been executed). Usually, you will have more than one test. You can then 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 (here, this would involve creating the account and doing the transactions) and clean up after the tests 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:
test-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. test-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.
Dart has added new language features to support asynchronous programming in version 1.9: notably, the async
functions and await
expressions. The code is asynchronous
when it calls a possibly time-consuming operation (such as I/O) and continues executing without waiting for the operation to complete.
In the following code snippet, computeResult
represents an operation that could take some time to complete and we will wait for its result to return by calling it with await
. We will wrap this call inside an anasyncfunc
function, which is marked as async
: the code that calls anasyncfunc
will continue to execute immediately after this call:
anasyncfunc() async { var result = await computeResult(); if (result > limit) { // Do something. } else { // Do something else. } }
We can even use the normal try
/catch
mechanism for exception handling on the functions called with await
, like this:
anasyncfunc() async { var result; try { result = await computeResult(); } catch (e) { // react on possible exception e } // process result }
If you want to call an asynchronous function inside your app's main()
function, the body of main()
must be marked as async
as follows:
main() async {
// call to asynchronous function:
await computeResult();
}
These constructs enable us to write code much more elegantly than before so that asynchronous code looks like normal synchronous code. For a more detailed discussion, you can look up https://www.dartlang.org/docs/dart-up-and-running/ch03.html#dartasync---asynchronous-programming.
We will now develop systematically a small but useful web app that takes as input an 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 the 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 the 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 will see that the special dart.js
script (which checks for the existence of the Dart VM and starts the JavaScript version if it 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 query
function:
variable = query('#id')
So, this is 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 the 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:
List fromTextToWords(String text)
Map analyzeWordFreq(List wordList)
List sortWords(Map wordFreqMap)
With this design in mind, our event handler becomes:
wordsBtn.onClick.listen((MouseEvent e) { wordsArea.value = 'Word: frequency '; var text = textArea.value.trim(); if (text != '') { var wordsList = fromTextToWords(text); var wordsMap = analyzeWordFreq(wordsList); var sortedWordsList = sortWords(wordsMap); sortedWordsList.forEach((word) => wordsArea.value = '${wordsArea.value} ${word}'), } });
In the last line, we appended the output for each word to wordsArea
.
Now, we will 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(' ', ' '),
This is a very ugly code! We can do better by defining a regular expression that assembles all these characters. We can do this with the W
expression that represents all the noncharacters (letters, digits, or underscores). Then, we will only have to apply replaceAll
once:
List fromTextToWords(String text) {
var regexp = new RegExp('W+'), (1)
var textWithout = text.replaceAll(regexp, ''),
return textWithout.split(' '), (2)
}
We used the RegExp
class in line (1)
, which is more often used to detect pattern matches in a String
. Then, we applied the split()
method of String
in line (2)
to produce a list of 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 will use the generated Map to produce the desired output in the sortWords
method:
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 word_frequency.dart
file.
Observatory is the Dart Virtual Machine's (VM) built-in profiling and debugging tool. It is a new class of development tools with a focus on live, immediate reporting of data. Observatory includes tools to profile memory and the CPU usage of Dart programs. Starting with Dart 1.9, Observatory includes a full debugger.
Observatory runs in your browser and allows you to peek inside the running Dart VM on demand while providing real-time reporting on your executing code. For standalone applications, execute your start up <script>.dart
dart file from the command line with:
dart --enable-vm-service --pause-isolates-on-start <script>.dart
Then, open your browser with the http://localhost:8181/
URL. The main screen of Observatory will now appear as follows:
From this screen, follow the links to more detailed screens:
To start Observatory for a web app, launch chromium from the command line with the HTML start up page (here script.html
) as an argument, like this:
<path to chromium>chrome.exe script.html
The command-line output includes a line similar to the following:
Observatory listening on http://127.0.0.1:49621
Open this URL in any browser to bring up the Observatory main screen.
For more detailed information, consult https://www.dartlang.org/tools/observatory/.
3.129.26.22