作者:TGCASCE,转自公众号 "黑金之路"
背景:
在 Flutter 开发过程中,有许多可以提高开发效率的工具。这些工具可以在开发初期,加速代码编写和减少代码出错风险,诸如 json_to_model、flutter_lints 等。此外,还有一些调试中可以用到的工具,除去基本的断点调试和界面查看,mock 工具的使用也必不可少,本文主要谈谈以下两种场景:
- Flutter channel 通信
- Flutter 页面路由
目录:
-
Flutter 与原生 channel 通信的 mock 方案
-
Flutter 页面路由的 mock 方案
-
小结
一、Flutter 与原生 channel 通信的 mock 方案
上期文章中介绍了 channel 通信的设计,通过自定义协议减少了 channel 数量,方便模块化的引入。在讨论 channel mock 方案前,先来回顾一下混合开发模式。用 Flutter 与原生混合开发时,iOS通常有3种方式:
-
通过 CocoaPods 引入 Flutter SDK
-
通过 framework 的形式引入
-
将 Flutter 应用和插件嵌入 Xcode
而 Android 有2种方式:
-
通过 AAR 引入
-
通过 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,这就是效率工具本身的效率提升。没有任何工具可以永远的提升效率,但是可以不断更新工具,使其适应新的变化,重新提升效率。
参考文章: