รู้จักและใช้งาน AsyncValue ใน Flutter Riverpod เบื้องต้น ตอนที่ 2

บทความใหม่ ไม่กี่เดือนก่อน โดย Ninenik Narkdee
notifierprovider streamprovider asyncvalue flutter riverpod futureprovider

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ notifierprovider streamprovider asyncvalue flutter_riverpod futureprovider

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


ต่อจากเนื้อหาตอนที่แล้ว เราได้รู้จักเกี่ยวกับ Riverpod ไปเบื้องต้น
ซึ่งสิ่งสำคัญหลักๆ ของ Riverpod ก็คือจัดการกับข้อมูลประเภท future
ที่จำเป็นต้องโหลดมาใช้งานในส่วนจัดการหนึ่งๆ ยกตัวอย่างเช่น เรามีส่วนที่
เกี่ยวกับสินค้า ในส่วนนั้น เราสามารถกดเข้าไปดูรายละเอียดสินค้า กดเพิ่มราย
การลงรถเข็น หรือดำเนินการอื่นใด เกี่ยวกับสินค้าในส่วนๆ นั้นจนกว่าจะออกไป
ส่วนอื่น การโหลดข้อมูลสินค้ามาใว้ในหน่วยความจำ แล้วเรียกใช้งานในหน้าที่เกี่ยวข้อง
จะทำให้เราจัดการกับข้อมูลได้ดีขึ้น โหลดเร็วขึ้น ไม่ใช่ว่า เรียกข้อมูลสินค้าทีก็ไปดึง
ข้อมูลสินค้าที แต่การใช้ Riverpod ทำให้เราแสดงข้อมุลสินค้าได้แทบทันทีที่เรียกใช้งาน
 
ทบทวนตอนที่ได้แล้วที่
ทำความรู้จักและใช้งาน Riverpod ใน Flutter เบื้องต้น ตอนที่ 1 http://niik.in/1107
 
อย่างไรก็ดี การใช้งาน Riverpod ถึงจะไม่ได้ดูยากอะไร แต่การเข้าใจการทำงาน และเลือก
รูปแบบของการใช้งานที่ถูกต้อง ก็มีส่วนช่วยในการจัดการข้อมูลได้ 
มาศึกษากันต่อเกี่ยวกับ Flutter Riverpod พยายามให้เข้าใจการทำงาน
 

AsyncValue ใน Flutter Riverpod คืออะไร

    AsyncValue ใน Flutter Riverpod เป็นคลาสที่ใช้เพื่อจัดการและแสดงสถานะของข้อมูล
ที่อาจใช้เวลาในการโหลดหรืออาจล้มเหลวได้ เช่น ข้อมูลที่ดึงมาจาก API หรือดาต้าเบส โดย 
AsyncValue รองรับการจัดการข้อมูลใน 3 สถานะหลัก ได้แก่:
 
loading: สถานะที่บ่งบอกว่ากำลังโหลดข้อมูลอยู่ (เช่น เมื่อทำการเรียก API แต่ข้อมูลยังไม่กลับมา)
data: สถานะที่ข้อมูลถูกโหลดสำเร็จ และข้อมูลถูกเก็บอยู่ในตัวแปรนี้
error: สถานะที่เกิดข้อผิดพลาดขณะโหลดข้อมูล เช่น เมื่อ API ล้มเหลว หรือมีข้อผิดพลาดอื่นๆ
 

โครงสร้างพื้นฐานของการใช้งานข้อมูล AsyncValue

final productAsyncValue = ref.watch(productProvider);

return productAsyncValue.when(
  data: (products) => Text('Loaded ${products.length} products'),
  loading: () => CircularProgressIndicator(),
  error: (error, stackTrace) => Text('Error: $error'),
);
 
data: รับฟังก์ชันที่มีข้อมูลซึ่งถูกโหลดสำเร็จแล้ว
loading: รับฟังก์ชันที่เรียกใช้เมื่อข้อมูลกำลังโหลด
error: รับฟังก์ชันที่จัดการข้อผิดพลาดที่เกิดขึ้น
 
เราจะจำลองการใช้งาน FutureProvider ให้เห็นภาพ จะเขียนทั้งหมดไว้ในไฟล์เดียว
 

ไฟล์ home.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Data models 
class Product {
  int id;
  String title;
  num price;
  DateTime accessDate;
 
  Product({
    required this.id,
    required this.title,
    required this.price,
    required this.accessDate,
  });
 
}
// ตัวอย่างการกำหนด
final productProvider = FutureProvider<List<Product>>((ref) async {
  print("debug: run provider");
  List<Product> productAll = [
    Product(id: 1, title: "Product 1", price: 100, accessDate: DateTime.now()),
    Product(id: 2, title: "Product 2", price: 300, accessDate: DateTime.now()),
    Product(id: 3, title: "Product 3", price: 500, accessDate: DateTime.now()),
  ];
  // จำลองการดึงข้อมูลจาก API หน่่วงเวลา 2 วินาที
  await Future.delayed(const Duration(seconds: 2));
  return productAll;
});

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

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

  @override
  ConsumerState<ConsumerStatefulWidget> createState() {
    return _HomeState();
  }
}

