Flutter状态管理最佳实践:实战分享

导读:在 Flutter 中,状态管理(State Management)是构建复杂应用时的核心难题。一套清晰、可维护、性能优越的状态管理方案能让项目更具扩展性、更易测试、更少 BUG。本文从最基础的 setState 到社区主流的 ProviderRiverpodBloc 等方案,结合代码示例图解实战心得,帮助你快速掌握 Flutter 状态管理的最佳实践。

目录

  1. 为什么需要状态管理
  2. 基础:setState 与局部状态

    • 2.1 setState 的原理
    • 2.2 局部状态如何拆分
    • 2.3 代码示例与图解
  3. 进阶:InheritedWidget 与通知机制

    • 3.1 InheritedWidget 原理简介
    • 3.2 自定义 InheritedNotifier / InheritedModel
    • 3.3 代码示例与示意图
  4. 推荐:Provider 生态

    • 4.1 Provider 基本用法
    • 4.2 ChangeNotifierProviderConsumer
    • 4.3 FutureProvider / StreamProvider
    • 4.4 代码示例与图解
  5. 替代与扩展:Riverpod

    • 5.1 为什么选择 Riverpod
    • 5.2 Provider vs Riverpod 对比
    • 5.3 Riverpod 代码示例
  6. 复杂场景:Bloc(业务逻辑组件)

    • 6.1 Bloc 概念与优缺点
    • 6.2 Cubit 简化版本
    • 6.3 代码示例与事件流图解
  7. 实战案例:Todo 应用对比多种方案

    • 7.1 需求描述与核心功能
    • 7.2 setState 实现
    • 7.3 Provider 实现
    • 7.4 Bloc 实现
    • 7.5 性能与可维护性对比分析
  8. 总结与最佳实践

一、为什么需要状态管理

Flutter 的 UI 完全由代码控制,几乎所有业务逻辑都要通过“状态(State)”来驱动界面渲染。当应用规模变大时,如果仅依赖散落在各个 StatefulWidget 中的 setState,会出现以下问题:

  • 状态分散难以复用:同一个数据需要在多个页面或组件中共享时,若各自用 setState,会导致重复逻辑、同步困难。
  • 难以追踪重建范围:不清楚何时某个组件会因父级 setState 被无谓重建,影响性能。
  • 测试不便:将业务逻辑与 UI 紧耦合,单元测试需要额外包装 Widget,很不直观。
  • 生命周期管理复杂:多个页面同时需要监听同一份数据变化时,如果不集中管理会出现内存泄漏、回调错乱等问题。

因此,Flutter 社区发展出多种状态管理方案,从简单到复杂,满足不同项目需求。本文将依次介绍并对比它们的原理和实战用法。


二、基础:setState 与局部状态

2.1 setState 的原理

  • 原理概述
    每个 StatefulWidget 对应一个 State<T> 对象,内部持有一颗“依赖于 build() 函数重绘”的 Widget 树。当调用 setState(() { ... }) 时,Flutter 会标记这个 State 为“需要重建”(dirty),并在下一个帧(frame)时调用其 build(),从而重新渲染该子树。
  • 示意图

    ┌────────────────────────────┐
    │       StatefulWidget       │
    │ (与其对应的 State 对象)    │
    └────────────────────────────┘
               │
         setState() 被调用
               │
    ┌────────────────────────────┐
    │   State<StatefulWidget>    │───► 被标记为 dirty
    │     build() 执行重绘       │
    └────────────────────────────┘
  • 注意事项

    • 只要 build() 中有 setState,整个 Widget 子树都会重建,可能会导致无谓布局和绘制。
    • setState 不要在 build()initState() 之外的异步回调中频繁调用,否则难以控制重建节奏。

2.2 局部状态如何拆分

当页面中存在多块需要维护状态的区域时,不建议把所有状态都放在同一个 StatefulWidget 中,而应拆分成多个更小的 StatefulWidget。这样:

  • 当一块状态变化时,只会触发对应局部 build(),其余部分不会被重建。
  • 可提高性能,也增强组件复用性。

代码示例:拆分计数器

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('局部状态拆分示例')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: const [
          Padding(padding: EdgeInsets.all(8), child: Text('上方静态内容')),
          SizedBox(height: 20),
          CounterSection(),    // 单独的计数器组件
          SizedBox(height: 20),
          StaticTextWidget(),   // 不依赖任何状态,永远不重建
        ],
      ),
    );
  }
}

