博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Flutter redux 进阶
阅读量:5924 次
发布时间:2019-06-19

本文共 7512 字,大约阅读时间需要 25 分钟。

目的

  1. 认识Flutter Redux局限性
  2. 引入Middleware必要性
  3. 全方位集成UT

Flutter Redux初代实现局限性

UT不好覆盖

  1. 页面

初代实现一个页面的结构是这样的:

class XXXScreen extends StatefulWidget {  @override  _XXXScreenState createState() => _XXXScreenState();}class _XXXScreenState extends State
{ @override Widget build(BuildContext context) { return StoreConnector
( converter: (store) => _XXXViewModel.fromStore(store), builder: (BuildContext context, _XXXViewModel vm) => Container()); }}复制代码

会有两个问题:UI视图和Redux数据通用逻辑耦和在一起,无发通过mock数据来对UI进行UT;大家习惯套路代码,上来就是一个stful,不会想是不是stless更科学点(事实上初代实现80%的Screen是Statefull的,重构后90%都能写成Stateless,提升了页面刷新效率)。

  1. API call

我们的API就是一个静态方法:

static fetchxxx() {    final access = StoreContainer.access;    final apiFuture = Services.rest.get( '/zpartner_api/${access.path}/${access.businessGroupUid}/xxxx/');    Services.asyncRequest(        apiFuture,        xxxRequestAction(),        (json) => xxxSuccessAction(payload: xxxInfo.fromJson(json)),        (errorInfo) => xxxFailureAction(errorInfo: errorInfo)); }复制代码

优点是简单,有java味,缺点是:静态方法无法使用;一个Api call触发,那就发出去了,无法撤销无法重试;自然也无法进行UT覆盖。

不够Functional

上面提到的页面和API call都体现了不Functional,还有我们初代Reducer的写法也是大家很熟悉的OO写法

class xxxReducer {  xxxState reducer(xxxState state, ActionType action) {    switch (action.runtimeType) {      case xxxRequestAction:        return state.copyWith(isLoading: );      case xxxSuccessAction:        return state.copyWith(isLoading: );      case xxxFailureAction:        return state.copyWith(isLoading: );              default:         return state;      }  }}复制代码

从上到下流水写法,static,switch case这都是我们OO的老朋友。但既然Dart是偏前端特性,Functional才是科学的方向啊。

引入Middleware必要性

业务已经写完,小伙伴边自测边写UT,为了达到50%的coverage可以说是非常蛋疼了。某大佬眉头一皱发现问题并不简单,UT不好写,是不是结构搓?于是召集大家讨论一波,得出这些局限性。改还是不改是个问题,不改开发算是提前完成,反正Rn也没有写UT;改的话,改动量是巨大的。大家都停下手中的工作,思考并深刻讨论这个问题,于是我们从三个方向衡量这个问题:

业务影响

离排期提测时间只有1个星期,加入Middleware会有80%的代码需要挪动,改完还要补UT,重新自测。emmm,工作量超大。和产品沟通了下,其实这个业务就是技术重构性质,线上Rn多跑一个礼拜也无碍,测试组也恰好特别忙,delay一周他们觉得ok。倾向改。

技术栈影响

从长远看,改动是进步的。对UT友好,更严谨的结构,也更Functional。小伙伴们觉得自己也能驾驭,不过是多写点套路代码~,技术栈倾向改。

伙伴支持度

引入Middleware带来的好处能否让小伙伴愿意加班把自己的模块都改写了,还补上UT?实践出真知,所以大家讨论决定,用半天时间理解并改写一个小模块,再投票决定是否改。讨论很激烈,话题一度跑偏。。。

讨论下来,最终决定是改,一星期后大家都说,真香!

改动点

增删

删掉原来Service的static API定义,加入Middleware和Repository。Middleware负责网络请求,数据处理,并根据数据状态进行Action的分发。Repository功能是定义了一个数据来源(可能来源于网络,也可能是数据库),因为引入Dio,所以会很精简,形式上可以看成是一个Endpoint定义。

  • Middleware
class XXXMiddlewareFactory extends MiddlewareFactory {  XXXMiddlewareFactory(AppRepository repository) : super(repository);  @override  List
> generate() { return [ TypedMiddleware
(_fetchXXX), ]; } void _fetchXXX(Store
store, FetchAction action, NextDispatcher next) { Services.asyncRequest( () => repository.fetch(), FetchRequestAction(), (json) => FetchSuccessAction(), (errorInfo) => FetchFailureAction(errorInfo: errorInfo)); }}复制代码
  • Repository
Future
fetchXXX(String uid) { return Services.rest.get( '/xxx_api/${path}/${groupUid}/manual_activities/$uid/'); }复制代码

修改

Screen把UI都抽到Presentation里,它依赖一个vm。数据填充并驱动UI变化,这样UI也可以写很全面的UT。Reducer则是利用Flutter_redux库提供的combineReducers方法,将原来一个大的Reducer粒度切到最小。方便写UT和业务增量迭代。

  • Screen
class XXXPresentation extends StatelessWidget {  final XXXViewModel vm;  const XXXPresentation({Key key, this.vm}) : super(key: key);  @override  Widget build(BuildContext context) {    return Container();  }}class XXXScreen extends StatelessWidget {  static const String routeName = 'xxx_screen';  @override  Widget build(BuildContext context) {    return StoreConnector
( distinct: true, onInit: (store) { store.dispatch(FetchXXXAction(isRefresh: true)); }, onDispose: (store) => store.dispatch(XXXResetAction()), converter: XXXViewModel.fromStore, builder: (context, vm) { return XXXPresentation(vm: vm); }, ); }}class XXXViewModel { static XXXViewModel fromStore(Store
store) { return XXXViewModel(); }}复制代码
  • Reducer
