今天,学习下阿里出品的数据管理库:fish_redux。

     在成品demo中,使用了fish_redux、网络请求dio、tabbar、数据模型model、基本传值、component、globalStrore、adapter。已经完成了一个基本项目的架子。
先看下完成之后的效果图:
fish_redux_result.gif
💡:简介
在一开始,需要先行了解下fish_redux为何物?何用?使用场景?
何物?
   Fish Redux是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用,它最显著的特征是 函数式的编程模型、可预测的状态管理、可插拔的组件体系、最佳的性能表现。
何用?
   管理项目的数据流、在redux的中心思想前提下,降低耦合度和提高可扩展性,解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。
使用场景?
   中大型的复杂应用。鲁迅说过大项目越使用越方便,小项目越使用越恶心。


参考资料:

fish redux pdf

阿里fish redux视频

阿里fish redux初识

阿里fish redux中文介绍

💡:分层架构图
fish_redux分层架构图.png
fish_redux由redux改良而来:
Redux
Redux 是来自前端社区的一个数据管理框架,对 Native开发同学来说可能会有一点陌生,我们做一个简单的介绍。
Redux 是做什么的?
Redux 是一个用来做[可预测][集中式][易调试][灵活性]的数据管理的框架。所有对数据的增删改查等操作都由 Redux 来集中负责。
Redux 是怎么设计和实现的?
Redux 是一个函数式的数据管理的框架。传统 OOP 做数据管理,往往是定义一些 Bean,每一个 Bean 对外暴露一些 Public-API 用来操作内部数据(充血模型)。
函数式的做法是更上一个抽象的纬度,对数据的定义是一些 Struct(贫血模型),而操作数据的方法都统一到具有相同函数签名 (T, Action) => T 的 Reducer 中。
FP:Struct(贫血模型) + Reducer = OOP:Bean(充血模型)
同时 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 机制,给框架带了极高的灵活性和扩展性。
贫血模型、充血模型请参考:
https://en.wikipedia.org/wiki/Plain_old_Java_object
Redux 的缺点
Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点。
在我们实际使用 Redux 中面临两个具体问题:
• Redux 的集中和 Component 的分治之间的矛盾;
• Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。
Fish Redux 的改良
Fish Redux 通过 Redux 做集中化的可观察的数据管理。然不仅于此,对于传统 Redux 在使用层面上的缺点,在面向端侧 flutter 页面纬度开发的场景中,我们通过更好更高的抽象,做了改良。
一个组件需要定义一个数据(Struct)和一个 Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,
我们解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。
我们得到了理想的集中的效果和分治的代码。
💡:构成
 1、数据
核心部分。定义了组件需要用到的数据,也是组件的重要组成,其分为两部分:
• 参与视图工作的 Redux
• 不参与视图工作的 LocalProps
 2、视图
最基本的,也就是最重要的部分,每一个组件都应该是可视的。
所以在组件构建时,我们必须为组件提供一个用于构建视图的函数。
 3、依赖
描述了组件与组件之间的关系,也是可插拔的组件式开发的一个重要特性。其分为两部分:
• 为列表而优化的 Adapter
• 组件整体的组成部分 slot
Component
组件是对局部的展示和功能的封装。 基于 Redux 的原则,我们对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用 Effect)。
于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的行为。
这是一种面向当下,也面向未来的拆分。在面向当下的 Redux 看来,是数据管理和其他。在面向未来的 UI-Automation 看来是 UI 表达和其他。
UI 的表达对程序员而言即将进入黑盒时代,研发工程师们会把更多的精力放在非修改数据的行为、修改数据的行为上。
组件是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。
关于 View
View 仅仅是一个函数签名: (T,Dispatch,ViewService) => Widget
它主要包含三方面的信息
• 视图是完全由数据驱动。
• 视图产生的事件/回调,通过 Dispatch 发出“意图”,不做具体的实现。
• 需要用到的组件依赖等,通过 ViewService 标准化调用。比如一个典型的符合 View 签名的函数。
关于 Effect
Effect 是对非修改数据行为的标准定义,它是一个函数签名: (Context, Action) => Object
它主要包含四方面的信息
• 接收来自 View 的“意图”,也包括对应的生命周期的回调,然后做出具体的执行。
• 它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们不崇尚持有数据,而通过上下文来获取最新数据。
• 它不修改数据, 如果修要,应该发一个 Action 到 Reducer 里去处理。
• 它的返回值仅限于 bool or Future, 对应支持同步函数和协程的处理流程。
Reducer
Reducer 是一个完全符合 Redux 规范的函数签名,即一个纯函数。
State
一个可变的 State 需要实现 Cloneable 类。其核心在于 clone 函数,它总是返回一个新的实例,
Action
在 Redux 中,修改 State 是通过调用 dispatch 函数去触发 Action 来进行的, 但需要注意的是,Action 仅仅是表达了修改 State 的意图。
• Action 的构造器接收两个参数:
• Object type - 必要参数,Action 实例的类型
dyanmic payload - 可选参数,Action 实例携带的参数
为了更好的协作开发和减少低级错误,建议声明一个 Action 类型的枚举类,以及定义一个集合返回 Action 的函数的类,这样可以约束到 payload 的类型
Adapter
https://github.com/alibaba/fish-redux/blob/master/doc/concept/adapter-cn.md
https://github.com/alibaba/fish-redux/blob/master/doc/concept/what's-adapter.md
在基本了解fish_redux的概念之后,照黑猫画白狗,写写代码熟悉下。

在正式使用之前,最好采用下IDE插件,快捷生成基本的文件和代码。

fish_redux模版工具FishReduxTemplateForAS-Android Studio

fish_redux模版工具fish-redux-template-VScode

这里,我使用的是Android Studio,首先,新建一个初始项目,然后先加入fish_redux库:
fish_redux+dio版本.png
然后Packages get。
然后我们新建若干文件夹分分类:
components:存放子界面
model:数据转模型
one_tab: 存放第一个Tab
service_api:网络请求等
store:根store
tabbar: Tabbar栏
two_tab:存放第二个Tab
app.dart: PageRoutes和MaterialApp的开始
main.dart: 起始文件
先选中one_tab,然后File->New->FishReduxTemplate,选择page,
Select Files选中所有文件,Module Name我们可以写One,然后OK,我们可以得到一些文件。以此类推,我们再在two_tab下新建Two,tabbar新建Tabbar。
ffish_redux_AS模版工具1.png
fish_redux_AS模版工具2.png
fish_redux_tabbar.png
基本文件新建完毕,开始写Tabbar:
tabbar--state.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:fish_redux/fish_redux.dart';

class TabbarState implements Cloneable<TabbarState> {
var index = 0;

@override
TabbarState clone() {
TabbarState newState = TabbarState()..index = index;
return newState;
}
}

TabbarState initState(Map<String, dynamic> args) {
return TabbarState()..index = 0;
}

如上所示,先定义一个用于控制当前index的值,默认给0位。

tabbar--view.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import 'package:fish_redux/fish_redux.dart';
import 'package:fishreduxdemo/tabbar/action.dart';
import 'package:flutter/material.dart';

import 'state.dart';
import '../one_tab/page.dart';
import '../two_tab/page.dart';

Widget buildView(
TabbarState state, Dispatch dispatch, ViewService viewService) {
var tabImages;
var appBarTitles = ['首页', '我的'];

/*
* 根据选择获得对应的normal或是press的icon
*/
Image getTabIcon(int curIndex) {
if (curIndex == state.index) {
return tabImages[curIndex][1];
}
return tabImages[curIndex][0];
}

/*
* 获取bottomTab的颜色和文字
*/
Text getTabTitle(int curIndex) {
if (curIndex == state.index) {
return new Text(appBarTitles[curIndex],
style: new TextStyle(fontSize: 14.0, color: Colors.red));
} else {
return new Text(appBarTitles[curIndex],
style: new TextStyle(fontSize: 14.0, color: Colors.black));
}
}

/*
* 根据image路径获取图片
*/
Image getTabImage(path) {
return new Image.asset(path, width: 24.0, height: 24.0);
}

/*
* 初始化选中和未选中的icon
*/
tabImages = [
[
getTabImage('images/tab/home.png'),
getTabImage('images/tab//home_select.png')
],
[
getTabImage('images/tab/mine.png'),
getTabImage('images/tab//mine_select.png')
]
];

return Scaffold(
body: IndexedStack(
children: <Widget>[
OnePage().buildPage(null),
TwoPage().buildPage(null),
],
index: state.index,
),
bottomNavigationBar: new BottomNavigationBar(
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(icon: getTabIcon(0), title: getTabTitle(0)),
new BottomNavigationBarItem(icon: getTabIcon(1), title: getTabTitle(1)),
],
type: BottomNavigationBarType.fixed,
//默认选中首页
currentIndex: state.index,
iconSize: 24.0,
//点击事件
onTap: (index) {
dispatch(TabbarActionCreator.switchIndex(index));
},
),
);
}

如上所示,先引入必须的文件,
切换方法改为fish_redux的触发action->reducer修改state->界面修改
tabbar--action.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:fish_redux/fish_redux.dart';

//TODO replace with your own action
enum TabbarAction { action, switchIndex }

class TabbarActionCreator {
static Action onAction() {
return Action(TabbarAction.action);
}

// 切换tab
static Action switchIndex(int index) {
return Action(TabbarAction.switchIndex, payload: index);
}
}

定义一个切换index的action ** switchIndex **
tabbar-reducer.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import 'package:fish_redux/fish_redux.dart';

import 'action.dart';
import 'state.dart';

Reducer<TabbarState> buildReducer() {
return asReducer(
<Object, Reducer<TabbarState>>{
TabbarAction.action: _onAction,
TabbarAction.switchIndex: _switchIndex,
},
);
}

TabbarState _onAction(TabbarState state, Action action) {
final TabbarState newState = state.clone();
return newState;
}

/*
* 切换tab点击
* */
TabbarState _switchIndex(TabbarState state, Action action) {
var index = action.payload;
final TabbarState newState = state.clone()..index = index;

return newState;
}

reducer里面处理切换方法。

effect 不变
tabbar-effect.dart

1
2
3
4
5
6
7
8
9
10
11
12
import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';

Effect<TabbarState> buildEffect() {
return combineEffects(<Object, Effect<TabbarState>>{
TabbarAction.action: _onAction,
});
}

void _onAction(Action action, Context<TabbarState> ctx) {}

tabbar-page.dart不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import 'package:fish_redux/fish_redux.dart';

import 'effect.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';

class TabbarPage extends Page<TabbarState, Map<String, dynamic>> {
TabbarPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<TabbarState>(
adapter: null,
slots: <String, Dependent<TabbarState>>{
}),
middleware: <Middleware<TabbarState>>[
],);

}

one_tabtwo_tab暂时不变。
main.dart

1
2
3
import 'package:flutter/material.dart';
import './app.dart';
void main() => runApp(createApp());

app.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart' hide Action;
import 'package:flutter/widgets.dart' hide Action;
import './tabbar/page.dart';

import './one_tab/page.dart';

import './two_tab/page.dart';

Widget createApp() {
final AbstractRoutes routes =
PageRoutes(pages: <String, Page<Object, dynamic>>{
'tabbar': TabbarPage(),
'one': OnePage(), //在这里添加页面
'two': TwoPage(),
});

return MaterialApp(
title: 'FishDemo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: routes.buildPage('tabbar', null), //把他作为默认页面
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<Object>(builder: (BuildContext context) {
return routes.buildPage(settings.name, settings.arguments);
});
},
);
}

这里需要配置路由。OK。基本界面已经完成。 fish_redx基本界面

然后我们试试使用globalStore配置全局state。修改全局的颜色
在store文件夹下新建文件
action.dart reducer.dart state.dart store.dart update.dart

store--action.dart

1
2
3
4
5
6
7
8
9
import 'package:fish_redux/fish_redux.dart';

enum GlobalAction { changeThemeColor }