class CounterSection extends StatefulWidget {
  const CounterSection({super.key});
  @override
  State<CounterSection> createState() => _CounterSectionState();
}

class _CounterSectionState extends State<CounterSection> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    print('CounterSection build'); // 可在控制台观察重建
    return Column(
      children: [
        Text('计数:$_count'),
        ElevatedButton(
          onPressed: () => setState(() => _count++),
          child: const Text('++'),
        ),
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print('StaticTextWidget build'); // 只会执行一次
    return const Text('永不重建的静态文本');
  }
}
  • 解释

    • CounterSection 中的 setState 只会重建自身;
    • StaticTextWidget 不含 setState,且为 const,永远不会重建。

2.3 代码示例与图解

图解:局部状态拆分前后差异

┌────────────────────────────┐      ┌────────────────────────────┐
│  SingleStatefulPage        │      │       StatelessPage        │
│  ┌─────────────────────┐   │      │  ┌─────────────────────┐   │
│  │ StatefulContent     │◄─┤ setState()│ CounterSection  │◄─┤ setState()
│  │ (所有状态混在一起)  │                └─────────────────────┘
│  └─────────────────────┘   │      │  ┌─────────────────────┐   │
│                             │      │  │ StaticTextWidget    │   │
│                             │      │  │ (永不重建)           │   │
│                             │      │  └─────────────────────┘   │
└────────────────────────────┘      └────────────────────────────┘
  • 左图:单个 StatefulWidget 管理所有状态,每次 setState 都会重建全部内容;
  • 右图:将可分离的部分拆为多个子组件,只重建所需部分。

三、进阶:InheritedWidget 与通知机制

在全局或跨多个页面共享状态时,单靠局部 setState 已难以满足需求,这时可以使用更底层的 InheritedWidget 及其衍生组件。

3.1 InheritedWidget 原理简介

  • 核心概念

    • InheritedWidget 是一个特殊的 Widget,放在 Widget 树上方,将数据“注入”到其子树,供后代通过 context.dependOnInheritedWidgetOfExactType<YourInherited>() 获取。
    • InheritedWidgetupdateShouldNotify(oldWidget) 返回 true 时,所有依赖该实例的子树都会收到通知并重建。
  • 适用场景

    • 某些数据需要在整个应用或大部分子树中共享,如“主题”、“语言”、“当前登录用户”等。
    • 比较低级,一般应用会用基于 InheritedWidget 的封装(如 ProviderBlocRiverpod)来简化。

基本实现示例

// 1. 自定义 InheritedWidget
class CounterInherited extends InheritedWidget {
  final int count;
  final Widget child;

  const CounterInherited({
    super.key,
    required this.count,
    required this.child,
  }) : super(child: child);

  static CounterInherited? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CounterInherited>();
  }

  @override
  bool updateShouldNotify(CounterInherited oldWidget) {
    // 只有 count 变化时才通知子树更新
    return oldWidget.count != count;
  }
}

// 2. 顶层 StatefulWidget 持有状态
class InheritedCounterPage extends StatefulWidget {
  const InheritedCounterPage({super.key});
  @override
  State<InheritedCounterPage> createState() => _InheritedCounterPageState();
}

