Flutter Bloc表单结合WillPopScope无法恢复初始状态问题
问题核心原因
两个问题本质都是状态边界不清晰导致的:
- 表单无法重置:你将编辑标记、临时编辑数据全部存在组件本地State中,切回只读模式时仅修改了编辑状态标记,既没有还原临时数据为初始值,也没有触发表单组件重置。且
TextFormField的initialValue参数仅在组件首次初始化时生效,后续数据变化不会同步更新输入框内容,自然会残留修改后的内容。 - 列表数据不同步:详情页路由内独立创建了
FamilyBloc实例,和列表页使用的Bloc实例完全隔离,详情页保存数据后没有任何机制通知列表页刷新,返回后列表自然展示旧数据。
具体修复步骤
1. 重构Bloc层状态逻辑
把编辑模式标记、原始表单数据、临时编辑数据全部收归Bloc管理,不要在组件本地存业务状态。
首先补全缺失的事件定义:
// 原有事件保留,新增以下3个事件 class ToggleEditModeEvent extends FamilyEvent {} class CancelEditEvent extends FamilyEvent {} class UpdateFormFieldEvent extends FamilyEvent { final String name; const UpdateFormFieldEvent({required this.name}); @override List<Object> get props => [name]; }
修改Bloc实现,新增原始数据缓存、编辑状态标记,补全对应事件处理逻辑:
class FamilyBloc extends Bloc<FamilyEvent, FamilyState> { FamilyBloc({required this.repository}) : super(LoadingState()) { on<BackEvent>(_onBackEvent); on<FetchFamiliesEvent>(_onFetchFamilies); on<FetchFamilyEvent>(_onFetchFamily); on<UpdateFamilyEvent>(_onUpdateFamily); on<CreateFamilyEvent>(_onCreateFamily); on<ToggleEditModeEvent>(_onToggleEditMode); on<CancelEditEvent>(_onCancelEdit); on<UpdateFormFieldEvent>(_onUpdateFormField); } final FamilyRepository repository; // 缓存已持久化的原始数据 Family? _originalFamily; // 编辑模式标记 bool isEditMode = false; FutureOr<void> _onFetchFamily(FetchFamilyEvent event, Emitter<FamilyState> emit) async { emit(LoadingState()); try { // 新建场景(id=0)初始化空对象,已存在场景从接口拉取详情 _originalFamily = event.item?.id == 0 ? const Family(id: 0, name: '') : await repository.getFamilyDetail(event.item!.id); // 新建场景默认进入编辑模式 isEditMode = event.item?.id == 0; emit(ItemLoadedState(family: _originalFamily!)); } catch (e) { emit(FailState(message: e.toString())); } } FutureOr<void> _onToggleEditMode(ToggleEditModeEvent event, Emitter<FamilyState> emit) { isEditMode = true; // 进入编辑模式时拷贝一份原始数据作为临时编辑对象 emit(ItemLoadedState(family: _originalFamily!.copyWith())); } FutureOr<void> _onCancelEdit(CancelEditEvent event, Emitter<FamilyState> emit) { isEditMode = false; // 取消编辑时直接返回原始数据,丢弃所有临时修改 emit(ItemLoadedState(family: _originalFamily!)); } FutureOr<void> _onUpdateFormField(UpdateFormFieldEvent event, Emitter<FamilyState> emit) { if (state is ItemLoadedState) { final current = (state as ItemLoadedState).family; emit(ItemLoadedState(family: current.copyWith(name: event.name))); } } FutureOr<void> _onUpdateFamily(UpdateFamilyEvent event, Emitter<FamilyState> emit) async { emit(LoadingState()); try { final savedData = await repository.updateFamily(event.item); // 保存成功后更新缓存的原始数据 _originalFamily = savedData; isEditMode = false; emit(IsSavedState(family: savedData)); } catch (e) { emit(FailState(message: e.toString())); } } FutureOr<void> _onCreateFamily(CreateFamilyEvent event, Emitter<FamilyState> emit) async { emit(LoadingState()); try { final savedData = await repository.createFamily(event.item); _originalFamily = savedData; isEditMode = false; emit(IsSavedState(family: savedData)); } catch (e) { emit(FailState(message: e.toString())); } } FutureOr<void> _onBackEvent(BackEvent event, Emitter<FamilyState> emit) { emit(InitialState()); } // 原有拉取列表逻辑保持不变即可 FutureOr<void> _onFetchFamilies(FetchFamiliesEvent event, Emitter<FamilyState> emit) async {} }
注意需要给Family实体类添加copyWith方法,支持对象拷贝和字段更新
2. 重构页面组件逻辑
移除组件本地存储的业务状态(_currentFamily、isUpdated),所有状态从Bloc获取;使用TextEditingController控制输入框内容,解决initialValue不动态更新的问题;修正WillPopScope的返回拦截逻辑。
class FamilyDetailsView extends StatefulWidget { const FamilyDetailsView({Key? key, required this.family}) : super(key: key); final Family family; @override State<FamilyDetailsView> createState() => FamilyDetailsViewState(); } class FamilyDetailsViewState extends State<FamilyDetailsView> { late FamilyBloc _familyBloc; final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<FormState> _formKey = GlobalKey<FormState>(debugLabel: '_appFamilyFormState'); late TextEditingController _nameController; @override void initState() { super.initState(); _nameController = TextEditingController(); _familyBloc = BlocProvider.of<FamilyBloc>(context); _familyBloc.add(FetchFamilyEvent(item: widget.family)); } @override void dispose() { _nameController.dispose(); _familyBloc.close(); super.dispose(); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { // 编辑模式下首次触发返回:重置表单、退出编辑模式,不退出页面 if (_familyBloc.isEditMode) { _familyBloc.add(CancelEditEvent()); _formKey.currentState?.reset(); return false; } // 非编辑模式:退出页面,返回刷新标记给列表页 Navigator.pop(context, true); return true; }, child: Scaffold( key: _scaffoldKey, appBar: AppBar( title: const Text('Family'), actions: <Widget>[ BlocBuilder<FamilyBloc, FamilyState>( builder: (context, state) { if (state is! ItemLoadedState && state is! IsSavedState) { return const SizedBox.shrink(); } return _familyBloc.isEditMode ? IconButton( onPressed: _saveFamily, icon: const Icon(Icons.save), ) : IconButton( onPressed: () => _familyBloc.add(ToggleEditModeEvent()), icon: const Icon(Icons.edit), ); }, ) ], ), body: BlocConsumer<FamilyBloc, FamilyState>( listener: (context, state) { // 状态变化时同步输入框内容 if (state is ItemLoadedState) { _nameController.text = state.family.name; } if (state is IsSavedState) { _nameController.text = state.family.name; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('保存成功')) ); } if (state is FailState) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('操作失败:${state.message}')) ); } }, builder: (context, state) { if (state is LoadingState) { return const Center(child: CircularProgressIndicator()); } if (state is ItemLoadedState || state is IsSavedState) { final familyData = state is ItemLoadedState ? state.family : (state as IsSavedState).family; return Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( children: [ TextFormField( controller: _nameController, readOnly: !_familyBloc.isEditMode, decoration: const InputDecoration( labelText: 'Name', border: OutlineInputBorder() ), onChanged: (val) => _familyBloc.add(UpdateFormFieldEvent(name: val)), validator: (val) { if (val?.trim().isEmpty ?? true) { return 'Name cannot be empty'; } return null; }, ) ], ), ), ); } return const Center(child: Text('No data')); }, ), ), ); } void _saveFamily() { if (!_formKey.currentState!.validate()) return; final currentState = _familyBloc.state; final familyData = currentState is ItemLoadedState ? currentState.family : (currentState as IsSavedState).family; _familyBloc.add( familyData.id == 0 ? CreateFamilyEvent(item: familyData) : UpdateFamilyEvent(item: familyData) ); } }
改完之后,从编辑模式切回只读模式时,Bloc会重新下发原始缓存数据,输入框控制器同步更新为初始值,表单就会完全恢复到修改前的状态,不会有内容残留。
3. 修复列表页同步问题
两种实现方式二选一即可:
- 方案一(推荐):将
FamilyBloc的创建位置上移到列表页路由层级,让列表页和详情页共用同一个Bloc实例,详情页保存成功后直接在Bloc内触发列表刷新事件,无需页面间传值。 - 方案二:保留独立Bloc实例的逻辑,在列表页跳转详情时等待返回结果,收到刷新标记后重新拉取列表数据:
// 列表页跳转详情的逻辑 final needRefresh = await Navigator.push<bool>( context, MaterialPageRoute( builder: (context) => FamilyDetailsPage(family: item) ) ); if (needRefresh == true) { // 触发列表刷新事件 context.read<FamilyBloc>().add(FetchFamiliesEvent()); }
内容的提问来源于stack exchange,提问作者mollusk
相关产品推荐
相关产品推荐

