ข้อมูล Stream การสร้าง และใช้งาน Stream ในภาษา Dart เบื้องต้น

เขียนเมื่อ 4 ปีก่อน โดย Ninenik Narkdee
using stream dart future stream

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ using stream dart future stream

ดูแล้ว 9,756 ครั้ง


ในบทความเกี่ยวกับการใช้งาน Asynchronous Programming 
เราได้รู้จักเกี่ยวกับการใช้งาน Future ไปแล้ว ซึ่งเป็นรูปแบบข้อมูล
async ที่เมื่อมีการเรียกใช้งาน จะมีเรื่องของเวลาที่ต้องรอเข้ามาเกี่ยวข้อง
และเมื่อทำการเสร็จสิ้น ก็จะส่งผลลัพธ์สุดท้ายกลับมา เพื่อใช้งานต่อ
หรืออีกความเข้าใจหนึ่งก็คือ ข้อมูลที่ไม่ได้ประมวณผลและส่งกลับผลลัพธ์ได้ทันที 
แต่ต้องรอผลเป็นระยะเวลาหนึ่ง และเมื่อทำงานเสร็จภายในเวลานั้นๆ แล้ว ก็จะ
ส่งค่ากลับมาเป็น Future ที่มีผลลัพธ์สุดท้ายกลับมาด้วย โดย Future จะบอก
เราให้ทราบว่า ข้อมูลผลลัพธ์สุดท้ายพร้อมใช้งานต่อแล้ว หรือกรณี error ก็จะบอก
เราว่ามีข้อมูลผิดพลาดเกิดขึ้น แล้วแต่กรณี 
    สามารถทบทวนเนื้อหาเกี่ยวกับ Future ได้ที่บทความด้านล่าง
    การใช้งาน Asynchronous Programming ในภาษา Dart เบื้องต้น http://niik.in/949 
 
สามารถทดสอบการเขียนโปรแกรมผ่านเว็บไซต์ DartPad
 
 

ข้อมูลปะรเภท Stream คืออะไร

    Stream คือข้อมูลในรูปแบบ async ที่มีการรับส่งวนลูปต่อเนื่อง ตามเงื่อนไข หรือ event ที่เกิดขึ้น เข้าใจอย่างง่าย
คือข้อมูลที่มีเรื่องของเวลาที่ต้องรอเข้ามาเกี่ยวข้อง และเมื่อได้รับข้อมูลชุดแรกแล้ว ก็ทำการเช็คเงื่อนไขหรือ event
ที่เกิดขึ้น ว่าข้อมูลมาครบหรือยัง ยังมีข้อมูลเหลืออยู่หรือไม่ หากยังมีข้อมูลส่งมา ก็เข้าลูปเดิม เช็คข้อมูล จัดเก็บหรือ
ใช้งาน.. วนลูปไปจนสิ้นสุด เมื่อเกิด end หรือ complete หรือ error แล้วแต่เงื่อนไข
    ในบางรูปแบบข้อมูล Stream เช่น การ ฟังวิทยุออนไลน์ ก็อาจจะไม่มีการสิ้นสุดของข้อมูล นั่นคืออยู่ในลูปของ Stream 
ไปเรื่อยๆ จนกว่าผู้ใช้จะยกเลิกการใช้งาน Stream ถึงจะสิ้นสุดหรือหลุดออกจาก ลำดับของ event ที่วนลูปต่อเนื่อง
    เราอาจจะเข้าใจได้ว่า Stream ก็คือลูปของข้อมูล Future หรือ ข้อมูล Future ที่เกิดอย่างต่อเนื่อง เป็นลำดับๆ ไปเรื่อยๆ นั่นเอง
 

    ปรเภทของข้อมูล Stream

    Single subscription streams
    เป็นรูปแบบ Stream ที่มีการใช้งานเป็นส่วนใหญ่ ตัวอย่างเช่น เวลาเข้าดูเนื้อหาเว็บไซต์ต่างๆ หรือการเปิดอ่านข้อมูลไฟล์ออนไลน์อย่าง
pdf ไฟล์ เหล่านี้ เป็นต้น โดยคำว่า Single subscription ก็คือ เมื่อเราทำการ request หรือร้องขอข้อมูลนั้นแล้ว เราก็ต้องรอรับข้อมูล
นั้นให้เสร็จจนครบ สมมติเช่น เราเปิดไฟล์ pdf ผ่านบราวเซอร์ ข้อมูลของไฟล์ pdf ก็จะถูกแบ่งย่อย ส่งมาหให้เราที่ละส่วน อย่างถ้ามี
10 ส่วน ก็จะเกิดการส่งทีละส่วนไปเรื่อยๆ จากส่วนแรก ไปจนถึงส่วนที่ 10 และเมื่อเราได้ครบทั้ง 10 ส่วนเรียบร้อยแล้ว ข้อมูลก็จะแสดง
รายละเอียดถูกต้องครบถ้วน  แต่ถ้าเกิดว่า ขณะที่ข้อมูลกำลังส่งมาได้แค่ 5 ส่วน แล้วเกิดเน็ตหลุด หรือเรายกเลิกการเปิดไฟล์ ข้อมูลก็
จะมาไม่ครบ ส่วนข้อมูล 5 ส่วนที่โหลดมาก่อนหน้า ก็จะใช้งานไม่ได้ เป็น cache แล้วสุดท้ายก็ถูกลบหายไป ถ้าเกิดเราอยากเปิดไฟล์
นั้นใหม่อีกครั้ง นั่นคือเราก็ต้องไปเริ่มต้นขอรับข้อมูลตั้งแต่ส่วนที่ 1 ใหม่แต่ต้น
 
    Broadcast streams
    เป็นรูปแบบ Stream ที่เราสามารถรับข้อมูล ณ จุดเวลาใดๆ ก็ได้ขณะที่ข้อมูลกำลังถูกกระจายหรือส่งออกมาใช้งานอยู่ ถ้ามองภาพ
