本文同步在個人部落格shymean.com上,歡迎關注
最近發現了之前沒寫完的一個Flutter版APP,於是打算重構並繼續開發,本文主要整理在Flutter中使用redux的一些開發經驗。
參考
基礎使用
跟在JS中使用Redux類似,主要分為下面幾個步驟
- 定義Action
- 實現Reducer,接收Action並返回更新後的State
- 在元件中使用State,並訂閱Store的變化,當資料變化時重新更新檢視
實現Store
首先實現store
// store/index.dart
import 'package:redux/redux.dart';
class IncrementAction {
final payload;
IncrementAction({this.payload});
}
class AppState {
int count;
AppState({
this.count,
});
static AppState initialState() {
return AppState(
count: 0,
);
}
AppState copyWith({count}) {
return AppState(
count: count ?? this.count,
);
}
}
AppState counterReducer(AppState state, dynamic action) {
switch (action.runtimeType) {
case IncrementAction:
return state.copyWith(count: state.count + action.payload);
}
return state;
}
// 暴露全域性store
Store store =
new Store<AppState>(counterReducer, initialState: AppState.initialState());
複製程式碼
方法一:手動訂閱store.onChange
我們可以將store的state
渲染到widget中,並通過dispatch
的方式更新state。當state更新後,會觸發訂閱的onChange
事件重新渲染檢視
// page.dart
import '../../store/index.dart' as BaseStore;
class BaseState extends State<MyPage> {
int _count = 0;
StreamSubscription _listenSub;
@override
void initState() {
super.initState();
print("init state");
// 註冊state變化的回撥
_listenSub = BaseStore.store.onChange.listen((newState) {
BaseStore.AppState state = newState;
setState(() {
_count = state.count;
});
});
}
@override
void dispose() {
super.dispose();
_listenSub.cancel();
}
@override
Widget build(BuildContext context) {
Widget btn2 = FloatingActionButton(
onPressed: () {
BaseStore.store.dispatch(BaseStore.IncrementAction(payload: 2));
},
child: Text(
_count.toString(), // 使用state裡面的值
style: Theme.of(context).textTheme.display1,
),
);
}
}
複製程式碼
這種方案需要我們手動訂閱和銷燬store.onChange,以及setState中的一些值更新邏輯,顯得比較繁瑣。在React中我們使用react-redux
的connect
方法來節省這些步驟,同理,在flutter中我們可以使用flutter_redux
來實現自動訂閱變化的邏輯。
方法二:使用flutter_redux
首先需要通過StoreProvider
在元件樹根節點注入store
// main.dart
import 'package:flutter_redux/flutter_redux.dart';
import './store/index.dart';
void main() {
runApp(new StoreProvider<AppState>(
store: store,
child: MainApp()));
}
複製程式碼
然後需要使用store.state
的地方宣告,通過StoreConnector
將組建和store關聯起來
// page.dart
import '../../store/index.dart' as BaseStore;
// 在元件中通過StoreConnector輸入元件
Widget btn1 = new StoreConnector<BaseStore.AppState, dynamic>(
// converter類似於mapStateToProps,其返回值會作為builder方法的第二個引數傳入
converter: (store) {
return store;
},
builder: (context, store) {
return FloatingActionButton(
onPressed: () {
// 觸發action修改state
store.dispatch(BaseStore.IncrementAction(payload: 10));
},
child: new Text(
store.state.count.toString(),
style: Theme.of(context).textTheme.display1,
),
);
},
);
複製程式碼
封裝StoreConnector
習慣了使用React中的connect
方法來注入store
,所以會覺得在元件中使用StoreConnector
不是很簡潔。因此可以進一步封裝一個flutter版本的connect
方法
typedef MapStateToPropsCallback<S> = void Function(
Store<AppState> store,
);
typedef BaseViewModelBuilder<ViewModel> = Widget Function(
BuildContext context,
ViewModel vm,
Store<AppState> store,
);
Widget connect(
MapStateToPropsCallback mapStateToProps, BaseViewModelBuilder builder) {
return StoreConnector<AppState, dynamic>(
converter: mapStateToProps,
builder: (context, props) {
// 傳入props和store
return builder(context, props, store);
},
);
}
複製程式碼
然後就可以通過connect(mapStateToProps, builder)
的方式來使用元件啦
Widget page = connect((store) {
return store.state.joke.jokes;
}, (context, jokes, store) {
return ListView.builder(
itemCount: jokes.length,
itemBuilder: (BuildContext context, int index) {
var joke = jokes[index];
return JokeItem(joke: joke));
});
});
複製程式碼
封裝名稱空間和非同步
上面的程式碼演示了在flutter中使用redux的基本步驟,然而對於一個大型應用而言,還必須考慮state的拆分、同步非同步Action等情況。
拆分state
將不同型別的state放在一起並不是一件很明智的事情,我們往往會根據業務或邏輯對state進行劃分,而不是在同一個reducer中進行很長的switch
判斷,因此拆分state是一種很常見的開發需求。
從前端的經驗來看
- vuex內建了
module
配置 - dva在封裝redux時使用了
model
的概念,並提供了app.model
介面
所幸的是redux.dart
也提供了combineReducers
的方法,我們可以用來實現state的拆分
首先我們來定義全域性的state
import 'package:redux/redux.dart';
import './module/test.dart';
import './module/user.dart';
class AppState {
// 把拆分了TestState和UserState兩個state
TestState test;
UserState user;
AppState({this.test, this.user});
static initialState() {
// 分別呼叫子state的initialState方法
AppState state = AppState(
test: TestState.initialState(), user: UserState.initialState());
return state;
}
}
// 全域性reducer,每次返回一個新的AppState物件
AppState _reducer(AppState state, dynamic action) {
return AppState(
test: testReducer(state.test, action),
user: userReducer(state.user, action));
}
// 暴露全域性store
Store store =
new Store<AppState>(_reducer, initialState: AppState.initialState());
複製程式碼
接下來我們看看單個state的封裝
// module/test.dart
import 'package:redux/redux.dart';
class TestState {
int count = 0;
TestState({this.count});
static initialState() {
return TestState(count: 1);
}
}
// 使用combineReducers關聯多個Action的處理方法
Function testReducer = combineReducers<TestState>([
TypedReducer<TestState, IncrementAction>(IncrementAction.handler),
]);
// 每次action都包含與之對應的handler,並返回一個新的State
class IncrementAction {
final int payload;
IncrementAction({this.payload});
// IncrementAction的處理邏輯
static TestState handler(TestState data, IncrementAction action) {
return TestState(count: data.count + action.payload);
}
}
複製程式碼
然後在UI元件中呼叫時使用store.state.test.count
的方式來訪問,為了訪問更簡單,我們也可以封裝注入getters
等快捷屬性訪問方式。
Widget btn1 = new StoreConnector<BaseStore.AppState, dynamic>(
converter: (store) {
// 直接返回store本身
return store;
},
builder: (context, store) {
return FloatingActionButton(
onPressed: () {
print('click');
store.dispatch(TestModule.IncrementAction(payload: 10)); // 觸發具體的Action
},
child: new Text(
store.state.test.count.toString(), // 通過store.state.test.xxx來呼叫
style: Theme
.of(context)
.textTheme
.display1,
),
);
},
);
複製程式碼
強型別的一個好處就是我們不需要通過字串或列舉值的方式來定義ACTION_TYPE
了。
當某個子state需要額外的欄位和action,直接在各自模組內定義和實現即可,這樣就實現了一種將全域性state進行拆分的方案。
非同步action
非同步action是業務場景中非常常見的action,在redux中我們可以通過redux-thunk
或redux-saga
等外掛來實現,同樣地,在flutter中我們也可以使用類似的外掛。
import 'package:flutter_redux/flutter_redux.dart';
// 在初始化的時候傳入thunkMiddleware
Store store = new Store<AppState>(_reducer,
initialState: AppState.initialState(), middleware: [thunkMiddleware]);
複製程式碼
註冊了thunkMiddleware
之後,就可以定義函式型別的Action
了
// 定義loginAction
Function loginAction({String username, String password}) {
return (Store store) async {
var response = await loginByPassword();
LoginModel.login res = LoginModel.login.fromJson(response.data);
store.dispatch(LoginSuccessAction(payload: res.data));
};
}
複製程式碼
最後在檢視中提交Action
void sumit(){
store.dispatch(loginAction(username: username, password: password));
}
複製程式碼
就這樣,我們將檢視中的網路請求、資料儲存等非同步操作等邏輯都封裝在Action的handler中了。
小結
本文主要整理了在Flutter中使用Redux的一些事項,包括
- 使用
redux
和flutter_redux
管理全域性狀態,並封裝了簡易的connect方法 - 拆分state,按業務邏輯管理State和Action
- 使用
redux_thunk
管理非同步Action
當然,上面的封裝在實際操作中,還是無法避免需要些很多State和Action的模板檔案;在實際的業務開發中,還需要進一步研究如何編寫更高質量的flutter程式碼。