Dart is a very young computer language with many interesting features. Dart is a class-based, object-oriented language with optional types, and it can help you write very powerful programs. In this chapter, we will cover the following topics:
Modularity and a namespace
Functions and closures in different scopes
Classes and mixins
Methods and operators
Complex things are the foundation of our world. To understand the complexity of the things around us, it is necessary to understand the parts that make them up. The evolution of complex things is due to functional and behavioral modularity. Functional modularity is the composition of smaller independent components with clear boundaries and functions. Behavioral modularity is mainly about traits and attributes that can evolve independently.
Modularity is nothing new. Earlier, product manufacturers figured out ways to increase the output and quality of the product, while still managing to reduce the cost pressures. They accomplished this through modularity. Modular design can be seen in automotive industry, buildings, and many other industries. Henry Ford introduced the notion of modularity in his assembly line with standardized and interchangeable parts. As a result, he reduced the production cycles and costs to achieve the mass production of his automobiles. A lot of these concepts are still used by many companies today.
Representation of complex things as a set of parts is called decomposition. By analogy, the real-world complex software may be broken into functional parts called modules. Each module can be created, changed, tested, used, and replaced separately.
Let's take a look at the benefits of modularity. For the sake of simplicity, we divide them into development and postproduction phases. Each of these phases has its own specific tasks to be solved in the scope of that phase.
The development phase has the following benefits:
Each module requires less code.
New features or changes can be introduced to modules in isolation, separate from the other modules.
Errors can be easily identified and fixed in a module.
Modules can be built and tested independently.
Programmers writing the modules can collaborate on the same application.
The same modules can be reused in many applications.
Applications have a main module and many auxiliary modules. Each module encapsulates a specific functionality and each one is integrated through loosely coupled communication channels provided by the main module.
The postproduction phase has the following benefits:
Modules kept in a versioning system can be easily maintained and tested
Fixed and noninfrastructural changes in a module can be done without affecting other modules
One significant disadvantage of modularity is that it increases complexity when managing many modules, especially when each one is individually versioned, updated, and has dependencies on the other modules.
The Dart language was designed by keeping the modules in mind. Modularity in Dart is realized through packages, libraries, and classes.
A library exposes functionality as a set of interfaces and hides the implementation from the rest of the world. As a concept, it's very similar to the separation of concern between objects in object-oriented programming (OOP). Separating an application into libraries helps minimize tight coupling and makes it easier to maintain the code. A library can be implemented as a simple function, a single class, several classes, or a collection of parts representing the entire API of a library. The Dart application is a library as well.
A package is simply a directory that contains a pubspec.yaml
file and may include any number of libraries and resources. The pubspec.yaml
file contains significant information about the package, its authors, and its dependencies on other packages. Here is a sample pubspec.yaml
file:
name: animation_library version: 0.1.0 author: Sergey Akopkokhyants description: Animation library for Web application dependencies: browser: any
The real pubspec.yaml
file can have more fields as specified at https://www.dartlang.org/tools/pub/pubspec.html.Before a package can be used, it must be published to a package management system, which is available as an online resource called pub at https://pub.dartlang.org/. To publish and retrieve packages from pub, we use a utility application of the same name. The pub utility uses information about dependencies from the pubspec.yaml
file to retrieve all the necessary packages from the following locations:
The recently updated packages at https://pub.dartlang.org/
The Git repository
The directory in the local filesystem
Dart Editor manages dependencies automatically for you. You can publish your packages right in Dart Editor.
A namespace is a container for all the members of a library. A namespace is defined by the library name. A library that is implicitly named has an empty namespace. This results in a conflict when trying to import libraries with the same namespaces. Import library namespace conflicts can be easily avoided with a prefix clause (as
) and a name prefix.
The following is an implicitly named library in which all the resources from dart:html
are made available within the scope of our library with the prefix dom
:
/** * Implicitly named library. * The dart:core library is automatically imported. */ import 'dart:html' as dom; /** * Get [Element] by [id]. */ dom.Element getById(String id) => dom.querySelector('#$id');
The library namespace make sense only in the Dart environment.
Dart implements encapsulation through privacy. Each member or identifier of a library has one of the two levels of access: private or public. Private members are visible only inside the library in which they are declared. Conversely, members with a public access are visible everywhere. The difference between them is the underscore prefix (_
), as shown in the following code:
// Animation library. library animation; // Class publicly available everywhere. class Animation { // ... } // Class visible only inside library. class _AnimationLibrary { // ... } // Variable publicly available everywhere. var animationSpeed;
The preceding code shows an animation library with two classes and one variable. The Animation
class and the animationSpeed
variable are public, and therefore visible outside the library. The _AnimationLibrary
class is private and it can be used only in the library.
Public access can be managed with the show
and hide
extensions of the import statement. Use the following show
extension with a specific class, which will then be available inside the library in which it is imported:
import 'animation.dart' as animation show Animation; // Main entry into Dart Web application. main() { // Animate new animation.Animation(); }
The animation
prefix in the import
statement defines the namespace to import the animation.dart
library. All members of the animation.dart
library are available in the global namespace via this prefix. We are referring to an Animation
class with the animation
prefix, as shown here:

