Flutter键盘弹出时含父组件在内的所有widget均重建问题求助
键盘弹出时,系统会更新根节点MediaQuery的MediaQueryData,主要变化包括viewInsets(键盘遮挡区域的高度)、可绘制区域尺寸等数值。MediaQuery是Flutter框架内置的InheritedWidget,只要任意组件在build阶段调用MediaQuery.of(context),该组件对应的Element就会自动注册为MediaQuery的依赖方。
当MediaQueryData发生变化时,所有已注册的依赖方都会被标记为脏节点,在下一帧触发build逻辑。这种依赖是跨节点关联的,不会因为BottomSheet是独立的浮层就自动隔离,这就是跨多层组件都出现重建的根本原因。
结合贴出的代码,触发跨层重建的路径有两个:
- CommentPage内部大量调用
MediaQuery.of(context)计算容器尺寸,这些调用本身只应触发BottomSheet内部的组件重建 - PageviewItem、更上层的PageView父组件同样触发重建,说明这些上层组件的build方法里也直接调用了
MediaQuery.of(context)获取屏幕尺寸,成为了MediaQuery的注册依赖,自然会跟随MediaQuery更新触发重建 - 虽然你在BottomSheet内部的Scaffold上设置了
resizeToAvoidBottomInset: false关闭键盘自动顶起逻辑,但这个配置只影响Scaffold自身的布局行为,不会阻止MediaQuery实例更新数据,也不会断开其他组件和MediaQuery的依赖关系
1. 隔离MediaQuery的依赖范围
不要在组件树的上层context上直接调用MediaQuery.of(context),所有需要读取MediaQuery数据的逻辑都用局部Builder包裹,让依赖关系只绑定到局部Element上,避免牵连上层组件。
改造示例:
原写法:
@override Widget build(BuildContext context) { // 此处的context属于PageviewItem本身,会把PageviewItem注册为MediaQuery依赖 final screenHeight = MediaQuery.of(context).size.height; return Container( height: screenHeight * 0.8, child: YourContent(), ); }
改造后写法:
@override Widget build(BuildContext context) { return Builder( builder: (localContext) { // localContext仅属于当前Builder节点,不会关联上层Element final screenHeight = MediaQuery.of(localContext).size.height; return Container( height: screenHeight * 0.8, child: YourContent(), ); }, ); }
改造完成后,就算MediaQuery数据更新,只有Builder内部的组件会触发重建,PageviewItem、上层PageView这类组件不会被牵连。
2. 输入框键盘适配的局部处理
因为已经关闭了Scaffold的自动resize逻辑,如果需要让输入框跟随键盘抬起,不要在页面顶层计算键盘高度,就在输入框组件的局部位置读取viewInsets参数:
Positioned( bottom: 0, left: 0, child: Builder( builder: (inputContext) { final keyboardHeight = MediaQuery.of(inputContext).viewInsets.bottom; final screenWidth = MediaQuery.of(inputContext).size.width; return Container( transform: Matrix4.translationValues(0, -keyboardHeight, 0), height: 50, width: screenWidth - 50, child: TextField(), ); }, ), )
3. 降低无意义重建的开销
- 给没有状态变化的子组件添加
const构造函数,避免每次build都重新创建Widget对象 - 对状态变化不敏感的组件,避免使用
listen: true的Provider调用,只在真正需要监听状态的局部组件里开启监听
很多开发者误以为build方法重新执行会销毁对应的State,实际上Flutter的重建机制只会重新运行build函数,对应的State对象会完整保留。但无意义的跨层重建依然会带来额外的布局、绘制开销,比如视频组件、图片组件反复重新走初始化逻辑,出现可感知的卡顿。
内容的提问来源于stack exchange,提问作者Ferhat Ismayilov

