เชี่ยวชาญ Future, async/await และ FutureBuilder โดยไม่บล็อก UI หรือยิงรีเควสต์ซ้ำโดยไม่ตั้งใจ
เปิดบทเรียนนี้ใน KodokonDart รันแอปพลิเคชันของคุณบน เธรดเดียว ที่ขับเคลื่อนด้วยลูปเหตุการณ์ (event loop) Future ไม่ใช่เธรด แต่มันคือคำสัญญาว่าจะมีค่าตามมาในภายหลัง await ไม่เคยบล็อกเธรดนั้น มันหยุดฟังก์ชันปัจจุบันไว้ชั่วคราว คืนการควบคุมกลับให้ลูป (UI ยังคงทำงานต่อไป) แล้วทำงานต่อเมื่อผลลัพธ์มาถึง ผลที่ตามมาคือ async/await ไม่ได้ทำงานแบบขนานเลยสำหรับการคำนวณล้วน ๆ สำหรับงานที่ใช้ CPU หนัก คุณต้องใช้ไอโซเลต (isolate)
Future<double> fetchRate() async {
await Future<void>.delayed(
const Duration(milliseconds: 300),
);
return 1.08;
}
Future<void> main() async {
try {
final rate = await fetchRate();
print('EUR/USD rate: $rate');
} on Exception catch (error) {
print('Rate loading failed: $error');
}
}await สองครั้งที่ต่อเนื่องกันจะทำงาน แบบเรียงลำดับ ตัวที่สองจะเริ่มก็ต่อเมื่อตัวแรกเสร็จ เมื่อการดำเนินการเป็นอิสระต่อกัน ให้เปิดใช้พร้อมกันด้วย Future.wait ข้อแลกเปลี่ยนที่ควรรู้คือ Future.wait เป็นแบบล้มเหลวเร็ว (fail-fast) นั่นคือ ข้อผิดพลาดแรกจะปฏิเสธทั้งหมด และผลลัพธ์ของ future ตัวอื่นจะสูญหายไป แม้ว่าพวกมันจะสำเร็จก็ตาม ถ้าคุณต้องการผลลัพธ์บางส่วน ให้จัดการข้อผิดพลาดภายใน future แต่ละตัว
Future<String> fetchProfile() async => 'profile';
Future<String> fetchOrders() async => 'orders';
Future<void> main() async {
final [profile, orders] = await Future.wait(
[fetchProfile(), fetchOrders()],
);
print('$profile / $orders');
}ในฝั่ง UI นั้น FutureBuilder เชื่อม Future เข้ากับ build ผ่าน snapshot กับดักอันดับหนึ่งในการใช้งานจริงคือการสร้าง future โดยตรงใน build ทุกครั้งที่มีการรีบิลด์ (แค่ setState ง่าย ๆ จากตัวแม่ก็เพียงพอ) จะเป็นการยิงรีเควสต์ซ้ำ จงเก็บ future ไว้ใน initState และทดสอบข้อผิดพลาดก่อนข้อมูลเสมอ
import 'package:flutter/material.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() =>
_ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
late final Future<String> _nameFuture;
@override
void initState() {
super.initState();
_nameFuture = _loadName();
}
Future<String> _loadName() async {
await Future<void>.delayed(
const Duration(seconds: 1),
);
return 'Ada Lovelace';
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _nameFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Text(snapshot.data!);
},
);
}
}await ทำอะไรบนเธรดหลัก?Future จะเสร็จสิ้นFuture.wait([a(), b()]) จะเกิดอะไรขึ้นถ้า a() ล้มเหลว?Future.wait ปฏิเสธพร้อมข้อผิดพลาดจาก a() และผลลัพธ์ของ b() สูญหายไปFuture.wait รอให้ b() เสร็จ แล้วคืนเฉพาะผลลัพธ์ของมันเพียงตัวเดียวb() สำเร็จfuture: fetchUser() โดยตรงใน build เมื่อใช้ FutureBuilder?build ไม่สามารถมีการเรียกแบบอะซิงโครนัสได้ โค้ดจะไม่คอมไพล์Future ตัวใหม่ จึงเป็นการยิงรีเควสต์เครือข่ายซ้ำsnapshot จะค้างอยู่ในสถานะรอ