Async in Dart (5) รู้จักกับ FutureBuilder/StreamBuilder, ตัวช่วยสร้าง Async-Widget ใน Futter

Blog-web_02-5
This entry is part 5 of 5 in the series Async-in-Dart-Series

เนื้อหาในบทนี้ เป็นคลาสเฉพาะที่มากับ Flutter Framework เท่านั้น .. ถ้าเขียน Dart ธรรมดาจะไม่มีให้ใช้นะ!!

เอาจริงๆ 99% ของคนที่ศึกษาภาษา Dart ก็เพื่อเอาไปเขียนแอพ (หรือเว็บ/เด็กส์ท็อป) แบบ cross-platform ด้วยเฟรมเวิร์ก Flutter นั่นแหละ

ตามความคิดของเรา จริงๆ Flutter น่าจะเลือกภาษา Kotlin มาใช้แทนมากกว่า แต่ก็มีเหตุผลหลายๆ อย่างนั่นแหละที่ทำให้ทำไม่ได้

สำหรับการเขียน Flutter นั้น ส่วนประกอบในแต่ละส่วนของแอพนั้นจะพูดออกแบบมาเป็น Component เอามาประกอบเข้าด้วยกันเป็นชั้นๆ เรียกว่า "Widget"

มี Widget อยู่ 2 ประเภทหลักๆ คือ

  • StatelessWidget: เป็น Widget ที่ไม่สามารถเปลี่ยนแปลงได้หลังจาก render ไปแล้ว
  • StatefulWidget: เป็น Widget ที่สามารถ render หน้าตา UI ของแอพใหม่ได้ถ้ามีการเปลี่ยนแปลงข้อมูล

ในบทความนี้เราจะโฟกัสกันที่ StatefulWidget เป็นหลักนะ หน้าตาของ StatefulWidget ก็จะเป็นประมาณนี้ (StatefulWidget จะมาพร้อมกับคลาส State ของมันเสมอ)

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text('This is my App.'),
    );
  }
}

เมธอดหลักที่ทำหน้าที่ในการกำหนดส่วนแสดงผลหรือ UI คือเมธอด build()

แน่นอน เวลาเราเขียนแอพจริงๆ โครงสร้างมันจะเป็นการเอา Widget มาซ้อนกันหลายๆๆๆ ชั้นมากๆ เช่น

Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Column(
        children: [
          Container(
            child: Row(
              children: [
                Text('This is my App.'),
              ]
            ),
          ),          
        ],
      ),
    ),
  );
}

ทีนี้ ปัญหามันก็อยู่ตรงนี้แหละ เพรายิ่งโครงสร้างซ้อนกันหลายชั้นมากๆ ถ้ามีการเปลี่ยนแปลงข้อมูล ก็ต้อง render หน้านี้ใหม่ (เรียกคำสั่ง build() ใหม่) แม้ว่าเราจะเปลี่ยนแปลงข้อมูลแค่ไม่กี่จุดเล็กๆ ก็ตาม

ลองมาดูตัวอย่างแอพมาตราฐานคือ Counter กัน (แอพตัวอย่างนี้เป็นแอพที่เวลาเราสั่ง new Flutter project มันจะสร้างให้เป็น default เลย)

แต่เราจะขอตัดมาแค่ส่วนที่จะใช้อธิบายนะ และขอแยก Widget ออกเป็นชิ้นๆ ด้วย

class MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('render - Scaffold');
    return Scaffold(
      body: columnWidget(),
    );
  }

  Widget columnWidget(){
    print('render - Column Widget');
    return Column(
      children: [
        incrementButtonWidget(),
        counterWidget(),
      ],
    );
  }

  Widget incrementButtonWidget(){
    print('render - Increment Button Widget');
    return RaisedButton(
      child: incrementButtonTextWidget(),
      onPressed: _incrementCounter,
    );
  }

  Widget incrementButtonTextWidget(){
    print('render - Increment Button Text Widget');
    return Text('Increment');
  }

  Widget counterWidget(){
    print('render - Counter Widget');
    return Text('count is $_counter');
  }
}

