So far in this book, you’ve used built-in types such as int, String and bool. You’ve also seen one way to make custom types using enum. In this chapter, you’ll learn a more flexible way to create your own types by using classes.

Note: Because there’s quite a bit to learn about classes and object-oriented programming, or OOP, in Dart, you’ll come back to the subject again in Chapter 9. In that chapter, you’ll learn how to create a hierarchy of classes using inheritance as well as some other advanced topics. Don’t be afraid of the word “advanced,” though. You can handle it. It’ll only be more advanced than this chapter is. If you have experience with any other OOP languages, you probably know most of it already anyway.

Dart classes

Classes are like architectural blueprints that tell the system how to make an object. The object is what you get back when you tell Dart to build something based on the blueprint at some given location in the computer memory.

All values in Dart are objects that are built from a class. This includes the values of basic types like int, double and bool. That’s different from other languages like Java, where basic types are primitive. For example, if you have x = 10 in Java, the value of x is 10 itself. However, Dart doesn’t have primitive types. Even for a simple int, the value is an object that wraps the integer. You’ll learn more on this concept later.

Classes are a core component of object-oriented programming. They’re used to combine data and functions inside a single structure.

The functions exist to transform the data. Functions inside of a class are known as methods, while constructors are special methods you use to create objects from the class.

It’s time to get your hands dirty. Working with classes is far more instructive than reading about them!

Defining a class

To get started creating your own types, you’ll create a simple User class that has id and name properties. This is just the kind of class that you’re highly likely to create in the future for an app that requires users to log in.

class User {
  int id = 0;
  String name = '';
}

Creating an object from a class

As mentioned above, the value you create from a class is called an object. Another name for an object is instance, so creating an object is sometimes called instantiating a class.

final user = User();

The optional keyword new

Before version 2.0 of Dart came around, you had to use the new keyword to create an object from a class. At that time, creating a new instance of a class would have looked like this:

final user = new User();

Assigning values to properties

Now that you have an instance of User stored in user, you can assign this object a name in place of the default empty string using dot notation to reference the property of your object:

user.name = 'Ray';
user.id = 42;
void main() {
  final user = User();
  user.name = 'Ray';
  user.id = 42;
}

class User {
  int id = 0;
  String name = '';
}

Printing an object

You can print any object in Dart. However, if you try to print user now, you won’t get quite what you hoped for. Add the following line at the bottom of the main function and run the code:

print(user);
Instance of 'User'
@override
String toString() {
  return 'User(id: $id, name: $name)';
}
User(id: 42, name: Ray)

Adding methods

Now that you’ve learned to override methods, you’re going to move on and add your own methods to the User class. But before you do, there’s a little background information you should have first that will make your life way easier.

Understanding object serialization

Being able to organize related data into a class is super useful, especially when you want to pass that data around as a unit within your app. One disadvantage, though, shows up when you’re saving the object or sending it over the network. Files, databases and networks only know how to handle simple data types, such as numbers and strings. They don’t know how to handle anything more complex, like your User data type.

Adding a JSON serialization method

You’re going to add another method to your class now that will convert a User object to JSON format. It’ll be similar to what you did in toString.

String toJson() {
  return '{"id":$id,"name":"$name"}';
}
print(user.toJson());
{"id":42,"name":"Ray"}

Cascade notation

When you created your User object above, you set its parameters like so:

final user = User();
user.name = 'Ray';
user.id = 42;
final user = User()
  ..name = 'Ray'
  ..id = 42;

Mini-exercises

  1. Create a class called Password and give it a string property called value.
  2. Override the toString method of Password so that it prints value.
  3. Add a method to Password called isValid that returns true only if the length of value is greater than 8.

Constructors

Constructors are methods that create, or construct, instances of a class. That is to say, constructors build new objects. Constructors have the same name as the class, and the implicit return type of the constructor method is also the same type as the class itself.

Default constructor

