ในตอนที่แล้วเรารู้จักกับ FutureBuilder ซึ่งเป็น async widgets
หนึ่งใน Flutter เนื้อหานี้เรามาดูต่อกับอีกหนึ่ง async widgets ที่มี
รูปแบบการใช้งานคล้ายๆ กัน แต่ใช้กับข้อมูลที่เป้น Stream ชื่อว่า
StreamBuilder widget ใครยังไม่ เข้าใจเกี่ยวกับข้อมูล stream
สามารถดูรายละเอียดเพิ่มเติมได้ที่ลิ้งค์ด้านล่างนี้
ข้อมูล Stream การสร้าง และใช้งาน Stream ในภาษา Dart เบื้องต้น http://niik.in/962
https://www.ninenik.com/content.php?arti_id=962 via @ninenik
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/961
*ควรอ่านเนื้อหาตอนที่แล้วเรื่อง FutureBuilder ก่อน
การใช้งาน FutureBuilder ที่เป็น Async widgets ใน Flutter http://niik.in/1036
https://www.ninenik.com/content.php?arti_id=1036 via @ninenik
จำลองข้อมูล Stream
เราจะจำลองข้อมูล stream สำหรับใช้ในการทดสอบ ข้อมูล stream เข้าใจอย่างง่าย
ก็คือข้อมูลที่มีการปล่อยออกมาอย่างต่อเนื่อง ในช่วงเวลาหนึ่ง หรือก็คือข้อมูล Future ที่มี
การส่งออกมาอย่างต่อเนื่อง ถ้าเปรียบเทียบกับข้อมูล Future ก็คือ
ข้อมูล future
เราเรียกใช้ - รอข้อมูล - ข้อมูลส่งกลับมา จบขั้นตอน
ข้อมูล stream
เราเรียกใช้ - รอข้อมูล - ข้อมูลส่งกลับมา - ใช้งานข้อมูล - ข้อมูลส่งกลับมา - ใช้งานข้อมูล
ข้อมูลลำดับต่อไปส่งกลับมาอีก เราใช้ข้อมูลต่อเนื่องไปเรื่อยๆ จนจบการ stream
// จำลองข้อมูล stream final Stream<int> _bids = (() async* { await Future<void>.delayed(const Duration(seconds: 3)); yield 1; await Future<void>.delayed(const Duration(seconds: 3)); yield 2; await Future<void>.delayed(const Duration(seconds: 3)); yield 3; await Future<void>.delayed(const Duration(seconds: 3)); })();
เราจำลองข้อมูลตัวเลขการประมูลตัวแปรชื่อ _bids เป็นข้อมูล Stream มีชนิดข้อมูลเป็น int ในการประมูล
แต่ละครั้ง เราสมมติเคาะราคาค่าประมูล 1 2 และ 3 ตามลำดับ โดยจำลองการหน่วงเวลาไว้ทุก 3 วินาที เสมือนว่า
ในทุกๆ 3 วินาทีในขณะกำลังประมูล ก็มีคนให้ราคาเพิ่มขึ้นเรื่อยๆ โดยคำสั่ง yield จะเป็นตัวทำหน้าที่ส่งค่าออก
มาจากข้อมูล stream การประมูลจะจบหรือสิ้นสุดหลังจากได้ค่าเป็น 3 แล้ว 3 วินาที
รูปแบบการใช้งาน StreamBuilder
StreamBuilder<int>( // ชนิดข้อมูล Stream stream: _bids, // ข้อมูล Stream builder: (BuildContext context, AsyncSnapshot<int> snapshot) { print("builder");// สำหรับทดสอบ print(snapshot.connectionState); // สำหรับทดสอบ List<Widget> children; // กำหนดตัวแปร สำหรับเก็บ widget ที่จะคืนค่ากลับ if (snapshot.hasError) { // กรณี error // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children // children = <Widget>[]; } else { // กรณีอื่นๆ // ตรวจสอบค่าสถานะการเชื่อมต่อ แล้วทำคำสั่งตามเงื่อนไขนั้นๆ switch (snapshot.connectionState) { case ConnectionState.none: // กรณีสถานะเป็น none // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children // children = <Widget>[]; break; case ConnectionState.waiting: // กรณีสถานะเป็น waiting // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children // children = <Widget>[]; break; case ConnectionState.active: // กรณีสถานะเป็น active // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children // children = <Widget>[]; break; case ConnectionState.done: // กรณีสถานะเป็น done // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children // children = <Widget>[]; break; } } // คืนค่าเป็นรูปแบบ widget ที่กำหนดจากตัวแปร children return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: children, ); }, ),
รูปแบบการกำหนดใช้งาน StreamBuilder แทบจะเหมือนกับการใช้งาน FutureBuilder มีการกำหนด
ชนิดข้อมูล Stream กำหนดข้อมูล Stream ในส่วนของ stream property และมีการใช้งานคำสั่ง builder
เพื่อสร้าง widget กลับออกไปแสดง โดยอาศัยข้อมูล snapshot แต่ใน StreamBuilder สิ่งที่เพิ่มเข้ามา
ก็คือสถานะของ ConnectionState.active ทั้งนี้ก็เพราะว่าข้อมูล stream มีการส่งออกมาต่อเนื่องตามลำดับ
จนกว่าจะสิ้นสุดข้อมูล ในขณะที่กรณีของข้อมูล future จะมีหลักๆ สองสถานะคือ waiting กับ done แต่ใน
ข้อมูล stream จะมี active เพิ่มเข้ามาด้วย และสถานะ active ก็ไม่ได้มีแค่ครั้งเดียว แต่จะมีไปเรื่อยๆ จนกว่า
จะสิ้นสุดการ stream ข้อมูล ดังนั้น ในคำสั่ง builder ที่เราใส่คำสั่ง print("builder") ไว้ ก็จะทำงานทุกครั้ง
ที่มีข้อมูลใหม่เข้ามา นั่นก็คือ widget ส่วนนี้จะถูกสร้างใหม่ตามข้อมูลไปเรื่อยๆ
การตรวจสอบเงื่อนไขของข้อมูล stream จึงใช้วิธีการดูค่า connectionState เป็นหลัก โดย
- waiting คือสถานะเริ่มต้นก่อนรับข้อมูล
- active คือสถานะได้รับและกำลังใช้งานข้อมูล
- done คือสิ้นสุดการ stream หรือสิ้นสุดข้อมูล stream แล้ว
- none สำหรับสถานะนี้มีทั้งใน stream และ future แต่จะเกิดขึ้นเมื่อมีการเปลี่ยนข้อมูลจากแหล่งใหม่
ตัวอย่างการกำหนดและใช้งานในไฟล์ home.dart
import 'package:flutter/material.dart'; class Home extends StatefulWidget { static const routeName = '/'; const Home({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _HomeState(); } } class _HomeState extends State<Home> { // จำลองข้อมูล stream final Stream<int> _bids = (() async* { await Future<void>.delayed(const Duration(seconds: 3)); yield 1; await Future<void>.delayed(const Duration(seconds: 3)); yield 2; // throw Exception('Intentional exception'); await Future<void>.delayed(const Duration(seconds: 3)); yield 3; await Future<void>.delayed(const Duration(seconds: 3)); })(); @override Widget build(BuildContext context) { print("build");// สำหรับทดสอบ return Scaffold( appBar: AppBar( title: Text('Home'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ StreamBuilder<int>( // ชนิดข้อมูล Stream stream: _bids, // ข้อมูล Stream builder: (BuildContext context, AsyncSnapshot<int> snapshot) { print("builder");// สำหรับทดสอบ print(snapshot.connectionState); // สำหรับทดสอบ List<Widget> children; // กำหนดตัวแปร สำหรับเก็บ widget ที่จะคืนค่ากลับ if (snapshot.hasError) { // กรณี error children = <Widget>[ const Icon( Icons.error_outline, color: Colors.red, size: 60, ), Padding( padding: const EdgeInsets.only(top: 16), child: Text('Error: ${snapshot.error}'), ), Padding( padding: const EdgeInsets.only(top: 8), child: Text('Stack trace: ${snapshot.stackTrace}'), ), ]; } else { // กรณีอื่นๆ // ตรวจสอบค่าสถานะการเชื่อมต่อ แล้วทำคำสั่งตามเงื่อนไขนั้นๆ switch (snapshot.connectionState) { case ConnectionState.none: // กรณีสถานะเป็น none // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children children = const <Widget>[ Icon( Icons.info, color: Colors.blue, size: 60, ), Padding( padding: EdgeInsets.only(top: 16), child: Text('Select a lot'), ) ]; break; case ConnectionState.waiting: // กรณีสถานะเป็น waiting // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children children = const <Widget>[ SizedBox( child: CircularProgressIndicator(), width: 60, height: 60, ), Padding( padding: EdgeInsets.only(top: 16), child: Text('Awaiting bids...'), ) ]; break; case ConnectionState.active: // กรณีสถานะเป็น active // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children children = <Widget>[ const Icon( Icons.check_circle_outline, color: Colors.green, size: 60, ), Padding( padding: const EdgeInsets.only(top: 16), child: Text('\$${snapshot.data}'), ) ]; break; case ConnectionState.done: // กรณีสถานะเป็น done // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children children = <Widget>[ const Icon( Icons.info, color: Colors.blue, size: 60, ), Padding( padding: const EdgeInsets.only(top: 16), child: Text('\$${snapshot.data} (closed)'), ) ]; break; } } // คืนค่าเป็นรูปแบบ widget ที่กำหนดจากตัวแปร children return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: children, ); }, ), ], ) ), ); } }
ผลลัพธ์ที่ได้
เราเริ่มต้นอยู่ที่หน้า profile จากนั้นกดมาที่หน้า home ที่มีการใช้งาน StreamBuilder เมื่อเข้ามาก็จะเกิด
waiting ขึ้นเริ่มการ stream ข้อมูล พอมีข้อมูล 1 2 และ 3 ค่อยๆ ถูกส่งมา ก็จะเข้าสู่ active จำนวนเงินที่ทำ
การประมูลก็เพิ่มขึ้นตามค่า ตัว builder ก็ทำการสร้าง widget ใหม่ทุกครั้งที่ข้อมูลในสถานะ active กำลังทำงาน
หลังจากได้ค่าจำนวนเลข 3 แล้ว 3 วินาที ก็ไม่มีการส่งข้อมูลมาอีก สิ้นสุดการประมูล เข้าสถานะ done ข้อมูลก็จะ
แสดง เคาะที่ราคา $3 ขึ้นข้อความปิดการประมูล
เช่นกัน เราสามารถสร้างเป็นฟังก์ชั่นแล้วเรียกใช้งานดังนี้ได้
class _HomeState extends State<Home> { @override Widget build(BuildContext context) { print("build");// สำหรับทดสอบ return Scaffold( appBar: AppBar( title: Text('Home'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ StreamBuilder<int>( // ชนิดข้อมูล Stream stream: _bidStream(), // ข้อมูล Stream builder: (BuildContext context, AsyncSnapshot<int> snapshot) { print("builder");// สำหรับทดสอบ print(snapshot.connectionState); // สำหรับทดสอบ List<Widget> children; // กำหนดตัวแปร สำหรับเก็บ widget ที่จะคืนค่ากลับ if (snapshot.hasError) { // กรณี error children = <Widget>[ const Icon( Icons.error_outline, color: Colors.red, size: 60, ), Padding( padding: const EdgeInsets.only(top: 16), child: Text('Error: ${snapshot.error}'), ), Padding( padding: const EdgeInsets.only(top: 8), child: Text('Stack trace: ${snapshot.stackTrace}'), ), ]; } else { // กรณีอื่นๆ // ตรวจสอบค่าสถานะการเชื่อมต่อ แล้วทำคำสั่งตามเงื่อนไขนั้นๆ switch (snapshot.connectionState) { case ConnectionState.none: // กรณีสถานะเป็น none // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children children = const <Widget>[ Icon( Icons.info, color: Colors.blue, size: 60, ), Padding( padding: EdgeInsets.only(top: 16), child: Text('Select a lot'), ) ]; break; case ConnectionState.waiting: // กรณีสถานะเป็น waiting // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children children = const <Widget>[ SizedBox( child: CircularProgressIndicator(), width: 60, height: 60, ), Padding( padding: EdgeInsets.only(top: 16), child: Text('Awaiting bids...'), ) ]; break; case ConnectionState.active: // กรณีสถานะเป็น active // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children children = <Widget>[ const Icon( Icons.check_circle_outline, color: Colors.green, size: 60, ), Padding( padding: const EdgeInsets.only(top: 16), child: Text('\$${snapshot.data}'), ) ]; break; case ConnectionState.done: // กรณีสถานะเป็น done // สร้าง widget สำหรับกรณีนี้ไว้ในตัวแปร children children = <Widget>[ const Icon( Icons.info, color: Colors.blue, size: 60, ), Padding( padding: const EdgeInsets.only(top: 16), child: Text('\$${snapshot.data} (closed)'), ) ]; break; } } // คืนค่าเป็นรูปแบบ widget ที่กำหนดจากตัวแปร children return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: children, ); }, ), ], ) ), ); } } // จำลองใช้เป็นแบบฟังก์ชั่น ให้เสมือนดึงข้อมูลจาก server Stream<int> _bidStream() async* { await Future<void>.delayed(const Duration(seconds: 3)); yield 1; await Future<void>.delayed(const Duration(seconds: 3)); yield 2; // throw Exception('Intentional exception'); await Future<void>.delayed(const Duration(seconds: 3)); yield 3; await Future<void>.delayed(const Duration(seconds: 3)); }
เราสร้างฟังก์ชั่นชื่อว่า _bidStream() แยกออกมา แล้วเรียกใช้ในส่วนของการกำหนดค่า stream property
ของ StreamBuilder widget ผลลัพธ์ที่ได้ก็จะเหมือนกับวิธีแรก สังเกตเพิ่มเติมในส่วนของการกำหนด async
ให้กับกฟังก์ชั่น สำหรับข้อมูล stream เราจะใช้เป็น async*
หวังว่าข้อมูลในเนื้อหานี้จะเป็นประโยชน์สำหรับทำความเข้าใจ และนำไปปรับประยุกต์ใช้งานต่อไป เนื้อหา
ตอนหน้าจะเป็นอะไร รอติดตาม