ทบทวนกระบวนการโหลดข้อมูล Future และการใช้งาน ValueNotifier

บทความใหม่ ไม่กี่เดือนก่อน โดย Ninenik Narkdee
valuelistenablebuilder refreshindicator valuenotifier

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

ดูแล้ว 392 ครั้ง


เนื้อหาตอนต่อไปนี้ เราจะกลับมาทบทวนกระบวนการ
หรือขั้นตอนในการโหลดข้อมูลหน้าๆ หนึ่งในแอป ที่เราอาจจะ
ได้ใช้งานบ่อยๆ ไม่ว่าจะเป็นการโหลดข้อมูลจาก server ผ่าน
http แล้วนำรายาการมาแสดง ซึ่งเนื้อหาเหล่านี้ เราได้เคยอธิบาย
ไว้แล้วในบทความต่างๆ ที่ผ่านมา ตามตัวอย่างลิ้งค์ด้านล่าง
 
  • การใช้งาน FutureBuilder ที่เป็น Async widgets ใน Flutter http://niik.in/1036
  • การใช้งาน RefreshIndicator ปัดเพื่อรีเฟรชข้อมูล ใน Flutter http://niik.in/1040
  • การใช้งาน Http ดึงข้อมูลจาก Server มาแสดงใน Flutter http://niik.in/1038
 
เนื้อหานี้ใช้โค้ดตัวอย่างเริ่มต้น จากบทความ ตามลิ้งค์นี้ http://niik.in/961
โดยใช้ โค้ดตัวอย่างจากส่วน เพิ่มเติมเนื้อหา ครั้งที่ 2 
 
เนื้อหาต่างๆ เหล่านี้ล้วนเป็นแนวทางที่เราสามารถนำไปปรับประยุกต์ใช้งานได้ 
อย่างไรก็ดี เพื่อให้เราสามารถเข้าใจ การทำงานที่ชัดเจน และประยุกต์ได้ง่ายขึ้น จึงจะนำมาอธิบาย
พร้อมกับเพิ่มเติมการทำงานให้ครอบคลุมมากยิ่งขึ้น 
 

สิ่งที่เราจะทำและเรียนรู้ในบทความนี้

    - รู้จักการใช้งานตัวแปร ValueNotifier สำหรับซ่อนหรือแสดง widget
    - การเรียกใช้งาน WidgetsBinding.instance.addPostFrameCallback()
    - การใช้งาน ValueListenableBuilder widget 
    - การจัดการกระบวนการโหลดข้อมูลด้วย FutureBuilder
    - สามารถกำหนดให้ปัดลงเพื่อโหลดข้อมูลใหม่ หรือกดปุ่มที่ตำแหน่งต่างๆ เพื่อโหลดข้อมูล
 
 

การใช้งาน ValueNotifier

    ValueNotifier เป็นคลาสใน Flutter ที่ใช้สำหรับการจัดการค่าที่สามารถเปลี่ยนแปลงได้
และแจ้งเตือนเมื่อค่ามีการเปลี่ยนแปลง ValueNotifier เป็นส่วนหนึ่งของแพ็กเกจ flutter และ
มีความเกี่ยวข้องกับการจัดการสถานะ (state management) ในแอปพลิเคชัน Flutter
 

    คุณสมบัติหลักของ ValueNotifier:

    เก็บค่าและแจ้งเตือน: ValueNotifier ใช้เก็บค่าหนึ่งค่า (value) และสามารถแจ้งเตือนไป
ยัง (listeners) เมื่อค่าของมันเปลี่ยนแปลง
    เชื่อมต่อกับ UI: มักใช้ร่วมกับ ValueListenableBuilder เพื่อสร้าง UI ที่ตอบสนองต่อ
การเปลี่ยนแปลงค่า โดยจะทำการ build เฉพราะส่วนที่มีการเรียกใช้งาน ไม่ได้ build ทั้งหมด
 

วิธีการใช้งาน

 
// สร้าง ValueNotifier
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
// ValueNotifier<ชนิดตัวแปร> _counter = ValueNotifier<ชนิดตัวแปร>(ค่าเริ่มต้น);

// เปลี่ยนค่า:
_counter.value = _counter.value + 1;

// การนำไปใช้งานกับ  ValueListenableBuilder widget 
// ถ้าค่า _counter จะทำการ build เฉพราะส่วนที่เรียกใช้งานเท่านั้น
ValueListenableBuilder<int>(
  valueListenable: _counter,
  builder: (context, value, child) {
    return Text('Counter value: $value');
  },
);
 
 

ข้อมูล Future และการบวนการใช้งาน FutureBuilder

    ในกระบวนการหรือขั้นตอนการดึงข้อมูลจาก server มาแสดงหรือข้อมูล future ใดๆ นั้น โดย