class _InheritedCounterPageState extends State<InheritedCounterPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return CounterInherited(
      count: _count,
      child: Scaffold(
        appBar: AppBar(title: const Text('InheritedWidget 示例')),
        body: const CounterDisplay(), // 读取 count
        floatingActionButton: FloatingActionButton(
          onPressed: () => setState(() => _count++),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

// 3. 子孙 Widget 通过 of() 获取 count
class CounterDisplay extends StatelessWidget {
  const CounterDisplay({super.key});
  @override
  Widget build(BuildContext context) {
    final inherited = CounterInherited.of(context);
    final count = inherited?.count ?? 0;
    print('CounterDisplay build: $count');
    return Center(child: Text('当前计数:$count', style: const TextStyle(fontSize: 24)));
  }
}
  • 运行流程

    1. _InheritedCounterPageState_count 变化 → setState() → 重新构建 CounterInherited
    2. CounterInherited.updateShouldNotify(old) 返回 true → 所有依赖 CounterInherited 的子 Widget(如 CounterDisplay)重新构建;
    3. CounterDisplay.build() 中通过 CounterInherited.of(context) 取到最新 count 并更新 UI。

3.2 自定义 InheritedNotifier / InheritedModel

  • InheritedNotifier

    • 当状态变动时,常用 ChangeNotifier 作为状态持有者,再包装成 InheritedNotifier 传递给子树。
    • 子树只要依赖,ChangeNotifier.notifyListeners() 就能触发 InheritedNotifier 更新。
    // 1. 定义 ChangeNotifier
    class CounterNotifier extends ChangeNotifier {
      int _count = 0;
      int get count => _count;
      void increment() {
        _count++;
        notifyListeners();
      }
    }
    
    // 2. InheritedNotifier 封装
    class CounterInheritedNotifier extends InheritedNotifier<CounterNotifier> {
      const CounterInheritedNotifier({
        super.key,
        required CounterNotifier notifier,
        required Widget child,
      }) : super(notifier: notifier, child: child);
    
      static CounterNotifier of(BuildContext context) {
        final inherited = context
            .dependOnInheritedWidgetOfExactType<CounterInheritedNotifier>();
        return inherited!.notifier!;
      }
    }
    
    // 3. 在顶层提供 notifier
    class InheritedNotifierPage extends StatefulWidget {
      const InheritedNotifierPage({super.key});
      @override
      State<InheritedNotifierPage> createState() => _InheritedNotifierPageState();
    }
    
    class _InheritedNotifierPageState extends State<InheritedNotifierPage> {
      final _notifier = CounterNotifier();
      @override
      Widget build(BuildContext context) {
        return CounterInheritedNotifier(
          notifier: _notifier,
          child: Scaffold(
            appBar: AppBar(title: const Text('InheritedNotifier 示例')),
            body: const CounterDisplay(),
            floatingActionButton: FloatingActionButton(
              onPressed: () => _notifier.increment(),
              child: const Icon(Icons.add),
            ),
          ),
        );
      }
    }
    
    // 4. 子 Widget 订阅 notifier
    class CounterDisplay extends StatelessWidget {
      const CounterDisplay({super.key});
      @override
      Widget build(BuildContext context) {
        final notifier = CounterInheritedNotifier.of(context);
        final count = notifier.count;
        print('CounterDisplay build: $count');
        return Center(child: Text('当前计数:$count', style: const TextStyle(fontSize: 24)));
      }
    }
  • InheritedModel

    • 允许按“关键字”区分通知粒度,只有使用到某个 aspect(方面)的子 Widget 在该“方面”变化时才重建。
    • 适合一个 InheritedWidget 管理多种状态,每个子 Widget 只关心其中一小部分变化。
    // 示例略;通常直接用 Provider 或 Riverpod 即可,无需手动 InheritedModel

3.3 代码示例与示意图

图解:InheritedWidget 更新流程

┌─────────────────────────────────┐
│ MyApp ──► InheritedCounterPage   │
│         (Stateful, 持有 _count)    │
│           │   │                    │
│           │ setState()             │
│           ▼   ▼                    │
│     CounterInherited (count=x)     │◄─── 数据分发
│           │                        │
│  ┌────────┴────────┐               │
│  │     子树 Widget   │               │
│  │  CounterDisplay  │◄─── dependOnInheritedWidgetOfExactType
│  └─────────────────┘               │
└─────────────────────────────────┘
  • 只要 CounterInheritedcount 变化(updateShouldNotify 返回 true),依赖它的子树就会重新构建。

四、推荐:Provider 生态

在实际项目中,InheritedWidget 虽然灵活,但较为底层,写起来冗长。不少团队更倾向于使用基于它的封装——Provider

4.1 Provider 基本用法

  • ProviderInheritedWidgetChangeNotifierConsumer 等都做了封装,使得状态管理更加简洁。
  • 只需在树顶用 ChangeNotifierProvider(或 ProviderStateNotifierProvider 等)包装,然后在子孙组件里用 context.watch<T>()context.read<T>()Consumer<T>() 等方式获取或监听数据。

简单示例:计数器

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

// 1. 定义 ChangeNotifier
class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;
  void increment() {
    _count++;
    notifyListeners();
  }
}

// 2. 在根部提供 CounterModel
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const CounterProviderPage(),
    );
  }
}

// 3. 子组件使用 Provider
class CounterProviderPage extends StatelessWidget {
  const CounterProviderPage({super.key});