As it stands, your User class doesn’t have an explicit constructor. In cases like this, Dart provides a default constructor that takes no parameters and just returns an instance of the class. You already saw this in your call to User(). For example, writing a class like this:

class Address {
  var value = '';
}
class Address {
  Address();
  var value = '';
}

Custom constructors

If you want to pass parameters to the constructor to modify how your class builds an object, you can. It’s similar to how you wrote functions with parameters in Chapter 5.

Long-form constructor

In Dart the convention is to put the constructor before the property variables. Add the following generative constructor method at the top of the class body:

User(int id, String name) {
  this.id = id;
  this.name = name;
}
class User {
  User(int id, String name) {
    this.id = id;
    this.name = name;
  }

  int id = 0;
  String name = '';

  // ...
}
final user = User(42, 'Ray');
print(user);
int id;
String name;

Short-form constructor

As mentioned briefly above, Dart also has a short-form constructor where you don’t provide a function body, but you instead list the properties you want to initialize, prefixed with the this keyword. Arguments you send to the short form constructor are used to initialize the corresponding object properties.

User(this.id, this.name);
class User {
  User(this.id, this.name);

  int id;
  String name;

  // ...
}

Named constructors

Dart also has a second type of generative constructor called a named constructor, which you create by adding an identifier on to the class name. From here on, this chapter will refer to a constructor without the identifier, that is, one which only uses the class name, as an unnamed constructor.

User.anonymous() {
  id = 0;
  name = 'anonymous';
}
final anonymousUser = User.anonymous();
print(anonymousUser);
User(id: 0, name: anonymous)

Forwarding constructors

In the named constructor example above, you set the class properties directly in the constructor body. However, this doesn’t follow the DRY principle you learned earlier. You’re repeating yourself by having two different locations where you can set the properties. It’s not a huge deal, but imagine that you have five different constructors instead of two. It would be easy to forget to update all five if you had to make a change, and if the constructor logic were complicated, it would be easy to make a mistake.

User.anonymous() : this(0, 'anonymous');
final anonymousUser = User.anonymous();

Optional and named parameters

Everything you learned about function parameters in Chapter 5 also applies to constructor method parameters. That means you can make parameters optional using square brackets:

MyClass([this.myProperty]);
MyClass({this.myProperty});
final user = User(42, 'Ray');
User({this.id = 0, this.name = 'anonymous'});
User.anonymous() : this();
final user = User(id: 42, name: 'Ray');
class User {
  // unnamed constructor
  User({this.id = 0, this.name = 'anonymous'});

  // named constructor
  User.anonymous() : this();

  int id;
  String name;

  // ...
}

Initializer lists

You might have discovered a small problem that exists with your class as it’s now written. Take a look at the following way that an unscrupulous person could use this class:

final vicki = User(id: 24, name: 'Vicki');
vicki.name = 'Nefarious Hacker';
print(vicki);
// User(id: 24, name: Nefarious Hacker)

Private variables

Dart allows you to make variables private by adding an underscore (_) in front of their name.

User({this._id = 0, this._name = 'anonymous'});
Named optional parameters can’t start with an underscore.
User({int id = 0, String name = 'anonymous'})
      : _id = id,
        _name = name;
User({int id = 0, String name = 'anonymous'})
    : _id = id,
      _name = name {
  print('User name is $_name');
}

Why aren’t the private properties private?

It turns out that your nefarious hacker can still access the “private” fields of User. Add the following two lines to main to see this in action:

final vicki = User(id: 24, name: 'Vicki');
vicki._name = 'Nefarious Hacker';

import 'user.dart';
vicki._name = 'Nefarious Hacker';
The setter '_name' isn't defined for the type 'User'.
// vicki._name = 'Nefarious Hacker';

Checking for errors

Initializer lists are a great place to check for errors in the constructor parameters, which you can do by adding assert statements. Think of asserts like sanity checks that make sure you aren’t doing anything silly, by checking that a condition is in fact true.