class _HomeState extends ConsumerState<Home> {

  @override
  Widget build(BuildContext context) {
    final productAsyncValue = ref.watch(productProvider);
    print("debug: build");
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
        leading: IconButton(
          icon: Icon(Icons.menu),
          onPressed: () {
            Scaffold.of(context).openDrawer();
          },
        ),
      ),
      body: productAsyncValue.when(
        data: (products) { // เมื่อมีข้อมูลสินค้ากลับมา วนลูปสร้าง widget
          return ListView.builder(
            itemCount: products.length,
            itemBuilder: (context, index) {
              final product = products[index];
              return ListTile(
                title: Text(product.title),
                subtitle: Text('$${product.price}'),
                trailing: Text('${product.accessDate.toString().substring(0,19)}'),
              );
            },
          );
        },
        loading: () => Center(child: CircularProgressIndicator()),
        error: (error, stackTrace) => Center(child: Text('Error: $error')),
      ),
    );
  }
}
 
จะเห็นว่ารูปแบบการเรียกใช้งานก็จะคล้าย FutureBuilder แต่รูปแบบนี้จะดูง่ายกว่า โดยเมื่อเรียก
ใช้งาน provider ครั้งแรก รายการสินค้าจะถูกเก็บไว้ในตัว provider และ cache ไว้ในหน่วย
ความจำ เมื่ออ่านค่ามาแสดงด้วย ref.watch(productProvider) จะส่งค่าข้อมูลที่เป็น 
AsyncValue มาเก็บไว้ในตัวแปร productAsyncValue ข้อมูลที่มีเรื่องของเวลาที่ต้องรอเข้ามา
เกี่ยวข้อง และเมื่อได้ข้อมูลหรือ data กลับมาก็เข้าสู่เงื่อนไขการสร้าง widget แสดงผลตามโครงสร้าง
รูปแบบตัวอย่างด้านบน
 
final productAsyncValue = ref.watch(productProvider);
 
ทันทีที่มีการอ่านค่าจาก provider ด้วย watch ในกรณีนี้ ข้อมูลจะเป็นข้อมูลแรกที่โหลดและเป็นค่า
เดิมไปตลอดจนกว่าจะเริ่มแอปใหม่ สังเกตได้จากตัวอย่าง เรามีการกำหนดวันที่เวลาข้อมูลเข้าไปใน
สินค้าเพื่อจำลองการทำงาน จะเป็นเวลาเดียวจนกว่าจะรันแอปใหม่
 
การใช้งานและการกำหนด Provider ในลักษณะนี้ เหมาะกับรายการข้อมูลที่ค่อนข้างจะคงที่ หรือไม่
ค่อยมีการเปลี่ยนแปลง ทำให้เราสามารถใช้งานข้อมุลนี้ได้เรื่อยๆ ซึ่งถ้าเราไปหน้าอื่น แล้วกลับมาหน้า
ข้อมูลนี้ ข้อมูลจะแสดงทันที ซึ่งถ้าเราเปลี่ยนจากข้อมูลสินค้าแบบกำหนดตายตัว แล้วใช้เป็นแบบดึงจาก
API ของ server วิธีการนี้ก็จะทำให้เราโหลดข้อมูลมาแค่ครั้งแรก จากน้้นก็จะใช้ค่าจากหน่วยความจำ
เป็นหลัก จนกว่าจะมีการดึงค่าใหม่
 
อย่างไรก็ดี เราสามารถกำหนดให้มีการเรียกข้อมูลใหม่ จาก provider โดยใช้รูปแบบการเรียกใช้งาน
ด้วย ref.refresh  เป็นฟังก์ชันใน Riverpod ที่ใช้เพื่อบังคับให้รีเฟรช (หรือโหลดข้อมูลใหม่) จาก 
provider ที่ระบุ โดยจะทำให้ FutureProvider, StreamProvider, หรือ NotifierProvider 
เริ่มทำงานใหม่และรีเซ็ตสถานะของ provider นั้นๆ
 

ต้วอย่างเรากำหนดในส่วนของ ปุ่ม action

actions: [
  IconButton(
    onPressed: () {
      ref.refresh(productProvider.future);
    },
    icon: Icon(Icons.refresh_outlined),
  ),
],  
 

หรือกำหนดในส่วนของ pull to refresh

