Flutter组件重建导致键盘失去焦点

202 阅读2分钟

在最近的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,保持状态。

总结

这是一个很典型也很容易忽略的问题,对于性能优化相关的也需要考虑这个。