在Flutter中封装redux的使用

最近在家办公,发现了之前没写完的一个Flutter版APP,于是打算重构并继续开发,本文主要整理在Flutter中使用redux的一些开发经验。

<!--more-->

参考

1. 基础使用

跟在JS中使用Redux类似,主要分为下面几个步骤

  • 定义Action
  • 实现Reducer,接收Action并返回更新后的State
  • 在组件中使用State,并订阅Store的变化,当数据变化时重新更新视图

1.1. 实现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());

1.2. 方法一:手动订阅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来实现自动订阅变化的逻辑。

1.3. 方法二:使用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,
            ),
        );
    },
);

1.4. 封装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));
          });
    });

2. 封装命名空间和异步

上面的代码演示了在flutter中使用redux的基本步骤,然而对于一个大型应用而言,还必须考虑state的拆分、同步异步Action等情况。

2.1. 拆分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进行拆分的方案。

2.2. 异步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中了。

3. 小结

本文主要整理了在Flutter中使用Redux的一些事项,包括

  • 使用reduxflutter_redux管理全局状态,并封装了简易的connect方法
  • 拆分state,按业务逻辑管理State和Action
  • 使用redux_thunk管理异步Action

当然,上面的封装在实际操作中,还是无法避免需要些很多State和Action的模板文件;在实际的业务开发中,还需要进一步研究如何编写更高质量的flutter代码。

使用Electron实现一个iPicpython基础语法