ลักษณะของ การรอรับข้อมูลของ Broadcast ก็เช่น การใช้รอรับคำสั่งจาก event ของ mouse ไม่ว่าเราจะ คลิก ดับเบิลคลิก หรือเลื่อน
scroll ก็สามารถดักจับ event นั้นได้ตลอดเวลา หรือ อีกตัวอย่าง การฟังวิทยุออนไลน์ หรือการดู Live สด ในขณะที่ข้อมูลมีการกระจาย
หรือ Broadcast เราสามารถเปิดเข้าไปฟังหรือดูได้ ในช่วงเวลาไหนก็ได้ ข้อมูลที่ได้รับก็จะเป็นข้อมูล ณ ขณะนั้น แล้วก็เข้าไปในลูปของ
การรับส่งข้อมูลแบบไม่สิ้นสุด ตามรูปแบบของข้อมูล Stream จนว่า การ Broadcast นั้นจะจบลงหรือหลุด Live  นอกจากนั้นในขณะที่
ยังมีการ Live อยู่ เราสามารถที่จะปิด และกลับเข้ามาดูใหม่ได้ โดยมาเริ่มที่ข้อมูล ณ ปัจจุบัน ในขณะรับส่งข้อมูลอยู่ เป็นต้น
 
 

การสร้างข้อมูล Stream ในภาษา Dart

    เราสามารถสร้างข้อมูล Stream ด้วยวิธีดังต่อไปนี้
  • สร้างข้อมูล Stream จากข้อมูลเดิมที่มีอยู่แล้ว
  • สร้าง Stream ใหม่โดยใช้ async* ฟังก์ชั่น
  • สร้าง Stream ใหม่โดยใช้ StreamController
 
 

    สร้างข้อมูล Stream จากข้อมูลเดิมที่มีอยู่แล้ว

    สมมติเรามีข้อมูล Stream เดิมอยู่แล้ว และต้องการข้อมูล Stream ใหม่จากของเดิม เช่น ชนิดข้อมูล Stream ที่อยู่ใระดับ byte หรือ
Binary Data แล้ว เราต้องการแปลงให้เป็นข้อมูล Stream ที่เป็นข้อความ ที่ถูกถอดรหัสเป็น UTF-8 รูปแบบอย่างที่เรามีการรับส่งข้อมูล
ในฟอร์มผ่านเว็บไซต์ เป็นต้น
    มาลองจำลองสร้าง Stream ข้อมูลที่เป็น String เสมือนว่าเป็นข้อมูลที่ได้จากการเปิดไฟล์ txt ดังนี้
 
Stream<String> genStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    yield 'This is Line $i\n';
  }
}
    จะเห็นว่าเราสร้าง Stream โดยใช้ async* ฟังก์ชั่น กำหนดชื่อเป็น genStream() รับค่าจำนวนบรรทัดข้อความที่ต้องการสร้าง แล้ว
วนลูปสร้าง ข้อมูล Stream ออกมาโดยใช้คำสั่ง yield นั่นคือทำการ return ข้อมูล Stream ที่เป็น ข้อความหรือ String ออกมา
ตามรูปแบบ Stream<String> เราก็จะได้ค่า Stream ข้อความในรูปแบบ ดังนี้
 
This is Line 1\n
This is Line 2\n
This is Line 3\n
This is Line 4\n
This is Line 5\n
 
    ต่อไปเราจะสร้างข้อมูล Stream ข้อความใหม่ จาก Stream ข้อความเดิมข้างต้น โดยมีเงื่อนไขว่า เราจะจัดการข้อมูลแต่ละส่วน
ของ Stream ข้อความเดิม โดยเอาเฉพาะข้อความก่อนขึ้นบรรทัดใหม่ มาสร้างเป็น Stream ข้อความใหม่
 
/// แยกข้อมูล stream ของข้อความ ออกเป็นบรรทัดๆ
/// ข้อมูลจาก Stream Source ถูกแบ่งเป็นส่วนๆ ย่อย
Stream<String> lines(Stream<String> source) async* {
  // สร้างตัวแปร เก็บข้อมูลบรรทัด บางส่วน จากส่วนของข้อมูล
  var partial = '';
  // รอข้อมูลแต่ละส่วน ส่วนไหนพร้อม ก็นำไปใช้งานต่อ
  await for (var chunk in source) { // วนลูปใช้งาน ส่วนที่พร้อม
    var lines = chunk.split('\n'); // แยกเป็นข้อมูล array จากการขึ้นบรรทัดใหม่
    // ถ้าข้อมูลมี \n จะได้ lines เป็น ['This is Line 1','']
    // แต่ถ้าไม่มี \n จะได้ lines เป็น ['This is Line 1']
    lines[0] = partial + lines[0]; // เก็บข้อมูลบรรทัดแรก
    partial = lines.removeLast(); // เก็บค่าบางส่วน
    // ถ้าข้อมูลมี \n partial จะเก็บค่า lines[1] ซึ่งเป็นค่า ว่าง
    // แต่ถ้าไม่มี \n partial จะเก็บค่าข้อความ lines[0]
    for (var line in lines) { // วนลูปส่งออกข้อมูล stream จากในลูป
      yield line; // ส่งออก output stream คล้ายคำสั่ง return
    }
  }
  // กรณีไม่มี \n จะทำให้ไม่มีการคืนค่าด้วย yield จากในลูป เพราะ lines ไม่มีข้อมูล
  // ข้อมูลถูกเก็บไว้ใน partial เราก็เช็คค่า partial ถ้ามีก็ให้ส่งออกไป เป็นข้อมูลทั้งหมด
  // จากค่า partial แทน
  if (partial.isNotEmpty) yield partial;
}
    เรากำหนดฟังก์ชั่น lines() เป็น async* ฟังก์ชั่น คืนค่าเป็น Stream<String> หรือ Stream ข้อความใหม่ สังเกตตรง parameter 