ทั่วไป เราจะจัดรูปแบบในลักษณะดังนี้คือ เมื่อเริ่มต้น เราจะต้องแสดงตัว loading ก่อน ซึ่งเราจะใช้
ValueNotifier สร้างตัวแปรกำหนดสถานะ การซ่อนหรือแสดงตัว loading จากนั้นต่อมา เราต้อง
มีตัวแปร สำหรับเก็บข้อมูล Future ที่จะแสดง จะต้องมีเสมอ
 
  // สร้างตัวแปรที่สามารถแจ้งเตือนการเปลี่ยนแปลงค่า 
  final ValueNotifier<bool> _visible = ValueNotifier<bool>(false);
  // ข้อมูลใน future
  Future<String?> _dataFuture = Future.value(null);
 
ในตัวอย่างจำลองนี้ เราจะจำลองข้อมูล future เป็น string โดยกำหนดค่าเริ่มต้นเป็น null
ซึ่งจริงๆ แล้วโดยทั่วไปเวลาใช้งานในรูปแบบจริง เรามักจะใช้ในรูปภาพ List<Object> ข้อมูลต่างๆ
ตัวอย่างเช่น สมมติเราไปดึงข้อมูล Article Object จากไฟล์ json บน server มา ก็อาจจะใช้เป็น
รูปแบบดังนี้ ในการกำหนดค่าเริ่มต้น
 
// ตัวอย่างกำหนดตัวแปร future สำหรับรับค่าและแสดง
Future<List<Article>> _articles = Future.value([]);
 
ในขั้นตอนต่อเรา เมื่อเราสร้างตัวแปรสำหรับรับค่าแล้ว เราจะต้องมีฟังก์ชั่นสำหรับ ทำการดึงข้อมูลนั้น
มาใส่ตัวแปรที่เรากำหนด ในที่นี้เราจำลองการดึงข้อมูลโดยหน่วงเวลา 2 วินาทีเป็นดังนี้
 
  // จำลองใช้เป็นแบบฟังก์ชั่น ให้เสมือนดึงข้อมูลจาก server
  Future<String> fetchData() async {
    print("debug: do function");
    final response = await Future<String>.delayed(
      const Duration(seconds: 2),
      () {
        return 'Data Loaded \n${DateTime.now()}';
      },
    );
    return response;
  }
 
โดยฟังก์ชั่นที่เราสร้าง จะต้องคืนค่าสัมพันธ์กับชนิดตัวแปรที่เรากำหนด ในตัวอย่างก็คือ
Future<String>
 
ตอนนี้เรามีตัวแปรสำหรับรับค่า และมีฟังก์ชั่นสำหรับดึงข้อมูลมาเก็บในตัวแปรแล้ว ต่อไป ขั้นตอน
การทำงานของโปรแกรม เมื่อโหลดหน้าแอป เราต้องทำการไปดึงข้อมูลแล้วนำมาเก็บไว้ในตัวแปร
โดยจะทำงานในส่วนของ initState ดังนี้
 
  @override
  void initState() {
    print("debug: Init");
    super.initState();
    _dataFuture = fetchData(); // ดึงข้อมูลค่าเริ่มต้น
  }
 
นั่นหมายความว่า เมื่อโหลดมาครั้งแรก จะไปทำการเรียกใช้งานฟังก์ชั่น fetchData() เพื่อดึงข้อมูล
มาเก็บไว้ในตัวแปร _dataFuture ซึ่งเป็นข้อมูล future
 
ต่อไปเข้าสู่กระบวนการ buid ดึงข้อมูล future มาแสดง
 
FutureBuilder<String?>( // การคืนค่าต้องตรงกัน ในที่นี่ เป็น string หรือ null
  future: _dataFuture, // ตัวแปร future
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {}
    if (snapshot.connectionState == ConnectionState.done) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        // Change state after the build is complete
        _visible.value = false; // ตัวไว้สำหรับซ่อนหรือแสดงสถานะตัว loading
      });
    }
    if (snapshot.hasData) {
      return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '${snapshot.data}',
            style: TextStyle(
              fontSize: 20,
            ),
          ),
        ],
      );
    } else if (snapshot.hasError) {
      return Center(child: Text('${snapshot.error}'));
    }
    return const Center(child: CircularProgressIndicator());
  },
),
 
ในตัวอย่าง เมื่อเข้ามาครั้งแรก ขณะที่ตัวฟังก์ชั่น fetchData() ทำงาน เงื่อนไขการ build
ของ FutureBuilder จะไปทำตัวสุดท้าย คือ
 
return const Center(child: CircularProgressIndicator());
 
จะแสดงตัว loading ในรูปแบบวงกลมหมุน ในขณะที่ snapshot.connectionState == 
ConnectionState.waiting และถ้ากรณีเกิด error ขึ้นก็จะแสดงในส่วนนี้
 