  @override
  Widget build(BuildContext context) {
    // 通过 context.watch<CounterModel>() 获取并订阅变化
    final counter = context.watch<CounterModel>().count;
    print('CounterProviderPage build: $counter');

    return Scaffold(
      appBar: AppBar(title: const Text('Provider 示例')),
      body: Center(child: Text('当前计数:$counter', style: const TextStyle(fontSize: 24))),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • 解释

    • ChangeNotifierProvider 创建并提供 CounterModel
    • context.watch<CounterModel>()build 时订阅 CounterModel,当 notifyListeners() 触发时,自动重建;
    • context.read<CounterModel>() 仅一次性获取实例,不订阅变化,可用来调用 increment()

4.2 ChangeNotifierProviderConsumer

  • Consumer<T>

    • 如果只想让 Widget 树中某个子树依赖变化,避免整个 build 重新执行,可用 Consumer 包裹一小段子树。
    • 例如在大型页面里,仅头部计数器区域需要跟随 CounterModel.count 更新,其它部分不用。
class ConsumerExamplePage extends StatelessWidget {
  const ConsumerExamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    print('ConsumerExamplePage build'); // 仅执行一次
    return Scaffold(
      appBar: AppBar(title: const Text('Consumer 示例')),
      body: Column(
        children: [
          const StaticWidget(), // 静态区
          // 只让 CounterConsumer 重建
          Consumer<CounterModel>(
            builder: (context, counterModel, child) {
              print('CounterConsumer build: ${counterModel.count}');
              return Text('计数:${counterModel.count}', style: const TextStyle(fontSize: 24));
            },
          ),
          const StaticWidget(), // 另一静态区
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • 效果

    • 点击按钮只会触发 CounterConsumer 内的 builder,外层 ConsumerExamplePage.buildStaticWidget 都不会重建。

4.3 FutureProvider / StreamProvider

  • FutureProvider

    • 当状态需要来自异步操作(如网络请求)时,用 FutureProvider 自动管理 Future 的生命周期,并根据 AsyncValue<T>dataloadingerror)自动刷新。
Future<String> fetchUsername() async {
  await Future.delayed(const Duration(seconds: 2));
  return '张三';
}

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

  @override
  Widget build(BuildContext context) {
    return FutureProvider<String>(
      create: (_) => fetchUsername(),
      initialData: '加载中...',
      child: Scaffold(
        appBar: AppBar(title: const Text('FutureProvider 示例')),
        body: Center(
          child: Consumer<String>(
            builder: (context, username, child) {
              return Text('用户名:$username', style: const TextStyle(fontSize: 24));
            },
          ),
        ),
      ),
    );
  }
}
  • StreamProvider

    • 若状态来自持续变化的数据流(如 WebSocket、数据库监听),可用 StreamProvider
Stream<int> counterStream() async* {
  int i = 0;
  while (true) {
    await Future.delayed(const Duration(seconds: 1));
    yield i++;
  }
}

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

  @override
  Widget build(BuildContext context) {
    return StreamProvider<int>(
      create: (_) => counterStream(),
      initialData: 0,
      child: Scaffold(
        appBar: AppBar(title: const Text('StreamProvider 示例')),
        body: Center(
          child: Consumer<int>(
            builder: (context, count, child) {
              return Text('流计数:$count', style: const TextStyle(fontSize: 24));
            },
          ),
        ),
      ),
    );
  }
}

4.4 代码示例与图解

图解:Provider 数据流向

┌────────────────────────────────────────┐
│         ChangeNotifierProvider        │
│  (创建 CounterModel 并注入子树)         │
│             ──► CounterModel          │
│                  │                     │
│  ┌───────────────┴───────────────┐     │
│  │    Consumer<CounterModel>     │     │
│  │    (监听并自动重建部分子树)     │     │
│  └───────────────────────────────┘     │
│                  │                     │
│          FloatingActionButton         │
│         (调用 CounterModel.increment)  │
└────────────────────────────────────────┘
  • 当子组件调用 increment() 时,ChangeNotifier.notifyListeners() 会通知所有依赖的 Consumercontext.watch 重新构建。

五、替代与扩展:Riverpod

虽然 Provider 生态非常成熟,但其依赖于 InheritedWidget,在某些场景下会有一些限制。RiverpodProvider 作者推出,解决了一些 Provider 的局限性。

5.1 为什么选择 Riverpod

  • 与 Widget 树解耦

    • ProviderChangeNotifierProvider 必须放在 Widget 树中,RiverpodProviderScope 也要放在最外面,但内部 Provider 的创建与使用不依赖于 BuildContext
  • 可测试性更强

    • Riverpod 中,可以轻易在单位测试中 override(覆盖)任何 Provider,无需依赖 Widget 测试。
  • 消除 context 范围限制

    • context.read<T>() 只能在 Widgetbuild() 中使用,而 Riverpodref.read() 可在任意地方使用。
  • 更丰富的高级特性

    • 支持 Provider 之间的依赖注入、自动销毁、状态复用(AutoDispose)、FamilyAsyncValue 等。

5.2 Provider vs Riverpod 对比

特性ProviderRiverpod
依赖注入通过 ChangeNotifierProvider 等包裹 Widget 树通过 ProviderScope,并用 ref.watch / ref.read 在任意地方获取状态
生命周期管理依赖 InheritedWidget,与 Widget 生存期紧耦合独立于 Widget 树,可使用 autoDispose 自动释放
测试友好需要借助 Provider 包装 Mock,并在 Widget 测试中使用可以直接在单元测试中用 ProviderContainer override Provider,不需 Widget
Async 状态封装FutureProvider / StreamProviderFutureProvider / StreamProvider + AsyncValue 强大的错误 / 载入 / 数据模型
依赖 Provider 依赖链通过 context.read<A>().someMethod()providerA = Provider((ref) => ...); providerB = Provider((ref) => ref.watch(providerA) + 1);

5.3 Riverpod 代码示例

下面演示一个用 Riverpod 实现的计数器示例。

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

// 1. 定义一个 StateProvider 管理简单的 int 状态
final counterProvider = StateProvider<int>((ref) => 0);

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: const CounterRiverpodPage());
  }
}

class CounterRiverpodPage extends ConsumerWidget {
  const CounterRiverpodPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 2. 通过 ref.watch 订阅 counterProvider
    final count = ref.watch(counterProvider);
    print('CounterRiverpodPage build: $count');
    return Scaffold(
      appBar: AppBar(title: const Text('Riverpod 示例')),
      body: Center(child: Text('当前计数:$count', style: const TextStyle(fontSize: 24))),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 3. 通过 ref.read 修改状态
          ref.read(counterProvider.notifier).state++;
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • 解释

    1. counterProvider 是一个 StateProvider<int>,管理一个整数状态。
    2. ref.watch(counterProvider) 相当于订阅了该状态,state 值变化时会触发 build()
    3. ref.read(counterProvider.notifier).state++ 修改状态,自动通知所有订阅该 Provider 的 Widget。
  • 优点

    • 不依赖于 BuildContext,可以在回调、API 层直接使用 ref.read
    • 自动处理生命周期,若页面销毁,相关状态可用 autoDispose 自动释放。

六、复杂场景:Bloc(业务逻辑组件)

当应用业务逻辑复杂,需要更明确地控制事件与状态转换时,Bloc(Business Logic Component)模式常用于将 UI 与逻辑完全分离。

6.1 Bloc 概念与优缺点

  • 概念

    • Bloc 强调“事件 -> 处理逻辑 -> 状态” 三个阶段,通过流(Stream)来传递这些数据。
    • 常用库:flutter_bloc,提供 BlocProviderBlocBuilderBlocListener 等封装。
  • 优点

    • 业务逻辑与 UI 完全解耦,易于测试。
    • 事件与状态变化可序列化,便于调试、日志与回放。
    • 支持复杂场景,如并行请求、序列化操作。
  • 缺点

    • 学习曲线较陡,新手理解事件流与状态流较为困难。
    • 代码量相对较多,模板化较重。

6.2 Cubit 简化版本

CubitBloc 的轻量化版本,只保留状态管理,不需要事件类,最小化模板。适用于中等复杂度场景。

Cubit 代码示例:计数器

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

// 1. 定义 CounterCubit,继承 Cubit<int>
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0); // 初始状态为 0
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

void main() {
  runApp(const MyBlocApp());
}

class MyBlocApp extends StatelessWidget {
  const MyBlocApp({super.key});
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      // 2. 在根部提供 CounterCubit
      create: (_) => CounterCubit(),
      child: const MaterialApp(home: CounterBlocPage()),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print('CounterBlocPage build');
    return Scaffold(
      appBar: AppBar(title: const Text('Cubit 示例')),
      body: Center(
        // 3. BlocBuilder 监听 CounterCubit 的状态
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, count) {
            print('BlocBuilder build: $count');
            return Text('当前计数:$count', style: const TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'inc',
            onPressed: () => context.read<CounterCubit>().increment(),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            heroTag: 'dec',
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
  • 流程

    1. CounterCubit 初始化 state=0
    2. BlocProviderCounterCubit 注入子树;
    3. BlocBuilder 订阅 CounterCubit,每当调用 emit() 改变状态时,builder 会被触发。

6.3 代码示例与事件流图解

图解:Cubit 状态流

┌───────────────────────────────────────────┐
│   CounterCubit (extends Cubit<int>)       │
│   Initial state: 0                        │
│             │                             │
│   increment()/decrement()                 │
│             │                             │
│        emit(newState) ──► 状态变更通知    │
│             │                             │
│   BlocBuilder<CounterCubit, int> listens │
│      to state and rebuilds on change      │
└───────────────────────────────────────────┘
  • BlocBuilder 对应 UI 部分,只会重建依赖状态的文本 Widget,其余页面部分不受影响。

七、实战案例:Todo 应用对比多种方案

下面通过一个Todo 列表应用,逐步用三种不同的状态管理方案实现:setStateProviderBloc。对比它们在实现难度、可维护性、性能、测试友好等方面的差异。

7.1 需求描述与核心功能

  • 显示一个 Todo 列表(List<TodoItem>),每条包含 titlecompleted
  • 可以新增 Todo:弹出对话框输入标题后加入列表。
  • 可以切换 Todo 的完成状态(勾选)。
  • 显示“已完成数量 / 总数量” 的统计。

7.2 setState 实现

7.2.1 完整代码

import 'package:flutter/material.dart';

class TodoItem {
  String title;
  bool completed;
  TodoItem({required this.title, this.completed = false});
}

class TodoSetStatePage extends StatefulWidget {
  const TodoSetStatePage({super.key});
  @override
  State<TodoSetStatePage> createState() => _TodoSetStatePageState();
}

class _TodoSetStatePageState extends State<TodoSetStatePage> {
  final List<TodoItem> _todos = [];

  void _addTodo() async {
    final title = await showDialog<String>(
      context: context,
      builder: (context) {
        String input = '';
        return AlertDialog(
          title: const Text('新建 Todo'),
          content: TextField(onChanged: (v) => input = v),
          actions: [
            TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
            TextButton(onPressed: () => Navigator.pop(context, input), child: const Text('确定')),
          ],
        );
      },
    );
    if (title != null && title.trim().isNotEmpty) {
      setState(() {
        _todos.add(TodoItem(title: title.trim()));
      });
    }
  }

  void _toggleCompleted(int index) {
    setState(() {
      _todos[index].completed = !_todos[index].completed;
    });
  }

  @override
  Widget build(BuildContext context) {
    final total = _todos.length;
    final completedCount = _todos.where((t) => t.completed).length;

    return Scaffold(
      appBar: AppBar(title: const Text('Todo (setState)')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8),
            child: Text('已完成 $completedCount / $total', style: const TextStyle(fontSize: 18)),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (ctx, i) {
                final item = _todos[i];
                return CheckboxListTile(
                  title: Text(item.title),
                  value: item.completed,
                  onChanged: (_) => _toggleCompleted(i),
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addTodo,
        child: const Icon(Icons.add),
      ),
    );
  }
}

7.2.2 分析

  • 优点:实现简单,上手快,适合小型页面或状态范围很局部的场景。
  • 缺点:当业务增大时:

    • _todos 集合仅在该 State 内,若从其他页面也需要访问 Todo 列表,就需要通过回调或路由参数传递;
    • 统计逻辑直接在 build 中计算,若列表很大,会导致每次重建都遍历一次;
    • 难以单独测试业务逻辑。

7.3 Provider 实现

7.3.1 定义 Model 与 Provider

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

// 1. Model
class TodoItem {
  String title;
  bool completed;
  TodoItem({required this.title, this.completed = false});
}

// 2. ChangeNotifier 提供全局状态
class TodoModel extends ChangeNotifier {
  final List<TodoItem> _todos = [];
  List<TodoItem> get todos => List.unmodifiable(_todos);

  void addTodo(String title) {
    _todos.add(TodoItem(title: title));
    notifyListeners();
  }

  void toggle(int index) {
    _todos[index].completed = !_todos[index].completed;
    notifyListeners();
  }

  int get total => _todos.length;
  int get completedCount => _todos.where((t) => t.completed).length;
}

7.3.2 主应用与页面

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: const TodoProviderPage());
  }
}

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

  Future<void> _addTodoDialog(BuildContext context) async {
    final model = context.read<TodoModel>();
    final title = await showDialog<String>(
      context: context,
      builder: (ctx) {
        String input = '';
        return AlertDialog(
          title: const Text('新建 Todo'),
          content: TextField(onChanged: (v) => input = v),
          actions: [
            TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
            TextButton(onPressed: () => Navigator.pop(ctx, input), child: const Text('确定')),
          ],
        );
      },
    );
    if (title != null && title.trim().isNotEmpty) {
      model.addTodo(title.trim());
    }
  }

  @override
  Widget build(BuildContext context) {
    print('TodoProviderPage build');
    return Scaffold(
      appBar: AppBar(title: const Text('Todo (Provider)')),
      body: Column(
        children: [
          // 3. Consumer 仅重建统计部分
          Consumer<TodoModel>(
            builder: (context, model, child) {
              return Padding(
                padding: const EdgeInsets.all(8),
                child: Text(
                  '已完成 ${model.completedCount} / ${model.total}',
                  style: const TextStyle(fontSize: 18),
                ),
              );
            },
          ),
          const Divider(),
          // 4. Consumer 重建列表
          Expanded(
            child: Consumer<TodoModel>(
              builder: (context, model, child) {
                return ListView.builder(
                  itemCount: model.todos.length,
                  itemBuilder: (ctx, i) {
                    final item = model.todos[i];
                    return CheckboxListTile(
                      title: Text(item.title),
                      value: item.completed,
                      onChanged: (_) => model.toggle(i),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _addTodoDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • 分析

    • TodoModel 抽离了业务逻辑:新增、切换完成状态、统计;
    • 页面层只专注于 UI,通过 Consumercontext.watch 监听 TodoModel 状态变化,重建相应子树;
    • 逻辑与 UI 解耦,可单独对 TodoModel 编写单元测试。

7.4 Bloc 实现

7.4.1 定义事件与状态

import 'package:equatable/equatable.dart';

class TodoItem {
  String title;
  bool completed;
  TodoItem({required this.title, this.completed = false});
}

// 1. 事件 (Event)
abstract class TodoEvent extends Equatable {
  @override
  List<Object?> get props => [];
}

class AddTodoEvent extends TodoEvent {
  final String title;
  AddTodoEvent(this.title);
  @override
  List<Object?> get props => [title];
}

class ToggleTodoEvent extends TodoEvent {
  final int index;
  ToggleTodoEvent(this.index);
  @override
  List<Object?> get props => [index];
}

// 2. 状态 (State)
class TodoState extends Equatable {
  final List<TodoItem> todos;
  const TodoState(this.todos);
  @override
  List<Object?> get props => [todos];
}

7.4.2 定义 Bloc

import 'package:flutter_bloc/flutter_bloc.dart';

class TodoBloc extends Bloc<TodoEvent, TodoState> {
  TodoBloc() : super(const TodoState([])) {
    on<AddTodoEvent>((event, emit) {
      final newList = List<TodoItem>.from(state.todos)
        ..add(TodoItem(title: event.title));
      emit(TodoState(newList));
    });
    on<ToggleTodoEvent>((event, emit) {
      final newList = List<TodoItem>.from(state.todos);
      newList[event.index].completed = !newList[event.index].completed;
      emit(TodoState(newList));
    });
  }
}

7.4.3 页面层

void main() {
  runApp(const MyBlocTodoApp());
}

class MyBlocTodoApp extends StatelessWidget {
  const MyBlocTodoApp({super.key});
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => TodoBloc(),
      child: const MaterialApp(home: TodoBlocPage()),
    );
  }
}

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

  Future<void> _addTodoDialog(BuildContext context) async {
    final bloc = context.read<TodoBloc>();
    final title = await showDialog<String>(
      context: context,
      builder: (ctx) {
        String input = '';
        return AlertDialog(
          title: const Text('新建 Todo'),
          content: TextField(onChanged: (v) => input = v),
          actions: [
            TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
            TextButton(onPressed: () => Navigator.pop(ctx, input), child: const Text('确定')),
          ],
        );
      },
    );
    if (title != null && title.trim().isNotEmpty) {
      bloc.add(AddTodoEvent(title.trim()));
    }
  }

  @override
  Widget build(BuildContext context) {
    print('TodoBlocPage build');
    return Scaffold(
      appBar: AppBar(title: const Text('Todo (Bloc)')),
      body: Column(
        children: [
          // 1. BlocBuilder 只监听状态变化
          BlocBuilder<TodoBloc, TodoState>(
            buildWhen: (previous, current) =>
                previous.todos.length != current.todos.length ||
                previous.todos.where((t) => t.completed).length !=
                    current.todos.where((t) => t.completed).length,
            builder: (context, state) {
              final total = state.todos.length;
              final completed = state.todos.where((t) => t.completed).length;
              return Padding(
                padding: const EdgeInsets.all(8),
                child: Text('已完成 $completed / $total', style: const TextStyle(fontSize: 18)),
              );
            },
          ),
          const Divider(),
          Expanded(
            child: BlocBuilder<TodoBloc, TodoState>(
              buildWhen: (previous, current) => previous.todos != current.todos,
              builder: (context, state) {
                return ListView.builder(
                  itemCount: state.todos.length,
                  itemBuilder: (ctx, i) {
                    final item = state.todos[i];
                    return CheckboxListTile(
                      title: Text(item.title),
                      value: item.completed,
                      onChanged: (_) => context.read<TodoBloc>().add(ToggleTodoEvent(i)),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _addTodoDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }
}

7.4.4 性能与可维护性对比

特性setStateProviderBloc
学习成本较高
代码量适中较多
解耦程度优秀
可测试性较差较好极佳
频繁重建控制需手动拆分局部组件通过 Consumercontext.watch 控制通过 buildWhen 精准控制
业务逻辑复用困难较方便容易
适用场景页面很小、状态范围局部小-中型应用,全局少量共享状态大型应用、业务逻辑复杂、多模块解耦场景

八、总结与最佳实践

  1. 先从最简单做起

    • 对于非常简单的页面,只需局部 setState 即可;避免过度设计。
    • 将状态拆分到最小粒度的 StatefulWidget,只重建必要子树。
  2. 中型场景首选 Provider 系列方案

    • 当需要跨多个页面或组件共享状态时,Provider(或 Riverpod)能快速上手,代码简洁,易于重构、测试。
    • 使用 Consumercontext.watch 可以精准控制重建范围,避免 UI 卡顿。
  3. 复杂业务逻辑使用 Bloc/Cubit

    • 当业务逻辑非常复杂,需要事件驱动、严格分层、可测试时,选择 Bloc 或其简化版 Cubit
    • 通过 buildWhenlistenWhenBlocListener 等可精细控制 UI 更新,结合 Equatable 简化状态比较。
  4. 更现代的工具 Riverpod

    • 如果项目注重自动销毁、依赖注入灵活性与测试友好性,可直接使用 Riverpod
    • Riverpod 让状态定义与使用更加直观,且易于 Mock 和覆盖,极大简化测试流程。
  5. 避免状态管理的常见错误

    • 不要把业务逻辑写在 build() 中,避免大量计算拖慢 UI;
    • 不要在 initState()build() 中直接执行耗时操作,应使用异步 Provider、FutureProvider、Bloc 在事件中处理;
    • 不要频繁调用 setState,避免重建范围过大;
    • 注意资源释放:对于 StreamTimerAnimationController 等,应在 dispose() 中手动释放,或使用 autoDispose(Riverpod)自动释放。
  6. 持续监测与优化

    • 在开发过程中使用 Flutter DevTools 的 PerformanceFlutter Inspector 查看到底哪些组件在重建、哪些操作在占用大量 CPU;
    • 定期做性能评测(Profile 模式),保证不同方案在设备上的流畅度。
结语
Flutter 状态管理无“万金油”方案,需结合项目规模、团队熟悉度及性能要求选型。本文所述的从“纯 setState”到“Provider/Riverpod”再到“Bloc”的三种实践思路,可覆盖绝大部分真实场景。希望你能在实战中找到最适合自己团队和项目的状态管理方式,写出高质量、易维护且性能出色的 Flutter 应用。
最后修改于:2025年06月03日 14:51

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日