# 用FishRedux完成一个登录页面

[toc]

用FishRedux完成一个登录页面

前言

经过不懈的软磨硬泡以及各种安利...cto终于对Flutter动心了,项目2.15版本将会接入Flutter模块,😂真的是喜大普奔...
考虑到未来的业务拓展,也是为了打好一个基础,我对接下来的flutter module进行了框架选型(其实是为了进一步安利,毕竟原生代码真的写得好死鬼烦啊...)。
其实目前flutter也没有什么特别好的框架,所谓框架,不太像android那样的mvc,mvp,mvvm那么成熟,更多的说的是状态管理。
目前flutter成熟的状态管理也就三(si)种:

  1. scoped_model(或者provide)
  2. bloc
  3. redux
  4. fish_redux

我们简单介绍下:

  1. scoped_model(或者provide)
    Google原生的状态管理,通过封装InheritedWidget实现了状态管理,而且一并提现Google的设计思想,单一原则,这个Package仅仅作为状态管理来用,几乎没有学习成本,如果是小型项目使用,只用Scoped_model来做状态管理,无疑是非常好的选择,但是越大的项目,使用scoped_model来做状态管理,会有点力不从心。
  2. bloc
    是早期比较流行的一个状态管理(其实现在也依旧很流行),不过我没有学习过,它能够很好地支持Stream方式,学习成本相对较高,不过大小项目皆宜。
  3. redux
    我之前一直使用的一个状态管理,学习成本较低,和前端框架的redux使用方式相似,如果是前端同学迁移到flutter,这个状态管理框架会是一个很好的学习入门方式。
  4. 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的不同点除了:

  1. 没有自己的initState方法
  2. 没有办法直接使用,需要使用Connector与父类挂载使用。

adapter

/(ㄒoㄒ)/~~ 哈哈哈,我不要你觉得,我要我觉得!!!!
看到这里是不是有点泪流满面了,通篇不知所云,好不容易看到了一个亲切的单词了...
然而...名字叫适配器,但是和android的用法还是有区别的。

不过这个我也是在摸索使用中。

页面具体实现

犹豫不决总是梦,其实上面说了那么多,我都不知道我在说啥...我们还是直接看代码吧。

页面展示

页面展示

这个是我们具体实现的登录页面,我用安卓的行话讲就是:
上面一个imageView,下面弄了一个tabLayout,里面丢了两个菜单类型:快速登录密码登录,最底下丢了一个viewpager,里面丢了两个fragment
简简单单...
然而,flutter的代码结构为:

代码结构图

重点代码

global_store

其实这块是照抄demo的,是一个实现切换主题色的小功能,真爽啊!!!,这块可以略过不讲,很容易看懂。

app.dart

是页面创建的根,主要用途有:

  1. 创建一个简单的路由,并注册页面(也就是我们页面的入口和路由配置)
  2. 对所需的页面进行和 AppStore 的连接
  3. 对所需的页面进行 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,主要注意几个地方

  1. 定义一个StateWithTickerProvider 类:
class StateWithTickerProvider extends ComponentState<LoginState> with TickerProviderStateMixin{
}
  1. 在page页面重写createState()方法:
  @override
  StateWithTickerProvider createState() => StateWithTickerProvider();
  1. 在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!!!!!
我们稍微理理,因为接下来就是点击事件,网络请求了。
我们理下思路:

  1. 我们在state中定义数据,比如说 定义了一个string叫 String boyMsg = boy,
  2. 我们在view中定义了一个button,它的child就是一个text,显示的文案叫**state.boyMsg **

看,你可以画页面了!!!完美!虽然它点了也没反应。

登录子模块

在这里我们稍微讲下难点的,比如说点击登录

我们先云coding一下:
  1. state中定义2个TextEditingController,就分别叫controllerForAccountcontrollerForPsd好了,这2个鬼主要用于提供给view里面的两个TextField用;再写一个String叫userInfoStr,这个鬼主要用来显示数据的
  2. action中定义一个枚举,就叫它doLogin好了,再定义一个枚举,叫showUserInfo,然后也记得在LoginQuickActionCreator中写对应的方法。
  3. 大道至简,我们直接在view中定义2个TextField,记得controler要用state.controllerForAccountstate.controllerForPsd来绑定控制器,然后加了一个button,这个是重点,文案一定要叫凌宇是个大帅比,不然会fc的,然后onTap里面,我们要发一个action出去,就是dispatch(LoginQuickActionCreator.onDoLogin()),然后再丢一个text,数据就绑定state.userInfoStr好了,记得??判空,不然会崩溃哦
  4. 然后effect就会收到了对应的action,前提是我们要在combineEffects中注册,并提供对应的方法,在方法里面我们判空啊Toast啊请求网络啊,丢数据去缓存啊...,请求成功啦,然后我们要更新数据呢,咋办..好办!!我们也发action!!在请求成功的callback里面,我们ctx.dispatch(TestPageActionCreator.onShowUserInfo());
  5. 然后在reducer中,我们写对应的方法,记得调用clone,返回一个新的newState!
  6. ok了,我们现在不止会画页面了,还会点击事件了。
实际代码

话都说到了这里了,你要不要试着按照上面的6个步骤试下,这样体会更深哦,我们贴下代码:

  1. 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);
  }
}
  1. 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());
}
  1. 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>>[
            ],);

}
  1. 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;
}
  1. 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();
}
  1. 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...哈哈哈,大半夜的啤酒加歌有点嗨。
说下遇到的两个点比较坑的:

  1. tabController的坑:
    确实新手嘛,刚接触的时候死活找不到改咋办,然后灵机一动,上gayhub搜Issues,确实也有同学反馈过,果然遇事不决看文档!对应的方案我写在上面了
  2. component更新的奇怪问题
    获取验证码模块需要有个倒数计时,这个简单,用了timer,但是我更新了state,页面死活没更新...我怀疑是TickerProvider,我到现在也没确诊,解决办法是我饶了个圈,把倒数计时的currCountTime放在page而不是component,问题解决了,虽然知其然不知其所以然,愁!!
  3. 再然后的坑,也没了吧。那晚安吧!
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容