จากตอนที่แล้ว เราได้เห็นความสำคัญของการจัดการข้อมูล state
โดยเฉพาะข้อมูลที่มีจำนวนมากและมีความซับซ้อน การเพิ่มความเร็ว
ด้วยวิธีการ cache เพื่อให้ข้อมูลถูกเรียกใช้งานได้เร็วขึ้นโดยวิธีการสร้าง
เป็นไฟล์แล้วเรียกใช้งาน ก็ถึอเป็นอีกวิธีจัดการกับข้อมูล state อย่างไรก็ดี
ยังมีวิธีที่จะเพิ่มความเร็วของการโหลดและแสดงข้อมูลในภาพรวมได้อีก นั่นคือ
การใช้งาน Riverpod ซึ่เป็น package สำหรับจัดการข้อมูล state โดยทำงาน
ในลักษณะเดียวกับ Provider ซึ่งเราเคยแนะนำไว้แล้ว ที่บทความตามลิ้งค์ด้านล่าง
การใช้งาน Provider จัดการข้อมูล App State ใน Flutter http://niik.in/1046
การใช้งาน Riverpod จะทำให้เราสามารถแสดงข้อมูลได้เร็วขึ้น อย่างตัวอย่างตอนที่แล้ว เมื่อเรา
ทำการโหลดข้อมูลจากไฟล์ แน่นอนว่าจะเร็วกว่าการโหลดจาก server แต่ถ้าเราเปิดไปหน้าอื่น
แล้วกลับมาหน้าเดิม เราจะพบว่าข้อมูลก็ยังมีจังหวะการโหลดข้อมูลจากไฟล์ให้เราได้เห็น โดย
สังเกตจากจังหวะที่มีตัว loading หมุนสักพัก ถึงแม้จะไม่นานมาก หรือบางครั้งอาจจะไม่เห็นเลย
แต่การใช้ riverpod เข้ามาช่วย จะทำให้เราเห็นความแตกต่างคือ ข้อมูลจะแสดงแทบจะทันทีที่
เรากลับมาหน้าเดิม เป็นไปในลักษณะที่ข้อมูลพร้อมใช้มากขึ้นกว่าเดิม เพราะมีการทำการ cache
เพิ่มเติมเข้ามาอีกโดยเก็บไว้ในหน่วยความจำ ทำให้บางครั้งเวลาเราสลับไปหน้าอื่นๆ อาจจะแทบไม่
เห็นการโหลด นอกจากนั้น riverpod ยังสามารถประยุกต์ใช้งานกับ state อื่นๆ ได้อีกมากมาย
อย่างไรก็ดี ด้วยความสามารถที่เพิ่มขึ้น เราก็จำเป็นจะต้องเข้าใจและจัดการการทำงานให้เหมาะสม
ไปด้วย ไม่เช่นนั้นก็อาจจะมีผลกับการใช้งานหน่วยความจำและส่งผลต่อประสิทธิภาพโดยรวมของ
แอปของเราได้
ติตดั้ง flutter_riverpod
ให้ทำการเพิ่มส่วนของ flutter_riverpod package เข้าไปสำหรับใช้งาน
flutter_riverpod: ^2.5.1
จากนั้นทำการเพิ่ม ProviderScope เข้าไปในส่วน main() ฟังก์ชั่น ดังรูป เพื่อให้แอปของเรา
พร้อมใช้งาน riverpod โดยเราจะใช้กับทั้งหมดของ แอป
void main() { runApp( ProviderScope( child: MyApp(), ), ); }
ก่อนจะลงไปต่อที่ตัวอย่างและรายละเอียดเรามาทำความรู้จักกับ Riverpod กันก่อนดังนี้
Riverpod คืออะไร
Riverpod เป็น state management library สำหรับ Flutter ที่ถูกพัฒนาโดยผู้สร้างเดียวกัน
กับ Provider โดยมีการออกแบบให้มีประสิทธิภาพและยืดหยุ่นมากขึ้น การใช้ Riverpod ช่วยให้การ
จัดการสถานะ (state) ในแอปพลิเคชัน Flutter ง่ายขึ้นและมีความปลอดภัยมากขึ้น
หลักการใช้งาน Riverpod
- State Management: Riverpod ถูกใช้ในการจัดการสถานะในแอปพลิเคชัน Flutter
เช่น การเก็บและอัปเดตข้อมูลที่ใช้งานร่วมกันในหลายๆ ส่วนของแอปพลิเคชัน
- Dependency Injection: Riverpod ช่วยจัดการการแทรกการพึ่งพา (dependency
injection) โดยง่าย โดยสามารถประกาศและใช้ providers เพื่อจัดการการพึ่งพาระหว่า Object
ต่างๆ ได้อย่างมีประสิทธิภาพ
- Auto Dispose: Riverpod มีความสามารถในการจัดการกับ providers ที่ไม่จำเป็นต้องใช้
งานอีกต่อไปโดยอัตโนมัติ (auto dispose) ซึ่งช่วยประหยัดหน่วยความจำ
- Type Safety: Riverpod รองรับการตรวจสอบประเภท (type safety) ซึ่งช่วยลดข้อ
ผิดพลาดในระหว่างการพัฒนา
ตัวอย่างการใช้งาน
- Provider ใช้สำหรับการจัดการค่าคงที่ (immutable values)
มักใช้กำหนด: ค่าคงที่หรือค่าที่ไม่เปลี่ยนแปลงบ่อย ๆ และไม่ต้องการการอัปเดตซ้ำบ่อยๆ
// ตัวอย่างการกำหนด final greetingProvider = Provider<String>((ref) { return 'Hello, Riverpod!'; });
- StateProvider ใช้สำหรับการจัดการ state แบบง่ายๆ ซึ่งสามารถเก็บค่าหรือข้อมูลใดๆ
ที่สามารถเปลี่ยนแปลงได้ในระหว่างการทำงานของแอปพลิเคชัน
ที่สามารถเปลี่ยนแปลงได้ในระหว่างการทำงานของแอปพลิเคชัน
มักใช้ในการกำหนดเงื่อนไข filter หรือใช้กับ state object ค่าง่ายๆ อย่างค่า int
bool, String หรือ List ที่สามารถเปลี่ยนแปลงได้
// ตัวอย่างการกำหนด final counterProvider = StateProvider<int>((ref) => 0);
- NotifierProvider ใช้สำหรับการจัดการสถานะโดยใช้คลาสที่สืบทอดจาก Notifier
ซึ่งเป็นคลาสที่สามารถกำหนดสถานะที่ต้องการและมีการจัดการสถานะเองภายในได้
มักใช้กำหนด: ค่าที่สามารถเปลี่ยนแปลงได้ โดยใช้ Notifier ในการจัดการสถานะที่อาจจะ
มีความซับซ้อนขึ้นเล็กน้อย
// ตัวอย่างการกำหนด class CounterNotifier extends Notifier<int> { @override int build() => 0; void increment() => state++; } final counterProvider = NotifierProvider<CounterNotifier, int>(() => CounterNotifier());
- *StateNotifierProvider ใช้สำหรับการจัดการสถานะที่ซับซ้อนขึ้น มีการอัปเดตสถานะ
ภายในอย่างมีแบบแผน โดยใช้ใช้คลาสที่สืบทอดจาก StateNotifier
มักใช้กำหนด: ค่าที่เปลี่ยนแปลงได้ โดยใช้ StateNotifier ซึ่งเหมาะสำหรับสถานะที่ซับซ้อนและ
ต้องการการควบคุมการเปลี่ยนแปลงที่ชัดเจน
// ตัวอย่างการกำหนด class CounterStateNotifier extends StateNotifier<int> { CounterStateNotifier() : super(0); void increment() => state++; } final counterProvider = StateNotifierProvider<CounterStateNotifier, int>(() => CounterStateNotifier());
*ปัจจุบันแนะนำให้ใช้เป็นแบบ NotifierProvider แทน
- FutureProvider ใช้สำหรับการจัดการค่าที่ได้จาก Future
มักใช้กำหนด: ค่าที่ได้จาก Future ซึ่งต้องรอการประมวลผล เช่น การดึงข้อมูลจาก API
// ตัวอย่างการกำหนด final userNameProvider = FutureProvider<String>((ref) async { // จำลองการดึงข้อมูลจาก API await Future.delayed(Duration(seconds: 2)); return 'John Doe'; });
- StreamProvider ใช้สำหรับการจัดการค่าที่ได้จาก Stream
มักใช้กำหนด: ค่าที่ได้จาก Stream ซึ่งมีการอัปเดตข้อมูลอย่างต่อเนื่อง เช่น การรับข้อมูล
แบบเรียลไทม์
// ตัวอย่างการกำหนด final counterStreamProvider = StreamProvider<int>((ref) async* { // จำลองการนับเลข int counter = 0; while (true) { await Future.delayed(Duration(seconds: 1)); yield counter++; } });
รูปแบบการกำหนด Provider ใน Riverpod จะอยู่ในลักษณะดังนี้
final name = SomeProvider.someModifier<Result>((ref) { <your logic here> });
name ก็คือ ชื่อตัวแปร provider ที่เราต้องการกำหนดเพื่อเรียกใช้งาน โดยจะเป็น
final และเป็นตัวแปร global (top-level) มักใช้รูปแบบชื่อเป็น lowerCamelCase
SomeProvider เป็นรูปแบบ provider ที่เราจะใช้งาน ซึ่งจะใช้งานแบบไหนขึ้นกับ
ผลลัพธ์ของข้อมูลที่เราต้องการใช้งานเป็นสำคัญ มักใช้รูปแบบชื่อเป็น PascalCase
someModifier เป็นส่วนปรับแต่งของ provider เพิ่มเติม จะกำหนดหรือไม่ก็ได้ ใน Riverpod
ตอนนี้มี 2 รายรูปแบบให้เลือกใช้ คือ
- autoDispose กำหนดให้กับ Provider ใน Riverpod เพื่อให้ระบบทำการล้างข้อมูลแคช
(cache) โดยอัตโนมัติเมื่อ Provider นั้นหยุดถูกใช้งาน เมื่อกำหนด autoDispose ให้กับ
Provider, หากไม่มี Widget ใด ๆ ใช้ Provider นั้นอีกต่อไป ข้อมูลที่เก็บไว้ในแคชของ Provider
จะถูกล้างออกโดยอัตโนมัติ ซึ่งช่วยลดการใช้หน่วยความจำที่ไม่จำเป็นและทำให้แอปพลิเคชันทำงานได้
อย่างมีประสิทธิภาพมากขึ้น
- family กำหนดให้สามารถส่งอาร์กิวเมนต์ไปยัง Provider เพื่อใช้ในการสร้างหรือประมวล
ผลข้อมูลได้ โดยปกติแล้ว Provider จะทำงานโดยไม่ต้องพึ่งพาข้อมูลภายนอก แต่เมื่อใช้ family เรา
สามารถส่งค่าเข้าไปเพื่อให้ Provider ทำงานตามค่าที่ได้รับมา ซึ่งเป็นประโยชน์ในการใช้งานที่ต้องการ
ค่าพารามิเตอร์ที่ต่างกันสำหรับแต่ละการเรียกใช้
Ref เป็นออบเจ็กต์ที่ใช้สำหรับการโต้ตอบกับ Provider อื่น ๆ ภายในแอปพลิเคชัน
Result เป็นส่วนกำหนดชนิดข้อมูล (data type) ที่ฟังก์ชัน Provider จะคืนค่าออกมา
<your logic here> เป็นส่วนสำหรับกำหนดฟังก์ชันของ Provider (The provider
function) เป็นที่ที่เราวางตรรกะและกระบวนการทำงานของ Provider ฟังก์ชันนี้จะถูกเรียกใช้เฉพาะ
ครั้งแรกที่มีการอ่าน Provider นั้น ๆ เท่านั้น เมื่ออ่าน Provider ครั้งต่อไป ฟังก์ชันจะไม่ถูกเรียกใช้
ใหม่ แต่จะคืนค่าที่เก็บไว้ในแคชจากการเรียกใช้ครั้งก่อนแทน
เราได้รู้จักเกี่ยวกับ riverpod เบื้องต้นคร่าวๆ ไปแล้ว มาลองใช้งานอย่างง่าย โดยสร้างรูปแบบ
การทำงานตัวอย่างคือ มีปุ่ม บวก เพิ่มจำนวนค่าตัวเลข ที่เรากำหนดเป็นค่า state คล้ายๆ กับ
demo เริ่มต้นของ flutter ที่เราคุ้นเคย
เนื้อหานี้ใช้โค้ดตัวอย่างเริ่มต้น จากบทความ ตามลิ้งค์นี้ http://niik.in/961
โดยใช้ โค้ดตัวอย่างจากส่วน เพิ่มเติมเนื้อหา ครั้งที่ 2
การใช้งาน Flutter Riverpod
ให้เราสร้างไฟล์ Provider ในตัวอย่างนี้จะใช้เป็น
lib > providers > counter_provider.dart
ไฟล์ counter_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart'; final counterProvider = StateProvider<int>((ref) => 0);
เป็นไฟล์อย่างง่ายมีแค่ 2 บรรทัด คือส่วนที่ import riverpod มาใช้งาน กับส่วนที่กำหนด
StateProvider โดยกำหนด ชื่อเป็น counterProvider ใช้รูปแบบ StateProvider คืน
ค่าเป็นข้อมูล <int> โดยค่าเริ่มต้นที่คืนออกมา มีค่าเป็น 0 ในตัวอย่างใช้รูปแบบ arrow function
// รูปแบบเต็มแบบกำหนดปีกกาฟังก์ชั่น final counterProvider = StateProvider<int>((ref) { return 0; });
ต่อไปก็เป็นส่วนของการนำไปใช้งาน สิ่งที่เราจะต้องปรับ เมื่อมีการใช้งานร่วมกับ riverpod ก็คือ
การใช้งาน widget จากรูปแบบเดิม เราใช้เป็น StatefulWidget
// เฉพาะบางส่วน 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> { ...... ....
ก็จะแก้ไขเป็น
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> {
โดยเพิ่มคำว่า Consumer เพิ่มเข้าไป
ถ้าเป็น StatelessWidget เราจะเปลี่ยนเป็นชื่อ ConsumerWidget แทน
class Home extends StatelessWidget { const Home({super.key}); @override Widget build(BuildContext context) {
เปลี่ยนเป็น
class Home extends ConsumerWidget { const Home({super.key}); @override // Notice how "build" now receives an extra parameter: "ref" Widget build(BuildContext context, WidgetRef ref) {
โดยในส่วนของฟังก์ชั่น build จะมี parameter "ref" เพิ่มเข้ามา
ในที่นี้ในตัวอย่างเราเป็นแบบ StatefulWidget
ไฟล์ home.dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/counter_provider.dart'; // Import the provider 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 counter = ref.watch(counterProvider); return Scaffold( appBar: AppBar( title: Text('Home'), leading: IconButton( icon: Icon(Icons.menu), onPressed: () { Scaffold.of(context).openDrawer(); }, ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text('$counter') ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { // Increment the counter value ref.read(counterProvider.notifier).state++; }, child: Icon(Icons.add), ), ); } }
ในโค้ดตัวอย่าง ทำการอ่านค่า state ใน Provider มาเก็บในตัวแปร เพื่อนำไปใช้งาน
final counter = ref.watch(counterProvider);
และในส่วนของการเพิ่มค่า เมื่อกดปุ่ม เพิ่ม + ค่าก็จะทำการเพิ่มค่าในรูปแบบ
ref.read(counterProvider.notifier).state++;
และทุกครั้งที่มีการเพิ่มค่า state มีการเปลี่ยนแปลงก็จะเกิดการ rebuild ใหม่ ซึ่งตามรูปแบบที่
เราใช้งาน จะเป็นการ rebuild ใหม่ทั้งหมด วิธีการที่เราจะใช้กรณีต้องการ build ใหม่เฉพาะ
ส่วน เราจะใช้ widget ที่ชื่อ Consumer มาช่วย ตามตัวอย่างด้านล่าง
ใน Riverpod มีการใช้คำศัพท์ที่เหมือนกับที่ใช้ใน Provider สำหรับการอ่านค่า providers
BuildContext.watch -> WidgetRef.watch BuildContext.read -> WidgetRef.read BuildContext.select -> WidgetRef.watch(myProvider.select)
กฎการใช้งาน context.watch และ context.read ใน Provider จะนำมาใช้ใน Riverpod
ด้วยเช่นกัน
ใช้ "watch" ภายในเมธอด build เพื่อให้วิดเจ็ตสามารถตรวจจับการเปลี่ยนแปลงของ provider และ rebuild เมื่อมีการเปลี่ยนแปลง ใช้ "read" ภายในคลิกแฮนด์เลอร์และอีเวนต์อื่น ๆ เพื่ออ่านค่าจาก provider โดยไม่ rebuild วิดเจ็ต ใช้ "select" เมื่อจำเป็นต้องกรองค่าหรือควบคุมการ rebuild ของวิดเจ็ตตามเงื่อนไขที่กำหนด
ใน Riverpod ยังมีอีกหนึ่งส่วน ที่ต่างจาก Provider ปกติ คือ ส่วนของ WidgetRef.listen
ใช้สำหรับตรวจจับการเปลี่ยนแปลงของค่า state แต่จะไม่ทำคำสั่ง rebuild ฟังก์ชันใน listen
สามารถใช้เพื่อจัดการกับสถานะหรือข้อผิดพลาดที่ไม่เกี่ยวข้องกับ UI
การกำหนดให้ rebuild เฉพาะส่วนที่เรียกใช้งาน Provider โดยเราจะใช้ widget ที่ชื่อ
Consumer และเรียกใช้ ref.watch() ในส่วนนี้แทน
class _HomeState extends ConsumerState<Home> { @override Widget build(BuildContext context) { // Listen to the counterProvider without rebuilding the widget ref.listen<int>(counterProvider, (previous, next) { print('debug: Counter changed from $previous to $next'); }); print("debug: build"); return Scaffold( appBar: AppBar( title: Text('Home'), leading: IconButton( icon: Icon(Icons.menu), onPressed: () { Scaffold.of(context).openDrawer(); }, ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Consumer(builder: (context, ref, child) { final counter = ref.watch(counterProvider); print("debug: build only here"); return Text('$counter'); }) ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { // Increment the counter value ref.read(counterProvider.notifier).state++; }, child: Icon(Icons.add), ), ); } }
เราไม่ควรเรียกใช้ ref.watch ภายในฟังก์ชั่นของ ref.listen เนื่องจากจะเป็นการ rebuild วนลูป
ไม่จบสิ้นดังนั้น ให้ระวังในส่วนนี้ หากมีการใช้งาน
ตัวอย่างผลลัพธ์
เนื่อหาเกี่ยวกับ riverpod เบื้องต้นในตอนแรกก็ขอจบเพียงเท่านี้ ยังมีส่วนให้ทำความเข้าใจเพิ่มเติม
ในตอนต่อไป รอติดตาม