Flutter Bloc中列表内对象更新后UI未变更的原因排查
我定义了一个Todo模型,同时有该模型可正常运行与无法运行的代码片段。点击复选框时,非工作版本里能看到isDone值在true和false间切换,但UI却未更新。我已经创建了新的tempList,理论上应该能触发UI更新,想知道问题出在哪?
Todo模型类
import 'dart:convert'; class ToDo { final String title; final String description; bool isDone; ToDo({ required this.title, required this.description, required this.isDone, }); ToDo copyWith({ String? title, String? description, bool? isDone, }) { return ToDo( title: title ?? this.title, description: description ?? this.description, isDone: isDone ?? this.isDone, ); } Map<String, dynamic> toMap() { return <String, dynamic>{ 'title': title, 'description': description, 'isDone': isDone, }; } @override bool operator ==(covariant ToDo other) { if (identical(this, other)) return true; return other.title == title && other.description == description && other.isDone == isDone; } @override int get hashCode => title.hashCode ^ description.hashCode ^ isDone.hashCode; }
可正常运行的代码
BlocBuilder中的代码
Checkbox( value: state.todoList[index].isDone, onChanged: (value) { context.read<TodoBloc>().add(UpdateToDo( todo: state.todoList[index].copyWith( isDone: !state.todoList[index].isDone), index: index)); }, ),
事件处理器中的代码
void _updateToDo( UpdateToDo event, Emitter<TodoState> emit, ) { try { final state = this.state; if (state is TodoLoaded) { List<ToDo> tasks = (state.todoList.map((task) { return task.title == event.todo.title ? event.todo : task; })).toList(); emit(TodoLoaded(todoList: tasks)); } } catch (e) { emit(TodoError(error: e.toString())); } }
无法运行的代码
BlocBuilder中的代码
Checkbox( value: state.todoList[index].isDone, onChanged: (value) { context.read<TodoBloc>().add(UpdateToDo( todo: state.todoList[index], index: index)); }, ),
事件处理器中的代码
void _updateToDo( UpdateToDo event, Emitter<TodoState> emit, ) { try { final state = this.state; if (state is TodoLoaded) { List<ToDo> tempList = List.from(state.todoList); for (int index = 0; index < tempList.length; index++) { if (index == event.index) { tempList[index].isDone = !tempList[index].isDone; } } emit(TodoLoaded(todoList: tempList)); } } catch (e) { emit(TodoError(error: e.toString())); } }
问题原因分析
核心问题在于Bloc的不可变状态设计原则以及状态对比机制:
你创建了新的
tempList,但列表中的Todo对象还是原状态里的引用。Bloc在判断是否需要更新UI时,会对比新旧TodoLoaded状态的相等性。虽然你修改了Todo的isDone属性,但由于列表元素是原对象引用,且Bloc依赖不可变状态变更来触发更新,直接修改对象属性属于可变操作,Bloc无法正确识别这是有效的状态变更。正常运行的版本中,你通过
copyWith创建了全新的Todo对象,替换了原列表中的元素。此时新列表的元素是新引用,TodoLoaded状态的todoList与旧状态的列表在元素层面完全不同,Bloc能准确识别状态变更,进而触发UI更新。即使你创建了新列表,只要列表内的对象是原引用且被直接修改,Bloc的状态对比逻辑可能无法感知到状态的有效变化——因为Bloc默认会认为状态没有发生本质变更(列表引用变了,但元素引用未变,且你的
==运算符虽然考虑了isDone,但Bloc的状态对比可能在列表层面判断时,由于元素引用相同而忽略了属性变化)。
总结:Bloc要求状态必须是不可变的,你必须通过创建新对象(而非修改现有对象属性)来生成新状态,这样Bloc才能正确对比新旧状态,触发UI更新。
内容的提问来源于stack exchange,提问作者Anish Hemachandran