User({String name = 'anonymous', int id = 0})
    : assert(id >= 0),
      assert(name != null && name.isNotEmpty),
      _name = name,
      _id = id;
final jb = User(id: -1, name: 'JB Lorenzo');
Failed assertion: line 3 pos 16: 'id >= 0': is not true.
// final jb = User(id: 100, name: 'JB Lorenzo');

Constant constructors

You’ve already learned how to keep people from modifying the properties of a class by making them private. Another thing you can do is to make the properties immutable, that is, unchangeable. By using immutable properties, you don’t even have to make them private.

Making properties immutable

There are two ways to mark a variable immutable in Dart: final and const. However, since the compiler won’t know what the properties are until runtime, your only choice here is to use final.

final String _name;
final int _id;

Making classes immutable

If the objects of a particular class can never change, because all fields of the class are final, you can add const to the constructor to ensure that all instances of the class will be constants at compile-time.

const User({String name = 'anonymous', int id = 0})
    : assert(id >= 0),
      assert(name != null),
      _name = name,
      _id = id;

const User.anonymous() : this();
const user = User(name: 'Ray', id: 42);
const anonymousUser = User.anonymous();

Benefits of using const

In addition to being immutable, another benefit of const variables is that they’re canonical instances, which means that no matter how many instances you create, as long as the properties used to create them are the same, Dart will only see a single instance. You could instantiate User.anonymous() a thousand times across your app without incurring the performance hit of having a thousand different objects.

Factory constructors

All of the constructors that you’ve seen up until now have been generative constructors. Dart also provides another type of constructor called a factory constructor.

factory User.ray() {
  return User(id: 42, name: 'Ray');
}
factory User.fromJson(Map<String, Object> json) {
  final userId = json['id'];
  final userName = json['name'];
  return User(id: userId, name: userName);
}
final map = {'id': 10, 'name': 'Manda'};
final manda = User.fromJson(map);

Constructor summary

Since there are so many ways that constructors can vary, here’s a brief comparison.

const User(this.id, this.name);

Mini-exercises

Given the following class:

class Password {
  String value = '';
}

Dart objects

Objects act as references to the instances of the class in memory. That means if you assign one object to another, the other object simply holds a reference to the same object in memory — not a new instance.

class MyClass {
  var myProperty = 1;
}
final myObject = MyClass();
final anotherObject = myObject;
print(myObject.myProperty);    // 1
anotherObject.myProperty = 2;
print(myObject.myProperty);    // 2

Getters

Right now the User class fields are private:

class User {
  // ...
  final int _id;
  final String _name;
  // ...
}
int get id => _id;
String get name => _name;

Calculated properties

You can even create getters that aren’t backed by a dedicated field value, but instead are calculated when called. Here’s an example:

bool get isBigId => _id > 1000;

Setters

If you need mutable data in a class, there’s a special set method to go along with get.

class Email {
  var _address = '';

  String get value => _address;
  set value(String address) => _address = address;
}
final email = Email();
email.value = 'ray@example.com';
final emailString = email.value;

Refactoring

You don’t always need to use getters and setters explicitly. In fact, if all you’re doing is shadowing some internal field variable, then you’re better off just using a public variable.

Refactoring the Email class

Refactoring the Email class from above to use a public variable would look like this:

class Email {
  var value = '';
}
final email = Email();
email.value = 'ray@example.com';
final emailString = email.value;
class Email {
  Email(this.value);
  final value;
}
final email = Email('ray@example.com');
final emailString = email.value;

Refactoring User

You can use these same principles to refactor the User class. Replace the entire class with the following code:

class User {
  const User({this.id = 0, this.name = 'anonymous'})
      : assert(id >= 0),
        assert(name != null);

  const User.anonymous() : this();

  final String name;
  final int id;

