来学学难搞的fish_redux框架吧,这个框架,官方的文档真是一言难尽,比flutter_bloc官网的文档真是逊色太多了,但是一旦知道怎么写,页面堆起来也是非常爽呀,结构分明,逻辑也会错落有致。
其实在当时搞懂这个框架的时候,就一直想写一篇文章记录下,但是因为忙(lan),导致一直没写,现在觉得还是必须把使用的过程记录下,毕竟刚上手这个框架是个蛋痛的过程,必须要把这个过程做个记录。
这不仅仅是记录的文章,文中所给出的示例,也是我重新构思去写的,过程也是力求阐述清楚且详细。
如果你在使用fish_redux的过程中遇到过上述的问题,那就来看看这篇文章吧!这里,会解答上面所有的问题点!
fish_redux相关地址
我用的是0.3.X的版本,算是第三版,相对于前几版,改动较大
fish_redux: ^0.3.4
#演示列表需要用到的库
dio: ^3.0.9 #网络请求框架
json_annotation: ^2.4.0 #json序列化和反序列化用的
此处我们需要安装代码生成插件,可以帮我们生成大量文件和模板代码
在Android Studio里面搜索”fish“就能搜出插件了,插件名叫:FishReduxTemplate
BakerJQ编写:Android Studio的Fish Redux模板。
huangjianke编写:VSCode的Fish Redux模板
在写代码前,先看写下流程图,这图是凭着自己的理解画的
通过俩个流程图对比,其中还是有一些差别的
这边写四个示例,来演示fish_redux的使用
///需要使用hide隐藏Page
import 'package:flutter/cupertino.dart'hide Page;
import 'package:flutter/material.dart' hide Page;
void main() {
runApp(MyApp());
}
Widget createApp() {
///定义路由
final AbstractRoutes routes = PageRoutes(
pages: >{
"CountPage": CountPage(),
},
);
return MaterialApp(
title: 'FishDemo',
home: routes.buildPage("CountPage", null), //作为默认页面
onGenerateRoute: (RouteSettings settings) {
//ios页面切换风格
return CupertinoPageRoute(builder: (BuildContext context) {
return routes.buildPage(settings.name, settings.arguments);
})
// Material页面切换风格
// return MaterialPageRoute
class CountState implements Cloneable {
int count;
@override
CountState clone() {
return CountState()..count = count;
}
}
CountState initState(Map args) {
return CountState()..count = 0;
}
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(CountState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FishRedux"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You have pushed the button this many times:'),
///使用state中的变量,控住数据的变换
Text(state.count.toString()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
///点击事件,调用action 计数自增方法
dispatch(CountActionCreator.countIncrease());
},
child: Icon(Icons.add),
),
);
}
enum CountAction { increase, updateCount }
class CountActionCreator {
///去effect层去处理自增数据
static Action countIncrease() {
return Action(CountAction.increase);
}
///去reducer层更新数据,传参可以放在Action类中的payload字段中,payload是dynamic类型,可传任何类型
static Action updateCount(int count) {
return Action(CountAction.updateCount, payload: count);
}
}
Effect buildEffect() {
return combineEffects(
Reducer buildReducer() {
return asReducer(
从上面的例子看到,如此简单数据变换,仅仅是个state中一个参数自增的过程,effect层就显得有些多余;所以,把流程简化成下面
注意:这边把effect层删掉,该层可以舍弃了;然后对view,action,reducer层代码进行一些小改动
搞起来
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(CountState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FishRedux"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You have pushed the button this many times:'),
Text(state.count.toString()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
///点击事件,调用action 计数自增方法
dispatch(CountActionCreator.updateCount());
},
child: Icon(Icons.add),
),
);
}
enum CountAction { updateCount }
class CountActionCreator {
///去reducer层更新数据,传参可以放在Action类中的payload字段中,payload是dynamic类型,可传任何类型
static Action updateCount() {
return Action(CountAction.updateCount);
}
}
Reducer buildReducer() {
return asReducer(
Widget createApp() {
///定义路由
final AbstractRoutes routes = PageRoutes(
pages: >{
///计数器模块演示
"CountPage": CountPage(),
///页面传值跳转模块演示
"FirstPage": FirstPage(),
"SecondPage": SecondPage(),
},
);
return MaterialApp(
title: 'FishRedux',
home: routes.buildPage("FirstPage", null), //作为默认页面
onGenerateRoute: (RouteSettings settings) {
//ios页面切换风格
return CupertinoPageRoute(builder: (BuildContext context) {
return routes.buildPage(settings.name, settings.arguments);
});
},
);
}
先来看看该页面的一个流程
state
class FirstState implements Cloneable {
///传递给下个页面的值
static const String fixedMsg = "\n我是FirstPage页面传递过来的数据:FirstValue";
///展示传递过来的值
String msg;
@override
FirstState clone() {
return FirstState()..msg = msg;
}
}
FirstState initState(Map args) {
return FirstState()..msg = "\n暂无";
}
Widget buildView(FirstState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(FirstState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FirstPage"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('下方数据是SecondPage页面传递过来的:'),
Text(state.msg),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
///跳转到Second页面
dispatch(FirstActionCreator.toSecond());
},
child: Icon(Icons.arrow_forward),
),
);
}
enum FirstAction { toSecond , updateMsg}
class FirstActionCreator {
///跳转到第二个页面
static Action toSecond() {
return const Action(FirstAction.toSecond);
}
///拿到第二个页面返回的数据,执行更新数据操作
static Action updateMsg(String msg) {
return Action(FirstAction.updateMsg, payload: msg);
}
}
/// 使用hide方法,隐藏系统包里面的Action类
import 'package:flutter/cupertino.dart' hide Action;
Effect buildEffect() {
return combineEffects(
Reducer buildReducer() {
return asReducer(
class SecondState implements Cloneable {
///传递给下个页面的值
static const String fixedMsg = "\n我是SecondPage页面传递过来的数据:SecondValue";
///展示传递过来的值
String msg;
@override
SecondState clone() {
return SecondState()..msg = msg;
}
}
SecondState initState(Map args) {
///获取上个页面传递过来的数据
return SecondState()..msg = args["firstValue"];
}
Widget buildView(SecondState state, Dispatch dispatch, ViewService viewService) {
return WillPopScope(
child: _bodyWidget(state),
onWillPop: () {
dispatch(SecondActionCreator.backFirst());
///true:表示执行页面返回 false:表示不执行返回页面操作,这里因为要传值,所以接管返回操作
return Future.value(false);
},
);
}
Widget _bodyWidget(SecondState state) {
return Scaffold(
appBar: AppBar(
title: Text("SecondPage"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('下方数据是FirstPage页面传递过来的:'),
Text(state.msg),
],
),
),
);
}
enum SecondAction { backFirst }
class SecondActionCreator {
///返回到第一个页面,然后从栈中移除自身,同时传回去一些数据
static Action backFirst() {
return Action(SecondAction.backFirst);
}
}
///隐藏系统包中的Action类
import 'package:flutter/cupertino.dart' hide Action;
Effect buildEffect() {
return combineEffects(
理解了上面俩个案例,相信你可以使用fish_redux实现一部分页面了;但是,我们堆页面的过程中,能体会列表模块是非常重要的一部分,现在就来学学,在fish_redux中怎么使用ListView吧!
void main() {
runApp(createApp());
}
Widget createApp() {
///定义路由
final AbstractRoutes routes = PageRoutes(
pages: >{
///导航页面
"GuidePage": GuidePage(),
///计数器模块演示
"CountPage": CountPage(),
///页面传值跳转模块演示
"FirstPage": FirstPage(),
"SecondPage": SecondPage(),
///列表模块演示
"ListPage": ListPage(),
},
);
return MaterialApp(
title: 'FishRedux',
home: routes.buildPage("GuidePage", null), //作为默认页面
onGenerateRoute: (RouteSettings settings) {
//ios页面切换风格
return CupertinoPageRoute(builder: (BuildContext context) {
return routes.buildPage(settings.name, settings.arguments);
});
},
);
}
按照流程走
准备工作
创建bean实体
创建item模块
文件结构
OK,bean文件搞定了,再来看看,item文件中的文件,这里component文件不需要改动,所以这地方,我们只需要看:state.dart,view.dart
import 'package:fish_redux/fish_redux.dart';
import 'package:fish_redux_demo/list/bean/item_detail_bean.dart';
class ItemState implements Cloneable {
Datas itemDetail;
ItemState({this.itemDetail});
@override
ItemState clone() {
return ItemState()
..itemDetail = itemDetail;
}
}
ItemState initState(Map args) {
return ItemState();
}
Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state);
}
Widget _bodyWidget(ItemState state) {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
elevation: 5,
margin: EdgeInsets.only(left: 20, right: 20, top: 20),
child: Row(
children: [
//左边图片
Container(
margin: EdgeInsets.all(10),
width: 180,
height: 100,
child: Image.network(
state.itemDetail.envelopePic,
fit: BoxFit.fill,
),
),
//右边的纵向布局
_rightContent(state),
],
),
);
}
///item中右边的纵向布局,比例布局
Widget _rightContent(ItemState state) {
return Expanded(
child: Container(
margin: EdgeInsets.all(10),
height: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
//标题
Expanded(
flex: 2,
child: Container(
alignment: Alignment.centerLeft,
child: Text(
state.itemDetail.title,
style: TextStyle(fontSize: 16),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
//内容
Expanded(
flex: 4,
child: Container(
alignment: Alignment.centerLeft,
child: Text(
state.itemDetail.desc,
style: TextStyle(fontSize: 12),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
)),
Expanded(
flex: 3,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
//作者
Row(
children: [
Text("作者:", style: TextStyle(fontSize: 12)),
Expanded(
child: Text(state.itemDetail.author,
style: TextStyle(color: Colors.blue, fontSize: 12),
overflow: TextOverflow.ellipsis),
)
],
),
//时间
Row(children: [
Text("时间:", style: TextStyle(fontSize: 12)),
Expanded(
child: Text(state.itemDetail.niceDate,
style: TextStyle(color: Colors.blue, fontSize: 12),
overflow: TextOverflow.ellipsis),
)
])
],
),
),
],
),
));
}
item模块,就这样写完了,不需要改动什么了,接下来看看List模块
首先最重要的,我们需要将adapter建立起来,并和page绑定
adapter创建及其绑定
class ListItemAdapter extends SourceFlowAdapter {
static const String item_style = "project_tab_item";
ListItemAdapter()
: super(
pool: >{
///定义item的样式
item_style: ItemComponent(),
},
);
}
class ListState extends MutableSource implements Cloneable {
///这地方一定要注意,List里面的泛型,需要定义为ItemState
///怎么更新列表数据,只需要更新这个items里面的数据,列表数据就会相应更新
List items;
@override
ListState clone() {
return ListState()..items = items;
}
///使用上面定义的List,继承MutableSource,就把列表和item绑定起来了
@override
Object getItemData(int index) => items[index];
@override
String getItemType(int index) => ListItemAdapter.item_style;
@override
int get itemCount => items.length;
@override
void setItemData(int index, Object data) {
items[index] = data;
}
}
ListState initState(Map args) {
return ListState();
}
class ListPage extends Page> {
ListPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies(
///绑定Adapter
adapter: NoneConn() + ListItemAdapter(),
slots: >{}),
middleware: >[],
);
}
正常page页面编辑
整体流程
view模块编写 —> action添加更新数据事件 —> effect初始化时获取数据并处理 —> reducer更新数据
view
Widget buildView(ListState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
appBar: AppBar(
title: Text("ListPage"),
),
body: _itemWidget(state, viewService),
);
}
Widget _itemWidget(ListState state, ViewService viewService) {
if (state.items != null) {
///使用列表
return ListView.builder(
itemBuilder: viewService.buildAdapter().itemBuilder,
itemCount: viewService.buildAdapter().itemCount,
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}
enum ListAction { updateItem }
class ListActionCreator {
static Action updateItem(var list) {
return Action(ListAction.updateItem, payload: list);
}
}
Effect buildEffect() {
return combineEffects(
Reducer buildReducer() {
return asReducer(
这次列表模块是非常的简单,基本不涉及什么流程,就是最基本初始化的一个过程,将state里初始化的数据在view中展示
state
class ListEditState extends MutableSource implements Cloneable {
List items;
@override
ListEditState clone() {
return ListEditState()..items = items;
}
@override
Object getItemData(int index) => items[index];
@override
String getItemType(int index) => ListItemAdapter.itemName;
@override
int get itemCount => items.length;
@override
void setItemData(int index, Object data) {
items[index] = data;
}
}
ListEditState initState(Map args) {
return ListEditState()
..items = [
ItemState(id: 1, title: "列表Item-1", itemStatus: false),
ItemState(id: 2, title: "列表Item-2", itemStatus: false),
ItemState(id: 3, title: "列表Item-3", itemStatus: false),
ItemState(id: 4, title: "列表Item-4", itemStatus: false),
ItemState(id: 5, title: "列表Item-5", itemStatus: false),
ItemState(id: 6, title: "列表Item-6", itemStatus: false),
];
}
Widget buildView(ListEditState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
appBar: AppBar(
title: Text("ListEditPage"),
),
body: ListView.builder(
itemBuilder: viewService.buildAdapter().itemBuilder,
itemCount: viewService.buildAdapter().itemCount,
),
);
}
class ListItemAdapter extends SourceFlowAdapter {
static const String itemName = "item";
ListItemAdapter()
: super(
pool: >{itemName: ItemComponent()},
);
}
class ListEditPage extends Page> {
ListEditPage()
: super(
initState: initState,
view: buildView,
dependencies: Dependencies(
///绑定适配器
adapter: NoneConn() + ListItemAdapter(),
slots: >{}),
middleware: >[],
);
}
class ItemState implements Cloneable {
int id;
String title;
bool itemStatus;
ItemState({this.id, this.title, this.itemStatus});
@override
ItemState clone() {
return ItemState()
..title = title
..itemStatus = itemStatus
..id = id;
}
}
ItemState initState(Map args) {
return ItemState();
}
Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
return Container(
child: InkWell(
onTap: () {},
child: ListTile(
title: Text(state.title),
trailing: Checkbox(
value: state.itemStatus,
///Checkbox的点击操作:状态变更
onChanged: (value) => dispatch(ItemActionCreator.onChange(state.id)),
),
),
),
);
}
enum ItemAction { onChange }
class ItemActionCreator {
//状态改变
static Action onChange(int id) {
return Action(ItemAction.onChange, payload: id);
}
}
Reducer buildReducer() {
return asReducer(
呼,终于将列表这块写完,说实话,这个列表的使用确实有点麻烦;实际上,如果大家用心看了的话,麻烦的地方,其实就是在这块:adapter创建及其绑定;只能多写写了,熟能生巧!
列表模块大功告成,以后就能愉快的写列表了!
abstract class GlobalBaseState{
Color themeColor;
}
class GlobalState implements GlobalBaseState, Cloneable{
@override
Color themeColor;
@override
GlobalState clone() {
return GlobalState();
}
}
enum GlobalAction { changeThemeColor }
class GlobalActionCreator{
static Action onChangeThemeColor(){
return const Action(GlobalAction.changeThemeColor);
}
}
import 'package:flutter/material.dart' hide Action;
Reducer buildReducer(){
return asReducer(
/// 建立一个AppStore
/// 目前它的功能只有切换主题
class GlobalStore{
static Store _globalStore;
static Store get store => _globalStore ??= createStore(GlobalState(), buildReducer());
}
void main() {
runApp(createApp());
}
Widget createApp() {
///全局状态更新
_updateState() {
return (Object pageState, GlobalState appState) {
final GlobalBaseState p = pageState;
if (pageState is Cloneable) {
final Object copy = pageState.clone();
final GlobalBaseState newState = copy;
if (p.themeColor != appState.themeColor) {
newState.themeColor = appState.themeColor;
}
/// 返回新的 state 并将数据设置到 ui
return newState;
}
return pageState;
};
}
final AbstractRoutes routes = PageRoutes(
///全局状态管理:只有特定的范围的Page(State继承了全局状态),才需要建立和 AppStore 的连接关系
visitor: (String path, Page
class CountState implements Cloneable,GlobalBaseState {
int count;
@override
CountState clone() {
return CountState()..count = count;
}
@override
Color themeColor;
}
CountState initState(Map args) {
return CountState()..count = 0;
}
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(CountState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FishRedux"),
///全局主题,仅仅在此处改动了一行
backgroundColor: state.themeColor,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You have pushed the button this many times:'),
Text(state.count.toString()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
///点击事件,调用action 计数自增方法
dispatch(CountActionCreator.updateCount());
},
child: Icon(Icons.add),
),
);
}
这片文章,说实话,花了不少精力去写的,也花了不少时间构思;主要是例子,必须要自己重写下,尤其在dio返回的数据不能被Response解析,那地方坑了我一下午,少引了json序列化库,他也不报错,蛋蛋
代码地址:https://github.com/CNAD666/ExampleCode/tree/master/Flutter/fish_redux_demo
fish_redux版-玩Android:https://github.com/CNAD666/flutter_wan
大家如果觉得有收获,就给我点个赞吧!你的点赞,是我码字的最大动力!