class GlobalActionCreator{
static Action onChangeThemeColor(){
return const Action(GlobalAction.changeThemeColor);
}
}

定义一个action:changeThemeColor
store--reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'package:fish_redux/fish_redux.dart';
import 'dart:ui';
import 'package:flutter/material.dart' hide Action;
import 'action.dart';
import 'state.dart';

Reducer<GlobalState> buildReducer() {
return asReducer(
<Object, Reducer<GlobalState>>{
GlobalAction.changeThemeColor: _onChangeThemeColor,
},
);
}

GlobalState _onChangeThemeColor(GlobalState state, Action action) {
final Color color =
state.themeColor == Colors.green ? Colors.blue : Colors.green;
return state.clone()..themeColor = color;
}

切换方法为 state.themeColor == Colors.green ? Colors.blue : Colors.green;
state.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'dart:ui';
import 'package:fish_redux/fish_redux.dart';

abstract class GlobalBaseState{
Color get themeColor;
set themeColor(Color color);
}

class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{
@override
Color themeColor;

@override
GlobalState clone() {
return GlobalState();
}
}

implements Cloneable 然后重写themeColor
store-store.dart

1
2
3
4
5
6
7
8
9
10
import 'package:fish_redux/fish_redux.dart';
import 'state.dart';
import 'reducer.dart';

class GlobalStore{
static Store<GlobalState> _globalStore;

static Store<GlobalState> get store =>
_globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer());
}

定义一个Store
store--update.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import 'package:fish_redux/fish_redux.dart';

import 'state.dart';

/// 全局刷新状态
/// 页面状态刷新,在 [createApp()] visitor 中注册
/// 如果是 `PageView` 中的页面做刷新,在 `Page` 构造函数中注册
/// 参考 [HomeArticlePage]
globalUpdate() => (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;
}

return newState;
}

return pageState;
};

store书写完毕。接下里修改下app.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
···
import './store/state.dart';
import './store/store.dart';
···
final AbstractRoutes routes = PageRoutes(
pages: <String, Page<Object, dynamic>>{
'tabbar': TabbarPage(),
'one': OnePage(), //在这里添加页面
'adapter_page': AdapterTestPage(),
'two': TwoPage(),
},
visitor: (String path, Page<Object, dynamic> page) {
if (page.isTypeof<GlobalBaseState>()) {
page.connectExtraStore<GlobalState>(GlobalStore.store,
(Object pageState, GlobalState appState) {
final GlobalBaseState p = pageState;
if (p.themeColor != appState.themeColor) {
if (pageState is Cloneable) {
final Object copy = pageState.clone();
final GlobalBaseState newState = copy;
newState.themeColor = appState.themeColor;
return newState;
}
}
return pageState;
});
}
},
);
···

往其中加入visitor 即可。
然后在界面中写入该themeColor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
···
import 'package:fish_redux/fish_redux.dart';
import 'dart:ui';
import '../components/child_view/state.dart';
import '../components/achild_view/state.dart';
import '../store/state.dart';

class OneState implements Cloneable<OneState>, GlobalBaseState {
ChildViewState childState;
AchildViewState aChildState;

@override
Color themeColor;

@override
OneState clone() {
return OneState()
..childState = childState
..aChildState = aChildState
..themeColor = themeColor;
}
}
···
1
2
3
4
5
6
7
···
return Scaffold(
appBar: AppBar(
backgroundColor: state.themeColor,
title: Text('one'),
),
···

修改:

1
2
3
4
5
6
···
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());

···
其中别忘了连接
connectExtraStore(GlobalStore.store, globalUpdate());

详细的可以看文末GitHub链接。

OK,我们再尝试下component。
在components文件夹下新建一个child_view文件夹,然后在其文件夹下新建component,选择File->New->FishReduxTemplate,选择Component,输入一个Module Name。点击OK
这个时候,会生成action.dart component.dart effect.dart reducer.dart state.dart view.dart:
然后我们可以在主page里面使用这个component:
one_tab--state.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import 'package:fish_redux/fish_redux.dart';
import 'dart:ui';
import '../components/child_view/state.dart';
import '../components/achild_view/state.dart';
import '../store/state.dart';

