如何在Flutter Bloc中实现无状态跳过的打字动画?
我正在开发一款采用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