เมื่อเราเปิดหน้าแอพขึ้นมาครั้งแรก เราจะพบว่า build() เริ่มทำงาน โดยมันจะเรนเดอร์ Widget ทุกชิ้นขึ้นมาก่อน

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget

จากนั้น เมื่อเรากด increment จะทำให้เมธอด _incrementCounter() ทำงาน ไปทำการ setState() เพื่อเปลี่ยนค่า _counter

ผลที่ได้คือเมื่อ state เปลี่ยนไป ทั้งหน้าจะต้องทำการเรนเดอร์ใหม่อีกครั้ง เมธอด build() ก็ต้องเริ่มทำงานใหม่ตั้งแต่ต้นอีกครั้ง ทำให้เราได้ผลแบบนี้ออกมาอีกที

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget

แต่จริงๆ แล้วส่วนที่มีการเปลี่ยนแปลงจริงๆ มีแค่วิดเจ็ต Text('count is $_counter') เท่านั้น

ทางทีม Flutter ของ Google เลยแนะนำมาว่าการเขียนแอพแบบนี้จะทำให้ประสิทธิภาพไม่ดี ถ้ามีข้อมูลแปลี่ยนเป็นบางตัว เราควรจะเรนเดอร์ใหม่เฉพาะวิดเจ็ตที่จำเป็นน่าจะดีกว่า

นั่นเลยเป็นที่มาของวิดเจ็ตพิเศษที่ชื่อว่า FutureBuilder และ StreamBuilder

FutureBuilder / StreamBuilder

มีวิธีการสร้างแบบนี้

FutureBuilder(
    future: _future,
    builder: (BuildContext context, AsyncSnapshot snapshot) {
        return ...
    },
)

StreamBuilder(
    stream: _stream,
    builder: (BuildContext context, AsyncSnapshot snapshot) {
        return ...
    },
)

ส่วนวิธีการใช้งานนั้นง่ายมาก คือวิดเจ็ตตัวไหนที่สามารถเรนเดอร์ใหม่ได้เฉพาะส่วน ให้เอา FutureBuilder ไม่ก็ StreamBuilder ครบมันลงไป แบบนี้

แล้วเมื่อไหร่จะใช้ Future / Stream

ถ้าข้อมูลของเราเปลี่ยนแปลงได้แค่ครั้งเดียว เราจะใช้ FutureBuilder เช่นต้องการโหลดข้อมูลจาก API แค่ครั้งเดียว

แต่ถ้าข้อมูลของเราเปลี่ยนได้เรื่อยๆ มากกว่า 1 ครั้ง เราจะใช้ StreamBuilder เช่นการกดปุ่มที่กดได้มากกว่า 1 ครั้ง

แน่นอนว่าเราสามารถใช้ StreamBuilder แทน FutureBuilder ได้แทบจะทุกกรณีเลย

ทีนี้ลองเอา StreamBuilder มาใช้กับโค้ด Counter ดูบ้าง

ในเคสนี้ เราเปลี่ยน Text ตรงที่แสดงตัวนับให้กลายเป็น StreamBuilder จากนั้นสร้าง StreamController ขึ้นมาหนึ่งตัว ซึ่งจะอัพเดทค่า counter แทนการสั่ง setState()

class MyHomePageState extends State<MyHomePage> {

  int _counter = 0;
  StreamController<int> controller = new StreamController<int>();

  @override
  void initState(){
    super.initState();
    controller.add(_counter);
  }

  @override
  void dispose(){
    super.dispose();
    controller.close();
  }

  void _incrementCounter() {
    //แทนที่จะใช้ setState ก็เซ็ตค่าผ่าน StreamController แทน
    controller.add(++_counter);
  }

  @override
  Widget build(BuildContext context) {
    print('render - Scaffold');
    return Scaffold(
      body: columnWidget(),
    );
  }

  Widget columnWidget(){
    print('render - Column Widget');
    return Column(
      children: [
        incrementButtonWidget(),
        counterWidget(),
      ],
    );
  }

