跳转至

10 Static Members

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

Putting static in front of a member variable or method causes the variable or method to belong to the class rather than the instance.

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

They work the same as top-level variables and functions but are wrapped in the class name.

You access them like so:

final value = SomeClass.myProperty;
SomeClass.myMethod();

In this case, you didn’t have to instantiate an object to access myProperty or to call myMethod. Instead, you used the class name directly to get the value and call the method.

The following sections will cover a few common use cases for static members.

Static Variables

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

Note

Variables receive different names according to where they belong or are located. Because static variables belong to the class, they’re called class variables. Non-static member variables are called instance variables because they only have a value after an object is instantiated. Variables within a method are called local variables. And top-level variables outside of a class are called global variables.

Constants

Many classes store useful values that never change. Some examples of these class constants include the following:

  • Mathematical constants like pi or e.
  • Predefined colors.
  • Default font sizes.
  • Hard-coded URLs.
  • Names for difficult-to-remember Unicode values or invisible characters.

Default Font Size Example

You can define class constants by combining the static and const keywords. For example, add the following class to your project below main:

class TextStyle {
  static const _defaultFontSize = 17.0;

  TextStyle({this.fontSize = _defaultFontSize});
  final double fontSize;
}

The constant is on the second line. Although you could have directly written this.fontSize = 17.0 right in the constructor, using the name _defaultFontSizemakes the meaning more clear. Also, if you used the default font size at several different places in your class, you’d only need to change a single line if you ever decided to update the default size.

Color Codes Example

In the previous example, the constant was private, but it can also be useful to have public constants. You’ll see this with Flutter’s Colors class. You’ll make a simplified version here.

Add the following class to your project:

class Colors {
  static const int red = 0xFFD13F13;
  static const int purple = 0xFF8107D9;
  static const int blue = 0xFF1432C9;
}

These are the hex values for shades of red, purple and blue. The 0x prefix tells Dart that you’re using hexadecimal values for the integers. The eight hex characters after 0x follow the AARRGGBB pattern, where AA is the amount of alpha or transparency, RR is the amount of red, GG is the amount of green and BB is the amount of blue.

Then, in main, add the following line:

final backgroundColor = Colors.purple;

This is a much more readable way of describing a color than scattering the hex values around your app.

Singleton Pattern

Another use of static variables is to create a singleton class. Singletons are a common design pattern with only one instance of an object. Although some people debate their benefits, they make certain tasks more convenient.

It’s easy to create a singleton in Dart. You wouldn’t want User to be a singleton because you’d likely have many distinct users requiring many distinct instances of User. But you might want to create a singleton class as a database helper to ensure you don’t open multiple connections to the database.

Here’s what a basic singleton class would look like:

class MySingleton {
  MySingleton._();
  static final MySingleton instance = MySingleton._();
}

The MySingleton._() part is a private, named constructor. Some people like to call it _internal to emphasize that it can’t be called from the outside. The underscore makes it impossible to instantiate the class normally. But the static property, which is only initialized once, provides a reference to the instantiated object.

Note

Static fields and top-level variables — global variables outside of a class — are lazily initialized. That means Dart doesn’t calculate and assign their values until you use them first.

You would access the singleton like so:

final mySingleton = MySingleton.instance;

Because factory constructors don’t need to return new instances of an object, you can also implement the singleton pattern with a factory constructor:

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

The advantage here is that you can hide the fact that it’s a singleton from whoever uses it:

final mySingleton = MySingleton();

From the outside, this looks exactly like a normal object. This allows you to change it back into a generative constructor later without affecting the code in other parts of your project.

The past two sections have been about static variables. Next, you’ll look at static methods.

Static Methods

You can do a few interesting things with static methods.

Utility Methods

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

Add the following Math class to your project:

class Math {
  static num max(num a, num b) {
    return (a > b) ? a : b;
  }

  static num min(num a, num b) {
    return (a < b) ? a : b;
  }
}

The static keyword goes at the beginning of the method signature. Static methods can’t access instance variables; they can only use static constants or the parameters that are passed in.

