如何避免Flutter弹出键盘时Widgets与Blocs重复重建
问题描述
聊天组件页面被用于检查Socket连接状态的HydratedBloc对应的BlocBuilder包裹。点击TextField弹出键盘时,BlocProvider中注册的所有Bloc会立即刷新,键盘随即消失,有时甚至会导致界面崩溃/冻结,但无错误日志输出。
界面表现:
- 正常状态:显示顶部标题栏、消息列表和底部输入框
- 冻结后:界面无响应,输入框键盘消失
相关代码
HekimaSupportPage
class HekimaSupportPage extends StatelessWidget { HekimaSupportPage({super.key}); final GlobalKey<FormState> formKey = GlobalKey<FormState>(); final TextEditingController messageController = TextEditingController(); @override Widget build(BuildContext context) { return RelativeBuilder( builder: (context, height, width, sy, sx) { return SizedBox( height: context.height, width: context.width, child: BlocBuilder<SupportChatBloc, SupportChatState>( builder: (context, state) { late Widget chatWidget; if (state is SupportChatConnecting) { chatWidget = const Center( child: LoaderWidget( color: AppColors.background, ), ); } else if (state is SupportChatNotConnected) { chatWidget = ExceptionWidget( exception: AppException( 'Sorry, failed to connect properly with chat service.', title: 'Chat not connected', ), titleColor: Colors.black, buttonColor: AppColors.background, buttonTextColor: Colors.white, onRetry: () => context.read<SupportChatBloc>().add(ConnectSupportChat()), ); } else if (state is SupportChatException) { chatWidget = Padding( padding: EdgeInsets.symmetric( horizontal: sx(20), ), child: ExceptionWidget( exception: state.exception, titleColor: Colors.black, buttonColor: AppColors.background, buttonTextColor: Colors.white, onRetry: () => context .read<SupportChatBloc>() .add(ConnectSupportChat()), ), ); } else { chatWidget = SupportChatWidget( messages: state.messages, formKey: formKey, messageController: messageController, ); } return AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: chatWidget, ); }, ), ); }, ); } }
SupportChatWidget
class SupportChatWidget extends StatelessWidget { const SupportChatWidget({ super.key, required this.messages, required this.formKey, required this.messageController, }); final List<models.Message> messages; final GlobalKey<FormState> formKey; final TextEditingController messageController; @override Widget build(BuildContext context) { return RelativeBuilder( builder: (context, height, width, sy, sx) { return SizedBox( height: context.height, width: context.width, child: Form( key: formKey, child: Column( children: [ Container( height: sy(35), width: context.width, padding: EdgeInsets.symmetric( horizontal: sx(20), ), decoration: const BoxDecoration( color: AppColors.background, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Hekima Support', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w500, fontSize: sy(9), ), ), Icon( CupertinoIcons.info, color: Colors.white, size: sy(16), ), ], ), ), Expanded( child: Container( width: context.width, padding: EdgeInsets.symmetric( horizontal: sx(20), vertical: sy(10), ), child: ListView( reverse: true, children: messages .map((Message message) => MessageBubble(message: message)) .toList(), ), ), ), MessageSenderWidget( controller: messageController, ), ], ), ), ); }, ); } }
MessageSenderWidget
class MessageSenderWidget extends StatelessWidget { const MessageSenderWidget({ super.key, required this.controller, }); final TextEditingController controller; @override Widget build(BuildContext context) { return RelativeBuilder( builder: (context, height, width, sy, sx) { return Container( width: context.width, constraints: BoxConstraints( minHeight: sy(32), maxHeight: sy(50), ), padding: EdgeInsets.symmetric( horizontal: sx(20), ), decoration: const BoxDecoration( color: AppColors.background, border: Border( bottom: BorderSide( color: Colors.white, ), ), ), child: Row( children: [ Expanded( child: TextFormField( style: TextStyle( color: Colors.white, fontWeight: FontWeight.w400, fontSize: sy(8), ), controller: controller, decoration: InputDecoration( hintText: 'Text Message', hintStyle: TextStyle( color: Colors.white70, fontWeight: FontWeight.w400, fontSize: sy(8), ), border: InputBorder.none, disabledBorder: InputBorder.none, errorBorder: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, ), maxLines: 3, minLines: 1, ), ), const Icon( CupertinoIcons.photo_on_rectangle, color: Colors.white, ), SizedBox( width: sx(15), ), InkWell( onTap: () { if (controller.text.isEmpty) { return; } else { final Message message = Message( type: MessageType.chatMessage, message: controller.text.trim(), messageFrom: '', messageTo: '', ); context.read<SupportChatBloc>().add(SendMessage(message)); controller.clear(); } }, child: const Icon( CupertinoIcons.arrow_up_circle_fill, color: Colors.white, ), ), ], ), ); }, ); } }
解决方案
1. 缩小BlocBuilder作用范围
当前BlocBuilder包裹了整个页面,状态变化时会重建所有组件,包括输入框区域。将BlocBuilder仅用于需要响应状态变化的部分(连接状态提示、消息列表),输入区域直接放在外部,避免键盘弹出时被不必要重建:
class HekimaSupportPage extends StatelessWidget { HekimaSupportPage({super.key}); final GlobalKey<FormState> formKey = GlobalKey<FormState>(); final TextEditingController messageController = TextEditingController(); @override Widget build(BuildContext context) { return RelativeBuilder( builder: (context, height, width, sy, sx) { return SizedBox( height: context.height, width: context.width, child: Column( children: [ // 顶部标题栏独立于Bloc状态 Container( height: sy(35), width: context.width, padding: EdgeInsets.symmetric(horizontal: sx(20)), decoration: const BoxDecoration(color: AppColors.background), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Hekima Support', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w500, fontSize: sy(9), ), ), Icon(CupertinoIcons.info, color: Colors.white, size: sy(16)), ], ), ), Expanded( // 仅消息列表和连接状态用BlocBuilder包裹 child: BlocBuilder<SupportChatBloc, SupportChatState>( builder: (context, state) { if (state is SupportChatConnecting) { return const Center(child: LoaderWidget(color: AppColors.background)); } else if (state is SupportChatNotConnected) { return ExceptionWidget( exception: AppException( '抱歉,无法正常连接聊天服务。', title: '聊天未连接', ), titleColor: Colors.black, buttonColor: AppColors.background, buttonTextColor: Colors.white, onRetry: () => context.read<SupportChatBloc>().add(ConnectSupportChat()), ); } else if (state is SupportChatException) { return Padding( padding: EdgeInsets.symmetric(horizontal: sx(20)), child: ExceptionWidget( exception: state.exception, titleColor: Colors.black, buttonColor: AppColors.background, buttonTextColor: Colors.white, onRetry: () => context.read<SupportChatBloc>().add(ConnectSupportChat()), ), ); } else { return Container( width: context.width, padding: EdgeInsets.symmetric(horizontal: sx(20), vertical: sy(10)), child: ListView( reverse: true, children: state.messages.map((message) => MessageBubble(message: message)).toList(), ), ); } }, ), ), // 输入区域脱离BlocBuilder,避免被重建 MessageSenderWidget(controller: messageController), ], ), ); }, ); } }
2. 排查HydratedBloc状态序列化
检查SupportChatState的toJson和fromJson实现,确保状态持久化逻辑正确,不存在意外的状态变更触发Bloc刷新。
3. 稳定全局对象生命周期
将formKey和messageController迁移到StatefulWidget的state中,或者提升到父组件管理,避免页面重建时这些对象被意外重新创建,引发输入框状态异常。
4. 检查布局变化触发的Bloc事件
排查SupportChatBloc中是否有监听布局变化的逻辑,键盘弹出会触发布局重绘,若Bloc在此过程中被意外发送事件,会导致不必要的状态刷新。
内容的提问来源于stack exchange,提问作者Ngonidzashe Mangudya
相关产品推荐
相关产品推荐