Use the hide
extension with a specific class, which will then be unavailable inside the library in which it is imported; everything else from the library will be available, as shown in the following code:
import 'animation.dart' as animation hide Animation; // Main entry into Dart Web application. main() { // Animate var speed = animation.animationSpeed; }
Now we hide the Animation
class, but all the other public members in the namespace animation are still available, as seen in the following screenshot:

As you can see, the members of the imported library become invisible. This happens because the library exports members from the public namespace. It can be possible to re-export the imported library with the export
statement if this necessary export
statement can be managed with show
and hide
as it was for the import
statement, as shown in the following code:
library animation.css; import 'animation.dart' as animation; export 'animation.dart' show Animation; class CssAnimation extends animation.Animation { // ... }
The preceding code shows the animation.css
library. We export the Animation
class as part of the library namespace. Let's take a look at how we can use them:

There are the exported Animation
and original CssAnimation
classes available for use in our main code. Without the export, the Animation
class would be inaccessible in the main code.
I like Dart because everything is an object. Functions are first-class citizens because they support all the operations that are generally available to other types. This means each function have the following properties:
They can be named by a variable
They can be passed as an argument to a function
They can be returned as the result of a function
They can be stored in data structures
They can be created in any scope
Let's see where and how can we use functions as usual or as first-class citizens.
Naming functions by variable means that we can create a reference to a function and assign it to a variable, as shown in the following code:
library function_var; // Returns sum of [a] and [b] add(a, b) { return a + b; } // Operation var operation; void main() { // Assign reference to function [add] operation = add; // Execute operation var result = operation(2, 1); print("Result is ${result}");}"); }
Here is the result of the preceding code:
Result is 3
We have the add
function and the operation
variable. We assign the reference of the add
function to a variable and call the variable as a function later.
Passing functions as arguments to other functions can be very useful in cases when we need to implement the strategy design pattern to enable the program code to be selected and executed at runtime, as shown in the following code:
library function_param; // Returns sum of [a] and [b] add(a, b) { return a + b; } // Operation executor executor(operation, x, y) { return operation(x, y); } void main() { // Execute operation var result = executor(add, 2, 1); print("Result is ${result}"); }
Here is the result of the preceding code:
Result is 3
The global executor
function from the preceding example can call any function that accepts two arguments. You can see the implementation of the strategy design pattern in the form of anonymous functions passed as parameters of methods in collections.
Sometimes, a function can be returned as a result of another function, as shown in the following code:
library function_return; // Returns sum of [a] and [b] add(a, b) => a + b; // Returns difference between [a] and [b] sub(a, b) => a - b; // Choose the function depends on [type] chooser(bool operation) =>operation ? add : sub; void main() { // Choose function depends on operation type var operation = chooser(true); // Execute it var result = operation(2, 1); // Result print("Result is ${result}"); }
Here is the result of the preceding code:
Result is 3
We can store a function in data structures in any collection, as shown in the following code:
library function_store; // Returns sum of [a] and [b] add(a, b) => a + b; // Returns difference between [a] and [b] sub(a, b) => a - b; // Choose the function depends on [type] var operations = [add, sub]; void main() { // Choose function from list var operation = operations[0]; // Execute it var result = operation(2, 1); // Result print("Result is ${result}"); }
Here is the result of the preceding code:
Result is 3
We have two functions and the array operations in our example that stores references to them.
A function can be created in the global scope or within the scope of another function. A function that can be referenced with an access to the variables in its lexical scope is called a closure, as shown in the following code:
library function_closure; // Function returns closure function. calculate(base) { // Counter store var count = 1; // Inner function - closure return () => print("Value is ${base + count++}"); } void main() { // The outer function returns inner var f = calculate(2); // Now we call closure f(); f(); }
Here is the result of the preceding code:
Value is 3 Value is 4
We have the calculate
function, which contains the count
variable and returns a an inner function. The inner function has an access to the count
variable because both are defined in the same scope. The count
variable exists only within the scope of calculate
and would normally disappear when the function exits. This does not happen in this case because the inner function returned by calculate
holds a reference to count
. The variable has been closed covered, meaning it's within a closure.
Finally, we know what a first-class function is, where we can use them, and how important it is to use closures. Let's move ahead to classes and mixins.
We all know its wasteful trying to reinvent the wheel. It's even more wasteful trying to do it each time we want to build a car. So how can a program code be written more efficiently and made reusable to help us develop more powerful applications? In most cases, we turn to the OOP paradigm when trying to answer this question. OOP represents the concept of objects with data fields and methods that act on that data. Programs are designed to use objects as instances of classes that interact with each other to organize functionality.
The Dart language is dynamically typed, so we can write programs with or without the type annotations in our code. It's better to use the type annotations for the following reasons:
The type annotations enable early error detection. The static analyzer can warn us about the potential problems at the points where you've made the mistakes.
Dart automatically converts the type annotations into runtime assertion checks. In the checked mode, the dynamic type assertions are enabled and it can catch some errors when types do not match.
The type annotations can improve the performance of the code compiled in JavaScript.
They can improve the documentation making it much easier to read the code.
They can be useful in special tools and IDE such as the name completion.
The fact that the type annotations were not included in our code does not prevent our program from running. The variables without the type annotations have a dynamic type and are marked with var or dynamic. Here are several recommendations where the type annotations are appropriate:
In the real world, we find many individual objects, all of the same kind. There are many cars with the same make and model. Each car was built from the same set of blueprints. All of them contain the same components and each one is an instance of the class of objects known as Car
, as shown in the following code:
library car; // Abstract class [Car] can't be instantiated. abstract class Car { // Color of the car. String color; // Speed of the car. double speed; // Carrying capacity double carrying; // Create new [Car] with [color] and [carrying] info. Car(this.color, this.carrying); // Move car with [speed] void move(double speed) { this.speed = speed; } // Stop car. void stop() { speed = 0.0; } }
Objects have methods and instance variables. The color
, speed
, and carrying
are instance variables. All of them have the value null
as they were not initialized. The instance methods move
and stop
provide the behavior for an object and have access to instance variables and the this
keyword. An object may have getters and setters—special methods with the get
and set
keywords that provide read and write access to the instance variables. The Car
class is marked with the abstract
modifier, so we can't create an instance of this class, but we can use it to define common characteristics and behaviors for all the subclasses.
Different kinds of objects can have different characteristics that are common with others. Passenger cars, trucks, and buses share the characteristics and behaviors of a car. This means that different kinds of cars inherit the commonly used characteristics and behaviors from the Car
class. So, the Car
class becomes the superclass for all the different kinds of cars. We allow passenger cars, trucks, and buses to have only one direct superclass. A Car
class can have unlimited number of subclasses. In Dart, it is possible to extend from only one class. Every object extends by default from an Object
class:
library passenger_car; import 'car.dart'; // Passenger car with trailer. class PassengerCar extends Car { // Max number of passengers. int maxPassengers; // Create [PassengerCar] with [color], [carrying] and [maxPassengers]. PassengerCar(String color, double carrying, this.maxPassengers) : super(color, carrying); }
The PassengerCar
class is not an abstract and can be instantiated. It extends the characteristics of the abstract Car
class and adds the maxPassengers
variable.
Each Car
class defines a set of characteristics and behaviors. All the characteristics and behaviors of a car define its interface—the way it interacts with the outside world. Acceleration pedal, steering wheel, and other things help us interact with the car through its interface. From our perspective, we don't know what really happens when we push the accelerator pedal, we only see the results of our interaction. Classes in Dart implicitly define an interface with the same name as the class. Therefore, you don't need interfaces in Dart as the abstract class serves the same purpose. The Car
class implicitly defines an interface as a set of characteristics and behaviors.
If we define a racing car, then we must implement all the characteristics and behaviors of the Car
class, but with substantial changes to the engine, suspension, breaks, and so on:
import 'car.dart'; import 'passenger_car.dart'; void main() { // Create an instance of passenger car of white color, // carrying 750 kg and max passengers 5. Car car = new PassengerCar('white', 750.0, 5); // Move it car.move(100.0); }
Here, we just created an instance of PassengerCar
and assigned it to the car
variable without defining any special interfaces.
Dart has a mixin-based inheritance, so the class body can be reused in multiple class hierarchies, as shown in the following code:
library trailer; // The trailer class Trailer { // Access to car's [carrying] info double carrying = 0.0; // Trailer can carry [weight] void carry(double weight) { // Car's carrying increases on extra weight. carrying += weight; } }
The Trailer
class is independent of the Car
class, but can increase the carrying weight capacity of the car. We use the with
keyword followed by the Trailer
class to add mixin to the PassengerCar
class in the following code:
library passenger_car; import 'car.dart'; import 'trailer.dart'; // Passenger car with trailer. class PassengerCar extends Car with Trailer { // Max number of passengers. int maxPassengers = 4; /** * Create [PassengerCar] with [color], [carrying] and [maxPassengers]. * We can use [Trailer] to carry [extraWeight]. */ PassengerCar(String color, double carrying, this.maxPassengers, {double extraWeight:0.0}) : super(color, carrying) { // We can carry extra weight with [Trailer] carry(extraWeight); } }
We added Trailer
as a mixin to PassengerCar
and, as a result, PassengerCar
can now carry more weight. Note that we haven't changed PassengerCar
itself, we've only extended its functionality. At the same time, Trailer
can be used in conjunction with the Truck
or Bus
classes. A mixin looks like an interface and is implicitly defined via a class declaration, but has the following restrictions:
It has no declared constructor
The superclass of a mixin can only be an Object
They do not contain calls to
super
What is the difference between well-designed and poorly-designed classes? Here are the features of a well-designed class:
It hides all its implementation details
It separates its interface from its implementation through the use of abstract classes
It communicates with other classes only through their interfaces
All the preceding properties lead to encapsulation. It plays a significant role in OOP. Encapsulation has the following benefits:
Classes can be developed, tested, modified, and used independently
Programs can be quickly developed because classes can be developed in parallel
Class optimization can be done without affecting other classes
Classes can be reused more often because they aren't tightly coupled
Success in the development of each class leads to the success of the application
All our preceding examples include public members. Is that right? So what is the rule that we must follow to create well-designed classes?
Let's follow the simple principles to create a well-designed class:
Define a minimal public API for the class. Private members of a class are always accessible inside the library scope so don't hesitate to use them.
It is not acceptable to change the level of privacy of the member variables from private to public to facilitate testing.
Nonfinal instance variables should never be public; otherwise, we give up the ability to limit the values that can be stored in the variable and enforce invariants involving the variable.
The final instance variable or static constant should never be public when referring to a mutable object; otherwise, we restrict the ability to take any action when the final variable is modified.
It is not acceptable to have the public, static final instance of a collection or else, the getter method returns it; otherwise, we restrict the ability to modify the content of the collection.
The last two principles can be seen in the following example. Let's assume we have a Car
class with defined final static list of parts. We can initialize them with Pedal
and Wheel
, as shown in the following code:
class Car { // Be careful with that code !!! static final List PARTS = ['Pedal', 'Wheel']; } void main() { print('${Car.PARTS}'); // Print: [Pedal, Wheel] // Change part Car.PARTS.remove('Wheel'); print('${Car.PARTS}'); // Print: [Pedal] }
However, there's a problem here. While we can't change the actual collection variable because it's marked as final, we can still change its contents. To prevent anyone from changing the contents of the collection, we change it from final to constant, as shown in the following code:
class Car { // This code is safe static const List PARTS = const ['Pedal', 'Wheel']; } void main() { print('${Car.PARTS}'); // Print: [Pedal, Wheel] // Change part Car.PARTS.remove('Wheel'); print('${Car.PARTS}'); }
This code will generate the following exception if we try to change the contents of PARTS
:
Unhandled exception: Unsupported operation: Cannot modify an immutable array #0 List.remove (dart:core-patch/array.dart:327) …
In the previous section, we mentioned that nonfinal instance variables should never be public, but is this always right? Here's a situation where a class in our package has a public variable. In our Car
class, we have a color
field and it is deliberately kept as public, as shown in the following code:
// Is that class correct? class Car { // Color of the car. String color; }
If the Car
class is accessible only inside the library, then there is nothing wrong with it having public fields, because they don't break the encapsulation concept of the library.
We defined the main rules to follow and create a well-designed class. Everything is perfect and we didn't break any rules. Now, it's time to use a well-designed class in our project. First, we will create a new class that extends the current one. However, that could be a problem as inheritance can break encapsulation.
It is always best to use inheritance in the following cases:
Inside the library, because we control the implementation and relationship between classes
If the class was specifically designed and documented to be extended
It's better not to use inheritance from ordinary classes because it's dangerous. Let's discuss why. For instance, someone developed the following Engine
class to start and stop the general purpose engine:
// General purpose Engine class Engine { // Start engine void start() { // ... } // Stop engine void stop() { // ... } }
We inherited the DieselEngine
class from the Engine
class and defined when to start the engine that we need to initialize inside the init
method, as shown in the following code:
import 'engine.dart'; // Diesel Engine class DieselEngine extends Engine { DieselEngine(); // Initialize engine before start void init() { // ... } void start() { // Engine must be initialized before use init(); // Start engine super.start(); } }
Then, suppose someone changed their mind and decided that the implementation Engine
must be initialized and added the init
method to the Engine
class, as follows:
// General purpose Engine class Engine { // Initialize engine before start void init() { // ... } // Start engine void start() { init(); } // Stop engine void stop() { // ... } }
As a result, the init
method in DieselEngine
overrides the same method from the Engine
superclass. The init
method in the superclass is an implementation detail. The implementation details can be changed many times in future from release to release. The DieselEngine
class is tightly-coupled with and depends on the implementation details of the Engine
superclass. To fix this problem, we can use a different approach, as follows:
import 'engine.dart'; // Diesel Engine class DieselEngine implements Engine { Engine _engine; DieselEngine() { _engine = new Engine(); } // Initialize engine before start void init() { // ... } void start() { // Engine must be initialized before use init(); // Start engine _engine.start(); } void stop() { _engine.stop(); } }
We created the private engine
variable in our DieselEngine
class that references an instance of the Engine
class. Engine
now becomes a component of DieselEngine
. This is called a composition. Each method in DieselEngine
calls the corresponding method in the Engine
instance. This technique is called forwarding, because we forward the method's call to the instance of the Engine
class. As a result, our solution is safe and solid. If a new method is added to Engine
, it doesn't break our implementation.
The disadvantages of this approach are associated performance issues and increased memory usage.
Now that we've introduced well-designed classes, we need to discuss methods.
The class constructors, methods, mutators (setters), and operators remove some restrictions on the values that must be passed into their parameters .What will happen if an invalid parameter value is passed to a method? One possibility is that the method will fail with a confusing exception or worse it will succeed but with a wrong result. In any case, it's dangerous not check the parameters of a method before using them. The rule here is to check whether the parameter value is valid as soon as possible. The best place to do that is at the beginning of the method.
The Dart VM can work in a developer-friendly checked mode and a speed-obsessed production mode. We usually use the checked mode when developing our applications. One of the benefits of this mode is the dynamic assertion. We should use the assert
statement to check whether the parameters of the method are valid before using it. The Dart VM continues the program execution if the Boolean result of the dynamic assertion is true
, otherwise stops it. This is shown in the following code:
/** * Return sum of [a] and [b]. * It throws [AssertionError] if any of [a] or [b] equals null */ sum(int a, int b) { assert(a != null); assert(b != null); return a + b; }
Note
The assert
statement has no effect when the program executes in the production mode or is compiled with the JavaScript code.
We must check the validity of the parameters stored in the method for later use. Ignoring this can lead to problems later because an error associated with the parameter can be thrown in a completely different place, making it harder to trace its source. This has serious implications, especially in constructors.
Sometimes, it is important to validate the internal state of a class in the method and generate a special error, as shown in the following code. The typical errors are StateError
, RangeError
, and ArgumentError
.
class Car { double petrol; /** * Start engine. * That method throws [StateError] if petrol is null * or less than 5 liters. */ void startEngine() { if (petrol == null || petrol <= 5.0) { throw new StateError('Not enough petrol'); } } }
Here, we have a Car
class with the petrol
variable and the startEngine
method. The startEngine
method checks whether there is enough petrol to start the engine; otherwise, it throws an error.
So, now that we've defined well-designed classes, it's time to define well-designed methods. We must remember that methods are part of a class' interface and the following simple rules can make them easier to use and also less error-prone:
Choose the right method name. Remember, Dart doesn't support method overloading. Instead, we can have different method names or optional parameters.
Use optional named parameters. This helps programmers to use your methods without the need to remember the position of each parameter.
Refer to objects in terms of their interfaces over classes as the type of parameters. For example, we have an interface and the class implements that interface. Use the interface as the parameter type of the method instead of a solid one. Don't restrict the solution to a particular implementation.
A car may have the following different types of engines:
// Engine interface abstract class Engine { void start(); } // Diesel engine class DieselEngine implements Engine { void start() { // ... } } // Carburetor engine class CarburetorEngine implements Engine { void start() { // ... } } // Car class Car { var engine; // Car may have any engine Car(Engine this.engine); }
It's better to pass the abstract Engine
class as a parameter of the constructor for the car to prevent any problems in future.
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
This chapter covered some of the most useful advanced features of the Dart language. The Dart language was designed with the modules in mind. Modularity in Dart is realized through packages, libraries, and classes. The code compiled in JavaScript loses all the library information.
Functions are first-class citizens because they support all the operations generally available to other types. A function that can be referenced with an access to the variables in its lexical scope is called a closure.
Programs are designed to use objects as instances of classes that interact with each other to organize functionality. The Dart language is dynamically typed, so we can write programs with or without the type annotations in our code.
In the next chapter, we will talk about generics, errors and exceptions, and annotations and reflection.