return Center(child: Text('${snapshot.error}'));
 
และสุดท้ายหากมีข้อมูลสุดท้ายถูกส่งกลับมา if (snapshot.hasData) { ก็จะทำการแสดงข้อความ
ที่เรากำหนดไว้ในตัวอย่างออกมา เป็นข้อความสุดท้ายของผลลัพธ์จากตัวแปร future
 
ในสถานะหรือขั้นตอนสุดท้ายของการได้ข้อมูลมา เราสามารถจัดการการทำงานเพิ่มเติม โดยใช้รูปแบบ
ดังต่อไปนี้ได้
 
    if (snapshot.connectionState == ConnectionState.done) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        // Change state after the build is complete
        _visible.value = false; // ตัวไว้สำหรับซ่อนหรือแสดงสถานะตัว loading
      });
    }
 
ข้างต้นคือสถานะที่ได้ข้อมูลมาแล้ว และ เราต้องการกำหนดค่า  _visible.value = false; หรือ
เพื่อให้ซ่อนตัว loading  แต่ในกระบวนการ build เราจะไม่สามารถ ทำการกำหนดการเปลี่ยนแปลง
ค่าใดๆ ได้ เพราะจะกลายเป็นการวนลูป build ไปเรื่อยๆ เราจึงมีการใช้ตัว 
 
 WidgetsBinding.instance.addPostFrameCallback((_) {   });
 
คลุมการทำงานอีกทีหนึ่ง ก็เพื่อบอกว่า ให้ทำงานหลักจาก การ build เสร็จเรียบร้อยแล้ว ซึ่งวิธีนี้จะทำ
ให้เราสามารถกำหนดหรือเปลี่ยนแปลงค่า state ใน การ build ได้แบบไม่มีปัญหา
 
ต่อไป เรามาดูต่อในส่วนของการ refresh ทั้งแบบเรียกฟังก์ชั่นผ่านปุ่มจากตำแหน่งต่างๆ และจากการ
ใช้งาน pull to refresh หรือปัดลงเพื่อโหลดใหม่ 
 
สิ่งที่เราต้องมีคือฟังก์ชั่นสำหรับการโหลดใหม่ เราจะสร้างขึ้นมาดังนี้
 
  Future<void> _refresh() async {
    setState(() {
      _dataFuture = fetchData();
    });
  }
 
ฟังก์ชั่น _refresh() ที่เราสร้างขึ้น จะทำการไปเรียกคำสั่ง fetchData() ใหม่เพื่อไปดึงข้อมูลมา
เก็บไว้ในตัวแปร _dataFuture และเราต้องทำการกำหนด setState เพื่อให้เกิดการ build ใหม่
อีกครั้ง
 
ในที่นี้ เรามีรูปแบบการโหลดข้อมูลใหม่อยู่ด้วยกัน 3 จุด คือ จากปุ่มเมนูบนขวา ตรง action
 
      appBar: AppBar(
        title: Text('Home'),
        actions: [
          IconButton(
            onPressed: () {
              _visible.value = true;
              _refresh();
            },
            icon: const Icon(Icons.refresh_outlined),
          )
        ],
      ),
 
ส่วนที่สองส่วนที่อยู่ตรง floatingActionButton 
 
      floatingActionButton: ValueListenableBuilder<bool>(
        valueListenable: _visible,
        builder: (context, visible, child) {
          return (visible == false)
              ? FloatingActionButton(
                  onPressed: () {
                    _visible.value = true;
                    _refresh();
                  },
                  shape: const CircleBorder(),
                  child: const Icon(Icons.refresh),
                )
              : SizedBox.shrink();
        },
      ),
 
