Lesson 6 of 77 intermediate

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

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. 1. Generic Result class — works with any type
  2. 2. Success case stores data of type T
  3. 3. Failure case stores an error message
  4. 4. Generic Repository with constraint — T must extend Entity
  5. 5. Abstract methods return Result — type-safe data access
  6. 6. Generic function — T is inferred from the argument
  7. 7. Extension on String — adds validation methods directly to all Strings
  8. 8. Email validation using regex directly on any String instance
  9. 9. Extension on DateTime — .timeAgo computed property
  10. 10. Mixin Loggable — any class can gain logging by adding 'with Loggable'
  11. 11. Mixin Cacheable — generic caching behavior
  12. 12. UserRepository combines inheritance AND two mixins
  13. 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

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery