Flutter Redux 项目实战

前言:

最近抽了一些下班后的时间,把项目里原有的局部状态和分散的数据流完整收敛到了 Redux。一开始学习 Redux 的时候,也看了不少文章和示例,很多内容都在讲 storeactionreducermiddleware 这些基础概念,但真正结合项目页面、网络请求、分页刷新、局部更新来讲的并不算多。
所以这篇文章就结合当前这个 Demo,聊一聊 Redux 在 Flutter 项目中的实际使用方式、设计思路和常见问题。项目中也顺手对状态树、页面组织、局部刷新、路由接入做了一轮整理,现在整体已经不只是"把状态丢进全局 store",而是逐步形成了一套更稳定、更可维护的 Redux 项目写法。
本篇文章更偏实战一些,重点讲的是 Redux 在项目中的落地,而不是单独讲 API。老规矩,先从整体思路开始。

正文:

当前项目中的 Redux 使用,主要从下面几个维度来理解:

  1. Redux 的基本思想
  2. Redux 在页面中的完整使用流程
  3. Redux 如何做局部刷新和颗粒化刷新
  4. Redux 如何组织网络请求和页面状态
  5. Redux 中容易踩的坑和推荐标准
Redux 基本思想

Redux 的核心目标很明确,就是让状态变化具备:

  • 单一数据源
  • 单向数据流
  • 可追踪的状态变更
  • 明确的状态边界

在传统页面局部写法里,我们通常是:

  • 页面里写状态
  • 页面里发请求
  • 页面里判断成功失败
  • 页面里自己决定哪里刷新

而在 Redux 里,思路会更清晰:

  • 页面 dispatch 一个 action
  • middleware/thunk 处理异步请求或副作用
  • reducer 根据 action 生成新状态
  • 页面通过 StoreConnector 订阅自己需要的状态并刷新

也就是说,Redux 更强调"状态的来源统一、变更路径统一、页面订阅清晰"。

这套思路最大的好处就是:

  • 数据流清楚

  • 状态来源统一

  • 调试和排查问题更容易

  • 页面和业务逻辑边界更明确

  • 更适合业务变复杂后的协作和维护

Redux 的核心概念
  • Store 全局状态容器。整个应用的状态树都挂在 Store<AppState> 上。

  • AppState 根状态树。项目里我更推荐它做"聚合器",只负责组合 feature state,不要把所有字段直接平铺在最外层。

  • Action 描述"发生了什么"。比如登录输入变化、列表加载成功、详情页滚动透明度变化等。

  • Reducer 纯函数。只负责接收旧状态和 action,然后返回新状态,不写请求、不写导航、不写副作用。

  • Thunk / Middleware 用来处理异步请求、持久化、日志、埋点等副作用。项目里这部分最适合放网络请求和本地存储。

  • Selector 从状态树中提取页面真正需要的数据。它的价值在于减少页面直接访问深层 state,方便后续调整状态结构。

  • ViewModel 页面层的转换模型。StoreConnector<AppState, ViewModel> 的第二个泛型,最好不要直接滥用成整个 state,而是传页面真正需要的模型。

  • StoreConnector flutter_redux 提供的核心连接组件。负责把 Store<AppState> 转换成页面需要的 ViewModel

使用 Redux 开发一个页面完整流程

下面以"网络列表页"为例,说明一个页面从 0 到 1 的常见写法。

  1. 定义页面状态
dart 复制代码
class ArticleListState {
  const ArticleListState({
    this.netState = NetState.loadingState,
    this.items = const <InfoModel>[],
    this.page = 0,
    this.isNoMoreData = false,
  });

  final NetState netState;
  final List<InfoModel> items;
  final int page;
  final bool isNoMoreData;

  ArticleListState copyWith({
    NetState? netState,
    List<InfoModel>? items,
    int? page,
    bool? isNoMoreData,
  }) {
    return ArticleListState(
      netState: netState ?? this.netState,
      items: items ?? this.items,
      page: page ?? this.page,
      isNoMoreData: isNoMoreData ?? this.isNoMoreData,
    );
  }
}
  1. 定义 action
dart 复制代码
class LoadArticleListRequestAction {
  LoadArticleListRequestAction({required this.isRefresh});

  final bool isRefresh;
}

class LoadArticleListSuccessAction {
  LoadArticleListSuccessAction({
    required this.items,
    required this.page,
    required this.isNoMoreData,
    required this.netState,
  });