และส่วนสุดท้ายส่วนที่กดปัดลง แล้วทำการ refresh หรือส่วนที่ใช้งาน RefreshIndicator
 
      body: RefreshIndicator(
        onRefresh: () async {
          _visible.value = true;
          _refresh();
        }, // Function to call when the user pulls to refresh
        child: ListView(
 
จะเห็นว่า ทั้ง 3 จุด การที่จะเรียกใช้งานฟังก์ชั่น _refresh() เราจะทำการกำหนดค่า _visible.value
ให้มีค่าเป็น true เพื่อใช้เป็นค่าสำหรับ สร้างการซ่อนหรือแสดงตัว loading ต่างๆ ซึ่งในตัวอย่าง
เรามึอยู่ 2 จุด จุดแรกก็ในตัว FutureBuilder ที่อธิบายไปแล้วด้านบน และอีกจุด เราใช้เป็นแบบ
เส้นแถบ ใต้ด appBar เรียกว่า  LinearProgressIndicator()
 
            ValueListenableBuilder<bool>(
              valueListenable: _visible,
              builder: (context, visible, child) {
                return Visibility(
                  visible: visible,
                  child: const LinearProgressIndicator(
                    backgroundColor: Colors.white60,
                  ),
                );
              },
            ),
 
การแสดงในลักษณะเช่นนี้ก็เพราะว่า เราไม่ต้องการล้างหน้าข้อมูลเดิมในขณะที่โหลดข้อมูลใหม่
แต่จะแสดงข้อมูลเดิมไว้ และมีสถานะกำลังโหลดข้อมูลใหม่เป็นแถบเส้นด้านบนแทน และถ้าข้อมูล
โหลดเรียบร้อยแล้ว ก็จะไปแทนที่ข้อมูลเก่าได้เลย
 
ตัวอย่างการทำงาน


 
โค้ดตัวอย่างทั้งหมด

ไฟล์ home.dart

 
import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  static const routeName = '/home';

  const Home({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _HomeState();
  }
}

class _HomeState extends State<Home> {
  // สร้างตัวแปรที่สามารถแจ้งเตือนการเปลี่ยนแปลงค่า
  final ValueNotifier<bool> _visible = ValueNotifier<bool>(false);
  // ข้อมูลใน future
  Future<String?> _dataFuture = Future.value(null);

  // จำลองใช้เป็นแบบฟังก์ชั่น ให้เสมือนดึงข้อมูลจาก server
  Future<String> fetchData() async {
    print("debug: do function");
    final response = await Future<String>.delayed(
      const Duration(seconds: 2),
      () {
        return 'Data Loaded \n${DateTime.now()}';
      },
    );
    return response;
  }

  Future<void> _refresh() async {
    setState(() {
      _dataFuture = fetchData();
    });
  }

  @override
  void initState() {
    print("debug: Init");
    super.initState();
    _dataFuture = fetchData();
  }

  @override
  void dispose() {
    _visible.dispose(); // Dispose the ValueNotifier
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print("debug: build");
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
        actions: [
          IconButton(
            onPressed: () {
              _visible.value = true;
              _refresh();
            },
            icon: const Icon(Icons.refresh_outlined),
          )
        ],
      ),
      body: RefreshIndicator(
        onRefresh: () async {
          _visible.value = true;
          _refresh();
        }, // Function to call when the user pulls to refresh
        child: ListView(
          padding: const EdgeInsets.all(8.0),
          children: [
            ValueListenableBuilder<bool>(
              valueListenable: _visible,
              builder: (context, visible, child) {
                return Visibility(
                  visible: visible,
                  child: const LinearProgressIndicator(
                    backgroundColor: Colors.white60,
                  ),
                );
              },
            ),
            FutureBuilder<String?>(
              future: _dataFuture,
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {}
                if (snapshot.connectionState == ConnectionState.done) {
                  WidgetsBinding.instance.addPostFrameCallback((_) {
                    // Change state after the build is complete
                    _visible.value = false;
                  });
                }
                if (snapshot.hasData) {
                  return Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        '${snapshot.data}',
                        style: TextStyle(
                          fontSize: 20,
                        ),
                      ),
                    ],
                  );
                } else if (snapshot.hasError) {
                  return Center(child: Text('${snapshot.error}'));
                }
                return const Center(child: CircularProgressIndicator());
              },
            ),
          ],
        ),
      ),
      floatingActionButton: ValueListenableBuilder<bool>(
        valueListenable: _visible,
        builder: (context, visible, child) {
          return (visible == false)
              ? FloatingActionButton(
                  onPressed: () {
                    _visible.value = true;
                    _refresh();
                  },
                  shape: const CircleBorder(),
                  child: const Icon(Icons.refresh),
                )
              : SizedBox.shrink();
        },
      ),
    );
  }
}
 
 
สิ่งสำคัญอีกประการที่ห้ามลืม คือ
 
  @override
  void dispose() {
    _visible.dispose(); // Dispose the ValueNotifier
    super.dispose();
  }
 
เมธอด dispose() ใช้เพื่อจัดการกับการทำความสะอาดทรัพยากรที่ใช้งานใน StatefulWidget 
เมื่อวิดเจ็ตไม่ถูกใช้งานอีกต่อไป หรือถูกลบออกจากโครงสร้างของวิดเจ็ต (widget tree) การทำ
เช่นนี้ช่วยป้องกันการรั่วไหลของหน่วยความจำและปัญหาอื่น ๆ ที่เกี่ยวข้องกับการจัดการทรัพยากร
 
หวังว่าแนวทางตัวอย่างเนื้อหานี้ จะสามารถนำไปศึกษาปรับใช้งานได้ต่อไปไม่มากก็น้อย


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



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



ทบทวนบทความที่แล้ว









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






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



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




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





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

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


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


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







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