body: RefreshIndicator(
        onRefresh: () async {
          ref.refresh(productProvider.future);
        },
        child: productAsyncValue.when(
......
 
เมื่อเรากำหนดในลักษณะนี้ลงไป แล้วเรียกใช้งานหรือกดรีเฟรสข้อมูล ก็จะเป็นการไปดึงข้อมูลใหม่มา
ใช้งาน สามารถสังเกตได้จากเวลาของข้อมูล ที่เปลี่ยนแปลงเป็นเวลา ณ ปัจจุบัน     
การทำงานของ refresh ก็จะเหมือนกับการล้างค่าข้อมูลเก่าออกจากหน่วยความจำ แล้วทำการสร้าง
และดึงค่าใหม่มาใช้งาน
    ในค่าเริ่มต้นของ Flutter Riverpod ค่าข้อมูลจาก Future และ Stream จะถูกแปลงค่าเป็น
ข้อมูลประเภท AsyncValue ทุกครั้ง เพื่อใช้งาน
 
 

ตัวอย่างการใช้ FutureProvider อ่านค่าจากไฟล์มาใช้งาน

final configProvider = FutureProvider<Configuration>((ref) async {
  final content = json.decode(
    await rootBundle.loadString('assets/configurations.json'),
  ) as Map<String, Object?>;

  return Configuration.fromJson(content);
});  
 

ตัวอย่างการใช้ FutureProvider อ่านข้อมูล API จาก server

final activityProvider = FutureProvider((ref) async {
  // Using package:http, we fetch a random activity from the Bored API.
  final response = await http.get(Uri.https('boredapi.com', '/api/activity'));
// Using dart:convert, we then decode the JSON payload into a Map data
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  // Finally, we convert the Map into an Activity instance.
  return Activity.fromJson(json);
});
 

ตัวอย่างแก้ไข เราดึงข้อมูลจาก api จำลอง

// Data models 
class Product {
  int id;
  String title;
  num price;
  DateTime accessDate;

  Product({
    required this.id,
    required this.title,
    required this.price,
    required this.accessDate,
  });

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'] as int,
      title: json['title'] as String,
      price: json['price'] as num,
      accessDate: DateTime.parse(json['accessDate'] as String),
    );
  }
}

List<Product> parseProducts(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
  return parsed.map<Product>((json) => Product.fromJson(json)).toList();
}  

// ตัวอย่างการกำหนด
final productProvider = FutureProvider<List<Product>>((ref) async {
  print("debug: run provider");
  const urlApi = "https://www.ninenik.com/demo/api/simpleproduct";
  final response = await http.get(Uri.parse(urlApi));
  return parseProducts(response.body);

});
 
ผลลัพธ์




    จะเห็นว่าการใช้งาน Riverpod เราไม่จำเป็นต้องกำหนดการตรวจสอบอะไรมากในส่วนของการ
เรียกใช้งาน API เพราะมีส่วนจัดการทั้งหมดไว้ให้แล้ว ไม่ต้องใช้ try ...  catch ก็ได้ อย่างไรก็ดี
หากอยากได้รูปแบบการแจ้งเตือนข้อผิดพลาดตามที่ต้องการ เราก็ยังสามารถใช้งาน try ... catch
เพื่อกำหนดรูปแบบเองได้ตามต้องการ ตัวอย่างการใช้งาน
 
import 'package:riverpod/riverpod.dart';

// สร้าง DataModel สำหรับข้อมูลของคุณ
class MyData {
  final String name;
  MyData(this.name);
}

// สร้าง AsyncNotifier พร้อมกับ try-catch เพื่อจัดการข้อผิดพลาด
class MyDataNotifier extends AsyncNotifier<MyData> {
  @override
  Future<MyData> build() async {
    return await fetchData();
  }

  // ฟังก์ชันสำหรับดึงข้อมูล
  Future<MyData> fetchData() async {
    try {
      // จำลองการดึงข้อมูลจาก API
      await Future.delayed(Duration(seconds: 2)); // จำลองการหน่วงเวลา

      // ตัวอย่างการดึงข้อมูลที่อาจเกิดข้อผิดพลาด
      if (DateTime.now().second % 2 == 0) {
        throw Exception('Failed to fetch data');
      }

      return MyData('Example Data'); // ส่งคืนข้อมูลตัวอย่าง
    } catch (error) {
      // จัดการข้อผิดพลาด
      print('Error occurred: $error');
      // คุณสามารถโยนข้อผิดพลาดต่อหรือคืนค่าอื่นที่เป็นการสำรองก็ได้
      throw Exception('Unable to load data: $error');
    }
  }
}

// สร้าง provider สำหรับ MyDataNotifier
final myDataProvider = AsyncNotifierProvider<MyDataNotifier, MyData>(() {
  return MyDataNotifier();
});
 
    จากตัวอย่าง เราจะเห็นว่า หากต้องการ การปรับแต่งที่มากกว่า และหลากหลายกว่า เราสามารถ
ใช้งาน  AsyncNotifierProvider แทน FutrueProvider หรือ StreamProvider ได้ เช่น
เดียวกัน เราก็ยังสามารถใช้ NotifierProvider แทน Provider อื่นๆ ได้ หากต้องการการปรับ
แต่งที่หลากหลาย แต่ถ้าเป้นลักษณะการใช้งานแบบง่าย ก็ไม่จำเป็นต้องกำหนดรูปแบบการใช้งาน
ที่ยุ่งยาก ดังนั้นเลือกใช้งานให้เหมาะสมตามต้องการ
 
เนื้อหาตอนหน้าเรายังอยู่เกี่ยวกับการใช้งาน Riverpod กันต่อ จะเป็นอะไร รอติดตาม


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



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



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









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






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

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

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

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



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




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





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

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


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


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







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