มีการกำหนด parameter ชื่อว่า source เป็น Stream<String> นั่นก็คือเราสร้างฟังก์ชั่น สำหรับสร้าง Stream จาก Stream เดิมนั้นเอง
อธิบายการทำงานคร่าวๆ สมมติส่วนของข้อมูลชุดแรก หรือ chunk เป็นข้อความของบรรทัดแรก
    "This is Line 1\n"
 
    เมื่อแยกข้อความด้วย \n จะได้ array ข้อมูลสองค่า คือ lines[0] ข้อความก่อน \n และ lines[1] ข้อความหลัง \n
    จะได้ lines[0] เท่ากับ "This is Line 1" และ lines[1] เท่ากับ "" หรือค่าว่าง
    ต่อไปเราเก็บข้อมูล lines[0] โดยถ้ามีค่าจากตัวแปร partial อยู่ก่อนหน้า ก็ให้เอาค่า lines[0] ไปต่อหลัง partial แต่ค่าแรก
    ตัวแปร partial ยังเป็นค่าว่าง จากนั้นเราก็เริ่มเก็บค่า partial จากคำสั่ง lines.removeLast() หรือก็คือค่าของตัวสุดท้ายของ
    ตัวแปร lines ที่เป็น อาเรย์ หรือ List ในภาษา Dart  คำสั่งนี้ทำให้ ตัวแปร lines เหลือแค่ อาเรย์เดียวคือ lines[0] ซึ่งเป็นข้อ
    ความของบรรทัดแรก ส่วนตัวแปร partial ก็ยังเป็นค่าว่างเหมือนเดิม จากนั้น เราก็วนลูปค่าของตัวแปร lines ส่งออกเป็นข้อมูล
    stream นั่นก็คือข้อความของบรรทัดแรก ถูกส่งออกไปเป็น stream ข้อความ "This is Line 1"
    คำสั่ง lines ก็จะทำการวนลูปข้อมูลแต่ละส่วนส่งออกเป็น Stream ข้อความไปจนครบ
    
    แต่ถ้ากรณีข้อความแต่ละ chunk ไม่มี \n นั้นคือ ตัวแปรอาเรย์ lines จะมีแค่ค่าเดียวแต่แรก นั่นคือ lines[0] เท่ากับ "This is Line 1"
    พอมาขั้นตอนการเก็บค่า partial มีการใช้งานคำสั่ง lines.removeLast() เอาค่าสุดท้ายมาเก็บในตัวแปร partial นั่นก็คือ ตัวแปร 
    partial เก็บค่าข้อความแรกแทน และตัวแปร lines ก็เป็นค่าว่าง ไม่มีข้อมูลเพราะข้อมูลถูกลบออกไป ตัวแปร partial ก็จะสะสมข้อมูล
    จาก chunk ไปเรื่อยๆ และเมื่อได้ข้อมูลสุดท้าย ก็จะถูกส่งออกเป็น output stream ในคำสั่ง if ล่างสุด ที่มีการตรวจสอบค่า partial
    ถ้าไม่ใช่ค่าว่าง ก็ให้ส่งออกค่านี้เป็น ข้อความ Stream แทน
 
    ข้อมูลที่ส่งออกจาก ลูป stream จากคำสั่ง yield line; 
    กับข้อมูลที่ส่งออกจาก คำสั่ง yield partial; จะเป็นคนละค่ากัน  โดยข้อมูลจาก partial จะเป็นข้อความเดียว ส่งออกไปครั้งเดียว
    ในขณะที่ข้อความจาก line จะส่งออกเป็น stream ข้อความแต่ละบรรทัด เพื่อให้เห็นภาพ ดูวิธีการเรียกใช้งานต่อ
 
    เราจะจำลองเรียกใช้งาน ข้อมูล Stream ที่เราสร้างโดยใช้ Future ดังนี้
 
Future<String> concatStream(Stream<String> stream) async {
  var concatStr = '';
  await for (var value in stream) {
    concatStr += "\"$value\" |";
  }
  return concatStr;
}
    เราสร้างฟังก์ชั่นชื่อ concatStream() เพื่อทำการต่อหรือรวมข้อความจาก Stream แต่ละส่วนเป็นข้อความเดียว โดยเรียกใช้คำสั่ง
"await for" เพื่อวนลูปทำข้อมูลของ Stream คล้ายกับการใช้งานคำสั่ง for loop
 
"This is Line 1" |"This is Line 2" |"This is Line 3" |"This is Line 4" |"This is Line 5" |
 
    จะเห็นว่าเราครอบชุดข้อมูลแต่ละชุดด้วย " (dobule qoute) และปิดท้ายด้ว | โดยคำสั่ง "await for" วนลูปชุดข้อมูล แล้วเก็บค่า
เพิ่มต่อเข้าไปในตัวแปร concatStr และเมื่อ "await for" วนลูปข้อมูลจนครบ ก็คือค่าตัวแปร concatStr ซึ่งเป็นผลลัพธ์สุดท้ายไปใช้งาน
 
    ทีนี้เราลองมาทดสอบ กรณีที่เราพูดถึงว่า ถ้าไม่มี \n ค่าตัวแปรที่ถูกส่งออกมาเป็น Stream ก็จะเป็นข้อความจากตัวแปร partial ถ้า
เราเรียกใช้งานใน Future ผลลัพธ์ที่ได้ จะเป็นดังนี้
 
"This is Line 1This is Line 2This is Line 3This is Line 4This is Line 5" |
 
    เนื่องจากตัวแปร partial เป็นข้อมูลที่ต่อๆ กันชุดเดียว และส่งออกเป็น output เป็น Stream ค่าสุดท้าย คำสั่ง "await for" จึงเหมือน
วนลูปข้อมูลเพียงครั้งเดียว ค่าที่ได้จึงเป็นตามที่แสดงด้านบน
 
    ดูโค้ดการใช้งานแบบเต็ม
 
void main() async {
  // สร้างข้อมูล Stream โดยใช้ async* ฟังก์ชั่น
  var stream = genStream(5);
  // สร้างข้อมูล Stream ดยใช้ Stream เดิม
  var newStream = lines(stream); 
  // เรียกใช้งาน Stream
  var output = await concatStream(newStream);
  print(output);
}

Stream<String> genStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    yield 'This is Line $i\n';
  }
}

Stream<String> lines(Stream<String> source) async* {
  var partial = '';
  await for (var chunk in source) { 
    var lines = chunk.split('\n'); 
    lines[0] = partial + lines[0]; 
    partial = lines.removeLast(); 
    for (var line in lines) {  
      yield line; 
    }
  }
  if (partial.isNotEmpty) yield partial;
}

Future<String> concatStream(Stream<String> stream) async {
  var concatStr = '';
  await for (var value in stream) {
    concatStr += "\"$value\" |";
  }
  return concatStr;
}
    นอกจากวิธีข้างต้นในการแปลงข้อมูล Stream เรายังสามารถใช้ method ที่ใช้งานกับ อาเรย์หรือ List เช่น map(), where(), 
expand() และ take() มาช่วนในการแปลงได้ 
    ตัวอย่างเช่น เรามีข้อมูล Stream ที่ชื่อ counterStream เป็น Stream ที่สร้างมาจากการใช้งาน named constructor ของ Stream
ที่มีชื่อว่า periodic ซึ่งจะทำการปล่อย events ทุกช่วงๆ จังหวะเวลาที่กำหนด โดยค่า argument ของ callback จะเป็นตัวเลขที่เริ่มต้น
ด้วย 0 และเพิ่มขึ้นเรื่อยๆ ทีละ 1 ในทุกๆ ช่วง events ที่ปล่อยออกมา
 
