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


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

เขียนเมื่อ 1 ปีก่อน โดย Ninenik Narkdee
refreshindicator valuelistenablebuilder valuenotifier

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

ปัจจุบัน นักพัฒนาสามารถ ใช้ ChatGPT | Gemini | Claude | Perplexity | Deepseek ช่วยในการแก้ไขปัญหาต่างๆ ในการเขียนโปรแกรม หรือหาข้อมูลเพิ่มเติมได้ง่ายและสะดวก แนะนำให้ทุกคนใช้งานเพื่อพัฒนาศักยภาพของตัวเอง

ดูแล้ว 2,097 ครั้ง


เนื้อหาตอนต่อไปนี้ เราจะกลับมาทบทวนกระบวนการ
หรือขั้นตอนในการโหลดข้อมูลหน้าๆ หนึ่งในแอป ที่เราอาจจะ
ได้ใช้งานบ่อยๆ ไม่ว่าจะเป็นการโหลดข้อมูลจาก 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) การทำ
เช่นนี้ช่วยป้องกันการรั่วไหลของหน่วยความจำและปัญหาอื่น ๆ ที่เกี่ยวข้องกับการจัดการทรัพยากร
 
หวังว่าแนวทางตัวอย่างเนื้อหานี้ จะสามารถนำไปศึกษาปรับใช้งานได้ต่อไปไม่มากก็น้อย





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



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









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






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

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

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

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



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




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








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