Use your static methods in main like so:

print(Math.max(2, 3));  // 3
print(Math.min(2, 3));  // 2

Just Because You Can, Doesn’t Mean You Should

In other languages, some developers like to group related static utility methods in classes to keep them organized. But in Dart, it’s usually better to put these utility methods in their own file as top-level functions. You can then import that file as a library wherever you need the utility methods contained within. The description below will show you how to refactor your Math class into a library of top-level functions.

Add a lib folder to the root of your project just like you did in Chapter 9, “Constructors”. Then create a file named math.dart inside lib. Paste in the following top-level functions:

num max(num a, num b) {
  return (a > b) ? a : b;
}

num min(num a, num b) {
  return (a < b) ? a : b;
}

Because they’re top-level functions rather than class methods, there’s no need to use the static keyword.

Now, go back to the file with your main function. Add the following import at the top of the file:

import 'package:starter/math.dart';

Again, if you aren’t using the starter project, change starter to your project name.

Then, replace the contents of main with the following code:

print(max(2, 3)); // 3
print(min(2, 3)); // 2

Your functions no longer reference an unnecessary Math wrapper class.

To prevent naming collisions, use the as keyword after the import. Replace the contents of the file your main method is in with the following:

import 'package:starter/math.dart' as math;

void main() {
  print(math.max(2, 3)); // 3
  print(math.min(2, 3)); // 2
}

The name you put after as can be anything. In this case, you used the word math. Then, you used math.max and math.min to reference the functions. This technique is useful when two libraries have functions with the same name. The standard dart:mathlibrary, for example, also has a max and min function.

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 the same result as in the previous chapter with the fromJson factory constructor.

Here’s the fromJson factory constructor from the User class in Chapter 9, “Constructors”:

factory User.fromJson(Map<String, Object> json) {
  final userId = json['id'] as int;
  final userName = json['name'] as String;
  return User(id: userId, name: userName);
}

And here’s the static method version:

static User fromJson(Map<String, Object> json) {
  final userId = json['id'] as int;
  final userName = json['name'] as String;
  return User(id: userId, name: userName);
}

From the outside as well, you use it as you did with the factory version:

final map = {'id': 10, 'name': 'Sandra'};
final sandra = User.fromJson(map);

This all shows that there are often multiple ways of accomplishing the same thing.

Comparing Static Methods With Factory Constructors

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

  • A factory constructor can only return an instance of the class type or subtype, whereas a static method can return anything. For example, a static method can be asynchronous and return a Future — which you’ll learn about in Dart Apprentice: Beyond the Basics, Chapter 12, “Futures” — but a factory constructor can’t do this.
  • A factory constructor can be unnamed, so from the caller’s perspective, it looks exactly like calling a generative constructor. The singleton example above is an example of this. A static method, on the other hand, must have a name.
  • A factory constructor can be const if it’s a forwarding constructor, but a static method can’t.

Challenges

Before moving on, here’s a challenge to test your knowledge of static members. It’s best if you try to solve it yourself, but a solution is available with the supplementary materials for this book if you get stuck.

Challenge 1: Spheres

Create a Sphere class with a const constructor that takes a radius as a named parameter. Add getters for the volume and surface area but none for the radius. Don’t use the dart:math package but store your 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

  • Adding the static keyword to a property or method makes it belong to the class rather than the instance.
  • Static constants are useful for storing values that don’t change.
  • A singleton is a class with only one instance of an object.
  • A utility method is a method that’s associated with the class but not with any particular instance.
  • Group top-level functions into their own library rather than wrapping a bunch of static methods in a utility class.
  • Static methods can replace factory constructors but have a few subtle differences from factory constructors.

Where to Go From Here?

This chapter touched on concepts such as singletons and factories. These concepts are known collectively as design patterns. Although you don’t need to know design patterns to code in Dart, understanding them will make you a better programmer. The most famous book on this topic is Design Patterns by “The Gang of Four”, but there are many other excellent works on the subject. A simple online search for software design patterns will provide you a wealth of information.