var counterStream =
    Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(15);
    ตัวแปร counterStream จะ Stream ข้อมูลที่เป็น ตัวเลข ทั้งหมด 15 ครั้ง (จากคำสั่ง take(15)) โดยจะ คืนค่าข้อมูลจากตัวแปร x
ในทุกๆ 1 วินาที ตามที่กำหนดใน parameter ของ periodic named contructor โดย x คือ argument ของ callback ที่เริ่มต้นจาก 0
เราสามารถใช้ await for สำหรับวนลูปแสดงข้อมูลของ Stream ได้ หรือจะใช้ method ของ List หรือ อาเรย์ อย่าง forEach() ก็ได้
 
void main() async {
  
  var counterStream =
    Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(15);  
  await for(var data in counterStream){
    print(data);
  }

}
    หรือใช้เป็น
 
void main() async {
  
  var counterStream =
    Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(15);  
  counterStream.forEach(print);

}
    ทั้งสองวิธีได้ผลลัพธ์เหมือนกัน
    ตัวอย่างการใช้ map() แปลงข้อมูลใหม่
 
void main() async {
  
  var counterStream =
    Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(15);  
  counterStream = counterStream.map((x) => x*2 );
  counterStream.forEach(print); // ค่าจะแสดงทุก 1 วินาทีเป้น 0.. 2.. 4 ...จะถึง 28
  
}
    นอกจากคำสั่ง map() เรายังใช้คำสั่งอื่นๆ เพิ่มเติมได้ เช่น
 
void main() async {
  
  var counterStream =
    Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(15);  
  counterStream = counterStream
    .where((int x) => x.isEven) // เช็คที่ event ค่า x เป็นเลขคู่
    .expand((var x) => [x, x]) // เพิ่มหรือทำซ้ำ event นี้
    .take(5); // หยุดที่ 5 รายการแรก
  counterStream.forEach(print); ค่าจแสดง แค่ 5 รายการ ใน 3 วินาที เพราะ ที่วินาทีแรก จะแสดง
    // 0 และ 0 นับเป็น 2 events ที่แสดงซ้ำเพราะเราใช้ expand() และวินาทีที่สองแสดงเป็น 2 และ 2
    // วินนาทีที่ 3 แสดง 4 แค่ค่าเดียว เพราะ ครบ 5 จากคำสั่ง take(5)
  
}
 

    สร้าง Stream ใหม่โดยใช้ async* ฟังก์ชั่น

    ในห้วข้อที่ผ่านมา เราได้เห็นการใช้งานการสร้าง Stream จากฟังก์ชั่น async* ไปแล้วในการสร้าง Stream ข้อความทดสอบ
โดยข้อมูล Stream จะถูกสร้างเมื่อฟังก์ชั่นถูกเรียกใช้งาน และเริ่มทำงานเมื่อผู้ใช้เรียกดูข้อมูล  ข้อมูล Stream จะหยุดทำงาน
หรือปิดตัวลงเมื่อมีการคืนค่าจากฟังก์ชั่น ในระหว่างที่ยังไม่คืนค่าจากฟังก์ชั่น ข้อมูล Stream สามารถปล่อย events โดยใช้คำสั่ง
yield หรือ yield* 
    ดูตัวอย่างการสร้าง Stream ตัวเลข ทุกๆ ช่วงเวลาที่กำหนด ด้านล่างประกอบทำความเข้าใจ
 
void main() {

  // สร้าง Stream ตัวเลข
 var stream = timedCounter(Duration(seconds:2),5);
  stream.listen(print); // เรียกใช้งาน
  // จะแสดงตัวเลข 0..1..2..3 และ 4 แต่ละค่าใน ทุกๆ 2 วินาที
  // จนครบตามจำนวน ที่กำหนดใน maxCount ถ้ามีกำหนด
  
}

Stream<int> timedCounter(Duration interval, [int maxCount]) async* {
  int i = 0;
  while (true) { // สร้างลูปแบบ infinite
    // รอข้อมูลโดยให้ delay ตามเวลาที่กำหนด ในที่นี้คือ 2 วินาที
    await Future.delayed(interval);
    yield i++; // เพิ่มค่า i และส่งออกเป็น output stream
    if (i == maxCount) break; // ออกจากลูปถ้าเข้าเงื่อนไข
  }
}
    ในตัวอย่างเรากำหนด maxCount เท่ากับ 5 นั่นคือ เมื่อค่า i มีค่าเท่ากับ 5 ก็จะออกจากลูปของ while สิ้นสุดการทำงานของลูป
แต่ถ้าเราไม่ได้กำหนดค่า maxCount ก็จะไม่มีเงื่อนไขในการหยุดหรือจบการทำงานของลูป ดังนั้นข้อมูล Stream ก็จะเพิ่มจำนวน
ตัวเลขมากขึ้นไปเรื่อยๆ จนกว่าผู้ใช้จะยกเลิกการใช้ข้อมูล
    ในขณะที่มีการเรียกใช้งานข้อมูล Stream จากการใช้งาน คำสั่ง listen() เราสามารถกำหนดให้ StreamSubscription object เรียกใช้
คำสั่ง cancel() เพื่อยกเลิกข้อมูล Stream ได้  โดยเมื่อหากถูกเรียกใช้งานแล้ว จะทำให้คำสั่ง yield ในลำดับต่อมา ทำหน้าที่เป็นการ
return ค่าแทน เพื่อจบการทำงานหรือปิด Stream ไป  บล็อกคำสั่งปิดท้ายๆ ใดๆ ถ้ามีการทำงาน และปิดการทำงานของฟังก์ชั่น
และพยายามที่จะทำคำสัง yield เพื่อส่งออกค่า Stream ก่อนปิดการทำงาน จะไม่สำเร็จ และจะทำหน้าที่คล้ายการ return ค่าแทน
    เมื่อหยุดการทำงานของฟังก์ชั่นท้ายสุดแล้ว จะถูก return ค่าด้วยคำสั่ง cancel() อย่างสมบูรณ์  กรณีฟังก์ชั่นปิดการทำงานพร้อมกับ
เกิด error ก็จะคืนค่าเป็นสถานะ complete พร้อมกับ error object หรือ null แล้วแต่กรณี
 
    ตัวอย่างรูปแบบ การใช้งาน Future เพื่อสร้างข้อมูล Stream เป็นวิธีการแปลงข้มมูล Future ที่มีต่อเนื่องไปเป็นข้อมูล Steam
 