  final List<InfoModel> items;
  final int page;
  final bool isNoMoreData;
  final NetState netState;
}
  1. 写 reducer
dart 复制代码
ArticleListState articleListReducer(ArticleListState state, dynamic action) {
  if (action is LoadArticleListRequestAction) {
    return state.copyWith(netState: NetState.loadingState);
  }

  if (action is LoadArticleListSuccessAction) {
    return state.copyWith(
      items: action.items,
      page: action.page,
      isNoMoreData: action.isNoMoreData,
      netState: action.netState,
    );
  }

  return state;
}
  1. 写 thunk 处理异步请求
dart 复制代码
AppThunkAction fetchArticleList({bool isRefresh = true}) {
  return (store, dependencies) async {
    store.dispatch(LoadArticleListRequestAction(isRefresh: isRefresh));

    final int nextPage = isRefresh ? 1 : selectArticlePage(store.state) + 1;
    final response = await dependencies.articleRepository.getArticleList(nextPage);

    final list = (response.data as InfoListModel).works;
    final nextItems = isRefresh
        ? list
        : [...selectArticleItems(store.state), ...list];

    store.dispatch(
      LoadArticleListSuccessAction(
        items: nextItems,
        page: nextPage,
        isNoMoreData: !isRefresh && list.isEmpty,
        netState: nextItems.isEmpty
            ? NetState.emptyDataState
            : NetState.dataSuccessState,
      ),
    );
  };
}
  1. 页面通过 StoreConnector 订阅 ViewModel
dart 复制代码
StoreConnector<AppState, ArticleListViewModel>(
  distinct: true,
  converter: (store) => ArticleListViewModel(
    netState: selectArticleListState(store.state).netState,
    items: selectArticleItems(store.state),
  ),
  builder: (context, vm) {
    return PageStateView(
      state: vm.netState,
      child: ListView.builder(
        itemCount: vm.items.length,
        itemBuilder: (_, index) => Text(vm.items[index].title),
      ),
    );
  },
)

完成上面几步,一个典型的 Redux 网络列表页就基本搭起来了。

这套流程和"页面自己管所有状态"最大的差异点就在于:

  • 页面不直接持有业务状态

  • 请求不直接散落在页面

  • 状态更新必须经过 action 和 reducer

  • 页面只订阅自己真正关心的数据

  • 页面刷新路径清晰、可追踪

Redux 适合做什么

如果从日常业务开发的角度看,Redux 特别适合下面这些场景:

  • 登录态、用户态、Tab 状态这类全局或跨页面状态
  • 列表页刷新和分页
  • 详情页复杂数据聚合
  • 多接口并发、串行请求管理
  • 多人协作时要求状态流清晰的页面
  • 想做明确可维护的"页面状态树"的项目

项目里现在就有这些实际例子:

  • redux-list

  • redux-grid

  • redux-stagger

  • redux-login

  • 单页面多接口并发请求 + 局部刷新案例

  • 单页面多接口串行请求 + 局部刷新案例

Redux 如何做局部刷新

很多同学第一次接触 Redux 时,会觉得它天生就是"全局刷新""整页刷新",其实这是一种误解。

flutter_redux 里,局部刷新通常靠三件事:

  1. StoreConnector 只订阅当前页面真正需要的状态
  2. ViewModel 精简字段
  3. distinct: true + 正确实现相等性

比如详情页里有两种变化:

  1. 页面初次进入,请求主数据、推荐数据、系列数据
  2. 页面滚动时,只让导航栏透明度变化

这时候最推荐的写法不是把所有字段都丢给一个页面 builder,而是拆成多个 StoreConnector

  • 内容区订阅 mainModelseriesListrecommendList
  • 顶部导航订阅 titleappBarAlphascrollOffset
  • 返回按钮只订阅 appBarAlpha

这也是项目里详情页的实际做法。

也就是说,Redux 的"局部刷新"不是靠魔法,而是靠:

  • 状态拆分

  • 订阅拆分

  • ViewModel 拆分

单页面多网络请求实现思路

在实际项目里,经常会碰到一个页面需要多个接口的情况,比如:

  • 详情主数据
  • 同系列数据
  • 推荐数据

Redux 下常见有两种写法:

  • 思路1:串行请求 适合有依赖关系的接口,前一个结果决定后一个请求参数。

  • 思路2:并行请求 适合相互独立的接口,使用 Future.wait 并行获取数据。

示例:

dart 复制代码
final results = await Future.wait<dynamic>([
  _getMainData(dependencies),
  _getSeriesData(dependencies),
  _getRecommendData(dependencies),
]);

