FutureBuilder และ StreamBuilder
ใน React คุณใช้ useEffect + useState, react-query หรือ Suspense เพื่อจัดการ async data ใน UI Flutter มี FutureBuilder และ StreamBuilder — widget ที่จัดการ loading, error และ data states แบบ declarative ตรงในตัว widget tree แทนที่จะต้องตั้ง flags และ effects คุณแค่ส่ง Future หรือ Stream ให้ widget แล้วอธิบายว่าจะ render อะไรสำหรับแต่ละ state
FutureBuilder
หัวข้อที่มีชื่อว่า “FutureBuilder”FutureBuilder คือคำตอบของ Flutter สำหรับ useEffect + fetch + loading boolean คุณส่ง Future และ callback builder ที่รับ AsyncSnapshot เข้าไป snapshot นั้นมี enum connectionState (waiting, done) และ data หรือ error — ไม่ต้องจัดการ flag เองเลย
// 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 — ไม่มี flags, ไม่มี effectsclass UserProfile extends StatelessWidget { final String userId; const UserProfile({super.key, required this.userId});
Future<String> _fetchUser() async { // จำลอง 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
หัวข้อที่มีชื่อว่า “StreamBuilder”StreamBuilder คือ FutureBuilder สำหรับข้อมูลต่อเนื่อง — เช่น WebSocket feeds, real-time database updates หรือ interval timers ใน React คุณอาจใช้ WebSocket, EventSource หรือ RxJS observable ที่ครอบด้วย useEffect ใน Flutter คุณส่ง Stream โดยตรงให้ 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 ต่อเชื่อมโดยตรงกับ 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)}'); }, ); }}
// การใช้งาน — ส่ง Stream<double> ใดก็ได้// StreamBuilder จะต่อเชื่อมตัวเองกับ stream โดยไม่ต้องมี cleanup codeตัวอย่างที่รันได้
หัวข้อที่มีชื่อว่า “ตัวอย่างที่รันได้”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),
),
],
),
),
);
},
);
}
}