Flutter 状态管理

运行代码生成: AsyncValue 常用方法: 在 build 方法中错误地使用 ref.read 会导致状态变化后 UI 不更新: 小组件用 StatefulWidget,应用级状态用 Riverpod:setState 适合局部 UI 状态(按钮选中、展开收起),跨页面共享状态改用 Riverpod 的 Provider/AsyncNotifier,避免 prop drilling。 Riverpod 优先用 AsyncNotifierProvider:异步数据(API 调用)统一用 AsyncNotifierProvider,自动处理 loadi

分享

官方文档:https://docs.flutter.dev/data-and-backend/state-mgmt/intro
适用版本:Flutter 3.x(2026-05-07 核实)

状态管理方案对比

方案 学习曲线 适用规模 测试友好度 备注
setState 小型/组件级 一般 Flutter 内置,无需依赖
Provider 低-中 小型-中型 良好 官方推荐入门方案
Riverpod 中型-大型 优秀 Provider 的改进版,推荐
Bloc 中-高 中型-大型 优秀 事件驱动,结构严格
GetX 中小型 较差 功能大而全,侵入性强

Riverpod(推荐方案)

安装

# pubspec.yaml
dependencies:
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

dev_dependencies:
  build_runner: ^2.4.9
  riverpod_generator: ^2.4.0

Provider 类型

Provider 类型 用途 返回值特征
Provider 只读依赖、常量计算值 不可变
StateProvider 简单状态(计数器、开关等) 可通过 ref.read().state = x 修改
StateNotifierProvider 复杂状态,Riverpod 1.x 推荐 StateNotifier 子类
FutureProvider 单次异步请求 AsyncValue<T>
StreamProvider 持续监听的数据流 AsyncValue<T>
NotifierProvider 复杂状态,Riverpod 2.x 推荐 Notifier 子类

ref.watch / ref.read / ref.listen 区别

方法 触发重建 使用场景
ref.watch(provider) Widget build 方法内,监听状态变化
ref.read(provider) 事件回调内,只需读取一次或调用方法
ref.listen(provider, callback) 否(手动处理) 监听变化并执行副作用(导航、弹窗等)

StateNotifier + StateNotifierProvider 示例

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 状态类
class CounterState {
  final int count;
  final bool isLoading;

  const CounterState({required this.count, this.isLoading = false});

  CounterState copyWith({int? count, bool? isLoading}) {
    return CounterState(
      count: count ?? this.count,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

// StateNotifier
class CounterNotifier extends StateNotifier<CounterState> {
  CounterNotifier() : super(const CounterState(count: 0));

  void increment() {
    state = state.copyWith(count: state.count + 1);
  }

  void decrement() {
    state = state.copyWith(count: state.count - 1);
  }

  Future<void> fetchFromServer() async {
    state = state.copyWith(isLoading: true);
    await Future.delayed(const Duration(seconds: 1));
    state = state.copyWith(count: 42, isLoading: false);
  }
}

// Provider 定义
final counterProvider =
    StateNotifierProvider<CounterNotifier, CounterState>((ref) {
  return CounterNotifier();
});

// 使用
class CounterWidget extends ConsumerWidget {
  const CounterWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(counterProvider);
    return Column(
      children: [
        Text('Count: ${state.count}'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

Notifier + NotifierProvider(Riverpod 2.x 新 API)

import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterNotifier2 extends Notifier<int> {
  @override
  int build() => 0; // 初始状态

  void increment() => state++;
  void decrement() => state--;
}

final counterProvider2 = NotifierProvider<CounterNotifier2, int>(() {
  return CounterNotifier2();
});

代码生成:@riverpod 注解

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter.g.dart';

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
}

// 异步 Provider
@riverpod
Future<List<String>> userList(UserListRef ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/users'));
  return parseUsers(response.body);
}

运行代码生成:

dart run build_runner build --delete-conflicting-outputs

AsyncValue 处理异步状态

@riverpod
Future<User> userDetail(UserDetailRef ref, int id) async {
  return await apiClient.getUser(id);
}

// 在 Widget 中使用
class UserDetailWidget extends ConsumerWidget {
  final int userId;
  const UserDetailWidget({super.key, required this.userId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncUser = ref.watch(userDetailProvider(userId));

    return asyncUser.when(
      data: (user) => Text(user.name),
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

AsyncValue 常用方法:

方法/属性 说明
when(data:, loading:, error:) 分支处理三种状态
maybeWhen(...) 仅处理部分分支,其余走 orElse
whenData((data) => ...) 仅转换 data,保留 loading/error
value 获取数据,可能为 null
hasValue 是否有数据(包括刷新中)
isLoading 是否正在加载
hasError 是否有错误

ref.invalidate() 刷新 Provider

// 强制重新执行 FutureProvider 或 StreamProvider
ref.invalidate(userListProvider);

// 带参数的 Provider
ref.invalidate(userDetailProvider(userId));

ProviderScope 覆盖(测试场景)

// 测试中覆盖 Provider
testWidgets('shows user name', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        userDetailProvider(1).overrideWith((ref) async => User(name: 'Test')),
      ],
      child: const MaterialApp(home: UserDetailWidget(userId: 1)),
    ),
  );
  expect(find.text('Test'), findsOneWidget);
});

Bloc

Event / State / Bloc 三层结构

// Event
abstract class CounterEvent {}
class CounterIncremented extends CounterEvent {}
class CounterDecremented extends CounterEvent {}

// State
class CounterState {
  final int count;
  const CounterState(this.count);
}

// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<CounterIncremented>((event, emit) {
      emit(CounterState(state.count + 1));
    });
    on<CounterDecremented>((event, emit) {
      emit(CounterState(state.count - 1));
    });
  }
}

BlocProvider / BlocBuilder / BlocListener / BlocConsumer

Widget/方法 参数 说明
BlocProvider create, child, lazy 创建并向下注入 Bloc
BlocBuilder bloc, buildWhen, builder 监听状态并重建 UI
BlocListener bloc, listenWhen, listener 监听状态执行副作用,不重建
BlocConsumer bloc, buildWhen, listenWhen, builder, listener 同时具备 Builder 和 Listener
// 提供 Bloc
BlocProvider(
  create: (context) => CounterBloc(),
  child: const CounterView(),
)

// 重建 UI
BlocBuilder<CounterBloc, CounterState>(
  buildWhen: (previous, current) => previous.count != current.count,
  builder: (context, state) => Text('${state.count}'),
)

// 副作用(导航、弹窗)
BlocListener<CounterBloc, CounterState>(
  listenWhen: (previous, current) => current.count == 10,
  listener: (context, state) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Reached 10!')),
    );
  },
  child: const CounterView(),
)