  String toJson() {
    return '{"id":$id,"name":"$name"}';
  }

  @override
  String toString() {
    return 'User(id: $id, name: $name)';
  }
}

Static members

There is just one more thing to cover for your well-rounded foundation in Dart classes. That’s the static keyword.

class SomeClass {
  static int myProperty = 0;
  static void myMethod() {
    print('Hello, Dart!');
  }
}
final value = SomeClass.myProperty;
SomeClass.myMethod();

Static variables

Static variables are often used for constants and in the singleton pattern.

Constants

You can define class constants by combining static and const. For example:

static const myConstant = '42';
static const _anonymousUserId = 0;
static const _anonymousUserName = 'anonymous';
const User({
  this.id = _anonymousUserId,
  this.name = _anonymousUserName,
})  : assert(name != null),
      assert(id >= 0);

Singleton pattern

A second use of static variables is to create a singleton class. Singletons are a common design pattern where there is only ever one instance of an object. While their benefits are debatable, they do make some tasks more convenient.

class MySingleton {
  MySingleton._();
  static final MySingleton instance = MySingleton._();
}
final mySingleton = MySingleton.instance;
class MySingleton {
  MySingleton._();
  static final MySingleton _instance = MySingleton._();
  factory MySingleton() => _instance;
}
final mySingleton = MySingleton();

Static methods

There are a few interesting things you can do with static methods.

Utility methods

One use for a static method is to create a utility or helper method that’s associated with the class, but not associated with any particular instance.

Creating new objects

You can also use static methods to create new instances of a class based on some input passed in. For example, you could use a static method to achieve precisely the same result as you did earlier with the fromJson factory constructor. Here’s the static method version:

static User fromJson(Map<String, Object> json) {
  final userId = json['id'];
  final userName = json['name'];
  return User(id: userId, name: userName);
}
final map = {'id': 10, 'name': 'Manda'};
final manda = User.fromJson(map);
User.fromJson(Map<String, Object> json)
    : id = json['id'],
      name = json['name'];

Comparing static methods with factory constructors

Factory constructors in many ways are just like static methods, but there are a few differences:

Challenges

Before moving on, here are some challenges to test your knowledge of classes and their various flavors. It’s best if you try to solve them yourself, but solutions are available if you get stuck. These are located with the supplementary materials for this book.

Challenge 1: Bert and Ernie

Create a Student class with final firstName and lastName String properties and a variable grade as an int property. Add a constructor to the class that initializes all the properties. Add a method to the class that nicely formats a Student for printing. Use the class to create students bert and ernie with grades of 95 and 85, respectively.

Challenge 2: Spheres

Create a Sphere class with a const constructor that takes a positive length radius as a named parameter. Add getters for the the volume and surface area but none for the radius. Don’t use the dart:math package but store your own version of pi as a static constant. Use your class to find the volume and surface area of a sphere with a radius of 12.

Key points

  • Classes package data and functions inside a single structure.
  • Variables in a class are called properties if public, or fields if private.
  • Functions in a class are called methods.
  • You can customize how an object is printed by overriding the toString method.
  • You create an object from a class by calling a constructor method.
  • Generative constructors can be unnamed or named.
  • Unnamed generative constructors have the same name as the class, while named generative constructors have an additional identifier after the class name.
  • You can forward from one constructor to another by using the keyword this.
  • Initializer lists allow you to check constructor parameters with assert and initialize field variables.
  • Adding const to a constructor allows you to create immutable, canonical instances of the class.
  • Factory constructors allow you to hide the implementation details of how you provide the class instance.
  • Classes have getters and setters which you can customize without affecting how the object is used.
  • Adding the static keyword to a property or method makes it belong to the class rather than the instance.

Where to go from here?

This chapter touched briefly on JSON as a standard way to serialize objects. You’ll certainly be using JSON in the future, so you can visit json.org to learn more about this format and why it’s gained so much traction as a standard.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.