[Dart Language Tour] Generics, Libraries and visibility

Generics, Libraries and visibility

Generics

If you look at the API documentation for the basic array type, List, you’ll see that the type is actually List<E>. The <…> notation marks List as a generic (or parameterized) type—a type that has formal type parameters. By convention, most type variables have single-letter names, such as E, T, S, K, and V.

Why use generics?

Generics are often required for type safety, but they have more benefits than just allowing your code to run:

  • Properly specifying generic types results in better generated code.

  • You can use generics to reduce code duplication.

If you intend for a list to contain only strings, you can declare it as List<String> (read that as “list of string”). That way you, your fellow programmers, and your tools can detect that assigning a non-string to the list is probably a mistake. Here’s an example:

1
2
3
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

Generic classes

Another reason for using generics is to reduce code duplication. Generics let you share a single interface and implementation between many types, while still taking advantage of static analysis. For example, say you create an interface for caching an object:

1
2
3
4
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}

You discover that you want a string-specific version of this interface, so you create another interface:

1
2
3
4
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}

Later, you decide you want a number-specific version of this interface… You get the idea.

Generic types can save you the trouble of creating all these interfaces. Instead, you can create a single interface that takes a type parameter:

1
2
3
4
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}

In this code, T is the stand-in type. It’s a placeholder that you can think of as a type that a developer will define later.

Using collection literals

List, set, and map literals can be parameterized. Parameterized literals are just like the literals you’ve already seen, except that you add <type> (for lists and sets) or <keyType, valueType> (for maps) before the opening bracket. Here is an example of using typed literals:

1
2
3
4
5
6
7
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
'index.html': 'Homepage',
'robots.txt': 'Hints for web robots',
'humans.txt': 'We are people, not machines'
};

Using parameterized types with constructors

To specify one or more types when using a constructor, put the types in angle brackets (<...>) just after the class name. For example:

1
var nameSet = Set<String>.from(names);

The following code creates a map that has integer keys and values of type View:

1
var views = Map<int, View>();

Generic collections and the types they contain

Dart generic types are reified, which means that they carry their type information around at runtime. For example, you can test the type of a collection:

1
2
3
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

Note: In contrast, generics in Java use erasure, which means that generic type parameters are removed at runtime. In Java, you can test whether an object is a List, but you can’t test whether it’s a List.


Restricting the parameterized type

When implementing a generic type, you might want to limit the types of its parameters. You can do this using extends.

1
2
3
4
5
6
class Foo<T extends SomeBaseClass> {
// Implementation goes here...
String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}

It’s OK to use SomeBaseClass or any of its subclasses as generic argument:

1
2
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

It’s also OK to specify no generic argument:

1
2
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'

Specifying any non-SomeBaseClass type results in an error:

1
2
var foo = Foo<Object>();
Using generic methods

Generic methods and functions

Initially, Dart’s generic support was limited to classes. A newer syntax, called generic methods, allows type arguments on methods and functions:

1
2
3
4
5
6
T first<T>(List<T> ts) {
// Do some initial work or error checking, then...
T tmp = ts[0];
// Do some additional checking or processing...
return tmp;
}

Here the generic type parameter on first (<T>) allows you to use the type argument T in several places:

  • In the function’s return type (T).

  • In the type of an argument (List<T>).

  • In the type of a local variable (T tmp).

Libraries and visibility

The import and library directives can help you create a modular and shareable code base. Libraries not only provide APIs, but are a unit of privacy: identifiers that start with an underscore (_) are visible only inside the library. Every Dart app is a library, even if it doesn’t use a library directive.

Libraries can be distributed using packages - https://dart.dev/guides/packages.

Using libraries

Use import to specify how a namespace from one library is used in the scope of another library.

For example, Dart web apps generally use the dart:html - https://api.dart.dev/stable/dart-html library, which they can import like this:

1
import 'dart:html';

The only required argument to import is a URI specifying the library. For built-in libraries, the URI has the special dart: scheme. For other libraries, you can use a file system path or the package: scheme. The package: scheme specifies libraries provided by a package manager such as the pub tool. For example:

1
import 'package:test/test.dart';

Note: URI stands for uniform resource identifier. URLs (uniform resource locators) are a common kind of URI.


Specifying a library prefix

If you import two libraries that have conflicting identifiers, then you can specify a prefix for one or both libraries. For example, if library1 and library2 both have an Element class, then you might have code like this:

1
2
3
4
5
6
7
8
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();

Importing only part of a library

If you want to use only part of a library, you can selectively import the library. For example:

1
2
3
4
5
// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

Lazily loading a library

Deferred loading (also called lazy loading) allows a web app to load a library on demand, if and when the library is needed. Here are some cases when you might use deferred loading:

  • To reduce a web app’s initial startup time.

  • To perform A/B testing—trying out alternative implementations of an algorithm, for example.

  • To load rarely used functionality, such as optional screens and dialogs.


Only dart2js supports deferred loading. Flutter, the Dart VM, and dartdevc don’t support deferred loading. For more information, see issue #33118 - https://github.com/dart-lang/sdk/issues/33118 and issue #27776 - https://github.com/dart-lang/sdk/issues/27776.


To lazily load a library, you must first import it using deferred as.

1
import 'package:greetings/hello.dart' deferred as hello;

When you need the library, invoke loadLibrary() using the library’s identifier.

1
2
3
4
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}

In the preceding code, the await keyword pauses execution until the library is loaded. For more information about async and await, see asynchrony support - https://dart.dev/guides/language/language-tour#asynchrony-support.

You can invoke loadLibrary() multiple times on a library without problems. The library is loaded only once.

Keep in mind the following when you use deferred loading:

  • A deferred library’s constants aren’t constants in the importing file. Remember, these constants don’t exist until the deferred library is loaded.

  • You can’t use types from a deferred library in the importing file. Instead, consider moving interface types to a library imported by both the deferred library and the importing file.

  • Dart implicitly inserts loadLibrary() into the namespace that you define using deferred as namespace. The loadLibrary() function returns a Future - https://dart.dev/guides/libraries/library-tour#future.

Implementing libraries

See Create Library Packages - https://dart.dev/guides/libraries/create-library-packages for advice on how to implement a library package, including:

  • How to organize library source code.

  • How to use the export directive.

  • When to use the part directive.

  • When to use the library directive.

  • How to use conditional imports and exports to implement a library that supports multiple platforms.

References

[1] Language tour | Dart - https://dart.dev/guides/language/language-tour

[2] Effective Dart: Design | Dart - https://dart.dev/guides/language/effective-dart/design