14  Asynchronous Programming

Let me start with the fundamental question: why do we need asynchronous programming? Imagine you’re reading a medical image from a server. If your program stops everything to wait for that image to download, your entire UI freezes. Asynchronous programming lets your app stay responsive while waiting for time-consuming operations.

14.0.1 The Core Concepts: Futures

In Dart, a Future represents a value that will be available at some point in the future. Think of it like ordering a medical report – you get a ticket (the Future) immediately, but the actual report comes later.

// A function that returns a Future
Future<String> fetchPatientData() {
  // Simulating a network delay
  return Future.delayed(
    Duration(seconds: 2),
    () => 'Patient: John Doe, MRN: 12345',
  );
}

When you call fetchPatientData(), it immediately returns a Future object, not the actual string. Your program can continue running other code while waiting for the data.

14.0.2 Working with Futures: The Two Approaches

There are two main ways to handle Futures in Dart:

1. Using .then() callbacks (similar to JavaScript promises):

void main() {
  print('Starting data fetch...');
  
  fetchPatientData().then((data) {
    print('Received: $data');
  }).catchError((error) {
    print('Error occurred: $error');
  });
  
  print('Continuing with other work...');
}

2. Using async and await (the cleaner approach):

void main() async {
  print('Starting data fetch...');
  
  try {
    String data = await fetchPatientData();
    print('Received: $data');
  } catch (error) {
    print('Error occurred: $error');
  }
  
  print('This runs after the data is fetched');
}

14.0.3 Understanding async and await

The async keyword transforms a function to work with Futures. When you mark a function as async, it automatically returns a Future. The await keyword pauses the function execution until the Future completes.

Here’s a practical example that might resonate with your medical imaging work:

// Simulating medical image processing
Future<Map<String, dynamic>> loadDicomImage(String path) async {
  // Simulate loading time
  await Future.delayed(Duration(seconds: 1));
  return {
    'path': path,
    'modality': 'CT',
    'size': '512x512',
    'patient': 'Anonymous'
  };
}

Future<List<double>> analyzeImage(Map<String, dynamic> imageData) async {
  // Simulate processing time
  await Future.delayed(Duration(milliseconds: 500));
  
  // Pretend we're calculating some measurements
  return [23.5, 45.2, 67.8]; // Example: HU values, dimensions, etc.
}

// Using both functions together
Future<void> processRadiologyStudy() async {
  try {
    print('Loading DICOM image...');
    final imageData = await loadDicomImage('/path/to/study.dcm');
    print('Image loaded: ${imageData['modality']} - ${imageData['size']}');
    
    print('Analyzing image...');
    final measurements = await analyzeImage(imageData);
    print('Analysis complete: $measurements');
    
  } catch (e) {
    print('Error in processing: $e');
  }
}

14.0.4 Parallel vs Sequential Execution

One powerful aspect of Futures is controlling whether operations run sequentially or in parallel:

Sequential execution (one after another):

Future<void> sequentialProcessing() async {
  // These run one after the other
  final result1 = await operation1();  // Waits for completion
  final result2 = await operation2();  // Then starts this
  final result3 = await operation3();  // Then starts this
}

Parallel execution (simultaneously):

Future<void> parallelProcessing() async {
  // Start all operations at once
  final future1 = operation1();  // No await - starts immediately
  final future2 = operation2();  // Starts without waiting for operation1
  final future3 = operation3();  // Starts without waiting
  
  // Wait for all to complete
  final results = await Future.wait([future1, future2, future3]);
  
  // Or individually:
  // final result1 = await future1;
  // final result2 = await future2;
  // final result3 = await future3;
}

14.0.5 Practical Example: Medical Image Batch Processing

Here’s an example that combines these concepts in a way relevant to your radiology work:

class ImageProcessor {
  // Process multiple images in parallel
  Future<List<Map<String, dynamic>>> processBatch(List<String> imagePaths) async {
    print('Processing ${imagePaths.length} images...');
    
    // Create a Future for each image
    final futures = imagePaths.map((path) => processImage(path));
    
    // Wait for all to complete
    final results = await Future.wait(futures);
    
    return results;
  }
  
  Future<Map<String, dynamic>> processImage(String path) async {
    // Simulate varying processing times
    final processingTime = 1 + (path.length % 3); // 1-3 seconds
    await Future.delayed(Duration(seconds: processingTime));
    
    return {
      'path': path,
      'timestamp': DateTime.now().toIso8601String(),
      'findings': 'Normal study',
      'processingTime': processingTime,
    };
  }
}

void main() async {
  final processor = ImageProcessor();
  
  final imagePaths = [
    '/scan/patient1/ct_chest.dcm',
    '/scan/patient2/mri_brain.dcm',
    '/scan/patient3/xray_chest.dcm',
  ];
  
  final stopwatch = Stopwatch()..start();
  
  final results = await processor.processBatch(imagePaths);
  
  stopwatch.stop();
  
  print('\nCompleted in ${stopwatch.elapsed.inSeconds} seconds');
  
  for (final result in results) {
    print('Processed: ${result['path']} in ${result['processingTime']}s');
  }
}

14.0.6 Common Pitfalls and Best Practices

  1. Forgetting to use await: This creates “fire and forget” behavior
// Wrong - doesn't wait for completion
void uploadReport() async {
  saveToDatabase();  // Missing await!
  print('Upload complete');  // This runs immediately, not after save
}

// Correct
void uploadReport() async {
  await saveToDatabase();
  print('Upload complete');  // This runs after save finishes
}
  1. Error handling: Always handle potential failures
Future<void> safeFetch() async {
  try {
    final data = await riskyOperation();
    // Process data
  } on NetworkException catch (e) {
    // Handle specific error type
    print('Network error: $e');
  } catch (e) {
    // Handle any other error
    print('Unexpected error: $e');
  } finally {
    // Cleanup code that always runs
    closeConnections();
  }
}
  1. Timeout handling: Important for network operations
Future<String> fetchWithTimeout() async {
  try {
    final data = await fetchData().timeout(
      Duration(seconds: 5),
      onTimeout: () => throw TimeoutException('Request took too long'),
    );
    return data;
  } catch (e) {
    return 'Failed to fetch data: $e';
  }
}

Would you like me to create some practice exercises for you to work through? I can design them around medical imaging scenarios to make them more relevant to your work. Or would you prefer to dive deeper into any specific aspect of async programming, such as Streams (for handling continuous data) or Isolates (for true parallel processing)?