在最近的Flutter项目开发过程中,我遇到了一个非常典型的问题:输入框(TextField)在弹出键盘后,因重建导致键盘突然消失,输入框失去焦点,一开始我还在寻找其他地方的原因,过了一会才发现这就是个很典型的组件重建嘛,重建后就失去焦点了。
问题场景描述
当时是在一个聊天界面,底部输入区域有一个图片列表展示和一个输入框,用户可以输入文字并配合上传照片。当我删除照片时,页面会根据照片数组是否为空做条件渲染:
photos.isEmpty ? Container() : PhotosGrid(photos: photos)
在删除照片的过程中,用户对输入框已经聚焦,键盘弹出。可是当我删到最后一张照片,照片数组变为空后,整个组件重新构建,导致输入框对应的Widget被销毁重建,结果输入框失去了焦点,键盘自动收起,体验非常不好。
简而言之,就是:
- 输入框聚焦,键盘弹出
- 条件判断照片数组为空,触发组件树重建
- 输入框被销毁,焦点丢失,键盘收起
原因分析
Flutter的UI是响应式的,Widget是轻量级的不可变对象,每次状态更新都会重新build树。
在我的代码中,由于使用了条件渲染,照片数组为空时,我直接用另一套Widget替换了之前包含输入框的布局。导致之前的输入框对应的Element节点被销毁,新的输入框创建后不再有之前的焦点状态。
这也就是说,键盘依赖于输入框的焦点(FocusNode),一旦焦点丢失,键盘就会关闭。
解决方案
1. 使用Visibility或Offstage
使用Visibility对组件进行隐藏,而不是直接替换Widget
Visibility(
visible: photos.isNotEmpty,
child: PhotosGrid(photos: photos),
),
使用Offstage保留Widget但不渲染
Offstage(
offstage: photos.isEmpty,
child: PhotosGrid(photos: photos),
),
2. 使用 FocusNode管理
对于StatefulWidget或Controller等中进行FocusNode声明
final FocusNode _focusNode = FocusNode();
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
绑定输入框
TextField(
focusNode: _focusNode,
controller: _controller,
)
删除后失去焦点后请求焦点
void _onPhotoDeleted() {
setState(() {
photos.removeLast();
});
// 延迟调用,确保build完成后请求焦点
Future.delayed(Duration(milliseconds: 100), () {
if (!_focusNode.hasFocus) {
_focusNode.requestFocus();
}
});
}
3. 给输入框使用 Key
Flutter通过Key识别Widget身份,指定一个稳定的Key能减少Widget被误判为新Widget,避免重建。
TextField(
key: ValueKey('chat_input_field'),
focusNode: _focusNode,
controller: _controller,
)
这样即使外层有重建,Flutter也会尽量复用这个输入框的Element,保持状态。
总结
这是一个很典型也很容易忽略的问题,对于性能优化相关的也需要考虑这个。