Prevent Stale Asynchronous Results from Overwriting UI State
Master request identity sequencing, cancellation tokens, and mounted checks to prevent out-of-order network responses from corrupting newer screens.
1. The Core Challenge & Problem
2. Architectural Principles & Resolution
3. Complete Tested Flutter Code
main.dart (Flutter 3.x+ ready)import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: SearchRaceConditionDemo()));
class SearchRaceConditionDemo extends StatefulWidget {
const SearchRaceConditionDemo({super.key});
@override
State<SearchRaceConditionDemo> createState() => _SearchRaceConditionDemoState();
}
class _SearchRaceConditionDemoState extends State<SearchRaceConditionDemo> {
int _activeRequestId = 0;
String _currentQuery = '';
List<String> _results = [];
bool _isLoading = false;
// Simulated backend API with unpredictable artificial latency
Future<List<String>> _mockApiSearch(String query, int simulatedLatencyMs) async {
await Future.delayed(Duration(milliseconds: simulatedLatencyMs));
return List.generate(3, (i) => 'Result ${i + 1} for "$query"');
}
void _onSearchChanged(String query) async {
// 1. Increment monotonic request ID
final int thisRequestId = ++_activeRequestId;
setState(() {
_currentQuery = query;
_isLoading = true;
});
// Simulate edge case: First query takes 800ms, subsequent query takes only 200ms
final latency = query == 'fl' ? 800 : 200;
final data = await _mockApiSearch(query, latency);
// 2. CRITICAL: Check both widget mounted status and monotonic request identity
if (!mounted) return;
if (thisRequestId != _activeRequestId) {
// Stale response! Discard quietly without corrupting active UI state
return;
}
setState(() {
_results = data;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('T01.03: Stale Async Prevention')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
onChanged: _onSearchChanged,
decoration: const InputDecoration(
labelText: 'Search (Type "fl" then quickly "flutter")',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
if (_isLoading) const LinearProgressIndicator(),
const SizedBox(height: 8),
Text('Active Request ID: $_activeRequestId', style: const TextStyle(fontWeight: FontWeight.bold)),
const Divider(),
Expanded(
child: ListView.builder(
itemCount: _results.length,
itemBuilder: (context, i) => ListTile(title: Text(_results[i])),
),
),
],
),
),
);
}
} 4. Expected Visual & Behavioral Result
5. Common Pitfalls & Traps
Pitfall #1: Only checking if (mounted) without monotonic request tokens
Remedy: mounted only verifies if the widget is in the element tree; it does NOT protect against two concurrent requests where the older request arrives after the newer one.
Pitfall #2: Calling setState directly inside an unawaited Future callback
Remedy: Always capture local request IDs and check if (thisRequestId == _activeRequestId && mounted) before mutating UI state.
Pitfall #3: Relying solely on UI debouncing to fix race conditions
Remedy: Debounce reduces request frequency, but variable network transit delays can still cause response inversion. Both debouncing and request IDs are required for bulletproof network code.