前言:
-
最近抽了一些下班后的时间,把项目里原有的局部状态和分散的数据流完整收敛到了
Redux。一开始学习Redux的时候,也看了不少文章和示例,很多内容都在讲store、action、reducer、middleware这些基础概念,但真正结合项目页面、网络请求、分页刷新、局部更新来讲的并不算多。 -
所以这篇文章就结合当前这个 Demo,聊一聊
Redux在 Flutter 项目中的实际使用方式、设计思路和常见问题。项目中也顺手对状态树、页面组织、局部刷新、路由接入做了一轮整理,现在整体已经不只是“把状态丢进全局 store”,而是逐步形成了一套更稳定、更可维护的Redux项目写法。 -
本篇文章更偏实战一些,重点讲的是
Redux在项目中的落地,而不是单独讲 API。老规矩,先从整体思路开始。
正文:
当前项目中的 Redux 使用,主要从下面几个维度来理解:
Redux的基本思想Redux在页面中的完整使用流程Redux如何做局部刷新和颗粒化刷新Redux如何组织网络请求和页面状态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,而是传页面真正需要的模型。 -
StoreConnectorflutter_redux提供的核心连接组件。负责把Store<AppState>转换成页面需要的ViewModel。 -
使用 Redux 开发一个页面完整流程
下面以“网络列表页”为例,说明一个页面从 0 到 1 的常见写法。
- 定义页面状态
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,
);
}
}
- 定义 action
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;
}
- 写 reducer
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;
}
- 写 thunk 处理异步请求
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,
),
);
};
}
- 页面通过
StoreConnector订阅 ViewModel
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 里,局部刷新通常靠三件事:
StoreConnector只订阅当前页面真正需要的状态ViewModel精简字段distinct: true+ 正确实现相等性
比如详情页里有两种变化:
- 页面初次进入,请求主数据、推荐数据、系列数据
- 页面滚动时,只让导航栏透明度变化
这时候最推荐的写法不是把所有字段都丢给一个页面 builder,而是拆成多个 StoreConnector:
- 内容区订阅
mainModel、seriesList、recommendList - 顶部导航订阅
title、appBarAlpha、scrollOffset - 返回按钮只订阅
appBarAlpha
这也是项目里详情页的实际做法。
也就是说,Redux 的“局部刷新”不是靠魔法,而是靠:
-
状态拆分
-
订阅拆分
-
ViewModel 拆分
-
单页面多网络请求实现思路
在实际项目里,经常会碰到一个页面需要多个接口的情况,比如:
- 详情主数据
- 同系列数据
- 推荐数据
Redux 下常见有两种写法:
-
思路1:串行请求 适合有依赖关系的接口,前一个结果决定后一个请求参数。
-
思路2:并行请求 适合相互独立的接口,使用
Future.wait并行获取数据。
示例:
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 时,会在页面里到处写:
store.state.article.list.items
store.state.comic.novelDetail.mainModel
store.state.shelf.bannerList
这样短期看能跑,但后面状态树一调整,页面会改得很痛苦。
所以项目里现在加了一层 selector,比如:
List<InfoModel> selectArticleItems(AppState state) =>
selectArticleListState(state).items;
CartoonModelData? selectNovelDetailMainModel(AppState state) =>
selectNovelDetailState(state).mainModel;
这样页面里只关心:
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
这会导致根状态树越来越大、越来越乱。更推荐的方式是:
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。
推荐做法:
StoreProvider<AppState>(
store: store,
child: MaterialApp(...),
)
- 坑2:在
initState()里直接依赖 inherited store
像 StoreProvider.of(context) 这类取值,如果在某些页面 initState() 里直接调用,容易踩到 inherited widget 的初始化时机问题。更稳妥的做法是:
WidgetsBinding.instance.addPostFrameCallback((_) {
_getData();
});
- 坑3:ViewModel 实现了
distinct: true,但没正确实现相等性
这样等于白写,页面还是会频繁重建。
- 坑4:把纯展示态和业务态混在一起
这块没有绝对答案,但要想清楚边界。项目里这次是为了完整展示 flutter_redux 的能力,连详情页滚动透明度都接进了 Redux;但在真实业务里,也要结合复杂度判断。
-
Redux 的推荐标准
如果从项目实践角度看,我比较推荐下面这套标准:
AppState只做 feature 聚合,不平铺所有字段- 一个 feature 自己维护
state/action/reducer/thunk/selector - 页面统一通过
StoreConnector<AppState, ViewModel>接入 ViewModel独立成文件,不写在页面尾部distinct: true要配合明确的==/hashCode- 页面少直接碰状态树,多走 selector
- thunk 负责异步流程,reducer 只负责纯更新
- 路由页面尽量在首帧后再发依赖 context 的请求
-
PageScaffold 和 PageStateView 在 Redux 下的搭配
项目里目前仍然保留了:
PageScaffoldPageStateView
它们的价值在 Redux 下依然成立:
- 页面标题、背景、安全区域统一
- 加载中、空数据、超时、错误页统一
- 页面关注自己的业务 UI
组合起来会比较直白:
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 在实际项目中会非常稳,也非常适合中大型页面和长期维护。
技术这东西,本质上还是拿来沟通和解决问题的。文章里有理解不到位或者更好的实践方式,也欢迎一起交流。
声明:
仅开源供大家学习使用,禁止从事商业活动,如出现一切法律问题自行承担。
仅学习使用,如有侵权,造成影响,请联系删除,谢谢。