FutureBuilder and StreamBuilder
In React you use useEffect + useState, react-query, or Suspense to handle async data in UI. Flutter provides FutureBuilder and StreamBuilder — widgets that declaratively handle loading, error, and data states right in the widget tree. Instead of wiring up flags and effects, you hand the widget a Future or Stream and describe what to render for each state.
FutureBuilder
Section titled “FutureBuilder”FutureBuilder is Flutter’s answer to useEffect + fetch + a loading boolean. You pass it a Future and a builder callback that receives a AsyncSnapshot. The snapshot carries a connectionState enum (waiting, done) and either data or error — no manual flag management required.
// React: useEffect + fetch + loading flagfunction UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { let cancelled = false; fetch(`/api/users/${userId}`) .then(r => r.json()) .then(data => { if (!cancelled) setUser(data); }) .catch(err => { if (!cancelled) setError(err); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [userId]);
if (loading) return <Spinner />; if (error) return <p>Error: {error.message}</p>; return <p>{user.name}</p>;}// Flutter: FutureBuilder — no flags, no effectsclass UserProfile extends StatelessWidget { final String userId; const UserProfile({super.key, required this.userId});
Future<String> _fetchUser() async { // simulating a network call await Future.delayed(const Duration(seconds: 1)); return 'Ava Tavos'; }
@override Widget build(BuildContext context) { return FutureBuilder<String>( future: _fetchUser(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } return Text(snapshot.data ?? ''); }, ); }}StreamBuilder
Section titled “StreamBuilder”StreamBuilder is the FutureBuilder equivalent for continuous data — think WebSocket feeds, real-time database updates, or interval timers. In React you might reach for a WebSocket, EventSource, or an RxJS observable wrapped in useEffect. In Flutter you hand a Stream directly to the widget.
// React: WebSocket feed via useEffectfunction LivePrice({ symbol }) { const [price, setPrice] = useState(null);
useEffect(() => { const ws = new WebSocket(`wss://prices.example.com/${symbol}`); ws.onmessage = e => setPrice(JSON.parse(e.data).price); return () => ws.close(); // cleanup on unmount / symbol change }, [symbol]);
if (price === null) return <p>Connecting…</p>; return <p>${price.toFixed(2)}</p>;}// Flutter: StreamBuilder — stream wired directly to the treeclass LivePrice extends StatelessWidget { final Stream<double> priceStream; const LivePrice({super.key, required this.priceStream});
@override Widget build(BuildContext context) { return StreamBuilder<double>( stream: priceStream, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Text('Connecting…'); } if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } final price = snapshot.data ?? 0.0; return Text('$${price.toStringAsFixed(2)}'); }, ); }}
// Usage — pass in any Stream<double>// StreamBuilder wires itself to the stream; no cleanup code needed.Runnable example
Section titled “Runnable example”import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: DataFetchDemo(),
),
),
);
}
}
class DataFetchDemo extends StatelessWidget {
const DataFetchDemo({super.key});
Future<String> _loadData() async {
await Future.delayed(const Duration(seconds: 2));
return 'Data loaded!';
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _loadData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Fetching data…', style: TextStyle(color: Colors.grey)),
],
);
}
if (snapshot.hasError) {
return Card(
color: Colors.red.shade50,
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(
'Error: ${snapshot.error}',
style: const TextStyle(color: Colors.red),
),
),
);
}
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 48),
const SizedBox(height: 12),
Text(
snapshot.data ?? '',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'The Future resolved successfully.',
style: TextStyle(color: Colors.grey),
),
],
),
),
);
},
);
}
}