Chapter 3. Structuring Code with Classes and Libraries

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. Nonetheless, coding classes in Dart is more succinct when 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. That's why they are called generic. As soon as you get a few code files in your project, structuring them by making libraries becomes essential for code maintainability. Also, your code will 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 is done with a built-in unit test library.

We will look at the following topics:

  • Using classes and objects
  • Collection types and generic classes
  • Structuring your code using libraries
  • Managing library dependencies with pub
  • Unit testing in Dart

We will wrap it all up in a small but useful project to calculate word frequencies in an extract of text.

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 keyword this 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 not final or constant, they also have a setter method to change the value (as in balance += 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;

    String get 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.

Tip

Start your class code with public properties and, later, change some of them to private if necessary with getters and/or setters without breaking client code!

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) { … }.

Tip

Use overloading of operators sparingly and only when it seems a good fit and that it would be unsurprising to fellow developers.

Types of constructors

All OO languages have class constructors, but Dart has some special kinds of constructors covered in the following sections.

Named constructors

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.

factory constructors

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)).

Tip

Static methods can be useful, but don't create a class containing static methods only to provide common or widely used utilities and functionality; use top-level functions instead.

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.

const constructors

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.

Abstract classes and methods

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.

The interface of a class – implementing interfaces

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.

Typing collections and generics

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.

The collection hierarchy and its functional nature

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):

The collection hierarchy and its functional nature

The collection hierarchy

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 no in 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 return 1
  • if a == b return 0

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:

The collection hierarchy and its functional nature

Choosing a collection type

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:
");
  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:	 ${calc()} rabbits";
  return out;
}

Note

GROWTH_FACTOR is defined in the file constants.dart.

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.

Tip

You can start coding your app (library) in a single file. Gradually, you begin to discover units of functionality of classes and/or functions that belong together;, then you can move these into part files, with the library file structuring the whole.

Using a library in an app

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.

Resolving name conflicts

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'
}

Visibility of objects outside a library

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:dartdart-sdkpkg (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:

Managing library dependencies with pub

Configuring pub specifications for the app

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:usersyournameDartEditor. 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 is Account 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):

Project – word frequency

Word frequency app

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>
      &nbsp; &nbsp; &nbsp; &nbsp;
      <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:

  1. The input text is a String; we need to clean it up (remove spaces and special characters).
  2. Then we must translate the text to a list of words. This will be programmed in the following function:
    List fromTextToWords(String text)
  3. 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)
  4. 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 
';
  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} 
${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('
', ' '),

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"(Ws?)");              (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.

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

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