เนื้อหาตอนต่อไปนี้ เรามาดูกันต่อเกี่ยวกับ Riverpod จะมาดูในส่วนของ
Modifiers หรือการปรับแต่งการใช้งานของการกำหนด Provider ซึ่งใน
Riverpod จะมีให้ใช้งาน อยู่ 2 แบบคือ autoDispose และ family ตาม
ที่ได้แนะนำเบื้องต้นไปแล้วในตอนแรก ของบทความ Riverpod
*ข้อควรรู้สำคัญ: Flutter Riverpod คือส่วนจัดการที่ทำได้เหมือน Provider และมีความ
สามารถมากกว่า ดังนั้น เราควรต้องเลือกใช้งานอย่างใดอย่างหนึ่ง ถ้าไม่จำเป็นไม่ควรใช้ร่วมกัน
เพราะจะทำให้โครงสร้างโค้ดดูซับซ้อนอาจเกิดความสับสนได้
*ข้อควรรู้สำคัญ: Flutter Riverpod คือส่วนจัดการที่ทำได้เหมือน Provider และมีความ
สามารถมากกว่า ดังนั้น เราควรต้องเลือกใช้งานอย่างใดอย่างหนึ่ง ถ้าไม่จำเป็นไม่ควรใช้ร่วมกัน
เพราะจะทำให้โครงสร้างโค้ดดูซับซ้อนอาจเกิดความสับสนได้
ทบทวนบทความก่อนหน้าได้ที่
ทำความรู้จักและใช้งาน Riverpod ใน Flutter เบื้องต้น ตอนที่ 1 http://niik.in/1107
รู้จักและใช้งาน AsyncValue ใน Flutter Riverpod เบื้องต้น ตอนที่ 2 http://niik.in/1108
การใช้งาน Provider Modifiers
ปกติแล้วเมื่อมีการใช้งาน Riverpod จัดการข้อมูล State ผ่าน Provider ข้อมูลจะถูก cache
เพื่อให้สามารถใช้งานผ่านส่วนต่างๆได้ และไม่มีการล้างค่าและลบออกค่าสถานะของ state อัตโนมัติ
ซึ่งถ้าแอปของเราไม่ได้มีหลายส่วน ที่แยกกันไม่เกี่ยวข้องกัน หรือทั้งแอปมีส่วนที่เกี่ยวข้องกันทั้งหมด
แค่ส่วนเดียว เราอาจจะไม่จำเป็นต้องพิจารณาส่วนนี้ก็ได้ เพื่อให้แอปทำงานได้เต็มประสิทธิภาพ และใช้
การล้างค่า แบบกำหนดเองแทนได้ อย่างไรก็ดี ส่วนใหญ่แล้วแอปจะมีหลายๆ ส่วนที่แยกกัน ดังนั้น
การเลือกใช้การล้างค่า cache อัตโนมัติ จึงเป็นวิธีที่เหมาะสม ป้องกันการรั่วไหลของการใช้งานหน่วย
ความจำ ทำให้เกิดป้ญหาได้ ตัว modifier ของ Riverpod ที่จัดการในส่วนนี้คือ autoDispose
autoDispose: จะทำการล้างแคชอัตโนมัติเมื่อ provider ไม่ถูกใช้งาน
family: ช่วยให้สามารถส่งอาร์กิวเมนต์ไปยัง provider ได้
สำหรับ family เป็น modifier อีกตัว ที่เราสามารถใช้เพื่อส่งค่าอาร์กิวเมนต์เข้าไปดำเนินการ
ในส่วนจัดการ state และหากมีการใช้งาน family ก็ย่อมทำให้เกิด provider ที่มีค่าที่เปลียน
แปลงไปตามอาร์กิวเมนต์ที่ส่งเข้าไป ทำให้มีการอ้างอิง provider จำนวนมาก เสี่ยงต่อการเกิดปัญหา
การใช้งานหน่วยความจำ ดังนั้น หากมีการใช้งาน family ก็มักจะใช้งานร่วมกับ autoDispose
เสมอ เพื่อป้องกันปัญหานี้
ดูตัวอย่างการกำหนด provider ข้างล่าง 2 อันนี้
// 1 provider แบบฟังก์ชั่น final activityProvider = FutureProvider.autoDispose((ref) async { // TODO: perform a network request to fetch an activity return fetchActivity(); }); // 2 แบบ class extends จาก notifier อีกที final activityProvider2 = AsyncNotifierProvider<ActivityNotifier, Activity>( ActivityNotifier.new, ); class ActivityNotifier extends AsyncNotifier<Activity> { @override Future<Activity> build() async { // TODO: perform a network request to fetch an activity return fetchActivity(); } }
อย่างที่ได้อธิบายไปในตอนต้นๆ หากเราต้องการให้สามารถปรับแต่งที่มากขึ้น เราสามารถใช้
ในรูปแบบ class ได้ ในตัวอย่างข้างต้น แบบแรก มีการใช้งาน modifier แบบ autoDispose
นั้นคือหาก provider ไม่ถูกเรียกใช้งาน หรือมีการเปลี่ยนไปหน้าที่ไม่ได้ใช้งาน provider นี้แล้ว
ค่า cache ของ provider จะถูกล้างค่าและลบออกไปอัตโนมัติ
ในตัวอย่างที่ 2 ไม่มีการใช้งาน modifier แต่แบบที่สองนี้ จะขออธิบายเพิ่มเติม เกี่ยวกับการ
เลือกใช้งาน class ที่จะ extends มาใช้งาน ดังนี้
ถ้าใช้ Provider -> ควรทำการ extends จากคลาส NotifierProvider -> Notifier AsyncNotifierProvider -> AsyncNotifier AsyncNotifierProvider.autoDispose -> AutoDisposeAsyncNotifier AsyncNotifierProvider.autoDispose.family -> AutoDisposeFamilyAsyncNotifier
ตัวอย่างที่ 2 ใช้แบบ AsyncNotifierProvider จึงต้องทำการ extends จาก AsyncNotifier
รูปแบบการใช้งาน AsyncNotifierProvider.autoDispose.family คือมีการใช้งาน modifier
ทั้ง autoDispose และ family
ถ้ามี Async ให้จำไว้ว่า เป็นการดำเนินการที่ต้องมีเวลาที่ต้องรอเข้ามาเกี่ยวข้อง เช่น ดึงข้อมูลจาก
File หรือจาก Serer ผ่าน API เหล่านี้เป็นต้น
หากมีการใช้งาน autoDispose จะไม่ซับซ้อนอะไร เป็นการกำหนดให้ สามารถล้างค่า cache หาก
ไม่ได้ใช้งานแล้วอัตโนมัติ แต่สำหรับ family จะเป็นกำหนดให้สามารถ ส่งค่าตัวแปรเข้าไปได้ ดังนั้น
จึงสามารถปรับได้เป็นดังนี้
// 1 provider แบบฟังก์ชั่น final activityProvider = FutureProvider.autoDispose .family<Activity, String>((ref, activityType) async { // รับค่า activityType ส่งเข้าไปใช้งานในฟังก์ชั่น fetchActivity return fetchActivity(activityType); }); // 2 แบบ class extends จาก notifier อีกที final activityProvider2 = AsyncNotifierProvider.autoDispose .family<ActivityNotifier, Activity, String>( ActivityNotifier.new, ); class ActivityNotifier extends AutoDisposeFamilyAsyncNotifier<Activity, String> { @override Future<Activity> build(String activityType) async { // สามารถดูค่า อาร์กิวเมนต์ผ่านการเรียกใช้งาน "this.arg" print(this.arg); // รับค่า activityType ส่งเข้าไปใช้งานในฟังก์ชั่น fetchActivity return fetchActivity(activityType); } }
ตอนนี้เราเห็นภาพการใช้งานภาพรวมเบื้องต้นแล้ว จะขอยกตัวอย่าง อธิบายเพิ่มเติมจากตอนที่แล้ว
เราจะปรับโค้ดที่ดึงข้อมูลจำลอง เป็นดังนี้ เพื่อใช้งาน autoDispose modifier
// ตัวอย่างการกำหนด final productProvider = FutureProvider.autoDispose<List<Product>>((ref) async { ref.onCancel((){ print("debug: No one listens to me anymore!"); }); ref.onDispose((){ print("debug: If I've been defined as `.autoDispose`, I just got disposed!"); }); 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); });
กรณี provider ข้างต้น เราสามารถำหนด autoDispose เพิ่มเข้าไปได้เลย และไม่ต้องทำอะไร
เมื่อไม่มีการใช้งาน ค่า cache ของ provider จะถูกทำลายและลบออกไปเอง
แต่ถ้าเราอยากให้สามารถกำหนดหรือเปรับแต่งเพิ่มขึ้น สามารถปรับมาใช้ตัว AsyncNotifierProvider.autoDispose แทนดังนี้
class ProductNotifier extends AutoDisposeAsyncNotifier<List<Product>> { @override Future<List<Product>> build() async { ref.onCancel((){ print("debug: No one listens to me anymore!"); }); ref.onDispose((){ print("debug: If I've been defined as `.autoDispose`, I just got disposed!"); }); const urlApi = "https://www.ninenik.com/demo/api/simpleproduct"; final response = await http.get(Uri.parse(urlApi)); return parseProducts(response.body); } } final productProvider = AsyncNotifierProvider.autoDispose<ProductNotifier, List<Product>>( ProductNotifier.new, );
การทำงานของ ref.onCancel((){}); จะเกิดขึ้นทุกครั้งเมื่อเราเปลี่ยนไปหน้าอื่่น
ส่วน ref.onDispose((){}); จะเกิดขึ้นก็ต่อเมื่อเรามีการใช้งาน autoDispose modifier และมี
การเปลี่ยนไปหน้าอื่นหรือเรียกข้อมูลใหม่ เราสามารถกำหนดให้ทำงานบางอย่างที่ต้องการในส่วนนี้ได้หากจำเป็น
ต่อไป เราจะจำลองการใช้งาน family modifier เราจะทำการส่งค่า รูปแบบการจัดเรียงข้อมูล เดิม
รายการสินค้าของเราแสดงราคาจากน้อยไปมาก เราจะให้สามารถส่งค่าอาร์กิวเมนต์ที่เลือกได้ว่าจะเรียง
ราคาจากมากไปน้อย หรือน้อยไปมาก เพื่อให้ provider แสดงข้อมูลตามที่เราต้องการ
ไฟล์ home.dart
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; // 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.autoDispose .family<List<Product>, String>((ref, [sortType ='asc']) async { print("debug: run provider"); const urlApi = "https://www.ninenik.com/demo/api/simpleproduct"; final response = await http.get(Uri.parse(urlApi)); // ส่งกลับข้อมูลไปแสดง เอาแค่ 20 รายการ List<Product> productAll = parseProducts(response.body).take(20).toList(); // ทดสอบแสดงข้อมูล ก่อนเรียง productAll.forEach((product) => print('debug: ${product.title}: ${product.price}')); if(sortType=='asc'){ print("debug: sort asc"); productAll.sort((a, b) => a.price.compareTo(b.price)); }else{ print("debug: sort desc"); productAll.sort((a, b) => b.price.compareTo(a.price)); } // ทดสอบแสดงข้อมูล หลังเรียง productAll.forEach((product) => print('debug: ${product.title}: ${product.price}')); 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> { String _sortType = 'asc'; // กำหนดค่าเริ่มต้น @override Widget build(BuildContext context) { final productAsyncValue = ref.watch(productProvider(_sortType)); print("debug: build"); return Scaffold( appBar: AppBar( title: Text('Home'), leading: IconButton( icon: Icon(Icons.menu), onPressed: () { Scaffold.of(context).openDrawer(); }, ), actions: [ IconButton( onPressed: () { // สลับค่าการเรียง 'asc' และ 'desc' setState(() { _sortType = _sortType == 'asc' ? 'desc' : 'asc'; }); ref.refresh(productProvider(_sortType).future); }, icon: Icon(Icons.sort_outlined), ), IconButton( onPressed: () { ref.refresh(productProvider(_sortType).future); }, icon: Icon(Icons.refresh_outlined), ), ], ), body: RefreshIndicator( onRefresh: () async { ref.refresh(productProvider(_sortType).future); }, child: productAsyncValue.when( data: (products) { // เมื่อมีข้อมูลสินค้ากลับมา วนลูปสร้างikpdki 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')), ), ), ); } }
การใช้งาน ทั้ง autoDispose และ family จะทำให้เราสามารถส่งข้อมูล String กำหนดรูปแบบ
การเรียงข้อมูลเข้าไปจัดรูปแบบรายการก่อนส่งกลับมาแสดงได้ ในตัวอย่างเราใช้ state ชื่อ _sortType
เข้ามาช่วย เวลาเรียกใช้งาน ก็จะต้องปรับเป็นดังนี้
final productAsyncValue = ref.watch(productProvider(_sortType)); // เดิมกรณีไม่ได้ส่งค่าอาร์กิวเมนต์เข้าไป จะเป็นลักษณะนี้ // final productAsynValue = ref.watch(productProvider);
มีการส่ง _sortType เข้าไปใน provider เป็นคุณสมบัติของ family modifier นอกจากนั้นสิ่ง
ที่เราจะสังเกตเห็นได้ชัดคือ รายการข้อมูลจะมีจังหวะแสดงตัวโหลดข้อมูลทุกครั้งที่มีการไปหน้าอื่นแล้ว
ย้อนกลับมาหน้ารายการ ทั้งนี้เพราะมีการล้างค่าทุกครั้งที่ไม่ได้ใช้งาน
ต่อไปลองปรับรูปแบบจาก FutureProvider มาเป็นแบบ class แทนจะได้เป็นดังนี้
class ProductNotifier extends AutoDisposeFamilyAsyncNotifier<List<Product>, String> { @override Future<List<Product>> build(String sortType) async { const urlApi = "https://www.ninenik.com/demo/api/simpleproduct"; final response = await http.get(Uri.parse(urlApi)); // ส่งกลับข้อมูลไปแสดง เอาแค่ 20 รายการ List<Product> productAll = parseProducts(response.body).take(20).toList(); // ทดสอบแสดงข้อมูล ก่อนเรียง productAll.forEach((product) => print('debug: ${product.title}: ${product.price}')); if(this.arg=='asc'){ print("debug: sort asc"); productAll.sort((a, b) => a.price.compareTo(b.price)); }else{ print("debug: sort desc"); productAll.sort((a, b) => b.price.compareTo(a.price)); } // ทดสอบแสดงข้อมูล หลังเรียง productAll.forEach((product) => print('debug: ${product.title}: ${product.price}')); return productAll; } } final productProvider = AsyncNotifierProvider.autoDispose .family<ProductNotifier, List<Product>, String>( ProductNotifier.new, );
โค้ดส่วนอื่นยังเหมือนเดิม เปลี่ยนแค่รูปแบบมาใช้เป็นแบบ class แทนก็จะได้ผลลัพธ์เหมือนกัน
ก่อนจบ เนื้อหาเกี่ยวกับ Flutter Riverpod ตัวอย่างอีกเล็กน้อย สมมติเราต้องการกำหนด state
การจัดเรียงข้อมูล ให้สามารถคงค่าเดิมไว้ แม้ออกไปแล้วกลับมาก็ยังให้ใช้ค่าการเรียงล่าสุดที่เลือก
สามารถกำหนดเป็นดังนี้แทนได้
// สร้าง StateProvider สำหรับเก็บ sortType final sortTypeProvider = StateProvider<String>((ref) => 'asc');
และในส่วนของคำสั่ง build ก็ดึงค่ามาใช้งาน
@override Widget build(BuildContext context) { // อ่านค่า sortType จาก provider final _sortType = ref.watch(sortTypeProvider); final productAsyncValue = ref.watch(productProvider(_sortType)); print("debug: build"); return Scaffold( appBar: AppBar(
และในส่วนของการกดปุ่มเพื่อเปลี่ยนค่า ก็ปรับเป็นดังนี้
IconButton( onPressed: () { // สลับค่าการเรียง 'asc' และ 'desc' final _newSortType = _sortType == 'asc' ? 'desc' : 'asc'; ref.read(sortTypeProvider.notifier).state = _newSortType; ref.refresh(productProvider(_sortType).future); }, icon: Icon(Icons.sort_outlined), ),
แบบนี้เราก็จะสามารถคงค่ารูปแบบการเรียงข้อมูลให้คงไว้จนกว่าจะปิดแอปไปได้
เนื้อหาโดยภาพรวมของ Flutter Riverpod ก็ขอจบลงเพียงเท่านี้ สามารถศึกษาเพิ่มเติมได้
ที่ลิ้งค์หมายเหตุด้านล่าง ประยุกต์ปรับใช้ให้เหมาะสมตามต้องการ
ในตอนหน้าเนื้อหาจะเกี่ยวกับอะไรใหม่ รอติดตาม