ต่อจากเนื้อหาตอนที่แล้ว เราได้รู้จักเกี่ยวกับ 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 กันต่อ จะเป็นอะไร รอติดตาม