当前位置: 首页 > news >正文

Flutter redux 进阶

目的

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

Flutter Redux初代实现局限性

UT不好覆盖

  1. 页面

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

class XXXScreen extends StatefulWidget {
  @override
  _XXXScreenState createState() => _XXXScreenState();
}

class _XXXScreenState extends State<XXXScreen> {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _XXXViewModel>(
        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味,缺点是:静态方法无法使用mockIto;一个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<Middleware<AppState>> generate() {
    return [
      TypedMiddleware<AppState, FetchAction>(_fetchXXX),
    ];
  }

  void _fetchXXX(Store<AppState> store, FetchAction action,
      NextDispatcher next) {
    Services.asyncRequest(
            () => repository.fetch(),
        FetchRequestAction(),
            (json) => FetchSuccessAction(), (errorInfo) =>
        FetchFailureAction(errorInfo: errorInfo));
  }
}
复制代码
  • Repository
  Future<Response> 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<AppState, XXXViewModel>(
      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<AppState> store) {
    return XXXViewModel();
  }
}
复制代码
  • Reducer
@immutable
class XXXState {

  final bool isLoading;

  XXXState({this.isLoading,
  });

  XXXState copyWith({bool isLoading,
  }) {
    return XXXState(
      isLoading: isLoading ?? this.isLoading,
    );
  }

  XXXState.initialState()
      : isLoading = false;
}

final xXXReducer = combineReducers<XXXState>([
  TypedReducer<XXXState, Action>(_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<AppState> store;
  setUp(() {
    store = Store<AppState>(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<AppState> store;
  setUpAll(() async {
    await mockApiSuc(repo);
  });
  setUp(() {
    store = Store<AppState>(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)

参考

flutter_architecture_samples

One More Thing

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

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

(完)

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

相关文章:

  • 为什么携程要做好持续交付?
  • 变频电源老化测试重要吗?需要做老化测试吗
  • JS笔记1
  • EOS区块链智能合约开发
  • Oracle 11g:bin目录下3个特效权限的文件:root用户所有者 + s权限
  • 如何使用虚拟机来运行linux,并通过ftp来访问linux服务器(多图详细教学)
  • FaaS 的简单实践
  • 身为极客,一道题测出你究竟有多机智!|活动推荐
  • java web service 写入图片到web/img/
  • 通过调研开源基准测试集,解读大数据的应用现状和开源未来
  • 如何保证以太坊DApp本地存储localStorage的安全性?
  • 数据库做分表查询
  • mount时候遇到mount: /dev/sdd1 写保护,将以只读方式挂载。mount: 未知的文件系统类型“(null)”...
  • 阿里云开发者工具上手体验
  • 5_添加购物车 B+M
  • 【面试系列】之二:关于js原型
  • Fundebug计费标准解释:事件数是如何定义的?
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • Python学习之路16-使用API
  • redis学习笔记(三):列表、集合、有序集合
  • 给自己的博客网站加上酷炫的初音未来音乐游戏?
  • 盘点那些不知名却常用的 Git 操作
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • 双管齐下,VMware的容器新战略
  • 体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
  • 一个项目push到多个远程Git仓库
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • elasticsearch-head插件安装
  • scrapy中间件源码分析及常用中间件大全
  • ​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案
  • ###C语言程序设计-----C语言学习(6)#
  • #多叉树深度遍历_结合深度学习的视频编码方法--帧内预测
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (02)Hive SQL编译成MapReduce任务的过程
  • (3)(3.2) MAVLink2数据包签名(安全)
  • (pytorch进阶之路)CLIP模型 实现图像多模态检索任务
  • (顶刊)一个基于分类代理模型的超多目标优化算法
  • (非本人原创)我们工作到底是为了什么?​——HP大中华区总裁孙振耀退休感言(r4笔记第60天)...
  • (算法)前K大的和
  • * 论文笔记 【Wide Deep Learning for Recommender Systems】
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .net Application的目录
  • .net core 实现redis分片_基于 Redis 的分布式任务调度框架 earth-frost
  • .Net CoreRabbitMQ消息存储可靠机制
  • .NET(C#) Internals: as a developer, .net framework in my eyes
  • .net6解除文件上传限制。Multipart body length limit 16384 exceeded
  • .net利用SQLBulkCopy进行数据库之间的大批量数据传递
  • .NET面试题(二)
  • .set 数据导入matlab,设置变量导入选项 - MATLAB setvaropts - MathWorks 中国
  • /deep/和 >>>以及 ::v-deep 三者的区别
  • @Autowired标签与 @Resource标签 的区别
  • @CacheInvalidate(name = “xxx“, key = “#results.![a+b]“,multi = true)是什么意思
  • @selector(..)警告提示
  • [ 常用工具篇 ] POC-bomber 漏洞检测工具安装及使用详解