ข้ามไปยังเนื้อหา

Provider & Riverpod

การเขียน InheritedWidget ด้วยมือคือ boilerplate Provider ห่อ pattern นั้นด้วย API ที่สะอาด และจับคู่กับ ChangeNotifier — class ง่าย ๆ ที่ถือ state และเรียก notifyListeners() เมื่อ state เปลี่ยน

// 1. model ที่ notify listener เมื่อมันเปลี่ยน
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
void add(String item) {
_items.add(item);
notifyListeners(); // บอก widget ที่ listen อยู่ให้ rebuild
}
}
// 2. provide มันไว้เหนือ widget ที่ต้องใช้
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
);
// 3. อ่านมัน watch() rebuild เมื่อเปลี่ยน; read() ไม่
final cart = context.watch<CartModel>(); // rebuild เมื่อ cart เปลี่ยน
context.read<CartModel>().add('Book'); // action ครั้งเดียว ไม่ rebuild

ความต่างระหว่าง watch กับ read คือหัวใจทั้งหมด context.watch<T>() subscribe — widget rebuild เมื่อ model notify context.read<T>() หยิบค่าครั้งเดียวโดยไม่ subscribe — ใช้ใน callback (เช่น onPressed) ที่คุณอยาก ทำ ไม่ใช่ ฟัง สลับสองอันนี้คือ bug ที่พบบ่อยที่สุดของ Provider (ไม่ rebuild ที่ควร หรือ rebuild มากเกิน)

Riverpod คือการคิดใหม่โดยผู้เขียนคนเดิมที่แก้จุดหยาบของ Provider หมัดเด็ดของตัวเอง: provider เป็น top-level object ไม่ผูกกับ widget tree ซึ่งทำให้ compile-safe (อ่าน provider ที่ไม่มีอยู่ไม่ได้) และ test กับ combine ได้ง่าย

// provider ประกาศที่ top level — ไม่ได้อยู่ใน widget tree
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
class Counter extends Notifier<int> {
@override
int build() => 0; // initial state คืนออกมาจาก build()
void increment() => state++; // การ reassign `state` จะ notify listener
}
// ใน ConsumerWidget, `ref` อ่าน provider:
class CounterView extends ConsumerWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // rebuild เมื่อเปลี่ยน
return TextButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('$count'),
);
}
}

Notifier<T> คืน initial state ออกมาจาก build() (ไม่ใช่ constructor super(initial)) และการแก้ state จะ notify listener สังเกตว่ามีความต่าง watch/read แบบเดียวกันผ่าน ref ความต่างจาก Provider เป็นเชิงโครงสร้าง: ไม่มี BuildContext lookup, ไม่มี “provider not found” ตอน runtime, และ provider compose กันได้ (provider ตัวหนึ่ง ref.watch อีกตัวได้)

สำหรับ async state ตัวเทียบเท่าคือ AsyncNotifier / AsyncNotifierProvider ที่ build() คืน Future นอกจากนี้ Riverpod ยังมีสไตล์ code-generation ผ่าน @riverpod ที่ generate provider ให้จาก class หรือ function ที่ annotate ไว้

API เดิมอย่าง StateNotifier / StateNotifierProvider และ StateProvider ตอนนี้เป็น legacy (deprecated ใน Riverpod 3) — คุณยังเจอใน code เดิมได้ แต่ code ใหม่ควรใช้ Notifier / NotifierProvider (หรือ @riverpod codegen)

flowchart TB
  prov["Provider
InheritedWidget + ChangeNotifier
ผูกกับ tree, lookup ตอน runtime"] --> use1["ดีสำหรับ app เล็ก,
ทีมที่ใช้อยู่แล้ว"]
  rp["Riverpod
top-level provider
compile-safe, test ได้, compose ได้"] --> use2["นิยมสำหรับ app ใหม่,
state ซับซ้อนหรือต้อง test"]
Provider vs Riverpod at a glance

ทั้งคู่ยอดเยี่ยม Provider ง่ายกว่าและมีอยู่ทุกที่; Riverpod ทนทานกว่าสำหรับ app ใหญ่ที่ต้อง test และเป็นทิศทางที่ ecosystem กำลังมุ่งไป ตัวไหนก็เป็น default ที่ดีกว่า InheritedWidget ที่เขียนเองสำหรับ shared state จริง

ความต่างระหว่าง context.watch กับ context.read ใน Provider คืออะไร?
การเรียก notifyListeners() บน ChangeNotifier ทำอะไร?
ความต่างเชิงโครงสร้างหลักของ Riverpod จาก Provider คืออะไร?
อันไหนคือ bug ที่พบบ่อยที่สุดของ Provider?