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

如何在Flutter Bloc中实现无状态跳过的打字动画?

Bloc状态管理下实现AI实时打字动画的状态丢失问题

我正在开发一款采用Bloc模式进行状态管理的Flutter应用,目标是实现类似ChatGPT的实时打字动画——让AI消息逐字符/逐段依次显示。但遇到Bloc的优化问题:快速发射中间状态时,Bloc会跳过这些状态,导致动画无法正常呈现。

现有实现代码

Bloc事件处理逻辑

on<SendMessageEvent>((SendMessageEvent event, Emitter<ChatState> emit) {
  // Create a new list of messages by adding the new message to the
  // existing list.
  final List<Message> updatedMessages = List<Message>.from(state.messages)
    ..add(Message(role: Role.user, content: StringBuffer(event.message)));
  // Emit a new state with the updated list of messages.
  emit(
    SentMessageState(messages: updatedMessages, language: state.language),
  );
  try {
    _chatRepository
        .sendChat(Chat(messages: state.messages, language: state.language))
        .listen((String line) => add(UpdateAiMessageEvent(line)));
  } catch (error) {
    _emitChatError(emit: emit, error: error);
  }
});

on<UpdateAiMessageEvent>((
  UpdateAiMessageEvent event,
  Emitter<ChatState> emit,
) {
  if (state.messages.isNotEmpty && state.messages.last.isAi) {
    state.messages.last.content.write(event.pieceOfMessage);
    emit(
      AiMessageUpdated(messages: state.messages, language: state.language),
    );
  } else {
    final List<Message> updatedMessages = List<Message>.from(state.messages)
      ..add(
        Message(
          role: Role.assistant,
          content: StringBuffer(event.pieceOfMessage),
        ),
      );
    emit(
      AiMessageUpdated(messages: updatedMessages, language: state.language),
    );
  }
});

Retrofit远程请求代码

@RestApi()
abstract class RetrofitClient {
  factory RetrofitClient(Dio dio, {String baseUrl}) = _RetrofitClient;

  @POST('chat')
  Stream<String> sendChatMessage(@Body() ChatRequest chatRequest);
}

我尝试过在状态发射间加延迟、重写Message的==和hashCode,但Bloc仍会跳过中间状态。已知用StatefulWidget+StreamBuilder能实现,但希望保留Bloc模式的一致性和可维护性,请问有没有符合Bloc模式的解决方案?


解决方案

1. 严格遵守状态不可变性原则

Bloc的核心要求是状态不可变,你当前直接修改state.messages.last.content的操作会导致状态引用未变,Bloc无法识别为新状态。必须创建全新的Message和消息列表实例:

修改UpdateAiMessageEvent的处理逻辑:

on<UpdateAiMessageEvent>((event, emit) {
  final currentMessages = List<Message>.from(state.messages);
  if (currentMessages.isNotEmpty && currentMessages.last.isAi) {
    // 复制最后一条AI消息,创建新的StringBuffer追加内容
    final lastAiMsg = currentMessages.last;
    final newContent = StringBuffer(lastAiMsg.content.toString())..write(event.pieceOfMessage);
    currentMessages[currentMessages.length - 1] = Message(
      role: Role.assistant,
      content: newContent,
    );
  } else {
    currentMessages.add(Message(
      role: Role.assistant,
      content: StringBuffer(event.pieceOfMessage),
    ));
  }
  // 发射全新状态
  emit(AiMessageUpdated(messages: currentMessages, language: state.language));
});

2. 正确实现Equatable的属性比较

确保ChatState和Message继承Equatable,并将所有影响状态唯一性的属性加入props:

// ChatState类
class ChatState extends Equatable {
  final List<Message> messages;
  final String language;

  const ChatState({required this.messages, required this.language});

  @override
  List<Object?> get props => [messages, language];
}

// Message类
class Message extends Equatable {
  final Role role;
  final StringBuffer content;

  const Message({required this.role, required this.content});

  @override
  List<Object?> get props => [role, content.toString()];
}

注:StringBuffer是引用类型,直接比较会判断为相同,因此要转换成字符串参与比较,确保内容变化时props不同。

3. 直接在事件处理中发射状态,避免事件队列延迟

将Stream的监听逻辑移到SendMessageEvent的处理函数内,直接发射状态而非添加新事件,减少Bloc对状态的批量合并:

on<SendMessageEvent>((event, emit) {
  final updatedMessages = List<Message>.from(state.messages)
    ..add(Message(role: Role.user, content: StringBuffer(event.message)));
  emit(SentMessageState(messages: updatedMessages, language: state.language));
  
  try {
    _chatRepository.sendChat(Chat(messages: updatedMessages, language: state.language))
        .listen((line) {
          final currentMessages = List<Message>.from(updatedMessages);
          if (currentMessages.isNotEmpty && currentMessages.last.isAi) {
            final lastAiMsg = currentMessages.last;
            final newContent = StringBuffer(lastAiMsg.content.toString())..write(line);
            currentMessages[currentMessages.length - 1] = Message(
              role: Role.assistant,
              content: newContent,
            );
          } else {
            currentMessages.add(Message(
              role: Role.assistant,
              content: StringBuffer(line),
            ));
          }
          emit(AiMessageUpdated(messages: currentMessages, language: state.language));
        });
  } catch (error) {
    _emitChatError(emit: emit, error: error);
  }
});

这种方式跳过了事件队列的中转,每个Stream数据块都会直接触发状态更新,降低Bloc合并状态的概率。


通过以上三点调整,就能在Bloc模式下实现流畅的实时打字动画,无需向Bloc库提交Issue——问题源于状态可变的错误用法,而非库本身的缺陷。

内容的提问来源于stack exchange,提问作者Dmytro T

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 04:07:08