Generics, Extensions, Mixins & Typedefs
Advanced Dart That Separates Mid from Senior
Open interactive version (quiz + challenge)Real-world analogy
Generics are like a universal adapter — one design works with any plug type. Extensions are like adding a custom button to your TV remote without buying a new one. Mixins are like collecting skill badges — your scout can earn swimming AND archery without being born a swimmer or archer.
What is it?
Generics provide type-safe code reuse. Extensions add methods to existing types. Mixins compose reusable behavior. Typedefs create readable type aliases. Together, these features enable the advanced patterns (Repository, BlocBase, Either) that define senior-level Flutter architecture.
Real-world relevance
In a Clean Architecture Flutter app, you define Repository as a generic base, use extensions for String and DateTime formatting helpers, add Loggable and Cacheable mixins to services, and typedef complex callback types. These aren't academic — they're in every production codebase.
Key points
- Generics — Type Parameters — Generics let you write code that works with ANY type safely: class Box { final T value; }. Box(42) holds an int, Box('hello') holds a String. Without generics, you'd use dynamic and lose type safety. Used everywhere: List, Future, Stream.
- Generic Constraints with extends — Restrict what types are allowed: class Repo. Now T must be Entity or a subclass. This prevents misuse. In Clean Architecture, Repository ensures only model types are stored.
- Extension Methods — Add methods to existing classes without modifying them: extension StringX on String { bool get isEmail => contains('@'); }. Now any String has .isEmail. Powerful for adding domain-specific helpers to SDK types. Interview: When would you use extensions vs utility functions?
- Extension Types (Dart 3.3+) — extension type Meters(double value) implements double {}. Creates a zero-cost wrapper type at compile time. Unlike regular extensions, extension types create new types. Used for type-safe units, IDs, and API wrappers without runtime overhead.
- Mixins — Reusable Behavior — mixin Loggable { void log(String msg) => print(msg); }. Classes use 'with' to gain mixin behavior: class UserService with Loggable {}. Unlike inheritance (one parent), you can mix in multiple behaviors. Perfect for cross-cutting concerns.
- Mixin Constraints — mixin CacheableService on BaseService { ... } means this mixin can only be used on classes that extend BaseService. This ensures the mixin can call methods from BaseService. Interview: Explain 'on' keyword in mixins.
- Typedefs — Type Aliases — typedef JsonMap = Map; typedef Callback = void Function(String); Typedefs create readable aliases for complex types. Makes function signatures cleaner: void register(Callback onSuccess) instead of void register(void Function(String) onSuccess).
- Covariance and Generics — Dart generics are covariant: List is assignable to List. This is convenient but can be unsafe. Interview: Explain covariance. Answer: If Cat extends Animal, then List can be used where List is expected, but adding a Dog to it would crash at runtime.
- Generic Functions — Functions can be generic too: T first(List items) => items[0]; The type is inferred from usage: first([1,2,3]) returns int, first(['a','b']) returns String. Useful for utility functions that work across types.
- Practical Patterns — Repository: generic data access. Either: error handling. BlocBase: generic state. Mapper: data transformation. These patterns form the backbone of Clean Architecture in Flutter.
Code example
// Generics, Extensions, Mixins, Typedefs
// --- GENERICS ---
// Generic class — type-safe container
class Result<T> {
final T? data;
final String? error;
final bool isSuccess;
const Result.success(this.data) : error = null, isSuccess = true;
const Result.failure(this.error) : data = null, isSuccess = false;
}
// Generic with constraint
abstract class Repository<T extends Entity> {
Future<Result<T>> getById(String id);
Future<Result<List<T>>> getAll();
Future<Result<void>> save(T entity);
Future<Result<void>> delete(String id);
}
// Generic function
T? firstWhereOrNull<T>(List<T> items, bool Function(T) test) {
for (final item in items) {
if (test(item)) return item;
}
return null;
}
// --- EXTENSIONS ---
extension StringValidation on String {
bool get isValidEmail => RegExp(r'^[\w-.]+@[\w-]+\.[a-z]{2,}$').hasMatch(this);
bool get isStrongPassword => length >= 8 && contains(RegExp(r'[A-Z]')) && contains(RegExp(r'[0-9]'));
String get capitalized => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}
extension DateTimeFormatting on DateTime {
String get timeAgo {
final diff = DateTime.now().difference(this);
if (diff.inDays > 0) return '${diff.inDays}d ago';
if (diff.inHours > 0) return '${diff.inHours}h ago';
if (diff.inMinutes > 0) return '${diff.inMinutes}m ago';
return 'Just now';
}
}
// --- MIXINS ---
mixin Loggable {
void log(String message) {
print('[${runtimeType}] $message');
}
}
mixin Cacheable<T> {
final Map<String, T> _cache = {};
T? getFromCache(String key) => _cache[key];
void addToCache(String key, T value) {
_cache[key] = value;
}
void clearCache() => _cache.clear();
}
// Using multiple mixins
class UserRepository extends BaseRepository<User>
with Loggable, Cacheable<User> {
@override
Future<Result<User>> getById(String id) async {
// Check cache first
final cached = getFromCache(id);
if (cached != null) {
log('Cache hit for user $id');
return Result.success(cached);
}
log('Fetching user $id from API');
final user = await _api.fetchUser(id);
addToCache(id, user);
return Result.success(user);
}
}
// Mixin with constraint
mixin NetworkAware on StatefulWidget {
// Can only be used with StatefulWidget
}
// --- TYPEDEFS ---
typedef JsonMap = Map<String, dynamic>;
typedef OnSuccess<T> = void Function(T data);
typedef OnError = void Function(String message);
typedef Predicate<T> = bool Function(T item);
// Usage — clean function signatures
void fetchData({
required OnSuccess<JsonMap> onSuccess,
required OnError onError,
}) async {
try {
final data = await _api.get('/data');
onSuccess(data);
} catch (e) {
onError(e.toString());
}
}Line-by-line walkthrough
- 1. Generic Result class — works with any type
- 2. Success case stores data of type T
- 3. Failure case stores an error message
- 4. Generic Repository with constraint — T must extend Entity
- 5. Abstract methods return Result — type-safe data access
- 6. Generic function — T is inferred from the argument
- 7. Extension on String — adds validation methods directly to all Strings
- 8. Email validation using regex directly on any String instance
- 9. Extension on DateTime — .timeAgo computed property
- 10. Mixin Loggable — any class can gain logging by adding 'with Loggable'
- 11. Mixin Cacheable — generic caching behavior
- 12. UserRepository combines inheritance AND two mixins
- 13. Typedef for clean function signatures — OnSuccess instead of void Function(T data)
Spot the bug
mixin DatabaseMixin on Widget {
Future<void> saveData(String data) async {
// save to DB
}
}
class MyService with DatabaseMixin {
void doWork() {
saveData('test');
}
}Need a hint?
Check the mixin constraint — what does 'on Widget' mean?
Show answer
The mixin has 'on Widget' constraint, meaning it can ONLY be used with classes that extend Widget. MyService doesn't extend Widget, so this is a compile error. Fix: either remove the 'on Widget' constraint, change it to 'on Object', or make MyService extend a Widget (if that's the intent).
Explain like I'm 5
Generics are like a box factory where you tell it what to make boxes for — 'make me a toy box' or 'make me a shoe box' — same factory, different contents. Extensions are like giving your bike a new bell without going to the bike factory. Mixins are like wearing different hats — you can be a firefighter hat AND a chef hat at the same time!
Fun fact
Dart's extension methods were one of the most requested language features, added in Dart 2.7 (2019). The ability to add methods to types you don't own (like String or int) without inheritance revolutionized how Flutter developers write helper utilities — no more StringUtils.isEmail(str), now it's just str.isEmail.
Hands-on challenge
Create a generic Either class with Left(L) and Right(R) subclasses. Add an extension on Either that provides fold(), map(), and getOrElse() methods. Then create a typedef for common patterns like ApiResult = Either.
More resources
- Dart Generics (Dart Official)
- Extension Methods (Dart Official)
- Mixins (Dart Official)
- Typedefs (Dart Official)