当前位置:   article > 正文

3月Flutter小报|读小报,涨知识

flutter _keyboardstatenotifier

本期内容

  1. 1. Flutter如何Mock MethodChannel进行单元测试

  2. 2. Flutter如何获取键盘的完整高度

  3. 3. Flutter快速实现新手引导气泡

Flutter如何Mock MethodChannel进行单元测试

在做Flutter单元测试的时候,有时候我们会遇到Flutter Widget的某个方法调用了Platform的方法,这时候就需要Mock这个MethodChannel来消除依赖,否则测试用例执行到Channel的方法就会抛出异常。

1. 获取TestDefaultBinaryMessenger

在测试环境下,Flutter给我们提供了TestDefaultBinaryMessenger来拦截MethodChannel,所以我们需要先获取到它。

  1. /// 依赖WidgetTester,需要在测试用例中获取
  2. testWidgets('one test case', (widgetTester) async {
  3.   final TestDefaultBinaryMessenger messenger = 
  4.     widgetTester.binding.defaultBinaryMessenger;
  5. });
  6. /// 通过单例获取,写在setUp中可以在所有测试用例执行前运行
  7. setUp(() {
  8.     final TestDefaultBinaryMessenger messenger = 
  9.       TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger;
  10. }
2. Mock Flutter与Platform间的相互调用
  • • Flutter调用Platform方法:

    • • TestDefaultBinaryMessenger#setMockMethodCallHandler,第一个参数是需要拦截的MethodChannel,第二个参数是Function表示Mock调用。

    • • TestDefaultBinaryMessenger#allMessagesHandler,和上面类似,但这里是拦截所有的MethodChannel,并且,此项设置后,setMockMethodCallHandler将不生效

  • • Platform调用Flutter方法:

    • • TestDefaultBinaryMessenger#handlePlatformMessage,第一个参数是MethodChannel名字,第二个参数是传给Flutter编码后的MethodCall,第三个参数是Flutter处理后结果的回调。

3. 示例

allMessagesHandler使用举例,假如我们有这样一个MethodChannel

  1. class PipeLinePlugin {
  2.   PipeLinePlugin({this.pipeLineId, this.onTextureRegistered}) {
  3.     _channel = MethodChannel('method_channel/pipeline_$pipeLineId');
  4.     
  5.     /// 调用start后,Platform会回调registerTextureId
  6.     _channel.setMethodCallHandler((call) {
  7.       if (call.method == 'registerTextureId' && call.arguments is Map) {
  8.         int textureId = (call.arguments as Map)?['textureId'];
  9.         onTextureRegistered?.call(textureId);
  10.       }
  11.     });
  12.   }
  13.   final String pipeLineId;
  14.   final MethodChannel _channel;
  15.   final Function(int textureId)? onTextureRegistered;
  16.   Future<bool?> start() async {
  17.     final bool? result = await _channel.invokeMethod('start', <String, dynamic>{'id': pipeLineId}) as bool?;
  18.     return result;
  19.   }
  20.   
  21.   Future<bool?> stop() async {
  22.     final bool? result = await _channel.invokeMethod('stop', <String, dynamic>{'id': pipeLineId}) as bool?;
  23.     return result;
  24.   }
  25. }

我们可以这样Mock它,然后我们的测试用例就能正常执行了。

  1. const StandardMethodCodec methodCodec = StandardMethodCodec();
  2. /// 如果channel名字是按规则生成的,可以拦截所有的MethodChannel,再从中找到你需要Mock的MethodChannel
  3. messenger.allMessagesHandler = (String channel, MessageHandler? handler, ByteData? message) async {
  4.   final MethodCall call = methodCodec.decodeMethodCall(message);
  5.   if (channel.startWith('method_channel/pipeline')) {
  6.     if (call.method == 'start') {
  7.       /// Platform收到start后,需要回调registerTextureId
  8.       final platformResultCall = MethodCall('registerTextureId', {'textureId'0});
  9.       messenger.handlePlatformMessage(channel, 
  10.         methodCodec.encodeMethodCall(platformResultCall), null);
  11.     }
  12.   }
  13.   /// Flutter的MethodCall统一返回true
  14.   return methodCodec.encodeSuccessEnvelope(true);
  15. }

Flutter如何获取键盘的完整高度

我们知道在Flutter中获取键盘高度可以通过向WidgetsBinding注册监听,当键盘弹出或消失时会回调didChangeMetrics方法。需要注意的是,在Flutter3新增IOS键盘动画后,IOS会和Android一样回调didChangeMetrics多次,并且每次回调中MediaQueryData.fromWindow(window).viewInsets.bottom的值都是此时键盘冒出来的高度,即键盘实时高度。如果我们想获取键盘的完整高度,只需要一直取和上次相比的最大值,然后保存下来就可以了。

  1. /// Flutter3 
  2. /// 获取键盘高度
  3. @override
  4. void didChangeMetrics() {
  5.   final bottom = MediaQueryData.fromWindow(window).viewInsets.bottom;
  6.   // 键盘存在中间态,回调是键盘冒出来的高度
  7.   keyboardHeight = max(keyboardHeight, bottom);
  8.   if (bottom == 0) {
  9.     isKeyboardShow = false;
  10.   } else if (bottom == keyboardHeight || keyboardHeight == 0) {
  11.     isKeyboardShow = true;
  12.   } else {
  13.     isKeyboardShow = null;
  14.   }
  15.   // 键盘完全收起或展开再刷新页面
  16.   if (isKeyboardShow != null && _preKeyboardShow != isKeyboardShow) {
  17.     _keyboardStateNotifier.notifyListeners();
  18.   }
  19.   if (bottom < keyboardHeight) {
  20.     _sp?.setDouble(KEYBOARD_MAX_HEIGHT, keyboardHeight);
  21.   }
  22. }
  23. /// Flutter3之前,获取键盘高度
  24. @override
  25. void didChangeMetrics() {
  26.   final bottom = MediaQueryData.fromWindow(window).viewInsets.bottom;
  27.   /// ios点击键盘表情时,键盘高度会增加,后面点拼音后也回不到这个高度了
  28.   if (Platform.isIOS) {
  29.     /// ios键盘有两种高度,但不存在中间态,回调就是键盘高度
  30.     isKeyboardShow = bottom > 0;
  31.     if (isKeyboardShow) {
  32.       keyboardHeight = bottom;
  33.     }
  34.   } else {
  35.     /// Android键盘存在中间态,回调是键盘冒出来的高度
  36.     keyboardHeight = max(keyboardHeight, bottom);
  37.     if (bottom == 0) {
  38.       isKeyboardShow = false;
  39.     } else if (bottom == keyboardHeight || keyboardHeight == 0) {
  40.       isKeyboardShow = true;
  41.     } else {
  42.       isKeyboardShow = null;
  43.     }
  44.   }
  45.   // 键盘完全收起或展开再刷新页面
  46.   if (isKeyboardShow != null && _preKeyboardShow != isKeyboardShow) {
  47.     _keyboardStateNotifier.notifyListeners();
  48.   }
  49.   if (bottom < keyboardHeight) {
  50.     _sp?.setDouble(KEYBOARD_MAX_HEIGHT, keyboardHeight);
  51.   }
  52. }

例如我们想在键盘上做一个输入框,只需要给输入框底部添加一个和键盘实时高度相同的Padding,就能做到像Scaffold#bottomSheet一样跟随键盘动画的效果了

99bcb505ceb56b1f663dfb11a0ff044a.gif

Flutter快速实现新手引导气泡

当我们上线一个新功能时经常需要做个气泡引导用户使用,但如何在不改变布局的情况下给Widget加上气泡呢? 我们可以通过OverlayEntry在更高的页面层级上插入气泡,同时根据相对的Widget位置来计算气泡的位置,具体代码实现如下。 需要注意的是,虽然我们在Widget的dispose中会销毁气泡,但如果Widget是页面并且页面有弹窗,气泡会出现在弹窗上,所以使用的时候需要在弹窗前主动销毁气泡。

  1. /// 以相对widget为中心扩展刚好容纳bubble大小的Rect
  2. Rect _expandRelativeRect(BuildContext relativeContext, Size bubbleSize, bool allowOutsideScreen) {
  3.   if ((relativeContext as Element)?.isElementActive != true) {
  4.     return Rect.zero;
  5.   }
  6.   // 找到相对widget的位置和大小
  7.   final RenderBox renderBox = relativeContext.findRenderObject();
  8.   final Offset offset = renderBox.localToGlobal(Offset.zero);
  9.   final Size size = renderBox.size;
  10.   // 以相对widget为中心扩展刚好容纳bubble大小的Rect
  11.   final rect = Rect.fromLTWH(offset.dx - bubbleSize.width, offset.dy - bubbleSize.height,
  12.                              bubbleSize.width * 2 + size.width, bubbleSize.height * 2 + size.height);
  13.   if (allowOutsideScreen) {
  14.     return rect;
  15.   } else {
  16.     final screenSize = MediaQueryData.fromWindow(ui.window).size;
  17.     final screenRect = Rect.fromLTWH(00, screenSize.width, screenSize.height);
  18.     return rect.intersect(screenRect);
  19.   }
  20. }
  1. /// 构建气泡Entry
  2. OverlayEntry bubbleEntry = OverlayEntry(builder: (bubbleContext) {
  3.   Rect rect = _expandRelativeRect(relativeContext, bubbleSize, allowOutsideScreen);
  4.   return Positioned.fromRect(
  5.       rect: rect,
  6.       child: Align(
  7.           alignment: alignment,
  8.           child: SizedBox(
  9.             child: bubble.call(bubbleContext),
  10.             width: bubbleSize.width,
  11.             height: bubbleSize.height,
  12.           )));
  13. });
  14. Overlay.of(context).insert(bubbleEntry);
  15. /// 关闭气泡
  16. VoidCallback hideBubbleCallback = () {
  17.   if (!bubbleEntry.mounted) {
  18.     return;
  19.   }
  20.   bubbleEntry.remove();
  21. };
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号