@immutableclass XXXState {  final bool isLoading;  XXXState({
this.isLoading, }); XXXState copyWith({
bool isLoading, }) { return XXXState( isLoading: isLoading ?? this.isLoading, ); } XXXState.initialState() : isLoading = false;}final xXXReducer = combineReducers
([ TypedReducer
(_onRequest),]);XXXState _onRequest(XXXState state, Action action) => state.copyWith(isLoading: false);复制代码

UT集成

现在的coverage是48%,核心模块有80%+,有必要的话达到95%以上时完全ok的。原因是解耦以后方方面面都可以UT了

  • widget(纯)
// 官方文档写的清楚明白https://flutter.io/docs/testing复制代码
  • Utils

被多次使用的才会抽成工具类,纯逻辑也很容易写测试,UT应该先满上。

group('test string util', () {    test('isValidPhone', () {      var boolNull = StringUtil.isValidPhone(null);      var boolStarts1 = StringUtil.isValidPhone('17012341234');      var boolStarts2 = StringUtil.isValidPhone('27012341234');      var boolLength10 = StringUtil.isValidPhone('1701234123');      var boolLength11 = StringUtil.isValidPhone('17012341234');      expect(boolNull, false);      expect(boolStarts1, true);      expect(boolStarts2, false);      expect(boolLength10, false);      expect(boolLength11, true);    }); }复制代码
  • Presentation

业务的载体。对于比较核心的业务,无论是流程规范定义还是数据边界条件都可以用UT来自动化保障。

group('test login presentation', () {  Store
store; setUp(() { store = Store
(reduxReducer, initialState: initialReduxState(), distinct: true); StoreContainer.setStoreForTest(store); }); testWidgets('test loading', (WidgetTester tester) async { final vm = LoginViewModel(isLoading: true, isSendPinSuccess: false); await TestHelper.pumpWidget(tester, store, LoginPresentation(vm: vm)); expect(find.byType(CupertinoActivityIndicator), findsOneWidget); ... }); testWidgets('test has data',(WidgetTester tester) async { ... }); testWidgets('test has no data',(WidgetTester tester) async { ... }); } 复制代码
  • Reducer

存放数据,可以用UT来验证特定Action是否改变了特定的数据。

group('notificationReducer', () {  test('FetchMessageUnreadRequestAction', () {    store.dispatch(FetchMessageUnreadRequestAction());    expect(store.state.notification.isLoading, true);  });  test('FetchMessageUnreadSuccessAction', () {    final payload = MessageUnreadInfo.initialState();    store.dispatch(FetchMessageUnreadSuccessAction(payload: payload));    expect(store.state.notification.messageUnreadInfo, payload);    expect(store.state.notification.isLoading, false);  });    ...}复制代码
  • Middleware

叫中间件代表它不是必须,是可以被插拔,可以叠加多个的。每个中间件会有一个明确的任务,我们引入的中间件在这里是处理网络数据,根据情况发对应Action。

group('Middleware', () {  final repo = MockAppRepository();  Store
store; setUpAll(() async { await mockApiSuc(repo); }); setUp(() { store = Store
(reduxReducer, initialState: initialReduxState(), middleware: initialMiddleware(repo), distinct: true); StoreContainer.setStoreForTest(store); }); group('NotificationMiddlewareFactory', () { test('FetchMessageUnreadAction', () { store.dispatch(FetchMessageUnreadAction()); verify(repo.fetchMessagesUnread()); }); test('FetchMessageForHomeAction', () { store.dispatch(FetchMessageForHomeAction()); verify(repo.fetchMessagesForHome()); }); ... } 复制代码

本文源码:[flutter_redux_sample](https://github.com/hyjfine/flutter_redux_sample)

参考

One More Thing

TNT,让你的工作效率提高几百倍,老罗认真严肃的说。开个玩笑,这个有待验证。但Live Templates,提升你的编程效率和体验,肯定是真的

使用地址:https://github.com/hui-z/live-templates

(完)

, 本文版权属于再惠研发团队,欢迎转载,转载请保留出处。

你可能感兴趣的文章
shell变量/环境变量和set/env/export用法_转
查看>>
删除 oracle
查看>>
Google图片加载库Glide的简单封装GlideUtils
查看>>
用java调用.net的wcf其实还是很简单的
查看>>
<html>
查看>>
Project Euler:Problem 32 Pandigital products
查看>>
React-Native视频组件react-native-video使用(安卓版)
查看>>
com.fasterxml.jackson.databind.JsonMappingException: No content to map due to end-of-input
查看>>
OpenGL® ES 3.0 Programming Guide - Book Website
查看>>
Sql Server 优化 SQL 查询:如何写出高性能SQL语句
查看>>
合并Spark社区代码的正确姿势
查看>>
【神经网络】神经网络结构在命名实体识别(NER)中的应用
查看>>
@Springboot搭建项目controller层接收json格式的对象失败
查看>>
实现网站验证码切换功能
查看>>
Atom编辑Markdown文件保存后行尾的空格自动消失的问题解决
查看>>
jquery.cookie.js 使用小结
查看>>
3.菜鸟教你一步一步开发 web service 之 axis 服务端创建
查看>>
Rabbitmq~对Vhost的配置
查看>>
HTML基础第四讲---图像
查看>>
html5--3.2 input元素(1)
查看>>