Kodokon kodokon.com

结构化的状态管理:ChangeNotifier 与 Provider

用 ChangeNotifier 和 Provider 共享应用状态,同时掌握 watch、read 和 select,实现有针对性的重建。

10 分钟 · 3 题

在 Kodokon 中打开本课

只要状态随单个界面而生、随它而灭,setState 就够用了。可一旦数据要被共享 - 购物车、会话、偏好设置 - 把它一层层从构造函数往下传就会变得无法管理。ChangeNotifier 提供了 SDK 中最精简的可观察对象;provider 把它注入到组件树中,并且只重建那些订阅了它的 widget。和 Riverpod 或 Bloc 相比,这套组合仍然是最容易审查的:几乎没有魔法,只有一个依赖,一套你能用一句话讲清楚的机制。

BASH
flutter pub add provider

一个设计良好的 notifier 会做好封装:私有字段、只读的 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>() 会让 widget 订阅:每次 notifyListeners 它都会重建。context.read<T>() 只读取而不订阅:它是像 onPressed 这样的回调所用的工具。context.select<T, R>() 只订阅一个投影:只有当那个确切的值发生变化时,widget 才会重建。黄金法则是:watchselect 只用在 build 里,read 用在处理函数里 - 千万不要反过来。

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),
        ),
      ),
    );
  }
}
在 build 里用 watch 来读取,在回调里用 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),
    );
  }
}
只有当 count 变化时,CartBadge 才会重建

知识检测

确认你已牢记本课的重点内容。

  1. 在一个 onPressed 内部,如何在不产生多余订阅的情况下触发 cart.add(...)
    • context.watch<CartModel>().add(...)
    • context.read<CartModel>().add(...)
    • context.select((CartModel c) => c.add(...))
  2. notifyListeners() 到底会触发什么?
    • 只重建那些用到了发生变化的数据的 widget。
    • 对整个应用进行一次完全的重建。
    • 重建所有订阅了该 notifier 的 widget,不管是哪个数据发生了变化。
  3. 为什么要暴露 List.unmodifiable(_items),而不是直接暴露 _items
    • 为了提升列表的读取性能。
    • 为了防止外部的修改在不调用 notifyListeners 的情况下改变状态,从而也不触发重建。
    • 因为 Provider 拒绝注入可变的列表。