  Widget incrementButtonWidget(){
    print('render - Increment Button Widget');
    return RaisedButton(
      child: incrementButtonTextWidget(),
      onPressed: _incrementCounter,
    );
  }

  Widget incrementButtonTextWidget(){
    print('render - Increment Button Text Widget');
    return Text('Increment');
  }

  Widget counterWidget(){
    return StreamBuilder(
      stream: controller.stream,
      builder: (context, snapshot){
        print('render - Counter Widget');
        return Text('count is ${snapshot.data}');
      },
    );
  }
}

ในกรณีนี้วิดเจ็ตทั้งหมดจะมีการเรนเดอร์แค่ครั้งแรกครั้งเดียว หลังจากนั้นถ้ามีการกดปุ่ม วิดเจ็ตตัวอื่นที่ไม่เกี่ยวข้องด้วย จะไม่มีการเรนเดอร์ใหม่ เรนเดอร์เฉพาะตัวของ StreamBuilder เท่านั้น

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget
render - Counter Widget
render - Counter Widget
render - Counter Widget

จะเห็นว่าวิธีนี้ทำให้ประสิทธิภาพของแอพเพิ่มขึ้นเยอะมาก เพราะไม่จำเป็นต้องเรนเดอร์ทั้งหน้าใหม่ทุกครั้งที่มีข้อมูลเปลี่ยนแปลงเพียงเล็กน้อย

ซึ่งก็แลกมากับการที่เราต้องเขียนโค้ดเยอะขึ้นล่ะนะ (ฮา)

AsyncSnapshot

ในการใช้ทั้ง FutureBuilder และ StreamBuilder จะมีสิ่งที่เรียกว่า AsyncSnapshot ส่งมาให้ตัว เจ้าตัวนี้เป็นเหมือนกับตัวที่บอกว่าตอนนี้ข้อมูลของเรามีสถานะเป็นยังไงบ้างแล้ว

// สถานะของ future/stream ในตอนนั้น
snapshort.connectionState

// มี error เกิดขึ้นไหม
snapshop.hasError
// error คืออะไร
snapshop.error

// ได้รับ data มาแล้วรึยัง
snapshop.hasData
// data คืออะไร
snapshop.data

ConnectionState

สำหรับ Future

  • waiting: ขณะกำลังรอข้อมูล
  • done: เมื่อได้รับข้อมูลมาแล้ว

สำหรับ Stream

  • waiting: ขณะกำลังรอข้อมูล
  • active: เมื่อได้รับข้อมูลมาแล้ว แต่ stream ยังไม่ close
  • done: เมื่อสั่งให้ stream close
(BuildContext context, AsyncSnapshot snapshot) {

  // เช็กก่อนว่ามี error มั้ย
  if(snapshot.hasError){
    return Text('เกิดข้อผิดพลาดในการโหลดข้อมูล ${snapshot.error}');
  }

  // ถ้าไม่มี ตอนนี้โหลดข้อมูลเป็นยังไงบ้างแล้ว
  switch(snapshort.connectionState){
      // ข้อมูลยังไม่มา กำลังโหลดอยู่
      case ConnectionState.waiting:
          return Text('กำลังโหลดข้อมูลอยู่');

      // ข้อมูลมาเรียบร้อยแล้ว แสดงผลได้
      case ConnectionState.done:
      case ConnectionState.active:
          return Text('ข้อมูลคือ ${snapshot.data}');
  }
}

ทิ้งท้าย...

การใช้ FutureBuilder / StreamBuilder ใน Flutter เป็นสิ่งที่ช่วยเพิ่มประสิทธิภาพให้ตัวแอพไม่ต้องเรนเดอร์วิดเจ็ตที่ไม่ได้เปลี่ยนแปลงซ้ำๆ

ซึ่งคอนเซ็ปนี้จะถูกเอาไปใช้ต่อในแพทเทิร์นที่ทาง Google แนะนำมาให้ใช้กับการวางโครงสร้าง UI ใน Flutter ที่ชื่อว่า BLoC หรือ Business Logic Component ซึ่งเดี๋ยวเราจะเอามาสอนกันต่อในบทความซีรีส์ Flutter ต่อไป

