Flutter+redux

redux
floc
flutter
app
移动

#1

flutter+redux

做 web 开发的朋友这两年应该听说过或者用过redux,特别是做响应式开发的时候,当工程比较复杂,状态/数据比较多的时候,很难管控和调试,很难搞清楚什么地方,什么时候,改了什么状态。有了 redux 就很方便,通过集中式的状态容器保存状态/数据,以规范化的 action 发起状态变更,由统一的 reducer 负责接收并处理 action 实现状态修改。除此之外 redux 还有 middleware 的概念,middleware 提供了在 reducer 处理 action 之前处理 action 的一个时机。dart 版 redux 原理同 redux 是一样的,flutter-redux 则是利用 dart 版 redux,实现 flutter 状态的集中管理,并为 widgets 访问状态提供方法,它的目的也是解决工程复杂,组件较多时,状态难以管理的问题,实现了状态集中管理,按要求有序流动的目标。

store

创建 store, store 是全局状态的容器,它应当是“只读”的,除了 reducer 外,不允许其它地方对他进行修改,在创建 store 之前研究设计好 store 里好存那些状态,并在 AppState 类中进行定义(这里的 AppState 自己定义),然后在 app 的根 widget 里定义一个 final 的 store 实例,类型为 Store<AppState>

// main.dart
// ...
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  final store = new Store<AppState>(
    appReducer,
    initialState: new AppState.init(),
    middleware: [authMiddleware, dataMiddleware],
  );

  @override
  Widget build(BuildContext context) {
    return new StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        title: 'Title',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        initialRoute: '/',
        routes: {
          '/': (context) => SplashScreen(),
          '/home': (context) => HomeScreen(),
          // ...
        },
      ),
    );
  }
}

action

action 就是触发状态变更的动作,除此之外,不能通过其它方式变更状态。

// actions.dart
class LoginAction{
  final String email;
  final String password;

  LoginAction({this.email, this.password});
}
// ...

reducer

负责处理 action,根据不同的 action,不同 action 对象内包含的信息按需修改状态,它接收 state 和 acton,返回新的 state,最终的状态变更在 store 内自动得以提现,根 widget 下的组件(StoreConnector 内)将响应式地得以提现数据变化。

// reducers.dart
AppState appReducer(AppState state, dynamic action) {
  // ...
  if (action is LoginSuccessAction) {
    print('<<<LoginSuccessAction>>>');
    print(action.jwt);
    print(action.user);
    state.user = action.user;
    state.jwt = action.jwt;
    return state;
  }
  // ...
  return state;
}

middleware

根据作者文档,middleware 一般用于处理时间较长的事务,比如远程数据访问、文件 IO 等异步的事情。

// middleware.dart
void dataMiddleware(Store<AppState> store, action, NextDispatcher next) async {
  if (action is FetchDataAction) {
    BackendData backendData = new BackendData();
    List<Data> dataList;
    try {
      dataList = await backendData.fetchData(action.token);
      store.dispatch(InitDataAction(dataList: dataList));
    } on GuardianException catch (e) {
      print(e.reason);
    } catch(e) {
      print('$e');
    }
  }
  // ...
  next(action);
}
// ...

StoreProvider

The base Widget. It will pass the given Redux Store to all descendants that request it.

main.dart,根 Widget 的 build 方法返回一个用 StoreProvider<AppState> 包装的 Widget,后续所有子 Widget 将得以访问 store。

StoreConnector

A descendant Widget that gets the Store from the nearest StoreProvider ancestor, converts the Store into a ViewModel with the given converter function, and passes the ViewModel to a builder function. Any time the Store emits a change event, the Widget will automatically be rebuilt. No need to manage subscriptions!

ViewModel 是自己定义的,StoreConnector 有很多有用的回调函数,比如下面的 onDidChange,具体用法参见 flutter-redux

// splash_screen.dart
class SplashScreen extends StatefulWidget {
  @override
  State createState() {
    return new SplashScreenState();
  }
}

class SplashScreenState extends State<SplashScreen> {
  Future<String> _loadAppState;

  @override
  void dispose() {
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    _loadAppState = _doLoadAppState();
  }

  Future<String> _doLoadAppState() async {
    // ...
    return 'succeed';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: FutureBuilder<String>(
          future: _loadAppState,
          builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
            return StoreConnector<AppState, _ViewModel>(
              converter: _ViewModel.fromStore,
              distinct: true,
              onDidChange: (_viewModel) {
                print('onDidChange in SpalshScreen');
                Navigator.of(context).pushNamed('/home');
                _viewModel.loadedHandledCallback();
              },
              builder: (BuildContext context, _ViewModel _viewModel) {
                return Container(
                  child: _viewModel.localLoaded ? Text('加载成功') : Text('加载中'),
                );
              },
            );
          },
        ),
      ),
    );
  }
}

class _ViewModel {
  final bool localLoaded;
  final Function loadedHandledCallback;

  _ViewModel({
    @required this.localLoaded,
    @required this.loadedHandledCallback,
  });

  static _ViewModel fromStore(Store<AppState> store) {
    return _ViewModel(
        localLoaded: store.state.localLoaded,
        loadedHandledCallback: () {
          // ...
        });
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is _ViewModel &&
          runtimeType == other.runtimeType &&
          localLoaded == other.localLoaded;

  @override
  int get hashCode => localLoaded.hashCode;
}

其它

2018年 Dart 大会,谷歌的Paolo Soares 和Cong Hui 提出了 BLoC 模型,BLoC 是 Business Logic Component 的缩写。BLoC 倡导将业务逻辑与界面完全分离实现解耦,widget sink 发送事件给 BLoC,BLoC 通过 Stream notify widget。UI 只干 UI 的事,BLoC 只负责业务逻辑,不关心UI。BLoC 感觉也很有意思,实践过再说说。