Class Constructors
Constructors allow you to define a method signature with the required arguments to instantiate a new object of a specific type.
Dart has support for “zero argument constructors.” You may have noticed that Example 4.5 didn’t define a constructor method, or a superclass. Let’s modify the Airplane class (as shown in Example 4.7) to be a bit more customizable upon instantiation:
EXAMPLE 4.7
class Airplane
{
static const double bodyWeight = 1000.00;
static const double fuelCapacity = 100.50;
String color;
String wing;
int seatCount;
Airplane(int seatCount, String color, String wing) {
this.seatCount = seatCount;
this.color = color;
this.wing = wing;
}
double getWeight() {
return bodyWeight + seatCount + fuelCapacity;
}
}
main() { //new scope
Airplane yourPlane = new Airplane(1, "White", "Fixed");
Airplane myPlane = new Airplane(2, "Gold", "Triangle");
print( 'yourplane weight:'+ yourPlane.getWeight().toString() );
print( 'myplane weight: '+ myPlane.getWeight().toString() );
}
//Output:
//yourplane weight: 1101.5
//myplane weight: 1102.5
Generative Constructor
Example 4.7 appended a named Airplane constructor function that accepts three arguments using standard comma-delimited parameters. The Airplane() constructor function matches the name of the Airplane class. This is referred to as a generative constructor. The hardcoded values for color, wing, or seatCount are gone. Instead of hardcoded values, the constructor requires that the class fields be assigned by the arguments that are passed in upon instantiation.
Automatic Class Member Variable Initialization
If you’ll notice, it’s not very DRY (Don’t Repeat Yourself) to have objects passed into the constructor and then immediately assign them to class fields. If you’re going to automatically assign an argument value to a class field, Dart encourages you to use the keyword this inside the method signature. You can refactor the constructor as shown in Example 4.8.
EXAMPLE 4.8
class Airplane
{
static const double bodyWeight = 1000.00;
static const double fuelCapacity = 100.50;
String color;
String wing;
int seatCount;
Airplane(int this.seatCount, String this.color, String this.wing) {
// You can leave off the { } altogether
// This whole block becomes optional
}
double getWeight() {
return bodyWeight + seatCount + fuelCapacity;
}
}
main() { //new scope
Airplane yourPlane = new Airplane(1, "White", "Fixed");
Airplane myPlane = new Airplane(2, "Gold", "Triangle");
print( 'yourplane weight:'+ yourPlane.getWeight().toString() );
print( 'myplane weight: '+ myPlane.getWeight().toString() );
}
//Output:
//yourplane weight: 1101.5
//myplane weight: 1102.5
Named Constructors
Named constructors replace the practice of default overriding and offer multiple ways to initialize the same class of object.
Example 4.8 created an Airplane object that is initialized with three optional fields. Let’s say you have a different, recurring use case that always has the same wing type and color, but requires a changing seat value. You have older dependencies that require the previous implementation, so let’s add a named constructor for the specific type of Airplane as follows:
EXAMPLE 4.9
class Airplane
{
static const double bodyWeight = 1000.00;
static const double fuelCapacity = 100.50;
String color;
String wing;
int seatCount;
Airplane(int this.seatCount, String this.color, String this.wing);
Airplane.sparrow(int this.seatCount){
wing = "Swept";
color = "Gold";
}
Airplane.robin(String this.color){
seatCount = 1;
wing = "Swept";
}
double getWeight() {
return bodyWeight + seatCount + fuelCapacity;
}
}
main() { //new scope
Airplane yourPlane = new Airplane(1, "White", "Fixed");
Airplane myPlane = new Airplane(2, "Gold", "Triangle");
Airplane brothersPlane = new Airplane.sparrow(10);
Airplane sistersPlane = new Airplane.robin('red');
print( 'yourplane weight:'+ yourPlane.getWeight().toString() );
print( 'myplane weight: '+ myPlane.getWeight().toString() );
print( 'brothersPlane weight: '+ brothersPlane.getWeight().toString() );
print( 'sistersPlane color: '+ sistersPlane.color );
}
//Output:
//yourplane weight:1101.5
//myplane weight: 1102.5
//brothersPlane weight: 1110.5
//sistersPlane color: Red
Example 4.9 adds a named constructor of Airplane.sparrow to the class. It’s using automatic initialization to assign the this.seatCount parameter. The constructor’s function block has hardcoded assignments for the class’s fields.
main() then instantiates a local variable named brothersPlane by leveraging the named constructor sparrow and the new keyword. All Airplane instances returned by Airplane.sparrow will have swept wings and a color of gold. The only argument accepted by the named constructor sparrow is a numeric value to increase the seat count.
A similar pattern is repeated for the named constructor robin and the variable instance sistersPlane. But in the case of robin, the constructor handles building single-seat airplanes of varying colors.
Factory Constructors
To understand a factory constructor, it’s important to understand that when a generative constructor is invoked, a new object instance is created in memory. For performance reasons, this can be a bad approach to acquiring objects. A common pattern to enforce object reuse is the pooling pattern.
In a pooling implementation, instead of creating a new object in memory on every request, the pattern requires a small group of objects to be created at run time. When a new pool member object is needed, it’s retrieved from the pool. Then, instead of destroying each instance, the unused object is returned to the pool for later reuse.
Factory constructor functions allow you to bypass the default object instantiation process and return an object instance in some other way. You could return an object from memory or use a different approach to initialization. Factories are intentionally flexible.
Factories offload object creation into the factory function’s statement while allowing the signature of the instantiator to use the familiar new NameOfClass() syntax. Let’s compare using a generative constructor against a factory constructor for class Pool():
class Pool
{
Pool() {
//do nothing for the generative constructor
//results in a new object instance
}
}
main() {
Pool aPool = new Pool();
print('aPool: ' + aPool.toString() ); //aPool: Instance of 'Pool'
}
Prefixing the existing constructor declaration with the keyword of factory will show the power of the factory pattern. With a factory constructor in place, you bypass object creation. Let’s take a look:
class Pool
{
factory Pool() {
//do nothing in the factory constructor – you'd usually return something
}
}
Note that the calling implementation does not change, but instead of an instance, you get null:
main() {
Pool aPool = new Pool();
print('aPool: ' + aPool.toString() ); //aPool: null
}
Let’s rig up a fake Pool implementation. For the sake of brevity, this pool will always return a new Pool instance:
class Pool
{
factory Pool() {
//a pool usually checks for previously constructed objects in cache
//if we had a cached item, we'd return it, but instead, for brevity,
//we'll build a new one using a named constructor
return new Pool.NamedPoolConstructor();
}
Pool.NamedPoolConstructor();
}
This will result in an instance of Pool in main(). It uses the named constructor functionality from earlier to return a new instance.
Regardless of what the constructor statement actually looks like, factory constructors and generative constructors provide a consistent interface for acquiring an object using the new NameOfClass() syntax.
Static Variables
A static variable is an object that exists on the class definition and not on the object instance. If your current scope has a class in the namespace, you can access the static variable on the class. The value of the static variable can be modified (Example 4.10).
EXAMPLE 4.10
class Tool
{
static List collection = ['wrench', 'saw', 'hammer'];
}
main() {
Tool.collection.add('socket');
Tool.collection.forEach( (String item){
print('Collection Has A: $item');
} );
}
// Output:
// Collection Has A: wrench
// Collection Has A: saw
// Collection Has A: hammer
// Collection Has A: socket
Rather than instantiate an instance of Tool, you simply access the static variable associated with the class Tool.
Final Variables
A final variable can receive only a single run-time assignment. Its assignment must occur upon declaration if being assigned at run time. Once a value is assigned, it cannot be changed. If it’s a primitive value, the primitive value is, in practice, immutable. If it’s an object like a List, the variable’s reference cannot be changed, but the referenced object’s fields are mutable. Final variables can also be declared as static like any other variable.
EXAMPLE 4.11
class Runway
{
final String item = "Asphalt";
final List materials = ["Asphalt", "Gravel", "Cement"];
}
main() {
Runway rw = new Runway();
//primitives
print(rw.item); //prints Asphalt
rw.item = "Concrete"; //Error 'item' cannot be used as a setter, its final
//object
rw.materials.add('Sand'); //adds 'Sand' to the List and modifies its value
rw.materials = ['Steel']; //Error 'Runway' has no instance setter...
}
In Example 4.11, you can access the value of String item, but you cannot change the reference of String item to a different string, because that would be a second assignment.
You can add() another object to List materials, or use any of its instance methods, as long as you do not assign a new reference to the final variable. The assignment of a List literal to the final variable encounters the same restriction as in the string example: no secondary assignments allowed.