今天,学习下阿里出品的数据管理库:fish_redux。
在成品demo中,使用了fish_redux、网络请求dio、tabbar、数据模型model、基本传值、component、globalStrore、adapter。已经完成了一个基本项目的架子。
先看下完成之后的效果图:
💡:简介
在一开始,需要先行了解下fish_redux为何物?何用?使用场景?
何物?
Fish Redux是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用,它最显著的特征是 函数式的编程模型、可预测的状态管理、可插拔的组件体系、最佳的性能表现。
何用?
管理项目的数据流、在redux的中心思想前提下,降低耦合度和提高可扩展性,解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。
使用场景?
中大型的复杂应用。鲁迅说过大项目越使用越方便,小项目越使用越恶心。
参考资料:
fish redux pdf
阿里fish redux视频
阿里fish redux初识
阿里fish redux中文介绍
💡:分层架构图
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库:
然后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。
基本文件新建完毕,开始写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_tab
和two_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。基本界面已经完成。
然后我们试试使用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中实现。后来的路上想了下,简单搭了个架子。
架子源码地址