Working with generic types in Dart

Pedro Lemos
4 min readJan 15, 2021
Generic Types in Dart

Dart is an incredible and flexible language. If you are coming from strongly typed and static languages like Java and C#, or from dynamic and weakly typed languages like JavaScript and Python, you’ll notice that Dart can behave almost like you are familiar to. It’s really amazing, and some features and concepts can be shared across multiple languages. Today, we’ll discuss a little about Generics, one of the most powerful tools in Java and C#.

If you make use of GetIt, Flutter Modular, GetX, or even a List of a certain data type, you already used Generics in Dart. Look at the following example:

Modular.get<AppController>();

This is the way we retrive an instance of a registered object using Flutter Modular. Notice that we have a static function named get which has no parameters. Instead, we just pass the class type inside the less than and greater than symbols. This is useful when we have different data types in a collection. But how we can do this by ourselves?

Let’s imagine that you need to add elements of any sort in a list within a class. How would you do that? You can create a class and use generics to tell the compiler that we want to be able to store any kind of data in the list.

class MyClass {
var _myList = <dynamic>[];
}

We need a method to add a new element in that list. This is simple, right?

void add(dynamic element) => _myList.add(element);

Now, imagine that you need to retrivie the data of a exact type in that list. Think about this: how can we tell the compiler which data type we want? We can’t pass the type as a parameter like we do with variables. The solution for this is expect a type named T (the name doesn’t matter), and then use it to make the comparisons between the list items. Our method signature will be like this:

get<T>();

When we don’t tell the return type to the compiler, it will be dynamic by default. Let’s say that you have 3 items of the exact same type. In this case, we need to return a list of items. We’ll know the type of the desired item in compile time, and we don’t need to return a dynamic type. We can change the signature to this:

List<T> get<T>();

The implementation of this method is very simple: first, we verify if the item in the list is of the same type we passed in the method, the T. If this evaluation is true, then we add the item to a list. We need to do this verification to all the items in _myList, and then return a list of items with type T. Any data in Dart will have an attribute named runtimeType. As you can guess, this attribute returns the type of the element. So, our implementation will be just like this:

List<T> get<T>() {
var elements = <T>[];
for (var element in _myList) {
if (element.runtimeType == T) {
elements.add(element);
}
}
return elements;
}

Conveniently, the Dart team included a method in Iterable class (which is implemented by List) that has this same purpose. Our code can be reduced to:

List<T> get<T>() => _myList.whereType<T>().toList();

Yeah, that simple. I added the toList() method just to make sure that we are returning a list. With these two methods, our class is finished:

class MyClass {
var _myList = <dynamic>[];

void add(dynamic element) => _myList.add(element);
List<T> get<T>() => _myList.whereType<T>().toList();
}

Let’s test our code:

void main() {
final instance = MyClass();
instance.add(1);
instance.add(2);
instance.add(3);
instance.add(4.0);
instance.add(5.0);
instance.add(6.0);
instance.add(true);
instance.add(false);
instance.add('Pedro');
instance.add('Lemos');
print(instance.get<int>());
print(instance.get<double>());
print(instance.get<bool>());
print(instance.get<String>());
}

The output will be:

[1, 2, 3] // All int values
[4.0, 5.0, 6.0] // All double values
[true, false] // All bool values
[Pedro, Lemos] // All String values

Okay, this example works, but is not realistic, right? Let’s do something more useful. Now, imagine that we are developing a system to manage orders for a Bakery. We have these classes:

class Item {
final String type;
final double price;
Item({this.type, this.price});
}
class Bread extends Item {
Bread({String type, double price}) : super(price: price, type: type);
}
class Cake extends Item {
Cake({String type, double price}) : super(price: price, type: type);
}
class Coffee extends Item {
Coffee({String type, double price}) : super(price: price, type: type);
}

We can rename MyClass to BakeryOrder:

class BakeryOrder {
var _order = <dynamic>[];
void addNewItem(dynamic item) => _order.add(item);
List<T> getItemsOfType<T>() => _order.whereType<T>().toList();
}

We can add new items to our orders using the addNewItem method, and retrivie a list of items of a certain type using getItemsOfType method:

void main() {
final instance = BakeryOrder();
instance.addNewItem(Bread());
instance.addNewItem(Coffee());
instance.addNewItem(Cake());
print(instance.getItemsOfType<Bread>());
print(instance.getItemsOfType<Coffee>());
print(instance.getItemsOfType<Cake>());
}

The output will be:

[Instance of 'Bread']
[Instance of 'Coffee']
[Instance of 'Cake']

That’s it! You can use generics to achieve this kind of result and much more. Hope you liked this article!

--

--