store.dispatch(
  LoadNovelDetailSuccessAction(
    mainModel: results[0] as CartoonModelData?,
    seriesList: results[1] as List<CartoonSeriesDataSeriesComics>,
    recommendList: results[2] as List<CartoonRecommendDataInfos>,
  ),
);

和页面局部状态写法相比,这里的关键优势是:

  • 请求入口统一

  • 状态更新路径统一

  • 页面不用自己管理"哪个接口回来了"

  • 更方便测试

Selector 的使用场景

很多同学刚开始用 Redux 时,会在页面里到处写:

dart 复制代码
store.state.article.list.items
store.state.comic.novelDetail.mainModel
store.state.shelf.bannerList

这样短期看能跑,但后面状态树一调整,页面会改得很痛苦。

所以项目里现在加了一层 selector,比如:

dart 复制代码
List<InfoModel> selectArticleItems(AppState state) =>
    selectArticleListState(state).items;

CartoonModelData? selectNovelDetailMainModel(AppState state) =>
    selectNovelDetailState(state).mainModel;

这样页面里只关心:

dart 复制代码
converter: (store) => DetailContentViewModel(
  netState: selectNovelDetailState(store.state).netState,
  mainModel: selectNovelDetailMainModel(store.state),
  seriesList: selectNovelDetailSeriesList(store.state),
  recommendList: selectNovelDetailRecommendList(store.state),
)

selector 的价值主要有三点:

  • 页面解耦状态树结构

  • thunk 和测试也能复用同一套取值逻辑

  • 后续扩展和重构更稳

Redux 中常见的几个误区
  • 误区1:把所有字段直接平铺在 AppState

这会导致根状态树越来越大、越来越乱。更推荐的方式是:

dart 复制代码
AppState
- auth
- tab
- article
  - list
  - detail
- comic
  - list
  - novelDetail
  - profileDetail
- shelf
  • 误区2:StoreConnector 第二个泛型直接传整个页面 state

虽然能跑,但可读性一般。更推荐显式 ViewModel,让页面真正只拿自己需要的数据。

  • 误区3:在 reducer 里做请求、副作用、导航

reducer 必须保持纯函数。请求、持久化、日志这类逻辑应该放 thunk 或 middleware。

  • 误区4:页面里到处直接访问 store.state.xxx

短期快,长期乱。更推荐 selector。

  • 误区5:以为 Redux 天生做不到局部刷新

其实问题不在 Redux,而在是否做了状态拆分和 ViewModel 拆分。

Redux 中最容易踩的坑

这里结合这次项目改造,重点说几个非常容易踩的点:

  • 坑1:StoreProvider 放得太低

如果只把 StoreProvider 包在 home 上,而不是包住整个 MaterialApp,那么通过路由 push 出来的页面和 overlay 里就可能拿不到 store。

推荐做法:

dart 复制代码
StoreProvider<AppState>(
  store: store,
  child: MaterialApp(...),
)
  • 坑2:在 initState() 里直接依赖 inherited store

StoreProvider.of(context) 这类取值,如果在某些页面 initState() 里直接调用,容易踩到 inherited widget 的初始化时机问题。更稳妥的做法是:

dart 复制代码
WidgetsBinding.instance.addPostFrameCallback((_) {
  _getData();
});
  • 坑3:ViewModel 实现了 distinct: true,但没正确实现相等性

这样等于白写,页面还是会频繁重建。

  • 坑4:把纯展示态和业务态混在一起

这块没有绝对答案,但要想清楚边界。项目里这次是为了完整展示 flutter_redux 的能力,连详情页滚动透明度都接进了 Redux;但在真实业务里,也要结合复杂度判断。

Redux 的推荐标准

如果从项目实践角度看,我比较推荐下面这套标准:

  1. AppState 只做 feature 聚合,不平铺所有字段
  2. 一个 feature 自己维护 state/action/reducer/thunk/selector
  3. 页面统一通过 StoreConnector<AppState, ViewModel> 接入
  4. ViewModel 独立成文件,不写在页面尾部
  5. distinct: true 要配合明确的 ==/hashCode
  6. 页面少直接碰状态树,多走 selector
  7. thunk 负责异步流程,reducer 只负责纯更新
  8. 路由页面尽量在首帧后再发依赖 context 的请求
PageScaffold 和 PageStateView 在 Redux 下的搭配

项目里目前仍然保留了:

  • PageScaffold
  • PageStateView

