全栈开发知识库

04 状态管理与架构

4.1 状态分类

类型 例子 方案
临时 UI 状态(Ephemeral) 按钮是否按下、Tab 下标、输入框内容 StatefulWidget + setState
跨组件/页面共享状态(App State) 登录用户、购物车、设置、列表数据 Provider / Riverpod / Bloc

原则:能用 setState 就不要上全局状态;需要共享时把状态“提升”到公共祖先,或交给状态管理库。

4.2 底层机制:InheritedWidget 与 Listenable

所有状态管理库都建立在这两个基础上,理解它们就理解了 Provider 等库在做什么。

InheritedWidget:沿树向下共享数据

class UserScope extends InheritedWidget {
  const UserScope({super.key, required this.user, required super.child});
  final User user;

  // 子孙通过 context 查找最近的 UserScope,同时注册依赖
  static User of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<UserScope>()!.user;

  // 返回 true 时,所有调用过 of(context) 的子孙会重建
  @override
  bool updateShouldNotify(UserScope old) => old.user != user;
}

Theme.of(context)、MediaQuery.of(context) 都是这样实现的。查找是 O(1) 的(每个 Element 持有祖先 InheritedElement 的 Map)。

ChangeNotifier / ValueNotifier:可监听对象(≈ LiveData)

final counter = ValueNotifier<int>(0);

ValueListenableBuilder<int>(
  valueListenable: counter,
  builder: (context, value, child) => Text('$value'),  // 只重建这一小块
);
counter.value++;

4.3 主流方案对比

方案 类比 优点 缺点 适用
Provider 手工 DI + LiveData 官方曾推荐,简单 依赖 context,运行时找不到会崩 小中型项目
Riverpod Hilt + StateFlow 编译期安全、不依赖 context、易测试、支持异步/缓存/自动释放 概念多 中大型项目,推荐
Bloc / Cubit MVI 事件→状态,严格单向数据流,可追溯 样板代码多 大团队、强规范
GetX — 上手快 侵入性强、隐式魔法多、维护风险 不推荐用于长期项目
signals / MobX — 细粒度响应式 生态较小 按需

团队选型建议:选 Riverpod 或 Bloc 其中一个并统一。下面分别给出写法。

4.4 Provider(入门)

// 1. 定义 ViewModel
class CartModel extends ChangeNotifier {
  final List<Item> _items = [];
  List<Item> get items => List.unmodifiable(_items);
  int get total => _items.fold(0, (s, e) => s + e.price);

  void add(Item item) {
    _items.add(item);
    notifyListeners();          // ≈ liveData.setValue
  }
}

// 2. 在树的上层提供
void main() => runApp(
  ChangeNotifierProvider(create: (_) => CartModel(), child: const MyApp()),
);

// 3. 使用
final cart = context.watch<CartModel>();       // 监听,变化时重建当前 Widget
context.read<CartModel>().add(item);           // 只读取,不监听(在回调里用)
final total = context.select<CartModel, int>((c) => c.total);  // 只监听某个字段

4.5 Riverpod(推荐)

dependencies:
  flutter_riverpod: ^3.0.0
  riverpod_annotation: ^3.0.0
dev_dependencies:
  riverpod_generator: ^3.0.0
  build_runner: ^2.4.0

(版本号仅示意,以 flutter pub add 结果为准。)

import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() => runApp(const ProviderScope(child: MyApp()));

// 依赖注入:Repository
final feedRepoProvider = Provider<FeedRepository>((ref) => FeedRepository(ref.watch(dioProvider)));

// 异步数据 + 状态(≈ ViewModel + StateFlow<UiState>)
class FeedNotifier extends AsyncNotifier<List<Feed>> {
  int _page = 1;

  @override
  Future<List<Feed>> build() async {
    _page = 1;
    return ref.read(feedRepoProvider).fetch(page: 1);   // 首次加载
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(build);              // 自动捕获异常为 AsyncError
  }