ส่วนบทความซีรีส์ Async in Dart ก็ขอจบลงแค่ตอนนี้ก่อนล่ะ

Series Navigation<< Async in Dart (4) ควบคุมข้อมูลในstreamอย่างเหนือชั้นด้วย StreamController
Total Page Visits: 46 - Today Page Visits: 8
Share on facebook
Facebook
Share on google
Google+
Share on twitter
Twitter
Share on linkedin
LinkedIn
Share on pinterest
Pinterest

Recent Post

ปี2020, จริงๆ เราไม่ต้องใช้ jQuery แล้วก็ได้นะ

jQuery เป็นหนึ่งใน JavaScript Library ที่โด่งดังมาก (เมื่อ 10 ปีที่แล้ว) เรียกว่าในยุคนั้นแทบจะทุกเว็บจะต้องมีการติดตั้ง jQuery เอาไว้อย่างแน่นอน แต่เมื่อยุคสมัยเปลี่ยนไป เบราเซอร์ใหม่ๆ ไม่มีปัญหาการรัน JavaScript ตั้งแต่ ES6 ขึ้นไปแล้ว การใช้งาน jQuery จึงลดน้อยลงเรื่อยๆ แต่ก็มีบางโปรเจคเหมือนกัน ที่เราต้องเข้าไปแก้โค้ดเก่าๆ ซึ่งเขียนด้วย jQuery เอาไว้ ในบทความนี้จะมาเปรียบเทียบว่าทุกวันนี้เราสามารถแปลง jQuery ให้กลายเป็น Vanilla

Vue3 มีอะไรเปลี่ยนแปลงไปบ้าง

Vue 3 เพิ่งเปิดตัวมาเมื่อเดือนที่แล้ว ซึ่งมาพร้อมกับฟีเจอร์ใหม่ๆ และสิ่งที่เปลี่ยนแปลงไป เรามาดูกันดีกว่า เขียนใหม่ด้วย TypeScript ภาษา JavaScript นั้นไม่มี Type ของตัวแปรทำให้เวลาเขียนโปรแกรมมีโอกาสเกิดข้อผิลพลาดเยอะ ดังนั้นการเขียนงานโปรเจคใหญ่ๆ คนเลยนิยมเปลี่ยนไปใช้ TypeScript แทน (ถ้ายังไม่รู้จัก TypeScript อ่านต่อได้ที่นี่) สำหรับ Vue 3.0 นี้ก็เป็นการเขียนใหม่ด้วย TypeScript แทน แต่เวลาเราเอามาใช้งาน เราสามารถเลือกได้ว่าจะใช้แบบ JavaScript ตามปกติ

สอนใช้ TypeScript ในโปรเจค Node.js + Express

Node.js กับ TypeScript Node.js เป็นหนึ่งในเฟรมเวิร์คยอดนิยมสำหรับเขียนโปรแกรมฝั่ง Backend แต่เพราะมันสร้างมาตั้งแต่ปี 2009 ยุคที่ JavaScript ยังเป็นเวอร์ชัน ES5 อยู่เลย โดยดีฟอลต์แล้ว Node.js เลยไม่ซัพพอร์ท TypeScript เลย ไม่เหมือนกับ Deno ที่เพิ่งสร้างขึ้นมาโดยซัพพอร์ท TypeScript เป็นค่า default ตั้งแต่แรก เพื่อชีวิตที่ดีกว่า มาเซ็ตโปรเจค Node.js + Express

UX UI Design เบื้องต้น Section 1 “พื้นฐาน UX UI”

จากที่เราได้รู้จักโลกของ UX UI ไปใน Section ที่แล้ว ใน Section นี้ เราจะมาเข้าใจให้ลึกขึ้นอีกนิด สำหรับ UX UI Design โดยมีคำกล่าวสั้นๆว่า UX คือ นามธรรม UI คือ รูปธรรม และในพื้นฐานแรก ที่เราจะมาเริ่มเรียนรู้คือ UI Design เคลียร์กันชัดๆ UI Design คืออะไร? นิยามของ

ทำยังไง? อยากให้ JavaScript เรียกฟังก์ชันในภาษา PHP เขียนโค้ดยังไงนะ

หนึ่งในคำถามที่เว็บโปรแกรมเมอร์มือใหม่ถามกันเยอะมากจนน่าจัดเก็บไว้เป็น FAQ เลยก็คือ "จะทำยังไงให้เราเรียกใช้ฟังก์ชันภาษา PHP จากสคริป JavaScript ได้" เช่นแบบนี้... <button onclick="<?php functionInPhp1(); ?>"> คลิกฉันสิ! </button> หรือแบบนี้... function functionInJs(){ let x = <?php functionInPhp2(); ?> } คำตอบคือ ด้วยการทำงานของเว็บที่ทำงานบนโปรโตคอล http นั้น...มันทำไม่ได้!! (แต่มีวิธีแก้

[How To] การติดตั้ง Google Maps for Flutter เบื้องต้น

วันนี้ผมจะมาแนะนำและวิธีการใช้งานเบื้องต้นของ plugin ที่น่าสนใจตัวหนึ่งที่มีชื่อว่า "Google Maps for Flutter" โดย plugin ตัวนี้จะให้ Google Maps ที่เป็น Widget มาให้เราได้เปิดใช้งานแผนที่ของกูเกิ้ล ขั้นตอนการติดตั้ง อันดับแรก เราต้องทำการขอ API Key ที่ลิ้งค์ https://cloud.google.com/maps-platform/ เมื่อเข้ามาหน้าเว็บไซต์แล้วให้เข้าไปที่ Console (ตรงขวามุมบนของหน้าจอ) สร้าง Project ของเราขึ้นมาก่อน เมื่อทำการสร้างเสร็จแล้วให้เปิดแท็บด้านขวามือ แล้วเลือกเมนูที่ชื่อว่า

UX UI Design เบื้องต้น Section 0 “โลกของ UX UI”

หากเปรียบเทียบการออกแบบ UX UI เป็นประตู ดังนั้น Designer คือผู้ที่ต้องแก้ไขปัญหา เพื่อให้ตอบโจทย์ผู้ใช้ ไม่ใช่คำนึงแค่ความสวยงามเท่านั้น โดยใน Sec.0 นี้ เราจะมองภาพง่ายๆแบบใกล้ตัว เช่น การออกแบบประตู ซึ่งการออกแบบ เราจะมีพื้นฐานด้วยกัน 4 อย่าง ดังนี้ พื้นฐานการออกแบบมี 4 อย่าง คือ การออกแบบหน้าตา (User Interface Design) :: หน้าตาของประตู

วิธีสร้างปุ่ม Login Facebook บน WordPress ด้วยปลั๊กอิน Nextend Social Login Plugin

วิธีสร้างปุ่ม Login Facebook บน WordPress ด้วยปลั๊กอิน Nextend Social Login Plugin WordPress คือ เว็บไซต์สำเร็จรูป ที่สามารถสร้างและจัดการเนื้อหาได้ง่ายๆ เพียงแค่คุณล็อกอินเข้าสู่ระบบ ก็สามารถสร้างเว็บไซต์ข่าวสาร หรือร้านค้าขายของออนไลน์ของตัวเองได้เลย เพราะมีระบบการจัดการข้อมูล และยังมีปลั๊กอินมากมาย ที่ช่วยให้คุณสร้างเว็บไซต์ของตัวเองได้ง่ายยิ่งขึ้น ต้องยอมรับว่าการ Login ด้วย Social นั้นเป็นสิ่งจำเป็นขั้นพื้นฐานไปแล้วสำหรับเว็บไซต์ปัจจุบันนี้ เพราะไม่ต้องเสียเวลากรอกแบบฟอร์มการสมัครสมาชิกของเว็บไซต์ ที่บางเว็บก็ต้องกรอกข้อมูลเยอะมาก จะดีกว่ามัย ถ้าเเว็บของเราสามารถ Login

สอนวิธีเซ็ตโปรเจค TypeScript / มาใส่ไทป์ให้ JavaScript เพื่อลดความผิดพลาดในการเขียนโค้ดกันดีกว่า

ภาษา JavaScript นั้นเป็นภาษาที่เริ่มเขียนได้ไม่ยาก ยิ่งถ้าใครเขียนภาษาสาย Structure/OOP เช่น C/C++, C#, Java มาก่อน อาจจะชอบในความเป็น Dynamic Type เราสร้างตัวแปรแล้วเก็บค่าอะไรก็ได้ ทำให้มีความคล่องตัวในการเขียนมากๆ ก่อนที่พอเขียนไปเรื่อยๆ เราจะพบความแปลกของมัน ยิ่งเขียนไปนานๆ เราก็พบว่ามันเป็นภาษาที่ทำให้เกิดบั๊กได้ง่ายมาก และเป็นหนึ่งในภาษาที่น่าปวดหัวที่สุดที่คนโหวตๆ กันเลย ด้วยเหตุผลแรกคือการที่ตัวแปรไม่มี Type นั่นเอง (อีกเหตุผลหนึ่งคือส่วนใหญ่ของคนที่เปลี่ยนมาเขียน JavaScript เคยชินกับภาษาแนว OOP มาก่อน พอมาเขียนภาษาแนว

รู้จักกับ TypeScript – ประวัติของภาษาที่เติมไทป์ให้กับ JavaScript

ในบทความนี้จะเล่าถึงที่มาที่ไปของ TypeScript อย่างเดียวนะ ส่วนเรื่องสอนว่าใช้งานยังไงได้บ้าง จะเขียนอีกทีในบล็อกต่อๆ ไป สำหรับภาษาโปรแกรมทุกวันนี้ ถ้าแบ่งคร่าวๆ ด้วยชนิดของตัวแปร เราจะแบ่งได้ 2 อย่าง คือ Static Type: ต้องกำหนดชนิดตัวแปร เช่น int, string ตั้งแต่สร้างตัวแปร และ Dynamic Type ตัวแปรประเภทนี้ไม่ต้องบอกว่าเก็บค่าชนิดไหน เปลี่ยนไปได้เรื่อยๆ สำหรับภาษาแบบ Dynamic Type ที่ไม่ต้องกำหนดชนิดตัวแปรให้แน่นอน จะเซ็ตค่าเป็นอะไรก็ได้นั้นอาจจะทำให้เขียนง่าย

Async in Dart (5) รู้จักกับ FutureBuilder/StreamBuilder, ตัวช่วยสร้าง Async-Widget ใน Futter

เนื้อหาในบทนี้ เป็นคลาสเฉพาะที่มากับ Flutter Framework เท่านั้น .. ถ้าเขียน Dart ธรรมดาจะไม่มีให้ใช้นะ!! เอาจริงๆ 99% ของคนที่ศึกษาภาษา Dart ก็เพื่อเอาไปเขียนแอพ (หรือเว็บ/เด็กส์ท็อป) แบบ cross-platform ด้วยเฟรมเวิร์ก Flutter นั่นแหละ ตามความคิดของเรา จริงๆ Flutter น่าจะเลือกภาษา Kotlin มาใช้แทนมากกว่า แต่ก็มีเหตุผลหลายๆ อย่างนั่นแหละที่ทำให้ทำไม่ได้ สำหรับการเขียน Flutter

Dependency Injection กับ Service Locator ต่างกันยังไงนะ?

ในโลกของ OOP เรามักจะสร้างคลาสต่างๆ ที่มีการเรียกใช้กันต่อเป็นทอดๆ มากมาย เช่นโค้ดข้างล่างนี่ class Car { private Engine engine; public Car() { this.engine = new Engine(); } } Car car = new Car(); เราสร้างคลาส Car และ Engine