3  Understanding Generics

Generics are one of the most powerful features in Dart, allowing you to write code that’s both type-safe and reusable. Since you’re coming from Python, JavaScript, and R backgrounds, I’ll relate Dart’s generics to concepts you might already be familiar with, particularly from Python’s type hints or TypeScript’s generics.

3.1 What Are Generics?

Generics allow you to create components that work with any data type while still providing compile-time type safety. Think of generics as a way to tell the compiler, “I don’t know exactly what type this will be, but whatever type it is, use it consistently.”

3.2 Why Are Generics Important?

Without generics, you might need to create separate implementations for different data types or resort to using Object (which is like Python’s Any or JavaScript’s loose typing) and lose type safety. Generics give you the best of both worlds: flexibility and type safety.

3.3 Basic Syntax of Generics in Dart

The basic syntax for generics involves angle brackets <T> where T is a type parameter:

class Box<T> {
  T value;

  Box(this.value);

  T getValue() {
    return value;
  }
}

In this example, T is a placeholder for any type. When you create a Box object, you specify what type T should be:

void main() {
  // Create a Box that contains a string
  var stringBox = Box<String>('Medical Report');
  String report = stringBox.getValue();  // Type-safe!
  
  // Create a Box that contains an integer
  var ageBox = Box<int>(45);
  int patientAge = ageBox.getValue();  // Type-safe!
}

3.4 Common Use Cases for Generics

// Explicit generic type - what you showed in your example
children: <Widget>[
  Center(child: Text("It's cloudy here")),
  Center(child: Text("It's rainy here")),
  Center(child: Text("It's sunny here")),
],

// Implicit type inference - Dart figures out it's a List<Widget>
children: [
  Center(child: Text("It's cloudy here")),
  Center(child: Text("It's rainy here")),
  Center(child: Text("It's sunny here")),
],

// Fully explicit with the List constructor 
children: List<Widget>.from([
  Center(child: Text("It's cloudy here")),
  Center(child: Text("It's rainy here")),
  Center(child: Text("It's sunny here")),
]),

3.5 Multiple Type Parameters

You can use multiple type parameters, just like in TypeScript:

class Pair<K, V> {
  K first;
  V second;
  
  Pair(this.first, this.second);
}

void main() {
  // A pair with a String key and an int value
  var patientRecord = Pair<String, int>('John Doe', 42);
  String name = patientRecord.first;    // Type-safe!
  int age = patientRecord.second;       // Type-safe!
  
  // A pair with two different types
  var medicalData = Pair<String, double>('Blood Pressure', 120.5);
}

3.6 Generics with Collections

Dart’s collection types are already generic. When you create a List, Map, or Set, you can specify the type of elements it will contain:

void main() {
  // A list of strings (like Python's List[str])
  List<String> patientNames = ['John', 'Sarah', 'Michael'];
  
  // A map with string keys and integer values (like Python's Dict[str, int])
  Map<String, int> patientAges = {
    'John': 45,
    'Sarah': 32,
    'Michael': 58,
  };
  
  // A set of integers
  Set<int> patientIDs = {1001, 1002, 1003};
}

This is similar to Python’s type hints like List[str] or TypeScript’s generic types, but Dart enforces these types at compile time.

3.7 Generic Functions

You can also create generic functions in Dart:

T first<T>(List<T> list) {
  if (list.isEmpty) {
    throw Exception('List is empty');
  }
  return list[0];
}

void main() {
  var names = ['John', 'Sarah', 'Michael'];
  String firstName = first<String>(names);  // Type-safe!
  
  var ages = [45, 32, 58];
  int firstAge = first<int>(ages);  // Type-safe!
}

The compiler can often infer the generic type argument, so you could also write:

String firstName = first(names);  // Type inference in action

3.8 Type Constraints (Bounds)

Sometimes you want to restrict the types that can be used with your generic. You do this with type bounds using the extends keyword:

// T must be a subtype of Comparable
T findMin<T extends Comparable<T>>(List<T> list) {
  if (list.isEmpty) {
    throw Exception('List is empty');
  }
  
  T min = list[0];
  for (var i = 1; i < list.length; i++) {
    if (list[i].compareTo(min) < 0) {
      min = list[i];
    }
  }
  return min;
}

void main() {
  // This works because int implements Comparable
  var ages = [45, 32, 58];
  int minAge = findMin<int>(ages);  // 32
  
  // This works because String implements Comparable
  var names = ['John', 'Sarah', 'Michael'];
  String firstName = findMin<String>(names);  // 'John' (alphabetically first)
}

This is similar to bounded type parameters in Java or TypeScript’s extends keyword.

3.9 A Medical Example with Generics

Let’s put this all together with a medical-themed example that might relate to your radiology background:

// A generic class for medical scan results
class ScanResult<T> {
  final String patientId;
  final DateTime scanDate;
  final T resultData;
  
  ScanResult(this.patientId, this.scanDate, this.resultData);
  
  void display() {
    print('Patient ID: $patientId');
    print('Scan Date: $scanDate');
    print('Result: $resultData');
  }
}

// Different types of scan result data
class MRIData {
  final List<String> observations;
  
  MRIData(this.observations);
  
  @override
  String toString() => observations.join(', ');
}

class CTData {
  final Map<String, double> densityReadings;
  
  CTData(this.densityReadings);
  
  @override
  String toString() => densityReadings.toString();
}

// A generic function to analyze scan results
void analyzeScan<T>(ScanResult<T> scan) {
  print('Analyzing scan for patient ${scan.patientId}...');
  print('Data type: ${T.toString()}');
  // Analysis would depend on the specific type T
}