  Future<void> loadMore() async {
    final current = state.value ?? [];
    final next = await ref.read(feedRepoProvider).fetch(page: ++_page);
    state = AsyncData([...current, ...next]);
  }
}

final feedProvider = AsyncNotifierProvider<FeedNotifier, List<Feed>>(FeedNotifier.new);

// UI
class FeedPage extends ConsumerWidget {
  const FeedPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final feed = ref.watch(feedProvider);
    return feed.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Text('加载失败:$e')),
      data: (list) => RefreshIndicator(
        onRefresh: () => ref.read(feedProvider.notifier).refresh(),
        child: ListView.builder(
          itemCount: list.length,
          itemBuilder: (_, i) => FeedItem.fromModel(list[i]),
        ),
      ),
    );
  }
}

要点:

  • ref.watch 在 build 中使用(订阅);ref.read 在回调中使用(一次性读取);ref.listen 用来做副作用(弹 Toast、跳转)。
  • autoDispose:没有监听者时自动销毁,等价于 ViewModel 随页面销毁。
  • family:带参数的 provider,例如 detailProvider(id)。
  • 测试时用 ProviderScope(overrides: [feedRepoProvider.overrideWithValue(FakeRepo())]) 替换依赖。

4.6 Bloc / Cubit

// Cubit:简化版,直接调用方法 emit 新状态
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void inc() => emit(state + 1);
}

// Bloc:事件驱动
sealed class LoginEvent {}
class LoginSubmitted extends LoginEvent { LoginSubmitted(this.phone, this.code); final String phone, code; }

sealed class LoginState {}
class LoginIdle extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {}
class LoginFailure extends LoginState { LoginFailure(this.msg); final String msg; }

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  LoginBloc(this._repo) : super(LoginIdle()) {
    on<LoginSubmitted>((e, emit) async {
      emit(LoginLoading());
      try {
        await _repo.login(e.phone, e.code);
        emit(LoginSuccess());
      } catch (err) {
        emit(LoginFailure('$err'));
      }
    });
  }
  final AuthRepository _repo;
}

// UI
BlocProvider(create: (_) => LoginBloc(repo), child: const LoginView());
BlocBuilder<LoginBloc, LoginState>(builder: (context, state) => switch (state) {
  LoginLoading() => const CircularProgressIndicator(),
  _ => ElevatedButton(
      onPressed: () => context.read<LoginBloc>().add(LoginSubmitted(phone, code)),
      child: const Text('登录')),
});
BlocListener<LoginBloc, LoginState>(listener: (context, state) { /* 跳转、Toast */ }, child: ...);

4.7 推荐分层架构

与 Android 官方推荐架构(UI Layer / Domain Layer / Data Layer)基本一致:

lib/
├── main.dart
├── app/                    # App 入口、路由表、主题、全局 Provider
├── core/                   # 网络、存储、日志、错误、工具、基础组件
│   ├── network/
│   ├── storage/
│   └── widgets/
├── features/               # 按业务模块拆分(≈ Android 的 feature module)
│   ├── feed/
│   │   ├── data/           # Repository 实现、DTO、API
│   │   ├── domain/         # 实体、用例(小项目可省略)
│   │   └── presentation/   # 页面、组件、Notifier/Bloc
│   └── profile/
└── l10n/                   # 国际化

规则:

  1. UI 只依赖状态对象(Notifier/Bloc),不直接调用 dio。
  2. Repository 负责组合远端与本地数据,返回领域模型而不是原始 JSON。
  3. 平台能力(MethodChannel)也封装在 data 层,对上暴露 Dart 接口,便于在测试中替换。
  4. 大型项目可用 melos 管理多 package 的 monorepo,按 feature 拆 package。

动手练习

  1. 用 Riverpod 改写第 3 章的 feed 流,实现 loading / error / data 三态与分页。
  2. 实现一个全局“登录态” provider:未登录时点击“点赞”跳转登录页,登录后自动回到原页面并完成点赞。
  3. 用 ProviderScope.overrides 写一个 Widget 测试,注入假的 Repository。
本页由 Markdown 自动渲染 · 共 628 篇笔记