[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,問題解決了,雖然知其然不知其所以然,愁!!
- 再然後的坑,也沒了吧。那晚安吧!