You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 17:42:25