Stream<T> streamFromFutures<T>(Iterable<Future<T>> futures) async* {
  for (var future in futures) { // วนลูป futures  
    var result = await future; // รอผลลัพธ์จาก future
    yield result;  // ส่งออกเป็น Stream 
  }
}
    การใช้งาน generic หรือ type parameter <T> ทำให้เราสามารถกำหนดชนิดข้อมูลของ Stream ได้โดยไม่ต้องแยกหลายฟังก์ชั่น
ใช้งาน  โดยเพียงแค่กำหนดชนิดข้อมูล เข้าไปใน type parameter ที่ต้องการ เช่น Stream<int> สำหรับข้อมูล Stream ตัวเลข  หรือ
Stream<String> ถ้าใช้เป็น Stream ข้อมูลที่เป็นข้อความเป็นต้น
 
    ฟังก์ชั่น streamFromFutures() ต้องการข้อมูลที่เป็น Future ที่เป็นข้อมูลแบบต่อเนื่อง เพื่อนำไปวนลูปแยกแต่ละ future ย่อย
ทำงาน แล้วรอผลลัพธ์เพื่อทำการส่งออกมา แล้ววนลูปไปจนครบจำนวนของ Future ทั้งหมด
 
    จะเห็นว่า เราใช้งาน async* ฟังก์ชั่นสร้างข้อมูล Stream จากค่าใดค่าหนึ่ง นั่นคือจำเป็นต้องมีข้อมูลมาใช้งานในฟังก์ชั่นนี้ อาจจะเป็น
จากข้อมูล Stream อื่น  หรือ จาก Future ที่เป็นข้อมูลแบบต่อเนื่อง เหมือนในตัวอย่างด้านบน ที่ข้อมูลอาจจะมาจากหลายแหล่งข้อมูล
อย่างไรก็ตามในหลายๆ กรณี ฟังก์ชั่น async* ดูเรียบง่ายเกินไปที่จะจัดการกับข้อมูลที่มาจากหลายๆ แหล่งให้ง่าย ซึ่งการใช้งาน
StreamController จะช่วยแก้ปัญหานี้ได้
 
 

    สร้าง Stream ใหม่โดยใช้ StreamController

    ในกรณีที่ events ของ Stream มาจากส่วนที่แตกต่างกันของโปรแกรม และไม่ใช่ Stream หรือ Future ที่สามารถเแปลงมาใช้งานโดย
ใช้งาน async* ฟังก์ชั่นได้ เราจะใช้งาน StreamController ในการสร้างและเก็บข้อมูล Stream แทน
    StreamController จะทำการสร้างข้อมูล Stream ใหม่ ที่สามารถเพิ่ม event แทรกเข้าไปในข้อมูล Stream ณ จุดเวลาใดๆ ก็ได้  สามารถ
กำหนดการจัดการในขั้นตอนการเรียกใช้งานและการหยุดการใช้งานชั่วคราวได้ นั่นคือสามารถควบคุมและจัดการข้อมูล Stream ได้เอง
    เมื่อเรามีการใช้งาน StreamController นั่นคือเราต้องกำหนดการทำงานสำหรับคำสั่งต่างๆ ไว้ด้วยเสมอ แตกต่างจากวิธีการสร้างด้วย
async* ฟังก์ชั่น ที่คำสั่งการทำงานต่างๆ ไม่ว่าจะเป็น onListen() onPause() onResume() หรือ onCancel() นั้นถูกเรียกใช้งานอัตโนมัติ
    มาดูตัวอย่างการสร้าง Stream โดยใช้ StreamController ดังนี้ โดยตัวอย่างด้านล่าง ยังไม่มีการควบคุมการทำงานเข้ามา
 
import 'dart:async';

void main() async {
  
  // สร้าง Steam โดยใช้ StreamController
  var stream = timedCounter(Duration(seconds:1),10);
  // เราจะหน่วงเวลา 5 วินาที ก่อนเรียกใช้งาน เพื่อดูว่า Stream ทำงานทันที
  // ทั้งที่ยังไม่ได้เรียกใช้งาน
  await Future.delayed(const Duration(seconds: 5));
  stream.listen(print); 

}

// NOTE: รูปแบบการใช้งานนี้ยังไม่สมบูรณ์
// ทำงานทันที ก่อนเรียกใช้งาน และยังไม่มีกำหนด กรณีทำการหยุดชั่วคราว 
Stream<int> timedCounter(Duration interval, [int maxCount]) {
  var controller = StreamController<int>(); // สร้างตัวแปรเก็บ StreamController
  // หรือใช้เป็น StreamController<int> controller; ก็ได้
  int counter = 0;
  
  void tick(Timer timer) {
    counter++; // บวกค่าเพิ่ม
    controller.add(counter); // ส่งข้อมูล stream ออกเป็น events
    if (maxCount != null && counter >= maxCount) { // ถึงค่าที่กำหนด ถ้ามี
      timer.cancel(); // ยกเลิการทำงานของเวลา
      controller.close(); // ปิดการใช้งานข้อมูล Stream
    }
  }

  Timer.periodic(interval, tick); // BAD: ทำงานก่อนที่จะเรียกใช้งาน
  return controller.stream;
}
    เมื่อมีการสร้าง Stream โดยใช้ StreamController ที่ยังไม่ได้กำหนดสมบูรณ์ ตัว Stream จะทำงานทันที ทั้งที่ยังไม่ได้เรียกใช้งาน
โดยในตัวอย่าง เราทำการสร้าง Stream ขึ้นมาในตัวแปร stream จากนั้น เราทำการจำลองให้หน่วงเวลา ก่อนเรียกใช้งาน 5 วินนาที
แต่เมื่อเราเรียกใช้งานหลังจากครบ 5 วินาทีด้วยคำสั่ง listen() ปรากฏว่า ในวินาทีที่ 6 ตัวเลข 1 - 5 ถูกพิมพ์ออกมาพร้อมกัน แล้วเริ่ม
แสดงเลข 6..7..8..9..และ 10 จนครบ นั่นคือ Stream ทำงานก่อนที่จะถูกเรียกใช้ สิ่งที่เราต้องการคือ เมื่อเรียกใช้งานหลังจากวินาทีที่ 5 
ควรเริ่มแสดงข้อมูลตั้งแต่ต้น นั้นคือ 1..2..3... ไปเรื่อยๆ นั่นคือสิ่งที่เราต้องทำให้ StreamController มีรูปแบบการใช้งานที่สมบูรณ์
โดยต้องรอคำสั่งเรียกใช้งานก่อน ถึงจะเริ่มทำงาน
 
    โค้ดด้านล่าง เป็นรูปแบบสร้าง Stream โดยใช้ StreamController ที่ถูกต้อง เราเพิ่มเติมคำสั่ง สำหรับเริ่มทำงาน และคำสั่ง สำหรับ
