Flutter效率工具之mock方案

863 阅读7分钟

作者:TGCASCE,转自公众号 "黑金之路"

背景:

  在 Flutter 开发过程中,有许多可以提高开发效率的工具。这些工具可以在开发初期,加速代码编写和减少代码出错风险,诸如 json_to_model、flutter_lints 等。此外,还有一些调试中可以用到的工具,除去基本的断点调试和界面查看,mock 工具的使用也必不可少,本文主要谈谈以下两种场景:

  1. Flutter channel 通信
  2. Flutter 页面路由

目录:

  • Flutter 与原生 channel 通信的 mock 方案

  • Flutter 页面路由的 mock 方案

  • 小结

一、Flutter 与原生 channel 通信的 mock 方案

  上期文章中介绍了 channel 通信的设计,通过自定义协议减少了 channel 数量,方便模块化的引入。在讨论 channel mock 方案前,先来回顾一下混合开发模式。用 Flutter 与原生混合开发时,iOS通常有3种方式:

  1. 通过 CocoaPods 引入 Flutter SDK

  2. 通过 framework 的形式引入

  3. 将 Flutter 应用和插件嵌入 Xcode

而 Android 有2种方式:

  1. 通过 AAR 引入

  2. 通过 submodule 源码引入

  此外,可能还需要考虑 web 应用,通常编译好之后可以直接通过 web 服务访问。无论上述何种混合/原生模式,在开发过程中都必然要使用 channel 进行通信,而在原生实现 channel 之前(考虑到多平台,开发人员不尽相同,很难同步进行调试),如何优雅的进行 channel 调试呢?此时就需要 mock 方案登场了。为了解决不同平台/不同开发方式调用 channel 时的一致性,我们设计了统一的抽象接口。该接口直接基于协议进行抽象,定义了4个基本协议方法和一个构造方法,定义如下:

abstract class Method {  

    Future<Map<String, dynamic>?> get(String path, [Map<String, dynamic>? data]);  
    
    Future<void> post(String path, [Map<String, dynamic>? data]);  
    
    void addObserver(String path,      
        Future<Map<String, dynamic>?> Function(Map<String, dynamic>?) callback);  
        
    void removeObserver(String path);    
    
    factory Method() => _getMethod();
}

  其中,get 方法用于发送并等待回复的 channel 请求;post 方法则用于发送仅通知原生的 channel 请求;addObserver 方法用于监听原生发送的 channel 请求,removeObserver 方法用于移除对应请求。

  最后,基于里氏替换原则来定义不同平台的 Method 实现类:

图片

  前两个文件分别定义了原生(iOS 和 Android)、web的实现,最后一个文件是今天的主角 —— channel mock,该文件实现如下:

class MethodMock implements Method {  
    @override  
    Future<Map<String, dynamic>?> get(String path, 
        [Map<String, dynamic>? data]) async {    
            return await mock(Params('', path, data));  
        }  
        
    @override  
    Future<void> post(String path, [Map<String, dynamic>? data]) async 
    {    
        await mock(Params('', path, data));    
        return Future.value();  
    }  
    
    @override  
    void addObserver(String path,      
        Future<Map<String, dynamic>?> Function(Map<String, dynamic>?) callback) {}  
    
    @override  
    void removeObserver(String path) {}
}
    

  通过实现可以看出 mock 方案并不复杂,通过将 get 和 post 两个方法调用一个写好的默认函数就好了,而 observer 的方式与此类似,可以通过一个写好的默认函数直接调用,这里不再赘述。

  有了实现以后,如何在上层业务调用 channel 的时候能转到相应的 mock 方法呢?我们在调用原生和web的文件时,使用的是引入判断的方式进行区分:  

    if (dart.library.io) 'method_nav.dart'    
    
    if (dart.library.js) 'method_web.dart';

  而对于 mock 的方式,则可以通过简单的变量进行控制即可,当然,下文会谈谈如何在不改变该变量的情况下自动在调试时进行 mock。

