Kodokon kodokon.com

การทำงานแบบอะซิงโครนัสใน Dart: Future, async/await, FutureBuilder

เชี่ยวชาญ Future, async/await และ FutureBuilder โดยไม่บล็อก UI หรือยิงรีเควสต์ซ้ำโดยไม่ตั้งใจ

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

Dart รันแอปพลิเคชันของคุณบน เธรดเดียว ที่ขับเคลื่อนด้วยลูปเหตุการณ์ (event loop) Future ไม่ใช่เธรด แต่มันคือคำสัญญาว่าจะมีค่าตามมาในภายหลัง await ไม่เคยบล็อกเธรดนั้น มันหยุดฟังก์ชันปัจจุบันไว้ชั่วคราว คืนการควบคุมกลับให้ลูป (UI ยังคงทำงานต่อไป) แล้วทำงานต่อเมื่อผลลัพธ์มาถึง ผลที่ตามมาคือ async/await ไม่ได้ทำงานแบบขนานเลยสำหรับการคำนวณล้วน ๆ สำหรับงานที่ใช้ CPU หนัก คุณต้องใช้ไอโซเลต (isolate)

DART
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 และ try/catch

await สองครั้งที่ต่อเนื่องกันจะทำงาน แบบเรียงลำดับ ตัวที่สองจะเริ่มก็ต่อเมื่อตัวแรกเสร็จ เมื่อการดำเนินการเป็นอิสระต่อกัน ให้เปิดใช้พร้อมกันด้วย Future.wait ข้อแลกเปลี่ยนที่ควรรู้คือ Future.wait เป็นแบบล้มเหลวเร็ว (fail-fast) นั่นคือ ข้อผิดพลาดแรกจะปฏิเสธทั้งหมด และผลลัพธ์ของ future ตัวอื่นจะสูญหายไป แม้ว่าพวกมันจะสำเร็จก็ตาม ถ้าคุณต้องการผลลัพธ์บางส่วน ให้จัดการข้อผิดพลาดภายใน future แต่ละตัว

DART
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');
}
การทำงานแบบขนานด้วย Future.wait และการแยกโครงสร้าง (destructuring) ของ Dart 3

ในฝั่ง UI นั้น FutureBuilder เชื่อม Future เข้ากับ build ผ่าน snapshot กับดักอันดับหนึ่งในการใช้งานจริงคือการสร้าง future โดยตรงใน build ทุกครั้งที่มีการรีบิลด์ (แค่ setState ง่าย ๆ จากตัวแม่ก็เพียงพอ) จะเป็นการยิงรีเควสต์ซ้ำ จงเก็บ future ไว้ใน initState และทดสอบข้อผิดพลาดก่อนข้อมูลเสมอ

DART
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!);
      },
    );
  }
}
future ถูกสร้างเพียงครั้งเดียว ใน initState

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. จริง ๆ แล้ว await ทำอะไรบนเธรดหลัก?
    • มันบล็อกเธรดจนกว่า Future จะเสร็จสิ้น
    • มันย้ายการคำนวณไปยังเธรดรองโดยอัตโนมัติ
    • มันหยุดฟังก์ชันปัจจุบันไว้ชั่วคราวและคืนการควบคุมกลับให้ลูปเหตุการณ์ UI จึงยังคงทำงานต่อไป
  2. เมื่อใช้ Future.wait([a(), b()]) จะเกิดอะไรขึ้นถ้า a() ล้มเหลว?
    • Future.wait ปฏิเสธพร้อมข้อผิดพลาดจาก a() และผลลัพธ์ของ b() สูญหายไป
    • Future.wait รอให้ b() เสร็จ แล้วคืนเฉพาะผลลัพธ์ของมันเพียงตัวเดียว
    • ข้อผิดพลาดจะถูกเพิกเฉยตราบใดที่ b() สำเร็จ
  3. ทำไมจึงควรหลีกเลี่ยงการเขียน future: fetchUser() โดยตรงใน build เมื่อใช้ FutureBuilder?
    • build ไม่สามารถมีการเรียกแบบอะซิงโครนัสได้ โค้ดจะไม่คอมไพล์
    • การรีบิลด์ทุกครั้งจะสร้าง Future ตัวใหม่ จึงเป็นการยิงรีเควสต์เครือข่ายซ้ำ
    • snapshot จะค้างอยู่ในสถานะรอ