หยุดเวลา เพื่อหยุดการทำงานชั่วคราว ดังนี้
 
import 'dart:async';

void main() async {
  
  // สร้าง Steam โดยใช้ StreamController
  var stream = timedCounter(Duration(seconds:1),10);
  StreamSubscription<int> subscription; // กำหนดตัวเรียกใช้งาน
  
  // เราจะหน่วงเวลา 3 วินาที ก่อนเรียกใช้งาน 
  await Future.delayed(const Duration(seconds: 3));
  
  // เรียกใช้ง่านโดยใช้ subscription เพิ่อที่เราะสามารถ หยุดชั่วคราวได้ หรือใช้คำสั่งอื่นๆ
  // จัดการกับข้อมูล Stream ได้
  subscription = stream.listen((int counter) {
    print(counter); // แสดงข้อมูล ทุกๆ 1 วินาที
    if (counter == 5) { // ถ้าข้อมูลเท่ากับ 5
      // จำลองถ้าข้อมูลเท่ากับ 5 เราจะหยุดชั่วคราว 2 วินาที แล้วค่อยทำงานต่อ
      subscription.pause(Future.delayed(const Duration(seconds: 2)));
    }
  });

}


Stream<int> timedCounter(Duration interval, [int maxCount]) {
  // กำหนด property หรือตัวแปรสำหรับใช้งาน
  // ข้อมูล StreamController และ Timer อยู่ใน async* library 
  StreamController<int> controller;
  Timer timer;
  int counter = 0;

  // ส่วนของคำสั่งกำหนดการทำงาน
  void tick(_) {
    counter++;
    controller.add(counter); // Ask stream to send counter values as event.
    if (counter == maxCount) {
      timer.cancel();
      controller.close(); // Ask stream to shut down and tell listeners.
    }
  }

  // ส่วนของคำสั่ง เริ่มทำงาน
  void startTimer() {
    // เริ่มเวลา และเรียกใช้งาน ข้อมูล Stream จากคำสั่ง tick()
    timer = Timer.periodic(interval, tick);
  }

  // ส่วนของคำสั่ง หยุดการทำงาน
  void stopTimer() {
    if (timer != null) { // เช็คว่าเวลายังเดินอยู่ไหม ถ้ายังเดินอยู่ ให้ยกเลิก
      timer.cancel(); // ยกเลิกเวลา
      timer = null; // กำหนดเป็น null
    }
  }

  // ส่วนของการควบคุมการทำงาน
  controller = StreamController<int>(
      onListen: startTimer, // เมื่อเรีียกใช้งาน
      onPause: stopTimer, // เมื่อหยุดชั่วคราว
      onResume: startTimer, // เมื่อกลับมาเริ่มต่อ
      onCancel: stopTimer); // เมื่อยกเลิก ไม่ใช้งานแล้ว

  // return ข้อมูล Stream ที่สามารถควบคุมการทำงานได้
  return controller.stream;
}
    เราใช้งาน StreamSubscription เป็นตัวแทนในการเรียกใช้งานข้อมูล Stream โดย StreamSubscription ให้เราสามารถที่จะใช้
คำสั่งต่างๆ ในการจัดการกับข้อมูล Stream ได้ เช่น การหยุดชั่วคราว การกลับมาใช้งานต่อ หรือแม้แต่การยกเลิกข้อมูล Stream โดยใช้
คำสั่ง cancel เช่น สมมติพอนับถึง 9 เราก็อาจจะยกเลิกข้อมูล Stream ในรูปแบบการใช้งานดังนี้
 
    if( counter == 9){
      subscription.cancel();
    }
    จากตัวอย่างการใช้งานข้างต้น StreamSubscription ที่กำหนดด้วยตัวแปร subscription เรียกใช้งานข้อมูล Stream หลังจากผ่านไป
แล้ว 3 วินาที ข้อมูลเริ่มแสดง 0..1..2..3..4.. และพอถึงข้อมูลที่ 5 เราก็ทำการหยุดชั่วคราว 2 วินาที เมื่อครบ 2 วินาที ก็จะเริ่มแสดงข้อมูล
ที่ 5...6...7...8...9... และ 10 แต่ถ้าหากเราใส่เงื่อนไข ให้หยุดการใช้งานข้อมูล Stream ด้วยคำสั่ง cancel() ข้อมูลก็จะแสดงถึงแค่ 9
 
    ในการใช้งาน StreamController เราจะต้องกำหนดการเรียกใช้งานคำสั่งเมื่อเกิด onListen, onCancel, onPause, และ onResume
เพื่อรองรับการเรียกใช้งานจาก StreamSubscription ไว้ด้วยเสมอ
 
 
 

การใช้งานข้อมูล Stream ในภาษา Dart

    ในหัวข้อการสร้างข้อมูล Stream เราอาจจะได้เห็นการเรียกใช้งานข้อมูล Stream ในรูปแบบต่างๆ ไปบ้างแล้ว หัวข้อนี้เราจะมาดูเกี่ยวกับ
การใช้งานข้อมูล Stream และการจัดการเพิ่มเติม
 
 

    การรับค่า Events ของข้อมูล Stream

    เราสามารถรับค่า Events ของข้อมูล Stream โดยวนลูปเรียกข้อมูลผ่านคำสั่ง "await for"
 
void main() async { 
  var stream = genStream(5); // สร้างข้อมูล Stream
  var sum = await sumStream(stream); // ใช้งานข้อมูล Stream
  print(sum); //output: 15
}

// ฟังก์ชั่น จำลองสร้างข้อมูล Stream
Stream<int> genStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    yield i;
  }
}

// ฟังก์ชั่น ใช้งานข้อมูล Events ของ Stream โดยหาค่าผลบวก
Future<int> sumStream(Stream<int> stream) async {
  var sum = 0;
  await for (var value in stream) {
    sum += value;
  }
  return sum;
}
 

    การเกิด Error Events

    ปกติเมื่อทำการอ่านค่า Events โดยใช้คำสัง "await for" เมื่อวนลูปจนครบเรียบร้อยแล้ว การทำงานของข้อมูล Stream ก็ถึอสิ้นสุด
