Skip to content

Provider 状态管理

什么是状态管理?为什么需要 Provider?

想象一个场景:你在 App 顶部有一个购物车图标,显示商品数量;在商品列表页点击"加入购物车"后,顶部的数字要立刻更新。

问题:这两个组件不在同一个 Widget 树层级,怎么让它们共享同一份数据?

答案:这就是 状态管理 要解决的事。Provider 是 Flutter 官方推荐的方案,简单、好用、够用。

💡 一句话理解 Provider:把数据放在树顶,任何子组件都能读到,数据变了自动刷新 UI。

安装

bash
flutter pub add provider

核心思路(只需记住 3 步)

1️⃣ 创建数据模型 → 2️⃣ 在顶层注入 → 3️⃣ 在子组件中读取

下面用一个「计数器」例子贯穿讲解。


第一步:创建数据模型

数据模型就是你的"数据仓库"。继承 ChangeNotifier,数据变化时调用 notifyListeners() 通知 UI 刷新。

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

// ─── ★ ChangeNotifier ──────────────
class CounterModel extends ChangeNotifier {
  int _count = 0;

  // 对外暴露的只读属性
  int get count => _count;

  // 修改数据后,必须调用 notifyListeners()
  void increment() {
    _count++;
    notifyListeners(); // 👈 告诉所有监听者:"数据变了,快刷新!"
  }

  void decrement() {
    _count--;
    notifyListeners(); // 👈 同上
  }
}
// ─── ☆ ChangeNotifier ──────────────

⚠️ 关键点:忘记写 notifyListeners() 是新手最常见的错误——数据改了但 UI 不更新。


第二步:在顶层注入

ChangeNotifierProvider 把数据模型挂到 Widget 树的顶部,这样所有子组件都能访问它。

dart
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(), // 创建数据模型
      child: const MyApp(),          // 你的 App
    ),
  );
}

可以理解为:在树根放了一个"公告栏",所有子组件都能看。


第三步:在子组件中读取

有两种方式,分别适用于不同场景:

方式 A:context.watch() — 显示数据

当你需要展示数据、数据变了要自动刷新 UI 时,用 watch

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

  @override
  Widget build(BuildContext context) {
    // watch:监听数据变化,数据变了 → 这个 build 方法会重新执行
    final counter = context.watch<CounterModel>();
    return Text('当前计数:${counter.count}');
  }
}

方式 B:context.read() — 调用方法

当你只需要调用方法(如点击按钮)、不需要刷新当前 UI 时,用 read

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

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      // read:只调用方法,不监听变化,不会触发重建
      onPressed: () => context.read<CounterModel>().increment(),
      child: const Text('+1'),
    );
  }
}

watch 和 read 怎么选?(最重要的一节)

watchread
用途读取数据并监听变化调用方法 / 一次性读取
数据变了会重建?✅ 会❌ 不会
典型场景显示文本、列表等按钮点击回调

记住这条规则

  • 要显示数据watch
  • 要调用方法read
dart
// ✅ 正确:显示数据用 watch
Text('${context.watch<CounterModel>().count}')

// ✅ 正确:点击按钮调用方法用 read
ElevatedButton(
  onPressed: () => context.read<CounterModel>().increment(),
  child: const Text('+1'),
)

// ❌ 错误:在按钮回调里用 watch(会导致不必要的重建)
ElevatedButton(
  onPressed: () {
    context.watch<CounterModel>().increment(); // 别这么写!
  },
  child: const Text('+1'),
)

Consumer — 精确控制重建范围

context.watch() 会让整个 build 方法重建。如果你只想重建某个小部件,用 Consumer

dart
class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('这部分不会重建'),
        // 只有 Consumer 内部会重建
        Consumer<CounterModel>(
          builder: (context, counter, child) {
            return Text('计数:${counter.count}');
          },
        ),
        const Text('这部分也不会重建'),
      ],
    );
  }
}

Consumer 的 child 参数(性能优化)

Consumerchild 参数可以传入不需要重建的静态部件:

dart
Consumer<CounterModel>(
  builder: (context, counter, child) {
    return Row(
      children: [
        Text('计数:${counter.count}'), // 会重建
        child!,                        // 不会重建,直接复用
      ],
    );
  },
  child: const Icon(Icons.star), // 👈 这个图标不会随数据变化而重建
)

初学阶段不必纠结 child 参数,等遇到性能问题再优化即可。


MultiProvider — 注入多个数据模型

实际项目中会有多个数据模型,用 MultiProvider 一次注入:

dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UserModel()),
        ChangeNotifierProvider(create: (_) => CartModel()),
        ChangeNotifierProvider(create: (_) => ThemeModel()),
      ],
      child: const MyApp(),
    ),
  );
}

子组件中正常使用 watch / read 即可,用法不变。


完整示例:购物车

dart
// ========== 1. 数据模型 ==========
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 removeAt(int index) {
    _items.removeAt(index);
    notifyListeners();
  }

  void clear() {
    _items.clear();
    notifyListeners();
  }
}

// ========== 2. 顶层注入 ==========
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CartModel(),
      child: const MyApp(),
    ),
  );
}

// ========== 3. 显示购物车数量 ==========
class CartBadge extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cart = context.watch<CartModel>();
    return Text('购物车 (${cart.count})');
  }
}

// ========== 4. 添加商品按钮 ==========
class AddToCartButton extends StatelessWidget {
  final String item;
  const AddToCartButton({super.key, required this.item});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => context.read<CartModel>().add(item),
      child: const Text('加入购物车'),
    );
  }
}

Provider 类型速查

类型适用场景示例
ChangeNotifierProvider需要修改和监听的状态(最常用)计数器、购物车、用户信息
Provider只读的静态值主题配置、API 地址
FutureProvider异步获取的数据网络请求结果
StreamProvider持续的数据流WebSocket、实时定位
ProxyProvider依赖其他 Provider 的值依赖用户信息的订单服务

初学者只需掌握 ChangeNotifierProvider,其他类型遇到需求再学。


什么时候用 Provider?

场景推荐方案
只在单个组件内部用setState 就够了
需要在多个组件间共享Provider
超大型项目、状态逻辑极复杂考虑 Riverpod / Bloc

常见错误排查

现象原因解决
数据改了但 UI 没更新忘记调用 notifyListeners()在修改数据的方法末尾加上
报错 ProviderNotFoundException没在顶层注入,或类型写错了检查 ChangeNotifierProvider 是否在父级
按钮点击后整个页面重建在回调中用了 watch改用 read

下一步

基于 Flutter 官方文档整理