การใช้ Riverpod สำหรับ State Management
ในบทความก่อนหน้า เราได้เรียนรู้เกี่ยวกับ BLoC Pattern ซึ่งเป็นสถาปัตยกรรมที่ช่วยให้เราจัดการ State ใน Flutter ได้อย่างมีประสิทธิภาพ ในบทความนี้ เราจะมาเรียนรู้เกี่ยวกับอีกหนึ่งทางเลือกที่น่าสนใจสำหรับการจัดการ State นั่นคือ Riverpod
Riverpod เป็น State Management Library ที่พัฒนาโดยผู้สร้าง Provider ซึ่งเป็น Library ที่ได้รับความนิยมอย่างมากใน Flutter Riverpod ได้รับการออกแบบมาเพื่อแก้ไขข้อจำกัดบางประการของ Provider และนำเสนอแนวทางที่ปลอดภัยและมีประสิทธิภาพยิ่งขึ้นในการจัดการ State
ทำไมต้อง Riverpod?
Riverpod มีข้อดีหลายประการเมื่อเทียบกับ Provider และ State Management Libraries อื่นๆ:
- Type Safety: Riverpod ใช้ Type Safety อย่างเต็มที่ ซึ่งช่วยลดข้อผิดพลาดที่อาจเกิดขึ้นจากการใช้งานข้อมูลผิดประเภท
- Compile-Time Safety: Riverpod ตรวจสอบความถูกต้องของ Provider ที่ Compile-Time ซึ่งช่วยให้เราตรวจพบข้อผิดพลาดได้ตั้งแต่เนิ่นๆ
- Testability: Riverpod ออกแบบมาให้ง่ายต่อการทดสอบ เราสามารถ Mock Providers ได้อย่างง่ายดาย
- No Dependency on `BuildContext`: Riverpod ไม่จำเป็นต้องใช้ `BuildContext` ในการเข้าถึง State ซึ่งทำให้โค้ดของเรามีความยืดหยุ่นมากขึ้น
- Simplified Code: Riverpod มักจะทำให้โค้ดสั้นลงและอ่านง่ายขึ้นเมื่อเทียบกับ Provider และ BLoC
แนวคิดหลักของ Riverpod
Riverpod มีแนวคิดหลักๆ ที่เราต้องทำความเข้าใจดังนี้:
- ProviderScope: เป็น Widget ที่ทำให้ Providers สามารถเข้าถึงได้ใน Widget Tree ทั้งหมด เราต้องใส่ ProviderScope ไว้ที่จุดเริ่มต้นของแอป
- Provider: เป็น Object ที่เก็บ State และสามารถสร้าง, แก้ไข, และเข้าถึง State ได้ Providers สามารถเป็นได้หลายประเภท เช่น `Provider`, `StateProvider`, `FutureProvider`, `StreamProvider`
- Consumer: เป็น Widget ที่ใช้ในการอ่าน State จาก Provider และ Rebuild Widget เมื่อ State เปลี่ยนแปลง
การเริ่มต้นใช้งาน Riverpod
ในการเริ่มต้นใช้งาน Riverpod เราต้องเพิ่ม Dependency ใน `pubspec.yaml`:
dependencies:
flutter_riverpod: ^2.0.0 # หรือเวอร์ชันล่าสุด
หลังจากนั้นให้รัน `flutter pub get` เพื่อติดตั้ง Dependency
ProviderScope
ก่อนที่เราจะสามารถใช้ Providers ได้ เราต้อง Wrap แอปของเราด้วย `ProviderScope` Widget:
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
`ProviderScope` ทำให้ Providers ทั้งหมดที่เราจะสร้างในภายหลังสามารถเข้าถึงได้จากทุกส่วนของแอป
Provider
`Provider` เป็น Provider ที่ง่ายที่สุด ใช้สำหรับเก็บค่าที่ไม่เปลี่ยนแปลงหรือคำนวณได้ง่าย ตัวอย่างเช่น:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final messageProvider = Provider((ref) => 'Hello Riverpod!');
ในตัวอย่างนี้ เราสร้าง `Provider` ที่ชื่อ `messageProvider` ซึ่งเก็บ String "Hello Riverpod!" ฟังก์ชันที่ส่งให้กับ `Provider` จะรับ `ref` เป็น Parameter ซึ่งเราสามารถใช้เพื่อเข้าถึง Providers อื่นๆ ได้ (จะกล่าวถึงในภายหลัง)
Consumer
`Consumer` เป็น Widget ที่ใช้ในการอ่านค่าจาก `Provider` และ Rebuild Widget เมื่อค่าใน `Provider` เปลี่ยนแปลง:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyWidget extends ConsumerWidget {
const MyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final message = ref.watch(messageProvider);
return Text(message);
}
}
ในตัวอย่างนี้ เราสร้าง `ConsumerWidget` ที่ชื่อ `MyWidget` ใน Method `build` เราใช้ `ref.watch(messageProvider)` เพื่ออ่านค่าจาก `messageProvider` และเก็บไว้ในตัวแปร `message` หลังจากนั้นเราก็ใช้ `message` ในการแสดงผลข้อความ
ข้อควรจำ: เราต้องใช้ `ConsumerWidget` หรือ `ConsumerStatefulWidget` เพื่อให้สามารถใช้ `ref.watch` ได้ หากเราต้องการใช้ `ref.watch` ใน Widget ปกติ เราสามารถ Wrap Widget นั้นด้วย `Consumer` Widget ได้
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
final message = ref.watch(messageProvider);
return Text(message);
},
);
}
StateProvider
`StateProvider` ใช้สำหรับเก็บ State ที่เปลี่ยนแปลงได้ ตัวอย่างเช่น:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateProvider((ref) => 0);
ในตัวอย่างนี้ เราสร้าง `StateProvider` ที่ชื่อ `counterProvider` ซึ่งเก็บ Integer เริ่มต้นที่ 0
ในการแก้ไขค่าใน `StateProvider` เราสามารถใช้ `ref.read(counterProvider.notifier).state = newValue` หรือ `ref.read(counterProvider.notifier).update((state) => newState)`:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyCounterWidget extends ConsumerWidget {
const MyCounterWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Counter App')),
body: Center(child: Text('Counter: $counter')),
floatingActionButton: FloatingActionButton(
onPressed: () {
// วิธีที่ 1: เปลี่ยนค่าโดยตรง
// ref.read(counterProvider.notifier).state = counter + 1;
// วิธีที่ 2: ใช้ update เพื่อสร้าง state ใหม่
ref.read(counterProvider.notifier).update((state) => state + 1);
},
child: const Icon(Icons.add),
),
);
}
}
ในตัวอย่างนี้ เมื่อกด FloatingActionButton ค่าใน `counterProvider` จะเพิ่มขึ้น 1 และ Widget จะ Rebuild เพื่อแสดงค่าใหม่
FutureProvider และ StreamProvider
Riverpod ยังมี `FutureProvider` และ `StreamProvider` สำหรับจัดการ Asynchronous Data:
- FutureProvider: ใช้สำหรับจัดการ Future
- StreamProvider: ใช้สำหรับจัดการ Stream
ตัวอย่างการใช้ `FutureProvider`:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dataProvider = FutureProvider<String>((ref) async {
await Future.delayed(const Duration(seconds: 2)); // Simulate API call
return 'Data from API';
});
ในการใช้งาน `FutureProvider` เราสามารถใช้ `ref.watch` และตรวจสอบสถานะของ Future:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyDataWidget extends ConsumerWidget {
const MyDataWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return data.when(
data: (value) => Text('Data: $value'),
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error: $error'),
);
}
}
`data.when` ช่วยให้เราจัดการสถานะต่างๆ ของ Future ได้อย่างง่ายดาย: `data` (เมื่อ Future สำเร็จ), `loading` (ระหว่างที่ Future กำลังทำงาน), และ `error` (เมื่อ Future เกิดข้อผิดพลาด)
ข้อผิดพลาดที่พบบ่อย
- ลืมใส่ ProviderScope: หากเราลืม Wrap แอปด้วย `ProviderScope` เราจะไม่สามารถใช้ Providers ได้
- ใช้ ref.read ใน Widget Build: `ref.read` ใช้สำหรับอ่านค่า Providers เพียงครั้งเดียวเท่านั้น ไม่ควรใช้ใน Widget Build เพราะจะทำให้ Widget ไม่ Rebuild เมื่อค่าใน Provider เปลี่ยนแปลง ควรใช้ `ref.watch` แทน
- การแก้ไข State โดยตรง: หากเราแก้ไข State โดยตรงโดยไม่ใช้ `ref.read(provider.notifier).state = newValue` หรือ `ref.read(provider.notifier).update((state) => newState)` Widget จะไม่ Rebuild
สรุป
Riverpod เป็น State Management Library ที่มีประสิทธิภาพและใช้งานง่าย ช่วยให้เราจัดการ State ใน Flutter ได้อย่างปลอดภัยและมีประสิทธิภาพมากขึ้น ในบทความนี้ เราได้เรียนรู้เกี่ยวกับแนวคิดหลักของ Riverpod เช่น `ProviderScope`, `Provider`, `StateProvider`, `FutureProvider`, และ `Consumer` รวมถึงข้อผิดพลาดที่พบบ่อยในการใช้งาน Riverpod
ในบทความถัดไป เราจะมาเรียนรู้เกี่ยวกับการใช้ Freezed Package เพื่อ Generate Data Classes ซึ่งจะช่วยให้เราจัดการ Data Model ได้อย่างมีประสิทธิภาพยิ่งขึ้น