Generics in Dart are a way to write flexible and reusable code while maintaining type safety. They allow us to create classes, methods, or functions that can work with different data types without losing type information.
For example, instead of creating multiple classes like IntBox, StringBox, etc., we can use a generic class:
class Box {
T value;
Box(this.value);
void display() {
print('Value: $value');
}
}
void main() {
var intBox = Box(10);
var stringBox = Box('Hello Dart');
intBox.display(); // Output: Value: 10
stringBox.display(); // Output: Value: Hello Dart
}
Here, <T> is the generic type parameter. When I create an object, I specify what type T should be. This helps catch type-related issues at compile time rather than runtime.
I applied generics often in my Flutter projects — especially when working with API response models. For instance, I had a BaseResponse<T> class that handled common response fields like status, message, and data. The data field would change depending on the API, so generics made it easy to reuse one structure for multiple endpoints.
One challenge I faced was understanding type inference and constraints. For example, sometimes Dart couldn’t infer the type properly when I had nested generics, especially in JSON serialization. I had to explicitly specify the type or use factory constructors to make parsing cleaner.
A limitation is that Dart’s generics are not reified, meaning type information is lost at runtime. So, for example, you can’t check the type parameter like if (T == String) — this can make runtime reflection difficult.
As an alternative, when I needed more control, I used interfaces or abstract classes with specific implementations. This provided better structure when generics became too complex, especially for large-scale codebases with multiple model hierarchies.
