在Flutter中封裝redux的使用

本文同步在我的博客shymean.com上,歡迎關注前端

最近發現了以前沒寫完的一個Flutter版APP,因而打算重構並繼續開發,本文主要整理在Flutter中使用redux的一些開發經驗。vue

參考react

基礎使用

跟在JS中使用Redux相似,主要分爲下面幾個步驟git

  • 定義Action
  • 實現Reducer,接收Action並返回更新後的State
  • 在組件中使用State,並訂閱Store的變化,當數據變化時從新更新視圖

實現Store

首先實現storegithub

// 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事件從新渲染視圖vuex

// 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來實現自動訂閱變化的邏輯。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關聯起來app

// 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代碼。

相關文章
相關標籤/搜索