好久沒在 SegmentFault 寫東西,唉,也不知道 是忙還是懶,以後有時間 再慢慢寫起來吧,最近開始學點新東西,有的寫了,個人博客跟這裏同步。
一直都在自己的 React Native 應用中使用 Redux,其實更大情況下也是使用它來管理應用的會話狀態以及當前登錄的用戶信息等等簡單的數據,很好用,自從 Google 發佈 Flutter 之後,就一直想着拿它來做點啥,準備拿一個新項目開刀,先研究下怎麼把以前在 React Native 中需要用到的一些技術在 Flutter 找到對應的實現方法,本文記錄下 Flutter + Redux + Redux Persist 的實現。
原文地址:Flutter + Redux + Redux Persist 應用
項目地址:https://github.com/pantao/flutter-redux-demo-app
<!--more-->
第一步:創建一個新的應用:redux_demo_app
flutter create redux_demo_app
cd redux_demo_app
code .
Flutter 項目必須是一個合法的 Dart 包,而 Dart 包要求使用純小寫字母(可包含下劃線),這個跟 React Native 是不一樣的。
第二步:添加依懶
我們依懶下面這些包:
- Redux : JavaScript Redux 的復刻版
- Flutter Redux:類似於 React Redux 一樣,讓我們在 Flutter 項目中更好的使用 Redux
- Redux Persist:Redux 持久化
- Redux Persist Flutter:Flutter Redux Persist 引擎
打開 pubspec.yaml
,在 dependencies
中添加下面這些依懶:
...
dependencies:
...
redux: ^3.0.0
flutter_redux: ^0.5.2
redux_persist: ^0.8.0
redux_persist_flutter: ^0.8.0
dev_dependencies:
...
...
第三步:瞭解需求
本次我想做的一個App有下面四個頁面:
- 首頁
- 個人中心頁
- 個人資料詳情頁
- 登錄頁
交互是下面這樣的:
- 應用打開之後,打開的是一個有兩個底部 Tab 的應用,默認展示的是首頁
-
當用戶點擊(我的)這個Tab時:
- 若當前用戶已登錄,則Tab切換爲個人中心頁
- 若當前用戶未登錄,則以 Modal 的方式彈出登錄頁
添加 lib/state.dart
文件
內容如下:
enum Actions{
login,
logout
}
/// App 狀態
///
/// 狀態中所有數據都應該是隻讀的,所以,全部以 get 的方式提供對外訪問,不提供 set 方法
class AppState {
/// J.W.T
String _authorizationToken;
// 獲取當前的認證 Token
get authorizationToken => _authorizationToken;
// 獲取當前是否處於已認證狀態
get authed => _authorizationToken.length > 0;
AppState(this._authorizationToken);
}
/// Reducer
AppState reducer(AppState state, action) {
switch(action) {
case Actions.login:
return AppState('J.W.T');
case Actions.logout:
return AppState('');
default:
return state;
}
}
在上面的代碼中,我們先聲明瞭 Actions
枚舉,以及一個 AppState
類,該類就是我們的應用狀態類,使用 _authorizationToken
保證認證的值不可被實例外直接被訪問到,這樣用戶就無法去直接修改它的值,再提供了兩個 get
方法,提供給外部訪問它的值。
接着我們定義了一個 reducer
函數,用於更新狀態。
創建 app.dart
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'state.dart';
import 'root.dart';
/// 示例App
class DemoApp extends StatelessWidget {
// app store
final Store<AppState> store;
DemoApp(this.store);
@override
Widget build(BuildContext context) {
return StoreProvider<AppState>(
store: store,
child: new MaterialApp(
title: 'Flutter Redux Demo App',
// home 爲 root 頁
home: Root()
),
);
}
}
在上面我們已經完成的 App
類的編碼,現在需要完成 Root
頁,也就是我們的App入口頁。
創建 Root
頁
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';
/// 狀態
import 'state.dart';
/// 登錄頁面
import 'auth.dart';
/// 我的頁面
import 'me.dart';
/// 首頁
import 'home.dart';
/// 應用入口頁
class Root extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _RootState();
}
}
/// 入口頁狀態
class _RootState extends State<Root> {
/// 當前被激活的 Tab Index
int _currentTabIndex;
/// 所有 Tab 列表頁
List<Widget> _tabPages;
@override
void initState() {
super.initState();
// 初始化 tab 爲第 0 個
_currentTabIndex = 0;
// 初始化頁面列表
_tabPages = <Widget>[
// 首頁
Home(),
// 我的
Me()
];
}
@override
Widget build(BuildContext context) {
// 使用 StoreConnector 創建 Widget
// 類似於 React Redux 的 connect,鏈接 store state 與 Widget
return StoreConnector<AppState, Store<AppState>>(
// store 轉換器,類似於 react redux 中的 mapStateToProps 方法
// 接受參數爲 `store`,再返回的數據可以被在 `builder` 函數中使用,
// 在此處,我們直接返回整個 store,
converter: (store) => store,
// 構建器,第二個參數 store 就是上一個 converter 函數返回的 store
builder: (context, store) {
// 取得當前是否已登錄狀態
final authed = store.state.authed;
return new Scaffold(
// 如果已登錄,則直接可以訪問所有頁面,否則展示 Home
body: authed ? _tabPages[_currentTabIndex] : Home(),
// 底部Tab航
bottomNavigationBar: BottomNavigationBar(
onTap: (int index) {
// 如果點擊的是第 1 個Tab,且當前用戶未登錄,則直接打開登錄 Modal 頁
if (!authed && index == 1) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Auth(),
fullscreenDialog: true
)
);
// 否則直接進入相應頁面
} else {
setState(() {
_currentTabIndex = index;
});
}
},
// 與 body 取值方式類似
currentIndex: authed ? _currentTabIndex : 0,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('首頁')
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
title: Text('我的')
)
],
),
);
},
);
}
}
創建 Home
與 Root
頁面類似,我們可以在任何頁面方便的使用 AppState
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'state.dart';
import 'auth.dart';
class Home extends StatefulWidget {
@override
State<StatefulWidget> createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, Store<AppState>>(
converter: (store) => store,
builder: (context, store) {
return Scaffold(
appBar: AppBar(
title: Text('首頁'),
),
body: Center(
child: store.state.authed
? Text('您已登錄')
: FlatButton(
child: Text('去登錄'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Auth(),
fullscreenDialog: true
)
);
},
)
),
);
},
);
}
}
完成 Auth
在前面的所有頁面中,都只是對 store
中狀態樹的讀取,現在的 Auth
就需要完成對狀態樹的更新了,看下面代碼:
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'state.dart';
class Auth extends StatefulWidget {
@override
State<StatefulWidget> createState() => _AuthState();
}
class _AuthState extends State<Auth> {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, Store<AppState>>(
converter: (store) => store,
builder: (context, store) {
return Scaffold(
appBar: AppBar(
title: Text('登錄'),
),
body: Center(
child: FlatButton(
child: Text('登錄'),
onPressed: () {
// 通過 store.dispatch 函數,可以發出 action(跟 Redux 是一樣的),而 Action 是在
// AppState 中定義的枚舉 Actions.login
store.dispatch(Actions.login);
// 之後,關閉當前的 Modal,就可以看到應用所有數據都更新了
Navigator.pop(context);
},
)
),
);
},
);
}
}
創建 Me
有了登錄之後,我們可以在做一個我的頁面,在這個頁面裏面我們可以完成退出功能。
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'state.dart';
class Me extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MeState();
}
class _MeState extends State<Me> {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, Store<AppState>>(
converter: (store) => store,
builder: (context, store) {
return Scaffold(
appBar: AppBar(
title: Text('退出'),
),
body: Center(
child: FlatButton(
child: Text('退出'),
onPressed: () {
store.dispatch(Actions.logout);
// 此處我們不需要去更新Tab Index,在 Root 頁面中,對 store 裏面的 authed 值已經做了監聽,如果
// Actions.logout 被觸發後, authed 的值會變成 false,那麼App將自動切換首頁
},
)
),
);
},
);
}
}
添加狀態持久化
在上面,我們已經完成了一個基於 Redux 的同步狀態的App,但是當你的App關閉重新打開之外,狀態樹就會被重置爲初始值,這並不理想,我們經常需要一個用戶完成登錄之後,就可以在一斷時間內一直保持這個登錄狀態,而且有一些數據我們並不希望每次打開App的時候都重新初始化一次,這個時候,可以考慮對狀態進行持久化了。
更新 state.dart
class AppState {
...
// 持久化時,從 JSON 中初始化新的狀態
static AppState fromJson(dynamic json) => json != null ? AppState(json['authorizationToken'] as String) : AppState('');
// 更新狀態之後,轉成 JSON,然後持久化至持久化引擎中
dynamic toJson() => {'authorizationToken': _authorizationToken};
}
這裏我們添加了兩個方法,一個是靜態的 fromJson
方法,它將在初始化狀態樹時被調用,用於從 JSON 中初始化一個新的狀態樹出來, toJson
將被用於持久化,將自身轉成 JSON。
更新 main.dart
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:redux_persist/redux_persist.dart';
import 'package:redux_persist_flutter/redux_persist_flutter.dart';
import 'app.dart';
import 'state.dart';
void main() async {
// 創建一個持久化器
final persistor = Persistor<AppState>(
storage: FlutterStorage(),
serializer: JsonSerializer<AppState>(AppState.fromJson),
debug: true
);
// 從 persistor 中加載上一次存儲的狀態
final initialState = await persistor.load();
final store = Store<AppState>(
reducer,
initialState: initialState ?? AppState(''),
middleware: [persistor.createMiddleware()]
);
runApp(new DemoApp(store));
}
重新 flutter run
當前應用,即完成了持久化,可以登錄,然後退出應用,再重新打開應用,可以看到上一次的登錄狀態是存在的。