[toc]
用FishRedux完成一个登录页面
前言
经过不懈的软磨硬泡以及各种安利...cto终于对Flutter动心了,项目2.15版本将会接入Flutter模块,😂真的是喜大普奔...
考虑到未来的业务拓展,也是为了打好一个基础,我对接下来的flutter module进行了框架选型(其实是为了进一步安利,毕竟原生代码真的写得好死鬼烦啊...)。
其实目前flutter也没有什么特别好的框架,所谓框架,不太像android那样的mvc,mvp,mvvm那么成熟,更多的说的是状态管理。
目前flutter成熟的状态管理也就三(si)种:
- scoped_model(或者provide)
- bloc
- redux
- fish_redux
我们简单介绍下:
- scoped_model(或者provide)
Google原生的状态管理,通过封装InheritedWidget实现了状态管理,而且一并提现Google的设计思想,单一原则,这个Package仅仅作为状态管理来用,几乎没有学习成本,如果是小型项目使用,只用Scoped_model来做状态管理,无疑是非常好的选择,但是越大的项目,使用scoped_model来做状态管理,会有点力不从心。 - bloc
是早期比较流行的一个状态管理(其实现在也依旧很流行),不过我没有学习过,它能够很好地支持Stream方式,学习成本相对较高,不过大小项目皆宜。 - redux
我之前一直使用的一个状态管理,学习成本较低,和前端框架的redux使用方式相似,如果是前端同学迁移到flutter,这个状态管理框架会是一个很好的学习入门方式。 - fish_redux
这个是我们这篇文章重点推荐的框架,但是带来的收益和效果也是最明显,fish_redux是基于redux封装,不仅仅能够满足状态管理,更是集成了配置式的组装项目,Page组装,Component实现,非常干净,易于维护,易于协作没,将集中、分治、复用、隔离做的更进一步,缺点就是代码量的急剧增大(而且是非常非常非常急剧增大)
FishRedux指北
FishRedux的gayhub地址为:FishRedux
我们clone项目,大致看下目录结构:
除了通用的global_store之外,页面大致分为三种类型:
page
官网上介绍,page(页面)是一个行为丰富的组件,因为它的实现是在组件(component)的基础上增强了aop能力,以及自有的state。
component也有自己的state,但是对比起来,page的具备了initState()方法而component没有。
比如我们后续的登录页面,我们暂且贴上代码,后面再做具体说明:
component初始化
login_quick_component代码
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter_module/page/dialog/component.dart';
import 'effect.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';
class LoginQuickComponent extends Component<LoginQuickState> {
LoginQuickComponent()
: super(
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<LoginQuickState>(
adapter: null,
slots: <String, Dependent<LoginQuickState>>{
'dialog': DialogConner() + CommDialogComponent()
}),
);
}
page初始化
login_page代码
import 'package:fish_redux/fish_redux.dart';
import 'StateWithTickerProvider.dart';
import 'effect.dart';
import 'login_quick/component.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';
class LoginPage extends Page<LoginState, Map<String, dynamic>> {
@override
StateWithTickerProvider createState() => StateWithTickerProvider();
LoginPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<LoginState>(
adapter: null,
slots: <String, Dependent<LoginState>>{
"login_quick": QuickConnector() + LoginQuickComponent(),
// "login_normal": QuickConnector() + LoginQuickComponent(),
}),
middleware: <Middleware<LoginState>>[],
);
}
component
组件(Component)是 Fish Redux 最基本的元素,其实page也是基于Component的,它与page的不同点除了:
- 没有自己的initState方法
- 没有办法直接使用,需要使用Connector与父类挂载使用。
adapter
/(ㄒoㄒ)/~~ 哈哈哈,我不要你觉得,我要我觉得!!!!
看到这里是不是有点泪流满面了,通篇不知所云,好不容易看到了一个亲切的单词了...
然而...名字叫适配器,但是和android的用法还是有区别的。
不过这个我也是在摸索使用中。
页面具体实现
犹豫不决总是梦,其实上面说了那么多,我都不知道我在说啥...我们还是直接看代码吧。
页面展示
这个是我们具体实现的登录页面,我用安卓的行话讲就是:
上面一个imageView,下面弄了一个tabLayout,里面丢了两个菜单类型:快速登录和密码登录,最底下丢了一个viewpager,里面丢了两个fragment。
简简单单...
然而,flutter的代码结构为:
重点代码
global_store
其实这块是照抄demo的,是一个实现切换主题色的小功能,真爽啊!!!,这块可以略过不讲,很容易看懂。
app.dart
是页面创建的根,主要用途有:
- 创建一个简单的路由,并注册页面(也就是我们页面的入口和路由配置)
- 对所需的页面进行和 AppStore 的连接
- 对所需的页面进行 AOP 的增强 (这块还在学习中)
我们这一块的留待补充吧,毕竟还没有吃透,也不敢随便说
分块讲解
基础先行
一个page(component)我们可以看到是由:
action,effect,page(component),reducer,state,view这几个模块组成的,他们分别的作用,我们先稍微了解下,以便后续的代码讲解:
action
用来定义在这个页面中发生的动作,例如:登录,清理输入框,更换验证码框等。
同时可以通过payload参数传值,传递一些不能通过state传递的值。
effect
这个dart文件在fish_redux中是定义来处理副作用操作的,比如显示弹窗,网络请求,数据库查询等操作。
page
这个dart文件在用来在路由注册,同时完成注册effect,reducer,component,adapter的功能。
reducer
这个dart文件是用来更新View,即直接操作View状态。
state
state用来定义页面中的数据,用来保存页面状态和数据。
view
view很明显,就是flutter里面当中展示给用户看到的页面。
反正我第一次看是头晕脑胀的...怎么这么多东西,想我年少时,一个.xml和一个.java走天下。
在这里建议下和我一个弄个记事本,把上面这块抄上去,写的时候忘记了就看看。
登录主界面
由上面的截图可以看出,登录页面由一个page加一个component组成。
我们先逐个逐个看代码,逐个逐个说:
login_state
数据先行,我们看下state类,这里比较重要的就是QuickConnector连接器了,等到我们讲login_quick的时候再细说:
import 'dart:async';
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_module/global_store/state.dart';
import 'login_quick/state.dart';
class LoginState implements GlobalBaseState, Cloneable<LoginState> {
// tab控制器
TabController tabControllerForLoginType;
// 菜单list
List<String> loginType = [];
// 从缓存拿的账号(可能有用到)
String accountFromCache;
// 倒数文字
String countDownTips;
// 最大倒数时间
int maxCountTime;
// 当前倒数时间
int currCountTime;
@override
LoginState clone() {
return LoginState()
..loginType = loginType
..tabControllerForLoginType = tabControllerForLoginType
..accountFromCache = accountFromCache;
}
@override
Color themeColor;
}
LoginState initState(Map<String, dynamic> args) {
LoginState state = new LoginState();
state.loginType.add('快速登录');
state.loginType.add('密码登录');
return state;
}
class QuickConnector
extends Reselect2<LoginState, LoginQuickState, String, String> {
@override
LoginQuickState computed(String sub0, String sub1) {
return LoginQuickState()
..account = sub0
..sendVerificationTips = sub1
..controllerForAccount = TextEditingController()
..controllerForPsd = TextEditingController();
}
@override
String getSub0(LoginState state) {
return state.accountFromCache;
}
@override
String getSub1(LoginState state) {
return state.countDownTips;
}
@override
void set(LoginState state, LoginQuickState subState) {
state.accountFromCache = subState.account;
state.countDownTips = subState.sendVerificationTips;
}
}
login_view
这个是用户直接看到的视图文件,我们稍微理下:
其实也是和我们上文说的布局文件类似,
一imageView
一tabBar
一TabBarView
二login_quick Component 而已
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';
import 'package:flutter_module/comm/ColorConf.dart';
import 'action.dart';
import 'state.dart';
Widget buildView(LoginState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
// appBar: AppBar(
// title: Text('Login'),
// ),
body: Column(
children: <Widget>[
Container(
child: Image.asset('images/img_logintop.webp'),
),
Container(
child: TabBar(
indicatorColor: ColorConf.color18C8A1,
indicatorPadding: EdgeInsets.zero,
controller: state.tabControllerForLoginType,
labelColor: ColorConf.color18C8A1,
indicatorSize: TabBarIndicatorSize.label,
unselectedLabelColor: ColorConf.color9D9D9D,
tabs: state.loginType
.map((e) => Container(
child: Text(
e,
style: TextStyle(fontSize: 14),
),
padding: const EdgeInsets.only(top: 8, bottom: 8),
))
.toList(),
),
),
Divider(
height: 1,
),
Expanded(
child: TabBarView(
children: <Widget>[
viewService.buildComponent('login_quick'),
viewService.buildComponent('login_quick'),
],
controller: state.tabControllerForLoginType,
),
flex: 3,
)
],
),
);
}
login_action
这里其实就是定义操作,我觉得可以有个类比,拿我半吊子的springboot来说,
action就是一个service接口
effect就是一个serviceImpl实现类
reducer就是根据发出来的action进行页面操作。
login_action只定义了一丢丢操作
import 'package:fish_redux/fish_redux.dart';
enum LoginAction { action, update }
class LoginActionCreator {
static Action onAction() {
return const Action(LoginAction.action);
}
static Action onUpdate(String countDownNumber) {
return Action(LoginAction.update, payload: countDownNumber);
}
}
login_effect
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_module/page/user/login_page/action.dart';
import 'StateWithTickerProvider.dart';
import 'state.dart';
Effect<LoginState> buildEffect() {
return combineEffects(<Object, Effect<LoginState>>{
LoginAction.action: _onAction,
Lifecycle.initState: _onInit,
});
}
void _onAction(Action action, Context<LoginState> ctx) {}
void _onInit(Action action, Context<LoginState> ctx) {
final TickerProvider tickerProvider = ctx.stfState as StateWithTickerProvider;
ctx.state.tabControllerForLoginType =
TabController(length: ctx.state.loginType.length, vsync: tickerProvider);
}
这个没有什么好说的,因为loginPage也没有什么特别的操作,唯一比较值得注意的是
这里相对多了一个StateWithTickerProvider
这个文件主要是为了给tabController提供TickerProvider,主要注意几个地方
- 定义一个StateWithTickerProvider 类:
class StateWithTickerProvider extends ComponentState<LoginState> with TickerProviderStateMixin{
}
- 在page页面重写createState()方法:
@override
StateWithTickerProvider createState() => StateWithTickerProvider();
- 在effect,根据Lifecycle.initState定义方法
void _onInit(Action action, Context<LoginState> ctx) {
final TickerProvider tickerProvider = ctx.stfState as StateWithTickerProvider;
ctx.state.tabControllerForLoginType =
TabController(length: ctx.state.loginType.length, vsync: tickerProvider);
}
login_reducer
这个页面主要是用于更新view,怎么更新呢?返回一个newState!!
import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';
Reducer<LoginState> buildReducer() {
return asReducer(
<Object, Reducer<LoginState>>{
LoginAction.action: _onAction,
LoginAction.update: _onUpdate,
},
);
}
LoginState _onAction(LoginState state, Action action) {
final LoginState newState = state.clone();
return newState;
}
LoginState _onUpdate(LoginState state, Action action) {
print('this is the _onUpdate in the buildReducer');
final LoginState newState = state.clone()..countDownTips = action.payload;
return newState;
}
login_page
page文件,在构造方法里面调用init初始化
import 'package:fish_redux/fish_redux.dart';
import 'StateWithTickerProvider.dart';
import 'effect.dart';
import 'login_quick/component.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';
class LoginPage extends Page<LoginState, Map<String, dynamic>> {
@override
StateWithTickerProvider createState() => StateWithTickerProvider();
LoginPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<LoginState>(
adapter: null,
slots: <String, Dependent<LoginState>>{
"login_quick": QuickConnector() + LoginQuickComponent(),
// "login_normal": QuickConnector() + LoginQuickComponent(),
}),
middleware: <Middleware<LoginState>>[],
);
}
总结
总结这就来了,是不是只有一个想法:乱!混乱!!好乱!!!什么鬼!!!!mmp!!!!!
我们稍微理理,因为接下来就是点击事件,网络请求了。
我们理下思路:
- 我们在state中定义数据,比如说 定义了一个string叫 String boyMsg = boy,
- 我们在view中定义了一个button,它的child就是一个text,显示的文案叫**state.boyMsg **
看,你可以画页面了!!!完美!虽然它点了也没反应。
登录子模块
在这里我们稍微讲下难点的,比如说点击登录
我们先云coding一下:
- 在state中定义2个TextEditingController,就分别叫controllerForAccount和controllerForPsd好了,这2个鬼主要用于提供给view里面的两个TextField用;再写一个String叫userInfoStr,这个鬼主要用来显示数据的
- 在action中定义一个枚举,就叫它doLogin好了,再定义一个枚举,叫showUserInfo,然后也记得在LoginQuickActionCreator中写对应的方法。
- 大道至简,我们直接在view中定义2个TextField,记得controler要用state.controllerForAccount和state.controllerForPsd来绑定控制器,然后加了一个button,这个是重点,文案一定要叫凌宇是个大帅比,不然会fc的,然后onTap里面,我们要发一个action出去,就是dispatch(LoginQuickActionCreator.onDoLogin()),然后再丢一个text,数据就绑定state.userInfoStr好了,记得??判空,不然会崩溃哦
- 然后effect就会收到了对应的action,前提是我们要在combineEffects中注册,并提供对应的方法,在方法里面我们判空啊,Toast啊,请求网络啊,丢数据去缓存啊...,请求成功啦,然后我们要更新数据呢,咋办..好办!!我们也发action!!在请求成功的callback里面,我们ctx.dispatch(TestPageActionCreator.onShowUserInfo());
- 然后在reducer中,我们写对应的方法,记得调用clone,返回一个新的newState!
- ok了,我们现在不止会画页面了,还会点击事件了。
实际代码
话都说到了这里了,你要不要试着按照上面的6个步骤试下,这样体会更深哦,我们贴下代码:
- action
import 'package:fish_redux/fish_redux.dart';
//TODO replace with your own action
enum TestPageAction { action, doLogin, showUserInfo }
class TestPageActionCreator {
static Action onAction() {
return const Action(TestPageAction.action);
}
static Action onDoLogin() {
return const Action(TestPageAction.doLogin);
}
static Action onShowUserInfo() {
return const Action(TestPageAction.showUserInfo);
}
}
- effect
import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';
Effect<TestPageState> buildEffect() {
return combineEffects(<Object, Effect<TestPageState>>{
TestPageAction.action: _onAction,
TestPageAction.doLogin: _onDoLogin,
});
}
void _onAction(Action action, Context<TestPageState> ctx) {}
void _onDoLogin(Action action, Context<TestPageState> ctx) {
print('this is _onDoLogin method in the effect');
ctx.dispatch(TestPageActionCreator.onShowUserInfo());
}
- page
import 'package:fish_redux/fish_redux.dart';
import 'effect.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';
class TestPagePage extends Page<TestPageState, Map<String, dynamic>> {
TestPagePage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<TestPageState>(
adapter: null,
slots: <String, Dependent<TestPageState>>{
}),
middleware: <Middleware<TestPageState>>[
],);
}
- reducer
import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';
Reducer<TestPageState> buildReducer() {
return asReducer(
<Object, Reducer<TestPageState>>{
TestPageAction.action: _onAction,
TestPageAction.showUserInfo: _onShowUserInfo,
},
);
}
TestPageState _onAction(TestPageState state, Action action) {
final TestPageState newState = state.clone();
return newState;
}
TestPageState _onShowUserInfo(TestPageState state, Action action) {
final TestPageState newState = state.clone();
newState..userInfoStr = "凌宇是个超级大帅逼!!!!";
return newState;
}
- state
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';
class TestPageState implements Cloneable<TestPageState> {
TextEditingController controllerForAccount, controllerForPsd;
String userInfoStr;
@override
TestPageState clone() {
return TestPageState()
..controllerForPsd = controllerForPsd
..controllerForAccount = controllerForAccount
..userInfoStr = userInfoStr;
}
}
TestPageState initState(Map<String, dynamic> args) {
return TestPageState()
..controllerForAccount = TextEditingController()
..userInfoStr=""
..controllerForPsd = TextEditingController();
}
- view
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';
import 'action.dart';
import 'state.dart';
Widget buildView(
TestPageState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
appBar: AppBar(
title: Text('测试'),
),
body: Column(
children: <Widget>[
Text(state.userInfoStr ?? '暂无信息'),
TextField(
controller: state.controllerForAccount,
),
TextField(
controller: state.controllerForAccount,
),
FlatButton(
onPressed: () {
dispatch(TestPageActionCreator.onDoLogin());
},
child: Text('凌宇是个大帅逼'))
],
),
);
}
总结
其实子模块的代码也没有必要贴了,无非就是action多了一点,加了判空,加了实际网络请求而已,对着上面的代码也是一样的。
再然后,
再还有aop,adapter(其实我有用的,但是登录模块咋讲嘛...且学且用吧)等等东西,越学越有趣,闲鱼大佬真厉害。
总结
突如其来的ending...哈哈哈,大半夜的啤酒加歌有点嗨。
说下遇到的两个点比较坑的:
- tabController的坑:
确实新手嘛,刚接触的时候死活找不到改咋办,然后灵机一动,上gayhub搜Issues,确实也有同学反馈过,果然遇事不决看文档!对应的方案我写在上面了 - component更新的奇怪问题
获取验证码模块需要有个倒数计时,这个简单,用了timer,但是我更新了state,页面死活没更新...我怀疑是TickerProvider,我到现在也没确诊,解决办法是我饶了个圈,把倒数计时的currCountTime放在page而不是component,问题解决了,虽然知其然不知其所以然,愁!! - 再然后的坑,也没了吧。那晚安吧!