# 用FishRedux完成一個登錄頁面 用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. 再然後的坑,也沒了吧。那晚安吧!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章