它们的价值在 Redux 下依然成立:

  • 页面标题、背景、安全区域统一
  • 加载中、空数据、超时、错误页统一
  • 页面关注自己的业务 UI

组合起来会比较直白:

dart 复制代码
return PageScaffold(
  title: 'redux-grid',
  body: StoreConnector<AppState, CartoonListViewModel>(
    distinct: true,
    converter: (store) => CartoonListViewModel(
      netState: selectComicListState(store.state).netState,
      items: selectComicItems(store.state),
    ),
    builder: (context, vm) {
      return PageStateView(
        state: vm.netState,
        child: mainWidget(vm.items),
      );
    },
  ),
);

这种写法的好处也很明显:

  • 页面结构统一

  • Redux 接入清晰

  • 页面不会被基础层反向绑死

当前项目中的 Redux 架构总结

经过这一轮整理,项目里的核心链路基本已经形成了比较稳定的 Redux 写法:

  • 登录页使用 auth state + selector + thunk
  • Tab 使用轻量 tab state
  • 列表页使用 netState + items + page + isNoMoreData
  • 详情页拆成内容区 ViewModel 和滚动区 ViewModel
  • 页面通过 StoreConnector + ViewModel + selector 组合
  • 网络层统一请求和错误处理
  • 页面不再直接散落状态逻辑
  • 根状态树按 feature 分组

如果只从学习成本和开发效率来看,Redux 在 Flutter 项目里并不算最轻的方案,但它在"状态边界明确、数据流可追踪、结构清晰"这几个方面确实非常稳定。

当然,它也不是"银弹"。如果页面极轻、交互极简单,直接局部状态或更轻的响应式方案可能更快;但如果项目开始进入:

  • 多页面联动
  • 多接口组合
  • 状态流复杂
  • 协作人数增加

Redux 的优势就会越来越明显。

结束:

这篇文章就先写到这里。相比纯页面局部状态,Redux 更强调结构、更强调路径、更强调状态流的统一。但真正落到项目里时,重点并不只是把数据丢进一个全局 store,而是要重新思考:

  • 状态应该怎么分组
  • 页面应该怎么订阅
  • 异步请求应该放在哪一层
  • selector 和 ViewModel 怎么配合
  • 基础层怎么做到"高复用但不过度设计"

如果这些点都理顺了,Redux 在实际项目中会非常稳,也非常适合中大型页面和长期维护。

技术这东西,本质上还是拿来沟通和解决问题的。文章里有理解不到位或者更好的实践方式,也欢迎一起交流。

声明:

仅开源供大家学习使用,禁止从事商业活动,如出现一切法律问题自行承担。

仅学习使用,如有侵权,造成影响,请联系删除,谢谢。

Demo下载地址Demo
相关推荐
AI_零食2 小时前
Flutter 框架跨平台鸿蒙开发 - 颜色听觉化应用
学习·flutter·信息可视化·开源·harmonyos
2301_822703202 小时前
大学生体质健康测试全景测绘台:基于鸿蒙Flutter的多维数据可视化与状态管理响应架构
算法·flutter·信息可视化·架构·开源·harmonyos·鸿蒙
独特的螺狮粉2 小时前
生命科学实验室经费极简记账簿:基于鸿蒙Flutter的极简主义状态响应与流式布局架构
flutter·华为·架构·开源·harmonyos
HH思️️无邪2 小时前
Flutter + iOS 实战指南:教程视频 PiP + 退桌面(可复用模板)
flutter·ios
提子拌饭1332 小时前
红细胞代偿性增殖与睡眠剥夺的对照演算引擎:基于鸿蒙Flutter的微观流体力学粒子渲染架构
flutter·华为·架构·开源·harmonyos·鸿蒙
浮芷.2 小时前
Flutter 框架跨平台鸿蒙开发 - 智能家电故障诊断应用
运维·服务器·科技·flutter·华为·harmonyos·鸿蒙
浮芷.2 小时前
Flutter 框架跨平台鸿蒙开发 - 急救指南应用
学习·flutter·华为·harmonyos·鸿蒙
提子拌饭1332 小时前
液相色谱质谱联用(LC-MS)数据可视化引擎:基于鸿蒙Flutter的高精度色谱卡与多维峰值拟合架构
flutter·华为·信息可视化·开源·harmonyos·鸿蒙
Utopia^2 小时前
Flutter 框架跨平台鸿蒙开发 - 社交星系
flutter·华为·harmonyos