赞
踩
1. Flutter如何Mock MethodChannel进行单元测试
2. Flutter如何获取键盘的完整高度
3. Flutter快速实现新手引导气泡
在做Flutter单元测试的时候,有时候我们会遇到Flutter Widget的某个方法调用了Platform的方法,这时候就需要Mock这个MethodChannel来消除依赖,否则测试用例执行到Channel的方法就会抛出异常。
在测试环境下,Flutter给我们提供了TestDefaultBinaryMessenger
来拦截MethodChannel,所以我们需要先获取到它。
- /// 依赖WidgetTester,需要在测试用例中获取
- testWidgets('one test case', (widgetTester) async {
- final TestDefaultBinaryMessenger messenger =
- widgetTester.binding.defaultBinaryMessenger;
- });
-
- /// 通过单例获取,写在setUp中可以在所有测试用例执行前运行
- setUp(() {
- final TestDefaultBinaryMessenger messenger =
- TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger;
- }
• Flutter调用Platform方法:
• TestDefaultBinaryMessenger#setMockMethodCallHandler
,第一个参数是需要拦截的MethodChannel,第二个参数是Function表示Mock调用。
• TestDefaultBinaryMessenger#allMessagesHandler
,和上面类似,但这里是拦截所有的MethodChannel,并且,此项设置后,setMockMethodCallHandler将不生效。
• Platform调用Flutter方法:
• TestDefaultBinaryMessenger#handlePlatformMessage
,第一个参数是MethodChannel名字,第二个参数是传给Flutter编码后的MethodCall,第三个参数是Flutter处理后结果的回调。
以allMessagesHandler
使用举例,假如我们有这样一个MethodChannel
- class PipeLinePlugin {
- PipeLinePlugin({this.pipeLineId, this.onTextureRegistered}) {
- _channel = MethodChannel('method_channel/pipeline_$pipeLineId');
-
- /// 调用start后,Platform会回调registerTextureId
- _channel.setMethodCallHandler((call) {
- if (call.method == 'registerTextureId' && call.arguments is Map) {
- int textureId = (call.arguments as Map)?['textureId'];
- onTextureRegistered?.call(textureId);
- }
- });
- }
-
- final String pipeLineId;
- final MethodChannel _channel;
- final Function(int textureId)? onTextureRegistered;
-
- Future<bool?> start() async {
- final bool? result = await _channel.invokeMethod('start', <String, dynamic>{'id': pipeLineId}) as bool?;
- return result;
- }
-
- Future<bool?> stop() async {
- final bool? result = await _channel.invokeMethod('stop', <String, dynamic>{'id': pipeLineId}) as bool?;
- return result;
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
我们可以这样Mock它,然后我们的测试用例就能正常执行了。
- const StandardMethodCodec methodCodec = StandardMethodCodec();
- /// 如果channel名字是按规则生成的,可以拦截所有的MethodChannel,再从中找到你需要Mock的MethodChannel
- messenger.allMessagesHandler = (String channel, MessageHandler? handler, ByteData? message) async {
- final MethodCall call = methodCodec.decodeMethodCall(message);
- if (channel.startWith('method_channel/pipeline')) {
- if (call.method == 'start') {
- /// Platform收到start后,需要回调registerTextureId
- final platformResultCall = MethodCall('registerTextureId', {'textureId': 0});
- messenger.handlePlatformMessage(channel,
- methodCodec.encodeMethodCall(platformResultCall), null);
- }
- }
-
- /// Flutter的MethodCall统一返回true
- return methodCodec.encodeSuccessEnvelope(true);
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
我们知道在Flutter中获取键盘高度可以通过向WidgetsBinding注册监听,当键盘弹出或消失时会回调didChangeMetrics
方法。需要注意的是,在Flutter3新增IOS键盘动画后,IOS会和Android一样回调didChangeMetrics
多次,并且每次回调中MediaQueryData.fromWindow(window).viewInsets.bottom
的值都是此时键盘冒出来的高度,即键盘实时高度。如果我们想获取键盘的完整高度,只需要一直取和上次相比的最大值,然后保存下来就可以了。
- /// Flutter3
- /// 获取键盘高度
- @override
- void didChangeMetrics() {
- final bottom = MediaQueryData.fromWindow(window).viewInsets.bottom;
- // 键盘存在中间态,回调是键盘冒出来的高度
- keyboardHeight = max(keyboardHeight, bottom);
- if (bottom == 0) {
- isKeyboardShow = false;
- } else if (bottom == keyboardHeight || keyboardHeight == 0) {
- isKeyboardShow = true;
- } else {
- isKeyboardShow = null;
- }
- // 键盘完全收起或展开再刷新页面
- if (isKeyboardShow != null && _preKeyboardShow != isKeyboardShow) {
- _keyboardStateNotifier.notifyListeners();
- }
- if (bottom < keyboardHeight) {
- _sp?.setDouble(KEYBOARD_MAX_HEIGHT, keyboardHeight);
- }
- }
-
- /// Flutter3之前,获取键盘高度
- @override
- void didChangeMetrics() {
- final bottom = MediaQueryData.fromWindow(window).viewInsets.bottom;
- /// ios点击键盘表情时,键盘高度会增加,后面点拼音后也回不到这个高度了
- if (Platform.isIOS) {
- /// ios键盘有两种高度,但不存在中间态,回调就是键盘高度
- isKeyboardShow = bottom > 0;
- if (isKeyboardShow) {
- keyboardHeight = bottom;
- }
- } else {
- /// Android键盘存在中间态,回调是键盘冒出来的高度
- keyboardHeight = max(keyboardHeight, bottom);
- if (bottom == 0) {
- isKeyboardShow = false;
- } else if (bottom == keyboardHeight || keyboardHeight == 0) {
- isKeyboardShow = true;
- } else {
- isKeyboardShow = null;
- }
- }
- // 键盘完全收起或展开再刷新页面
- if (isKeyboardShow != null && _preKeyboardShow != isKeyboardShow) {
- _keyboardStateNotifier.notifyListeners();
- }
- if (bottom < keyboardHeight) {
- _sp?.setDouble(KEYBOARD_MAX_HEIGHT, keyboardHeight);
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
例如我们想在键盘上做一个输入框,只需要给输入框底部添加一个和键盘实时高度相同的Padding,就能做到像Scaffold#bottomSheet
一样跟随键盘动画的效果了
当我们上线一个新功能时经常需要做个气泡引导用户使用,但如何在不改变布局的情况下给Widget加上气泡呢? 我们可以通过OverlayEntry在更高的页面层级上插入气泡,同时根据相对的Widget位置来计算气泡的位置,具体代码实现如下。 需要注意的是,虽然我们在Widget的dispose中会销毁气泡,但如果Widget是页面并且页面有弹窗,气泡会出现在弹窗上,所以使用的时候需要在弹窗前主动销毁气泡。
- /// 以相对widget为中心扩展刚好容纳bubble大小的Rect
- Rect _expandRelativeRect(BuildContext relativeContext, Size bubbleSize, bool allowOutsideScreen) {
- if ((relativeContext as Element)?.isElementActive != true) {
- return Rect.zero;
- }
-
- // 找到相对widget的位置和大小
- final RenderBox renderBox = relativeContext.findRenderObject();
- final Offset offset = renderBox.localToGlobal(Offset.zero);
- final Size size = renderBox.size;
- // 以相对widget为中心扩展刚好容纳bubble大小的Rect
- final rect = Rect.fromLTWH(offset.dx - bubbleSize.width, offset.dy - bubbleSize.height,
- bubbleSize.width * 2 + size.width, bubbleSize.height * 2 + size.height);
- if (allowOutsideScreen) {
- return rect;
- } else {
- final screenSize = MediaQueryData.fromWindow(ui.window).size;
- final screenRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
- return rect.intersect(screenRect);
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
- /// 构建气泡Entry
- OverlayEntry bubbleEntry = OverlayEntry(builder: (bubbleContext) {
- Rect rect = _expandRelativeRect(relativeContext, bubbleSize, allowOutsideScreen);
- return Positioned.fromRect(
- rect: rect,
- child: Align(
- alignment: alignment,
- child: SizedBox(
- child: bubble.call(bubbleContext),
- width: bubbleSize.width,
- height: bubbleSize.height,
- )));
- });
- Overlay.of(context).insert(bubbleEntry);
-
- /// 关闭气泡
- VoidCallback hideBubbleCallback = () {
- if (!bubbleEntry.mounted) {
- return;
- }
- bubbleEntry.remove();
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。