DMarlon Studio

Lesson 9 · 9 of 20

Generics · Dart

Write generic classes and functions in Dart, with bounded types (extends) and type inference, using an in-memory repository and a groupBy function.

Dart · Generics in DMarlon Studio: the example's code and its output in the terminal

Generics let you write a class or a function once and use it with any type, without losing type checking. You probably use them every day already: List<String> and Map<int, T> are generic classes.

Repository<T extends Entity> is an in-memory repository for any entity. T is a type parameter and extends Entity bounds it: only types that have an id qualify, and thanks to that save can use item.id as the key. A Repository<Customer> stores customers and nothing else; customers.save('Elena') would not compile. findById returns T? because the customer may not exist, and for id 9 it gives null.

groupBy<K, V> is a generic function: it takes items of type V and a function that gets a key of type K from each one. You do not have to write the types when calling it, because Dart infers them from the arguments: with customers and c.city, K is String; with words and s.length, K is int. The ??= operator creates each group's list the first time its key turns up.

Unlike Java, Dart keeps generic types at run time (they are reified): runtimeType prints Repository<Customer>, and a check such as is List<int> genuinely works.

Code · main.dart

abstract class Entity { int get id; }class Customer implements Entity {  @override final int id;  final String name, city;  Customer(this.id, this.name, this.city);  @override String toString() => name;}// One generic class, valid for any type that is an Entity.class Repository<T extends Entity> {  final Map<int, T> _items = {};  void save(T item) => _items[item.id] = item;  T? findById(int id) => _items[id];  List<T> get all => _items.values.toList();}// A generic function: K and V are inferred from the arguments.Map<K, List<V>> groupBy<K, V>(Iterable<V> items, K Function(V) keyOf) {  final groups = <K, List<V>>{};  for (final item in items) (groups[keyOf(item)] ??= []).add(item);  return groups;}void main() {  final customers = Repository<Customer>()    ..save(Customer(1, 'Elena', 'Madrid'))    ..save(Customer(2, 'Jorge', 'Malaga'))    ..save(Customer(3, 'Nuria', 'Madrid'));  print('Found: ${customers.findById(2)}, missing: ${customers.findById(9)}');  print(groupBy(customers.all, (c) => c.city));  print(groupBy(['kiwi', 'fig', 'pear', 'plum'], (s) => s.length));  print('Type kept at runtime: ${customers.runtimeType}');}

Output · dart run main.dart

Found: Jorge, missing: null
{Madrid: [Elena, Nuria], Malaga: [Jorge]}
{4: [kiwi, pear, plum], 3: [fig]}
Type kept at runtime: Repository<Customer>

Key points

  • <T> is a type parameter, fixed when the class is used.
  • T extends Base restricts the types and lets you use Base's members.
  • Functions can be generic too, and Dart infers their types.
  • Dart's generics are kept at run time.

Exercise

Create a Product implements Entity class with id, name and price, store three in a Repository<Product> and group them with groupBy by whether they cost more or less than 20 euros.