void main() {
  // Create an MRI scan result
  var mriData = MRIData(['Normal brain structure', 'No abnormalities detected']);
  var mriScan = ScanResult<MRIData>(
    'P12345',
    DateTime.now(),
    mriData
  );
  
  // Create a CT scan result
  var ctData = CTData({'Lung': 0.32, 'Liver': 0.48, 'Bone': 1.25});
  var ctScan = ScanResult<CTData>(
    'P67890',
    DateTime.now(),
    ctData
  );
  
  // Display results
  mriScan.display();
  ctScan.display();
  
  // Analyze scans
  analyzeScan<MRIData>(mriScan);
  analyzeScan<CTData>(ctScan);
}

3.10 Advanced Generic Concepts

3.10.1 Generic Methods in Classes

You can define generic methods within non-generic classes:

class DiagnosticUtility {
  // A generic method in a non-generic class
  T? findAbnormalReading<T extends num>(List<T> readings, T threshold) {
    for (var reading in readings) {
      if (reading > threshold) {
        return reading;
      }
    }
    return null;
  }
}

void main() {
  var util = DiagnosticUtility();
  
  // Using the generic method with integers
  List<int> bpReadings = [120, 135, 142, 118, 125];
  int? abnormalBP = util.findAbnormalReading<int>(bpReadings, 140);
  print('Abnormal BP reading: $abnormalBP');  // 142
  
  // Using the same method with doubles
  List<double> glucoseReadings = [5.2, 4.8, 7.1, 5.5, 6.2];
  double? abnormalGlucose = util.findAbnormalReading<double>(glucoseReadings, 7.0);
  print('Abnormal glucose reading: $abnormalGlucose');  // 7.1
}

3.10.2 Type Inference with Generics

Dart’s type inference is quite powerful. In many cases, you don’t need to explicitly specify type parameters:

void main() {
  // Type inference - Dart knows this is a List<String>
  var names = <String>[];
  names.add('John');  // This works
  // names.add(42);   // This would fail - type safety in action
  
  // Type inference with constructor
  var stringBox = Box('Hello');  // Dart infers Box<String>
  String value = stringBox.getValue();  // Type-safe!
}

3.10.3 Reified Generics

Unlike Java, Dart has reified generics, which means type information is preserved at runtime:

void main() {
  var names = <String>['John', 'Sarah'];
  var ages = <int>[45, 32];
  
  print(names.runtimeType);  // Outputs: List<String>
  print(ages.runtimeType);   // Outputs: List<int>
}

This is different from Java where type erasure occurs, and similar to TypeScript when compiled to JavaScript with proper type information.

3.11 Comparing with Your Existing Knowledge

Since you’re familiar with Python, JavaScript, and R, here’s how Dart’s generics compare:

  1. Python: Dart’s generics are similar to Python’s type hints (introduced in Python 3.5+), but Dart’s are enforced at compile time while Python’s are just hints.
# Python with type hints
from typing import List, TypeVar

T = TypeVar('T')

def first(items: List[T]) -> T:
    return items[0]

names: List[str] = ["John", "Sarah"]
first_name: str = first(names)
  1. JavaScript/TypeScript: Dart’s generics are very similar to TypeScript’s generics, both in syntax and behavior.
// TypeScript
class Box<T> {
  value: T;
  
  constructor(value: T) {
    this.value = value;
  }
  
  getValue(): T {
    return this.value;
  }
}

const stringBox = new Box<string>("Hello");
const value: string = stringBox.getValue();
  1. R: R doesn’t have built-in generics like Dart, but S3/S4 methods in R serve a similar purpose by allowing polymorphic behavior.

3.12 Common Use Cases for Generics in Flutter

In Flutter development, you’ll encounter generics frequently:

  1. Future - For asynchronous operations returning a specific type:
Future<String> fetchPatientName(String id) async {
  // Simulating API call
  await Future.delayed(Duration(seconds: 1));
  return 'John Doe';
}

void main() async {
  String name = await fetchPatientName('P12345');
  print(name);  // John Doe
}
  1. Stream - For continuous sequences of data:
Stream<int> generateHeartRateData() async* {
  final random = Random();
  for (int i = 0; i < 10; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield 60 + random.nextInt(40);  // Heart rates between 60-100
  }
}

void main() async {
  await for (final heartRate in generateHeartRateData()) {
    print('Current heart rate: $heartRate bpm');
  }
}
  1. State management - Many state management solutions in Flutter use generics:
// A simple generic state holder (similar pattern to provider packages)
class ValueNotifier<T> {
  T _value;
  final _listeners = <Function>[];
  
  ValueNotifier(this._value);
  
  T get value => _value;
  
  set value(T newValue) {
    if (newValue != _value) {
      _value = newValue;
      _notifyListeners();
    }
  }
  
  void addListener(Function listener) {
    _listeners.add(listener);
  }
  
  void _notifyListeners() {
    for (var listener in _listeners) {
      listener();
    }
  }
}

void main() {
  // For patient temperature
  final temperatureNotifier = ValueNotifier<double>(37.0);
  
  temperatureNotifier.addListener(() {
    print('Temperature updated: ${temperatureNotifier.value}°C');
  });
  
  temperatureNotifier.value = 37.5;  // Will trigger the listener
}

3.13 Practice Exercise

To solidify your understanding of generics in Dart, try implementing a MedicalDatabase<T> class that can store and retrieve medical records of different types. The class should:

  1. Allow adding records with a unique ID
  2. Have methods for retrieving, updating, and deleting records
  3. Include a method to find records based on a custom filter function

This exercise will help you practice using generics in a way that’s relevant to your medical background.