Flutter BLoC模式下结合StreamBuilder做表单验证时TextFormField异常问题
问题根因
- BLoC层的
name流绑定了验证转换器,验证失败时仅返回错误信息,不向下游发送当前输入的文本值,导致StreamBuilder拿到的snapshot.data为空,直接重置了输入框内容 - UI层每次StreamBuilder触发重绘时都新建
TextEditingController,完全没有复用控制器,导致输入光标状态异常、无法连续输入 - TabBarView默认不缓存子页面状态,切换Tab后Tab1的Widget被销毁,重绘时没有保留输入状态的载体
修复方案
第一步:修改BLoC逻辑,拆分「原始值流」和「验证结果流」
不要直接对展示用的流加转换器,单独拆出验证错误的流:
class SimpleBloc { final SimpleRepository repo; SimpleBloc(this.repo); final _name = BehaviorSubject<String>(); // 原始输入值流,不做验证拦截,始终返回当前输入的内容 Stream<String> get rawName => _name.stream; // 单独的错误信息流,验证逻辑放这里 Stream<String?> get nameError => _name.stream.transform(validateName); // 合法校验流改从原始值判断 Stream<bool> get isValid => _name.stream.map((val) => val.length >=7); Function(String) get changeName => _name.sink.add; Future initializedAsync() async { var setting = await repo.get(); _name.add(setting.name); } dispose() { _name.close(); } // 验证转换器修改为返回错误内容/空 final validateName = StreamTransformer<String, String?>.fromHandlers( handleData: (settingsName, sink) { if (settingsName.length < 7) { sink.add("Name must be at least 7 characters"); } else { sink.add(null); } }); final validateAutomaticSend = StreamTransformer<bool, bool>.fromHandlers(handleData: (value, sink) { sink.add(value); }); submit() async { var setting = await repo.get(); setting.name = _name.value; repo.update(setting); } }
第二步:UI层缓存TextEditingController和页面状态
- 把
SimpleRouteTabWidget改成StatefulWidget,在initState里一次性初始化TextEditingController,监听原始值流更新控制器内容 - 给页面加
AutomaticKeepAliveClientMixin,防止切换Tab时页面被销毁
class SimpleRouteTabWidget extends StatefulWidget { const SimpleRouteTabWidget({Key? key}) : super(key: key); @override State<SimpleRouteTabWidget> createState() => _SimpleRouteTabWidgetState(); } class _SimpleRouteTabWidgetState extends State<SimpleRouteTabWidget> with AutomaticKeepAliveClientMixin { late TextEditingController _nameController; StreamSubscription? _nameSub; @override bool get wantKeepAlive => true; // 开启页面状态缓存 @override void initState() { super.initState(); _nameController = TextEditingController(); // 监听BLoC的原始值流,仅在初始加载/值被BLoC侧修改时更新输入框内容 WidgetsBinding.instance.addPostFrameCallback((_) { final bloc = Provider.of<SimpleBloc>(context, listen: false); _nameSub = bloc.rawName.listen((val) { if (_nameController.text != val) { _nameController.value = TextEditingValue( text: val, selection: TextSelection.collapsed(offset: val.length) ); } }); }); } @override void dispose() { _nameController.dispose(); _nameSub?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { super.build(context); var bloc = Provider.of<SimpleBloc>(context); return FutureBuilder( future: bloc.initializedAsync(), builder: (_builder, AsyncSnapshot _snapshot) { if (_snapshot.connectionState == ConnectionState.done) { if (_snapshot.error != null) { return const Scaffold( body: Center( child: Text('Error occured :/'), ), ); } else { return _finalBuilder(context, bloc); } } else { return const Scaffold( body: Center( child: Text('Please wait...'), ), ); } }); } Widget _finalBuilder(BuildContext ctx, SimpleBloc bloc) { return DefaultTabController( length: 2, child: Scaffold( body: TabBarView(children: [ Container( margin: const EdgeInsets.only(left: 20.0, right: 20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox( height: 20, ), // 只监听错误流更新提示,不再用流重建整个输入框 StreamBuilder<String?>( stream: bloc.nameError, builder: (_, errorSnapshot) { return TextFormField( controller: _nameController, onChanged: bloc.changeName, decoration: InputDecoration( helperText: "Name", errorText: errorSnapshot.data)); }, ) ])), Container( margin: const EdgeInsets.only(left: 20.0, right: 20.0), child: const Text("Something else")), ]), appBar: AppBar( title: const Text('Parameters'), actions: [ StreamBuilder<bool>(stream: bloc.isValid, builder: (context, snapshot) { return IconButton( icon: const Icon(Icons.done), onPressed: snapshot.hasData ? () { bloc.submit(); Navigator.popUntil(context, ModalRoute.withName('/')); } : null, ); }) ], bottom: const TabBar( tabs: [ Tab( text: "General", icon: Icon(Icons.directions_car), ), Tab( text: "Report", icon: Icon(Icons.summarize), ) ], ), ), )); } }
第三步:补充Provider的dispose逻辑
给Provider添加释放方法,避免内存泄漏:
class SimpleRoute extends StatelessWidget { const SimpleRoute({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Provider( create: (context) => SimpleBloc(SimpleRepository()), dispose: (context, SimpleBloc bloc) => bloc.dispose(), child: const SimpleRouteTabWidget()); } }
修复效果
- 验证失败时仅展示错误提示,不会清空输入内容,可正常连续输入
- 初始值可以正常加载展示
- 切换Tab再切回来,输入内容不会丢失
- 输入长度达标后完成按钮自动激活,提交逻辑正常运行
内容的提问来源于stack exchange,提问作者Seamus
相关产品推荐
相关产品推荐