แต่ในบางกรณี ก็อาจจะเกิด Error หรือข้อผิดพลาดเกิดขึ้นก่อนที่ข้อมูล Events จะถูกเรียกใช้งานเสร็จ เช่น อินเตอร์เน็ตล่ม ขณะอ่าน
ข้อมูลไฟล์จาก Server หรือกรณีคำสั่งที่สร้าง Events ยังไม่สมบูรณ์ ยังมีข้อผิดพลาดในการทำงานอยู่ เป็นต้น
    นอกจาก Events ที่เป็นข้อมูล Stream แล้ว Stream ยังอาจจะมี Events ที่เป็น Error ส่งกลับมาด้วย และส่วนใหญ่จะหยุดทำงานทันที
ที่เกิด Error ขึ้น หรือบางครั้งก็อาจจะเกิดมากกว่า Error เดียว
    ในการใช้งาน "await for" อ่านข้อมูล Events หากเกิด Error ขึ้น ก็จะจบการทำงานและออกจากลูปพร้อมข้อมูล Error  เราสามารถใช้
งาน "try..catch" เพื่อจัดการข้อมูลกรณีเกิด Error ขึ้นได้  เราจะจำลองสร้างมูล Stream ที่เกิด Error Events ขึ้น พร้อมกับตรวจสอบ
และจัดการ Error โดยใช้ "try..catch" ดังนี้
 
void main() async { 
  var stream = genStream(5); // สร้างข้อมูล Stream
  var sum = await sumStream(stream); // ใช้งานข้อมูล Stream
  print(sum); //output: -1
}

// ฟังก์ชั่น จำลองสร้างข้อมูล Stream
Stream<int> genStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    if (i == 4) { // จำลอง Error ข้อมูลที่ 4
      throw new Exception('Intentional exception');
    } else {
      yield i;
    }
  }
}

// ฟังก์ชั่น ใช้งานข้อมูล Events ของ Stream โดยหาค่าผลบวก
Future<int> sumStream(Stream<int> stream) async {
  var sum = 0;
  try {
    await for (var value in stream) {
      sum += value;
    }
  } catch (e) {
    return -1;
  }
  return sum;
}
    ผลลัพธ์ที่ได้เท่ากับ -1 เนื่องจากว่า เมื่อข้อมูลที่เท่ากับ 4 เป็น Error Events และเราใช้ "try..catch" จัดการกับข้อมูลกรณีเกิด Error
โดยให้มีค่าผลลัพธ์ที่ส่งกลับมาเท่ากับ -1
 
 

    การใช้งานข้อมูล Stream

    ใน Stream class มีคำสั่งหรือ method ที่ใช้จัดการข้อมูล Stream  เป็นลักษณะรูปแบบการใช้งานคล้ายๆ กับรูปแบบของข้อมูลอาเรย์
เช่น สมมมติเราต้องการข้อมูลสุดท้ายของ Stream ที่เป็นตัวเลขจำนวนเต็มบวก โดยใช้คำสั่ง lastWhere() จาก Stream API
 
void main() async { 
  var stream = genStream(5); // สร้างข้อมูล Stream
  var sum = await lastPositive(stream); // ใช้งานข้อมูล Stream
  print(sum); //output: 5
}

// ฟังก์ชั่น จำลองสร้างข้อมูล Stream
Stream<int> genStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    yield i;
  }
}

// รูปแบบ Fat Arrow
Future<int> lastPositive(Stream<int> stream) =>
    stream.lastWhere((x) => x >= 0);

// รูปแบบเต็ม
// Future<int> lastPositive(Stream<int> stream) {
//   return stream.lastWhere((x){
//     return x>=0;
//   });
// }
 

    คำสั่งต่างๆ ที่ใช้งานกับข้อมูล Stream

    เป็นคำสั่งที่ใช้สำหรับจัดการข้อมูล Stream และ return ค่าผลลัพธ์กลับมา
 
Future<T> get first;
Future<bool> get isEmpty;
Future<T> get last;
Future<int> get length;
Future<T> get single;
Future<bool> any(bool Function(T element) test);
Future<bool> contains(Object needle);
Future<E> drain<E>([E futureValue]);
Future<T> elementAt(int index);
Future<bool> every(bool Function(T element) test);
Future<T> firstWhere(bool Function(T element) test, {T Function() orElse});
Future<S> fold<S>(S initialValue, S Function(S previous, T element) combine);
Future forEach(void Function(T element) action);
Future<String> join([String separator = ""]);
Future<T> lastWhere(bool Function(T element) test, {T Function() orElse});
Future pipe(StreamConsumer<T> streamConsumer);
Future<T> reduce(T Function(T previous, T element) combine);
Future<T> singleWhere(bool Function(T element) test, {T Function() orElse});
Future<List<T>> toList();
Future<Set<T>> toSet();
 
    คำสั่งส่วนใหญ่เกือบทั้งหมดทำงาน คล้ายกับฟังก์ชั่นที่ใช้การกับข้อมูลอาเรย์ หรือ List  ยกเว้น drain() และ pipe() ตัวอย่างการใช้งาน
การจัดการข้อมูล Stream บางส่วน
 
// สร้างฟังก์ชั่น หาข้อมูลที่ต้องการใน Stream
Future<bool> contains(Object needle) async {
  await for (var event in this) {
    if (event == needle) return true;
  }
  return false;
}

// สร้างฟังก์ชั่น ทำคำสั่งจัดการข้อมูล Stream อีกที
Future forEach(void Function(T element) action) async {
  await for (var event in this) {
    action(event);
  }
}

// สร้างฟังก์ชั่นแปลงชนิดข้อมูล
Future<List<T>> toList() async {
  final result = <T>[];
  await this.forEach(result.add);
  return result;
}

// สร้างฟังก์ชั่น เชื่อมข้อมูล
Future<String> join([String separator = ""]) async =>
    (await this.toList()).join(separator);
 

    คำสั่งที่ใช้แก้ไขข้อมูล Stream

    คำสั่งต่อไปนี้ จะเป็นสร้าง Stream ข้อมูลใหม่จากข้อมูล Stream เดิม การทำงานคือเมื่อมีการเรียกใช้งาน จะเรียกข้อมูลจาก Stream
ใหม่ที่ใช้งานคำสั่งก่อน แล้วถึงไปเรียกใช้งานข้อมูล Stream ต้นฉบับ
 
