在Flutter中封裝redux的使用

橙紅年代發表於2020-03-04

本文同步在個人部落格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-reduxconnect方法來節省這些步驟,同理,在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-thunkredux-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的一些事項,包括

  • 使用reduxflutter_redux管理全域性狀態,並封裝了簡易的connect方法
  • 拆分state,按業務邏輯管理State和Action
  • 使用redux_thunk管理非同步Action

當然,上面的封裝在實際操作中,還是無法避免需要些很多State和Action的模板檔案;在實際的業務開發中,還需要進一步研究如何編寫更高質量的flutter程式碼。

相關文章