Flutter Async Await and Futures
Mobile apps fetch data from the internet, read files, and query databases. These operations take time. Dart uses Future, async, and await to handle time-consuming tasks without freezing the app.
The Problem — Blocking Code
Imagine a restaurant kitchen. If the chef waits at the oven doing nothing while the bread bakes, no other food gets cooked. A good chef starts the bread, then works on other dishes while waiting. Async programming works the same way.
Blocking (bad): ───────────────────────────────────────── Start app → Fetch data from internet [App freezes] [App freezes] [App freezes] → Data arrives → App works again Non-blocking (good): ───────────────────────────────────────── Start app → Start data fetch (in background) → Show loading spinner → User can still scroll → Data arrives → Update UI automatically
What Is a Future
A Future is a promise of a value that will arrive later. It represents work that is happening in the background.
Future<String> getUserName() Right now: [?] (not ready yet) Later: "Ravi Kumar" (resolved) OR Error (failed)
// A function that returns a Future
Future<String> fetchUsername() async {
await Future.delayed(Duration(seconds: 2)); // Simulate delay
return 'Ravi Kumar';
}
async and await
Mark a function with async to use await inside it. The await keyword pauses that function until the Future completes — but it does NOT freeze the rest of the app.
// Without async/await (harder to read)
fetchUsername().then((name) {
print('Hello, $name');
}).catchError((error) {
print('Error: $error');
});
// With async/await (cleaner)
Future<void> greetUser() async {
try {
String name = await fetchUsername(); // Waits here
print('Hello, $name');
} catch (error) {
print('Error: $error');
}
}
Future States
Future lifecycle:
─────────────────────────────────────────
Created → Pending → Completed (value) OR Error
│
│ During "Pending": show loading spinner
│ On "Completed": show data
└ On "Error": show error message
FutureBuilder — Futures in Flutter UI
FutureBuilder connects a Future to the widget tree. It automatically rebuilds the UI when the Future resolves.
FutureBuilder<String>(
future: fetchUsername(), // The async operation
builder: (context, snapshot) {
// snapshot.connectionState tells you the current state
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // Loading
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}'); // Error
}
return Text('Hello, ${snapshot.data}!'); // Success
},
)
FutureBuilder States Diagram
FutureBuilder
─────────────────────────────────────────────
ConnectionState.none → Future not started
ConnectionState.waiting → Future running → show spinner
ConnectionState.done → Future finished:
snapshot.hasError → show error widget
snapshot.hasData → show data widget
Practical Example — Fetch User Profile
// Simulated API call (returns after 2 seconds)
Future<Map<String, String>> fetchProfile() async {
await Future.delayed(Duration(seconds: 2));
return {'name': 'Priya', 'email': 'priya@mail.com', 'city': 'Mumbai'};
}
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: FutureBuilder<Map<String, String>>(
future: fetchProfile(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Failed to load profile'));
}
final profile = snapshot.data!;
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${profile['name']}', style: TextStyle(fontSize: 20)),
Text('Email: ${profile['email']}'),
Text('City: ${profile['city']}'),
],
),
);
},
),
);
}
}
Streams — Continuous Data
A Future delivers one value and finishes. A Stream delivers multiple values over time — like a live score update or a chat feed.
Future: [Delivery truck] → delivers one package → done Stream: [Water pipe] → flows continuously Future: fetchWeather() → one result Stream: stockPriceStream → new value every second
// Create a stream that emits a number every second
Stream<int> countStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // yield sends a value into the stream
}
}
// Use StreamBuilder to display stream data
StreamBuilder<int>(
stream: countStream(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Text('Waiting...');
return Text('Count: ${snapshot.data}');
},
)
Error Handling
Future<String> riskyOperation() async {
try {
String result = await someNetworkCall();
return result;
} catch (e) {
print('Caught error: $e');
return 'Default value';
}
}
Key Rules
- Add
asyncto any function that usesawait. - The return type becomes
Future<T>automatically. - Always handle errors with
try/catchor.catchError(). - Use
FutureBuilderto display async data in the UI. - Use
StreamBuilderfor data that changes over time.