二、Flutter 页面路由的 mock 方案

  Flutter 页面通过 Navigator 类进行管理,在内部进行跳转时,Navigator 根据路由配置决定显示哪个 route。在 Navigator 1.0 时期,通过 initialRoute 来设置初始化页面,原生页面在其引擎中写上对应的 route 名称即可。这种方式简单,但是存在以下问题:

  • 原始 API 中的 initialRoute 参数,即系统默认的初始页面,在应用运行后就不能再更改了。这种情况下,如果用户接收到一个系统通知,点击后想要从当前的路由栈状态 [Main -> Profile -> Settings] 重启切换到新的 [Main -> List -> Detail] 路由栈,旧的 Navigator API 并没有一种优雅的实现方式实现这种效果。
  • 原始的命令式 Navigator API 只提供给了开发者一些非常针对性的接口,如 push()、pop() 等,而没有给出一种更灵活的方式让我们直接操作路由栈。这种做法其实与 Flutter 理念相违背,试想如果我们想要改变某个 widget 的所有子组件只需要重建所有子组件并且创建一系列新的 widget 即可,而将此概念应用在路由中,当应用中存在一系列路由页面并想要更改时,我们只能调用 push()、pop() 这类接口来回操作, 这样的 Flutter 食之无味。
  • 嵌套路由下,手机设备自带的回退按钮只能由根 Navigator 响应。在目前的应用中,我们很多场景都需要在某个子 tab 内单独管理一个子路由栈。假设有这个场景,用户在子路由栈中做一系列路由操作之后,点击系统回退按钮,消失的将是整个上层的根路由,我们当然可以使用某种措施来避免这种状况,但归咎起来,这也不应该是应用开发者应该考虑的问题。

  因此,Flutter 官方最终推出了 Navigator 2.0 对之前的路由进行了重构,而我们的路由 mock 方式正好算钻了个空子,使用了新的API Router,通过其管理应用的状态。

图片

  正如前文所述,在1.0时期构建 Flutter 初始页面时,通常调用 MaterialApp 的默认构造方法,然后传入 initialRoute 即可。这种方法在调试时还是很方便的,根据路由表里的名称,可以任意导航到指定的页面。我们就在这个命名路由上写了一个路由 mock 变量,通常不给该变量赋值(因为原生通过引擎确定初始页面),而调试时一定是要手动输入你要调试的页面,此时就是我们的命名路由 mock。最后,将 main 方法的默认 page 定义如下:

class DefaultPage extends StatelessWidget {  
    const DefaultPage({Key? key}) : super(key: key); 
    
    static final navigatorKey = GlobalKey<NavigatorState>();  
    
    @override  
    Widget build(BuildContext context) => initialRouteMock == null      
    ? MaterialApp.navigation(          
        navigatorKey: navigatorKey,          
        onGenerateTitle: (context) => PincoLocal.current.appName,          
        initialRoute: initialRouteMock,          
        onGenerateRoute: onGenerateRoute,          
        builder: EasyLoading.init(),        
    )      
    : MaterialApp(          
        navigatorKey: navigatorKey,          
        onGenerateRoute: onGenerateRoute,          
        child: onGeneratePage(context, initialRouteMock!),        
    );
}   

  这里 initialRouteMock 可以充当两种角色,一个就是路由 mock 开关,另一个是命名路由本身的名称。当该变量为 null 时,我们认为从原生引擎启动,调用 Navigator 2.0 的API进行构造,否则直接使用1.0版本即可。

  在上文中,我们的 channel mock 变量,可以在 router 的某个地方显式赋值,而不必每次手动修改。这一功能也由 routeDelegate 实现,我们这里就简单的将调试时默认开启,接入原生时默认关闭。例如,这里我们在 setNewRoutePath 方法中对其进行赋值:

图片

@override
Future<void> setNewRoutePath(String configuration) {    
    if (configuration == '/') {      
        channel_mock.channelMock = true;      
        _initialRoute = initialRoute;    
    } else {      
        channel_mock.channelMock = false;      
        _initialRoute = configuration;    
    }    
    
    return SynchronousFuture<void>(null);
}

三、小结

  在开发和调试 Flutter 的过程中,有许多可以帮助我们提升效率的工具。本文仅涉及了项目从早期到现在所使用的 mock 工具,从混合开发的背景出发,介绍了基本项目模式及可以与之兼容的 channel 通信 mock 方案。第二部分简单介绍了 Flutter 路由从1.0到2.0的升级及其与 mock 的关系,最后给出了项目中目前使用的路由 mock 方案及其如何与 channel 通信 mock 结合使用。效率工具本身也需要经历多次迭代,我们的 mock 方案也不例外,从最开始每次调试的时候去手动改变 mock 变量,到现在的调试时自动 mock,这就是效率工具本身的效率提升。没有任何工具可以永远的提升效率,但是可以不断更新工具,使其适应新的变化,重新提升效率。

参考文章:

  1. docs.flutter.dev
  2. flutter.cn/community/t…