Stream<R> cast<R>();
Stream<S> expand<S>(Iterable<S> Function(T element) convert);
Stream<S> map<S>(S Function(T event) convert);
Stream<T> skip(int count);
Stream<T> skipWhile(bool Function(T element) test);
Stream<T> take(int count);
Stream<T> takeWhile(bool Function(T element) test);
Stream<T> where(bool Function(T event) test);
 
    คำสั่งข้างต้น ทำงานคล้ายกับการใช้งานกับข้อมูลอาเรย์ เป็นการแปลงจากข้อมูลหนึ่ง เป็นอีกข้อมูลหนึ่ง เราสามารถเขียนคำสั่งทำงาน
ในลักษณะแบบเดียวกันกับคำสั่งข้างบนโดยวนลูปโดยใช้ "await for" และจัดการข้อมูลใน async ฟังก์ชั่น แต่หากใช้คำสั่ง ก็จะกระชับและ
สะดวกกว่า
 
Stream<E> asyncExpand<E>(Stream<E> Function(T event) convert);
Stream<E> asyncMap<E>(FutureOr<E> Function(T event) convert);
Stream<T> distinct([bool Function(T previous, T next) equals]);
 
    คำสั่ง asyncExpand() และ asyncMap() เหมือนกับ expand() และ map() ที่เพิ่มเข้ามาคือ อนุญาตให้สามารถกำหนดใช้งาน async
ฟังก์ชั่นได้  สำหรับคำสั่ง distinct() เป็นคำสั่งที่ไม่มีใน การใช้งานอาเรย์ ที่เพิ่มเข้ามาใหม่ แสดงข้อมูล Stream ใหม่ที่ไม่เอาข้อมูลที่ซ้ำกัน
หรือแสดงข้อมูลที่ซ้ำกันแค่ครั้งเดียว
 
Stream<T> handleError(Function onError, {bool test(error)});
Stream<T> timeout(Duration timeLimit,
    {void Function(EventSink<T> sink) onTimeout});
Stream<S> transform<S>(StreamTransformer<T, S> streamTransformer);
 
    สามคำสั่งสุดท้ายด้านบน เป็นคำสั่งพิเศษ ที่ใช้ในการจัดการกับข้อมูล Error ที่ไม่สามารถจัดการในขั้นตอนการใช้งาน "await for" 
คำสั่ง handleError() สร้าง Stream ใหม่ที่้ตัด Error ออกไป คือจะไม่มี Error เกิดขึ้น สมมติเช่น ข้อมูลเดิมลำดับ 1..2..3 ไม่มี Error
แต่เกิด Error ในข้อมูลที่ 4 การสร้างข้อมูล Stream ใหม่โดยใช้ handleError() จะได้เฉพาะข้อมูล ก่อน Error นั่นคือ 1..2..3 เป็นต้น
 
    ตัวอย่างการใช้ handleError()
 
// ฟังก์ชั่น ใช้งานข้อมูล Events ของ Stream โดยหาค่าผลบวก
Future<int> sumStream(Stream<int> stream) async {
  var sum = 0;
  var streamWithoutErrors = stream.handleError((e) => null);
  await for (var value in streamWithoutErrors) {
    sum += value;
  }
  return sum;
}
    

    การใช้งาน transform() ฟังก์ชั่น

    ตัวอย่างการใช้งานคำสั่ง transform() 
 
import 'dart:convert';
import 'dart:io';

Future<void> main(List<String> args) async {
  var file = File(args[0]);
  var lines = utf8.decoder
      .bind(file.openRead())
      .transform(LineSplitter());
  await for (var line in lines) {
    if (!line.startsWith('#')) print(line);
  }
}
    ตัวอย่างโค้ดข้างต้น จะทำการอ่านข้อมูลไฟล์ และทำการแปลงข้อมูล Stream สองครั้ง โดยครั้งแรกแปลงข้อมูลเข้ารหัส UTF8
จากนั้น ทำการแปลงข้อมูลแยกเป็นแต่ละบรรทัด โดยจะแสดงข้อมูลทั้งหมดออกมา ยกเว้นบรรทัดที่ขึ้นต้นด้วย "#"
    เนื้อหาเกี่ยวกับ I/O library เราจะได้นำมาอธิบายเพิ่มเติมภายหลังในหัวข้อต่อๆ ไป
 
 

    การใช้คำสั่ง listen()

    เป็นคำสั่งเรียกใช้งานข้อมูล Stream เป็นคำสั่งที่อยู่ในระดับ "low-level" นั้นคือคำสั่งต่างๆที่ใช้งานข้อมูล Stream ล้วนมาจากรูปแบบ
การใช้งานคำสั่ง listen() 
 
StreamSubscription<T> listen(void Function(T event) onData,
    {Function onError, void Function() onDone, bool cancelOnError});
    คำสั่ง listen() เป็นการเริ่มต้นใช้งานข้อมูล Stream นั่นคือข้อมูล Stream จะเป็นเพียง Object หนึ่งที่ยังไม่มีอะไรเกิดขึ้น หรืออยู่นิ่ง
จนกว่าเราจะมีการเรียกใช้งาน เมื่อเราเรียกใช้งานข้อมูลด้วยคำสัง listen() ก็จะคืนค่าเป็น StreamSubscription object หรือตัวแทนการ
ใช้งานข้อมูลที่บอกว่ากำลังใช้งานข้อมูล Stream อะไรอยู่ โดยทำการสร้าง Events ข้อมูลอย่างต่อเนื่อง ซึ่ง StreamSubscription สามารถ
จะหยุด หรือใช้งานข้อมูลนั้น ตอนไหนก็ได้  หรือจะปิดการใช้งานไปเลยก็ได้เช่นกัน ตามที่ได้เห็นรูบแบบการใช้งานมาแล้วจากตัวอย่าง
 
    เนื้อหาข้อมูลเกี่ยวกับ Stream ถือว่ามีรายละเอียดค่อนข้างเยอะ แต่ก็เป็นส่วนทีมีประโยชน์ และอาจจะต้องใช้งาน
ในอนาคต กับส่วนต่างๆ เช่นการใช้งาน I/O library การอ่าน เขียน รับ ส่งไฟล์ เหล่านี้เป็นต้น
 
    หว้งว่าเนื้อหานี้จะเป็นแนวทางทำความเข้าใจเบื้องต้น เพื่อนำไปใช้ในเนื้อหาต่อๆ ไป


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ









เนื้อหาที่เกี่ยวข้อง









URL สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