Cubit(简化版 Bloc)

// 无 Event,直接调用方法修改状态
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

context.read vs context.watch

方法 触发重建 使用场景
context.read<MyBloc>() 事件回调中触发 Bloc 事件
context.watch<MyBloc>() build 方法中监听状态
// 事件回调中用 read
ElevatedButton(
  onPressed: () => context.read<CounterBloc>().add(CounterIncremented()),
  child: const Text('+'),
)

// build 中用 watch
Text('${context.watch<CounterBloc>().state.count}')

Bloc 测试

import 'package:bloc_test/bloc_test.dart';

void main() {
  group('CounterBloc', () {
    late CounterBloc bloc;

    setUp(() => bloc = CounterBloc());
    tearDown(() => bloc.close());

    blocTest<CounterBloc, CounterState>(
      'emits [CounterState(1)] when CounterIncremented is added',
      build: () => CounterBloc(),
      act: (bloc) => bloc.add(CounterIncremented()),
      expect: () => [const CounterState(1)],
    );
  });
}

踩坑与注意事项

Riverpod 中 ref.read 不触发重建

build 方法中错误地使用 ref.read 会导致状态变化后 UI 不更新:

// 错误:ref.read 不监听变化
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.read(counterProvider); // 只读一次,不更新
  return Text('$count');
}

// 正确:在 build 中使用 ref.watch
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.watch(counterProvider);
  return Text('$count');
}

Riverpod 中 Provider 不可在 build 之外使用 ref.watch

// 错误:在回调中使用 ref.watch
ElevatedButton(
  onPressed: () {
    final value = ref.watch(someProvider); // 会抛出异常
  },
  child: const Text('OK'),
)

// 正确:回调中用 ref.read
ElevatedButton(
  onPressed: () {
    final value = ref.read(someProvider);
  },
  child: const Text('OK'),
)

Bloc 中 emit 不可在 close 后调用

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(MyInitial()) {
    on<LoadData>((event, emit) async {
      // 如果 Bloc 在请求返回前被关闭,emit 会抛出 StateError
      final data = await repository.fetchData();
      // 需要检查是否已关闭
      if (!isClosed) {
        emit(MyLoaded(data));
      }
    });
  }
}

Riverpod ProviderScope 必须在 runApp 最外层

void main() {
  runApp(
    const ProviderScope( // 必须包裹整个应用
      child: MyApp(),
    ),
  );
}

最佳实践

小组件用 StatefulWidget,应用级状态用 RiverpodsetState 适合局部 UI 状态(按钮选中、展开收起),跨页面共享状态改用 Riverpod 的 Provider/AsyncNotifier,避免 prop drilling。

Riverpod 优先用 AsyncNotifierProvider:异步数据(API 调用)统一用 AsyncNotifierProvider,自动处理 loading/error/data 三态,配合 ref.watch 在 UI 层声明式消费:

@riverpod
class UserList extends _$UserList {
  @override
  Future<List<User>> build() => fetchUsers();

  Future<void> refresh() => ref.refresh(userListProvider.future);
}

Bloc 场景下 Event → State 单向数据流:所有状态变更通过 add(Event) 触发,禁止在 Bloc 外部直接修改 State,保持可测试性和可追踪性。

避免在 Provider 构建函数中执行副作用build() 方法可能被多次调用,副作用(网络请求、写文件)应放在 AsyncNotifier.build 里(返回 Future),而不是构造函数。

ref.listen 替代命令式导航:在 Riverpod 中监听状态变化后触发导航,比 StreamBuilder 更简洁,避免在 build 里写命令式逻辑:

ref.listen(authProvider, (prev, next) {
  if (next.isLoggedOut) context.go('/login');
});

常见陷阱

陷阱:Riverpod ref.watch 在非 build 方法中调用

现象: 在事件处理函数或 initState 中调用 ref.watch,Provider 不更新或报断言错误。
原因: ref.watch 只在 build 方法中有效,非构建期调用不会建立订阅关系。
解决:build 期读取用 ref.read,监听变化用 ref.listen

陷阱:Bloc emit 相同 State 不触发重建

现象: emit(state.copyWith(count: count)) 后 UI 没有更新。
原因: Bloc 默认用 == 比较新旧 State,若引用或值相同则不会触发 rebuild。
解决: 确保 State 实现 Equatableprops 包含所有可变字段,或每次 emit 新实例而非同一引用。

陷阱:全局 Provider 持有 BuildContext 导致内存泄漏

现象: 页面销毁后 Provider 仍存在,持有的 context 引用导致整棵 Widget 树无法 GC。
原因: 在 Provider 的构造函数或 build() 中捕获了 context,而 Provider 生命周期比 Widget 长。
解决: Provider 层禁止持有 context,UI 交互回调通过 ref.read 获取服务后执行操作。


参见

Flutter入门
GoRouter完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog