Mobile Development 9 min read

Flutter: Mocking MethodChannel for Unit Tests, Retrieving Keyboard Height, and Quick Guide Bubbles

This guide explains how to mock Flutter MethodChannels for unit testing using TestDefaultBinaryMessenger, capture the maximum keyboard height across iOS and Android by tracking viewInsets in didChangeMetrics, and quickly create new‑user guide bubbles with OverlayEntry by calculating a target widget’s rectangle and inserting a positioned overlay.

Xianyu Technology
Xianyu Technology
Xianyu Technology
Flutter: Mocking MethodChannel for Unit Tests, Retrieving Keyboard Height, and Quick Guide Bubbles

1. Mock MethodChannel for Unit Tests

When a Flutter widget calls a platform method during unit testing, the MethodChannel must be mocked to avoid exceptions.

Get TestDefaultBinaryMessenger

/// 依赖WidgetTester,需要在测试用例中获取
testWidgets('one test case', (widgetTester) async {
  final TestDefaultBinaryMessenger messenger =
    widgetTester.binding.defaultBinaryMessenger;
});

/// 通过单例获取,写在setUp中可以在所有测试用例执行前运行
setUp(() {
    final TestDefaultBinaryMessenger messenger =
      TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger;
});

Mock Flutter ↔ Platform interactions

Flutter calls Platform method: use TestDefaultBinaryMessenger#setMockMethodCallHandler to intercept a specific MethodChannel, or TestDefaultBinaryMessenger#allMessagesHandler to intercept all channels (note that setting this disables setMockMethodCallHandler ).

Platform calls Flutter method: use TestDefaultBinaryMessenger#handlePlatformMessage with the channel name, encoded MethodCall, and a callback for the result.

Example

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
start() async {
    final bool? result = await _channel.invokeMethod('start',
{'id': pipeLineId}) as bool?;
    return result;
  }

  Future
stop() async {
    final bool? result = await _channel.invokeMethod('stop',
{'id': pipeLineId}) as bool?;
    return result;
  }
}

Mocking it with allMessagesHandler enables the test to run normally.

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 MethodCall platformResultCall = MethodCall('registerTextureId', {'textureId': 0});
      messenger.handlePlatformMessage(channel,
        methodCodec.encodeMethodCall(platformResultCall), null);
    }
  }
  /// Flutter的MethodCall统一返回true
  return methodCodec.encodeSuccessEnvelope(true);
};

2. Retrieve Full Keyboard Height

Flutter can listen to didChangeMetrics to get keyboard height via MediaQueryData.fromWindow(window).viewInsets.bottom . iOS now behaves like Android, calling the method multiple times. To obtain the maximum (full) height, keep the largest value observed.

/// 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;
  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);
  }
}

Using the maximum height as padding lets a widget follow the keyboard animation just like Scaffold#bottomSheet .

3. Quick Implementation of New‑User Guide Bubbles

OverlayEntry can insert a bubble above the current UI without changing layout. The bubble’s position is calculated from the target widget’s rectangle.

/// 以相对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);
  }
}
/// 构建气泡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();
};
FlutterUnit TestingOverlayGuide BubbleKeyboard HeightMethodChannel
Xianyu Technology
Written by

Xianyu Technology

Official account of the Xianyu technology team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.