การใช้งาน FutureBuilder และ StreamBuilder อย่างละเอียด
ในโลกของการพัฒนาแอปพลิเคชัน ข้อมูลมักไม่ได้มาทันทีทันใดเสมอไป บ่อยครั้งที่เราต้องรอข้อมูลจากแหล่งภายนอก เช่น API, ฐานข้อมูล หรือแม้แต่การคำนวณที่ใช้เวลานาน Flutter เตรียมเครื่องมือที่ทรงพลังไว้ให้เราจัดการกับข้อมูลที่ไม่พร้อมใช้งานทันที นั่นคือ FutureBuilder และ StreamBuilder บทความนี้จะเจาะลึกการใช้งานเครื่องมือทั้งสองนี้อย่างละเอียด
ทำความเข้าใจข้อมูล Async
ก่อนที่เราจะไปดู FutureBuilder และ StreamBuilder มาทำความเข้าใจกันก่อนว่า Async คืออะไร Async ย่อมาจาก Asynchronous ซึ่งหมายถึง การทำงานที่ไม่จำเป็นต้องรอให้เสร็จสิ้นก่อนที่จะเริ่มทำงานอื่นได้ ตัวอย่างเช่น การเรียก API เพื่อดึงข้อมูล เราไม่จำเป็นต้องรอให้ API ตอบกลับมาก่อนถึงจะสามารถทำงานอื่นได้ เราสามารถเริ่มทำงานอื่นไปก่อน แล้วค่อยกลับมาจัดการข้อมูลเมื่อ API ตอบกลับมา
ใน Flutter, Async operation มักจะ return เป็น Future หรือ Stream
- Future: เป็นตัวแทนของค่าที่อาจจะยังไม่พร้อมใช้งานในตอนนี้ แต่จะพร้อมใช้งานในอนาคต (คล้ายๆ สัญญาว่าจะให้ค่าในอนาคต)
- Stream: เป็นลำดับของข้อมูลที่อาจจะยังไม่พร้อมใช้งานในตอนนี้ แต่จะทยอยพร้อมใช้งานในอนาคต (คล้ายๆ ท่อน้ำที่ข้อมูลไหลเข้ามาเรื่อยๆ)
FutureBuilder: จัดการข้อมูลจาก Future
FutureBuilder เป็น Widget ที่ช่วยให้เราสร้าง UI ตามสถานะของ Future ได้ง่ายขึ้น มันจะ rebuild UI ทุกครั้งที่สถานะของ Future เปลี่ยนไป
ส่วนประกอบหลักของ FutureBuilder
- future:
Futureที่เราต้องการติดตามสถานะ - builder: Function ที่รับ
BuildContextและAsyncSnapshotเป็น argument แล้ว return Widget
AsyncSnapshot คืออะไร?
AsyncSnapshot เป็น Object ที่เก็บข้อมูลเกี่ยวกับสถานะของ Future มันมี properties ที่สำคัญดังนี้:
- connectionState: สถานะของการเชื่อมต่อ (กำลังรอ, กำลังโหลด, เสร็จสิ้น, หรือมีข้อผิดพลาด)
- data: ข้อมูลที่
Futurereturn (ถ้ามี) - error: ข้อผิดพลาดที่เกิดขึ้น (ถ้ามี)
ConnectionState
connectionState เป็น Enum ที่บอกสถานะของการเชื่อมต่อ มีค่าที่เป็นไปได้ดังนี้:
- ConnectionState.none: ยังไม่มี
Futureเริ่มทำงาน - ConnectionState.waiting:
Futureกำลังทำงาน - ConnectionState.active:
Streamกำลังส่งข้อมูล - ConnectionState.done:
FutureหรือStreamทำงานเสร็จสิ้น
ตัวอย่างการใช้งาน FutureBuilder
สมมติว่าเรามี function ที่ชื่อว่า fetchData() ซึ่ง return Future<String> ที่จำลองการดึงข้อมูลจาก API:
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2)); // จำลองการรอ 2 วินาที
return 'Hello from the Future!';
}
เราสามารถใช้ FutureBuilder เพื่อแสดงผล UI ตามสถานะของ fetchData() ได้ดังนี้:
class FutureBuilderExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('FutureBuilder Example')),
body: Center(
child: FutureBuilder<String>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // แสดง Loading Indicator
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}'); // แสดงข้อผิดพลาด
} else if (snapshot.hasData) {
return Text('Data: ${snapshot.data}'); // แสดงข้อมูล
} else {
return Text('No data available'); // กรณีอื่นๆ
}
},
),
),
);
}
}
คำอธิบาย:
- เราส่ง
fetchData()ให้กับ propertyfutureของFutureBuilder - ใน
builderfunction เราตรวจสอบsnapshot.connectionStateเพื่อดูสถานะของการเชื่อมต่อ - ถ้า
connectionStateเป็นConnectionState.waitingเราจะแสดงCircularProgressIndicator - ถ้ามีข้อผิดพลาด เราจะแสดงข้อความ error
- ถ้ามีข้อมูล เราจะแสดงข้อมูล
Error Handling
FutureBuilder มีกลไกในการจัดการ error ในกรณีที่ Future throw exception เราสามารถเข้าถึง error ได้จาก snapshot.error
StreamBuilder: จัดการข้อมูลจาก Stream
StreamBuilder คล้ายกับ FutureBuilder แต่ใช้สำหรับจัดการข้อมูลจาก Stream แทนที่จะเป็น Future มันจะ rebuild UI ทุกครั้งที่ Stream ส่งข้อมูลใหม่
ส่วนประกอบหลักของ StreamBuilder
- stream:
Streamที่เราต้องการติดตาม - initialData: ข้อมูลเริ่มต้นที่จะแสดงก่อนที่
Streamจะส่งข้อมูล (optional) - builder: Function ที่รับ
BuildContextและAsyncSnapshotเป็น argument แล้ว return Widget
ตัวอย่างการใช้งาน StreamBuilder
สมมติว่าเรามี function ที่ชื่อว่า numberStream() ซึ่ง return Stream<int> ที่ส่งตัวเลขทุกๆ 1 วินาที:
Stream<int> numberStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
เราสามารถใช้ StreamBuilder เพื่อแสดงผลตัวเลขที่ Stream ส่งมาได้ดังนี้:
class StreamBuilderExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('StreamBuilder Example')),
body: Center(
child: StreamBuilder<int>(
stream: numberStream(),
initialData: 0, // ข้อมูลเริ่มต้น
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
return Text('Number: ${snapshot.data}');
} else {
return Text('Stream completed');
}
},
),
),
);
}
}
คำอธิบาย:
- เราส่ง
numberStream()ให้กับ propertystreamของStreamBuilder - เรากำหนด
initialDataเป็น 0 เพื่อแสดงผลก่อนที่Streamจะส่งข้อมูล - ใน
builderfunction เราตรวจสอบsnapshot.connectionStateเพื่อดูสถานะของStream - ถ้า
connectionStateเป็นConnectionState.waitingเราจะแสดงCircularProgressIndicator - ถ้ามีข้อผิดพลาด เราจะแสดงข้อความ error
- ถ้ามีข้อมูล เราจะแสดงข้อมูล
- เมื่อ
Streamทำงานเสร็จสิ้น (ส่งข้อมูลครบ)connectionStateจะเป็นConnectionState.done
การยกเลิก Stream
เมื่อ Stream ไม่จำเป็นต้องใช้งานอีกต่อไป เราควรยกเลิก StreamSubscription เพื่อป้องกัน memory leak เราสามารถทำได้โดยการใช้ dispose() method ใน StatefulWidget:
class StreamBuilderExample extends StatefulWidget {
@override
_StreamBuilderExampleState createState() => _StreamBuilderExampleState();
}
class _StreamBuilderExampleState extends State<StreamBuilderExample> {
StreamSubscription<int>? _streamSubscription;
@override
void initState() {
super.initState();
_streamSubscription = numberStream().listen((data) {
// ทำอะไรบางอย่างกับข้อมูล
});
}
@override
void dispose() {
_streamSubscription?.cancel(); // ยกเลิก StreamSubscription
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('StreamBuilder Example')),
body: Center(
child: StreamBuilder<int>(
stream: numberStream(), // ใช้ numberStream() โดยตรงใน StreamBuilder
initialData: 0,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
return Text('Number: ${snapshot.data}');
} else {
return Text('Stream completed');
}
},
),
),
);
}
}
ข้อควรระวัง: ในตัวอย่างนี้ เราใช้ numberStream() ทั้งใน initState (เพื่อสร้าง StreamSubscription) และใน StreamBuilder ซึ่งไม่จำเป็นและอาจทำให้เกิด side effects ได้ ทางที่ดีควรใช้ numberStream() ใน StreamBuilder เท่านั้น และยกเลิก StreamSubscription ที่สร้างขึ้นภายนอก (ถ้ามี) ใน dispose()
สรุป
FutureBuilder และ StreamBuilder เป็นเครื่องมือที่สำคัญในการจัดการข้อมูล Async ใน Flutter การเข้าใจการทำงานของ ConnectionState และ AsyncSnapshot จะช่วยให้เราสร้าง UI ที่ตอบสนองต่อสถานะของข้อมูลได้อย่างถูกต้องและมีประสิทธิภาพ
ในบทความถัดไป เราจะเรียนรู้เกี่ยวกับการสร้าง Animation ขั้นสูงด้วย Implicit และ Explicit Animations