class OneState implements Cloneable<OneState>, GlobalBaseState {
ChildViewState childState;
AchildViewState aChildState;

@override
Color themeColor;

@override
OneState clone() {
return OneState()
..childState = childState
..aChildState = aChildState
..themeColor = themeColor;
}
}

OneState initState(Map<String, dynamic> args) {
OneState state = OneState();
state.childState = ChildViewState();
state.aChildState = AchildViewState();
return state;
}

class ChildViewConnector extends ConnOp<OneState, ChildViewState> {
@override
ChildViewState get(OneState state) {
return state.childState;
}

@override
void set(OneState state, ChildViewState subState) {
state.childState = subState;
}
}

class AchildViewConnector extends ConnOp<OneState, AchildViewState> {
@override
AchildViewState get(OneState state) {
return state.aChildState;
}

@override
void set(OneState state, AchildViewState subState) {
state.aChildState = subState;
}
}

如上所示:
先引入 child_view下的state文件。初始化、新建ChildViewConnector。
然后在one_tab-page.dart下修改:
one_tab-page.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import 'package:fish_redux/fish_redux.dart';

import 'effect.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';

import '../components/child_view/component.dart';
import '../components/achild_view/component.dart';

import '../store/store.dart';
import '../store/update.dart';

class OnePage extends Page<OneState, Map<String, dynamic>> {
OnePage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<OneState>(
adapter: null,
slots: <String, Dependent<OneState>>{
'ChildViewComponent':
ChildViewConnector() + ChildViewComponent(),
'AchildViewComponent':
AchildViewConnector() + AchildViewComponent(),
}),
middleware: <Middleware<OneState>>[],
) {
connectExtraStore(GlobalStore.store, globalUpdate());
}
}

slots下引入该component。
然后就可以在view中显示了:
one_tab--view.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';

import 'state.dart';

Widget buildView(OneState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
appBar: AppBar(
backgroundColor: state.themeColor,
title: Text('one'),
),
body: ListView(
children: <Widget>[
_childView(state, viewService),
_achildView(state, viewService),
],
),
);
}

Align _childView(OneState state, ViewService viewService) {
return Align(child: viewService.buildComponent('ChildViewComponent'));
}

Align _achildView(OneState state, ViewService viewService) {
return Align(child: viewService.buildComponent('AchildViewComponent'));
}

其他的,如网络请求只需要在 Lifecycle.initState: _init,执行即可。如:
two_bar-effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import 'dart:convert';

import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';

import '../service_api/ServiceApi.dart';
import '../model/twoModel.dart';

Effect<TwoState> buildEffect() {
return combineEffects(<Object, Effect<TwoState>>{
Lifecycle.initState: _init,
TwoAction.action: _onAction,
});
}

void _onAction(Action action, Context<TwoState> ctx) {}

void _init(Action action, Context<TwoState> ctx) {
ServiceApi().twoGetData().then((value) {
ctx.dispatch(TwoActionCreator.onloadData(value));
});
}

ServiceApi.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'package:dio/dio.dart';
import 'dart:async';

import '../model/twoModel.dart';

class ServiceApi {
Future twoGetData() async {
try {
Response response;
Dio dio = new Dio();
response = await dio.get('https://gank.io/api/v2/banners');
TwoModel model = TwoModel.fromJson(response.data);
return model;
} catch (e) {
print('发送错误');
print(e);
}
}
}

其中model转换采用最为直接的
https://javiercbk.github.io/json_to_dart/
其model需要在state中进行赋值。

其他的如跳转、传参、adapter可以在下面的GitHub链接中找到。

以上就是简单版的fish_redux学习了.如果不明白的下面有源码地址,可以去那里看看.
本文源码地址
最简单的demo地址
flutter学习历程


ps:前几天面试,面试官有个需求,就是不同角色登录需要展示不同的tabbar,不同的界面,需要各自一个store,但只能是在一个app中实现。后来的路上想了下,简单搭了个架子。


架子源码地址