Kodokon kodokon.com

การจัดการสถานะอย่างมีโครงสร้าง: ChangeNotifier และ Provider

แบ่งปันสถานะของแอปพลิเคชันด้วย ChangeNotifier และ Provider พร้อมเชี่ยวชาญ watch, read และ select เพื่อการรีบิลด์แบบเจาะจง

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

setState เพียงพอตราบใดที่สถานะเกิดและดับไปพร้อมกับหน้าจอเดียว แต่ทันทีที่ข้อมูลถูกใช้ร่วมกัน ไม่ว่าจะเป็นตะกร้าสินค้า เซสชัน หรือการตั้งค่า การส่งมันลงไปจากคอนสตรักเตอร์สู่คอนสตรักเตอร์ก็จะจัดการไม่ไหว ChangeNotifier มอบ observable ขั้นต่ำที่สุดของ SDK ส่วน provider ฉีดมันเข้าไปในทรี (tree) และรีบิลด์เฉพาะวิดเจ็ตที่สมัครรับ (subscribe) ไว้เท่านั้น เมื่อเทียบกับ Riverpod หรือ Bloc คู่นี้ยังคงตรวจสอบได้ง่ายที่สุด มีความมหัศจรรย์ซ่อนอยู่น้อย ใช้ดีเพนเดนซีเพียงตัวเดียว และเป็นกลไกที่คุณอธิบายได้ในประโยคเดียว

BASH
flutter pub add provider

notifier ที่ออกแบบมาดีย่อม ห่อหุ้ม (encapsulate) ข้อมูล นั่นคือ ฟิลด์ที่เป็นส่วนตัว getter แบบอ่านอย่างเดียว และการเปลี่ยนแปลงค่าผ่านเมท็อดที่ลงท้ายด้วย notifyListeners() เท่านั้น สัญญานี้เองที่รับประกันว่าไม่มีการเปลี่ยนสถานะใดหลุดรอดสายตา UI ไปได้

DART
import 'package:flutter/foundation.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  List<String> get items => List.unmodifiable(_items);

  int get count => _items.length;

  void add(String item) {
    _items.add(item);
    notifyListeners();
  }

  void remove(String item) {
    _items.remove(item);
    notifyListeners();
  }
}
สถานะที่เป็นส่วนตัว อ่านได้อย่างเดียวจากภายนอก และการแจ้งเตือน

สามช่องทางเข้า สามการใช้งาน context.watch<T>() สมัครรับให้วิดเจ็ต มันจะรีบิลด์ทุกครั้งที่มี notifyListeners context.read<T>() อ่านโดยไม่สมัครรับ เป็นเครื่องมือสำหรับคอลแบ็กอย่าง onPressed context.select<T, R>() สมัครรับเฉพาะการฉายภาพ (projection) วิดเจ็ตจะรีบิลด์ก็ต่อเมื่อค่าที่เจาะจงนั้นเปลี่ยนแปลงเท่านั้น กฎทองคือ watch และ select ใช้ใน build เท่านั้น ส่วน read ใช้ในตัวจัดการ (handler) อย่าทำสลับกันเด็ดขาด

DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel extends ChangeNotifier {
  int _value = 0;

  int get value => _value;

  void increment() {
    _value++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const CounterApp(),
    ),
  );
}

class CounterApp extends StatelessWidget {
  const CounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    final value = context.watch<CounterModel>().value;
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('Count: $value')),
        floatingActionButton: FloatingActionButton(
          onPressed: () =>
              context.read<CounterModel>().increment(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
watch ใน build เพื่ออ่าน read ในคอลแบ็กเพื่อลงมือทำ
DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  int get count => _items.length;
}

class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select(
      (CartModel cart) => cart.count,
    );
    return Badge(
      label: Text('$count'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}
CartBadge จะรีบิลด์เฉพาะเมื่อ count เปลี่ยนแปลงเท่านั้น

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ภายใน onPressed คุณจะเรียก cart.add(...) โดยไม่สร้างการสมัครรับที่ไม่จำเป็นได้อย่างไร?
    • context.watch<CartModel>().add(...)
    • context.read<CartModel>().add(...)
    • context.select((CartModel c) => c.add(...))
  2. จริง ๆ แล้ว notifyListeners() กระตุ้นอะไร?
    • การรีบิลด์เฉพาะวิดเจ็ตที่ใช้ข้อมูลที่เปลี่ยนไปเท่านั้น
    • การรีบิลด์แอปพลิเคชันทั้งหมด
    • การรีบิลด์วิดเจ็ตทั้งหมดที่สมัครรับกับ notifier ไม่ว่าข้อมูลใดจะเปลี่ยนไปก็ตาม
  3. ทำไมจึงเปิดเผย List.unmodifiable(_items) แทนที่จะเป็น _items โดยตรง?
    • เพื่อปรับปรุงประสิทธิภาพการอ่านของรายการ
    • เพื่อป้องกันการเปลี่ยนแปลงจากภายนอกที่จะเปลี่ยนสถานะโดยไม่ผ่าน notifyListeners และจึงไม่มีการรีบิลด์
    • เพราะ Provider ปฏิเสธที่จะฉีดรายการที่เปลี่ยนแปลงได้