用FishRedux完成一個登入頁面

Mr_凌宇發表於2019-10-28

[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能力,以及自有的statecomponent也有自己的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

這個是使用者直接看到的檢視檔案,我們稍微理下: 其實也是和我們上文說的佈局檔案類似, 一imageViewtabBarTabBarViewlogin_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. 再然後的坑,也沒了吧。那晚安吧!

相關文章