Flutter中TextFormField/TextField获焦触发不必要重建问题求助
当TextFormField或TextField获得焦点时,会触发不必要的组件重建,导致资源占用飙升,模拟器及电脑运行缓慢甚至无法使用。已尝试多种方案但未解决问题:
- 将表单key设置为static final:
static final GlobalKey<FormState> _signInFormKey = GlobalKey<FormState>(); - 使用sizer替代MediaQuery获取尺寸
- 尝试从ConsumerWidget、StatefulWidget、StatelessWidget及ConsumerStatefulWidget中调用表单字段
目前所有尝试均无效,请问是操作有误,还是Flutter状态管理结合60Hz刷新导致的资源消耗问题?
表单字段代码
class CustomFormField extends StatefulWidget { const CustomFormField({ Key? key, required TextEditingController controller, required bool prefixCurrencySymbol, required String? currencySymbol, required List<String> autoFillHints, required bool blockSystemKeyboard, required double width, required FocusNode focusNode, required TextInputType keyboardType, required TextInputAction inputAction, required String label, required TextCapitalization textCapitalization, required Function(String value) validator, required bool obscurePasswordOption, required bool saveAutofillData, required bool passwordCreationField, required Future<void> Function(String?)? onChanged, }) : _formController = controller, _prefixCurrencySymbol = prefixCurrencySymbol, _currencySymbol = currencySymbol, _autoFillHints = autoFillHints, _blockSystemKeyboard = blockSystemKeyboard, _width = width, _formFocusNode = focusNode, _keyboardType = keyboardType, _inputAction = inputAction, _textCapitalization = textCapitalization, _label = label, _validator = validator, _obscurePasswordOption = obscurePasswordOption, _saveAutofillData = saveAutofillData, _passwordCreationField = passwordCreationField, _onChanged = onChanged, super(key: key); final TextEditingController _formController; final bool _prefixCurrencySymbol; final String? _currencySymbol; final List<String> _autoFillHints; final bool _blockSystemKeyboard; final double _width; final FocusNode _formFocusNode; final TextInputType _keyboardType; final TextInputAction _inputAction; final String _label; final bool _obscurePasswordOption; final TextCapitalization _textCapitalization; final Function(String) _validator; final bool _saveAutofillData; final bool _passwordCreationField; final Future<void> Function(String?)? _onChanged; @override State<CustomFormField> createState() => _CustomFormFieldState(); } class _CustomFormFieldState extends State<CustomFormField> { bool _obscurePassword = true; @override void dispose() { widget._formFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SizedBox( width: widget._width, child: TextFormField( style: ThemeEndpoints.textFieldTextStyle(), autofillHints: widget._autoFillHints, enableInteractiveSelection: true, enableSuggestions: false, autocorrect: false, textAlignVertical: const TextAlignVertical(y: 0.1), readOnly: widget._blockSystemKeyboard, maxLines: 1, controller: widget._formController, focusNode: widget._formFocusNode, keyboardType: widget._keyboardType, obscureText: (widget._obscurePasswordOption) ? _obscurePassword : false, textCapitalization: widget._textCapitalization, textInputAction: widget._inputAction, validator: (text) => widget._validator(text!), onChanged: (text) => (widget._passwordCreationField && text.isNotEmpty) ? widget._onChanged!(text) : null, onEditingComplete: (widget._saveAutofillData) ? () => TextInput.finishAutofillContext() : null, toolbarOptions: const ToolbarOptions( copy: true, paste: true, cut: true, selectAll: true, ), decoration: InputDecoration( filled: true, fillColor: ThemeEndpoints.textFieldBackgroundColor(), alignLabelWithHint: true, labelText: widget._label, labelStyle: ThemeEndpoints.textFieldLabelStyle(), contentPadding: const EdgeInsets.fromLTRB(32, 24, 12, 16), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), borderSide: const BorderSide( width: 0, style: BorderStyle.none, ), ), errorStyle: ThemeEndpoints.textFieldErrorTextStyle(), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), borderSide: const BorderSide( color: BrandColors.appLightRed, width: 2, ), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), borderSide: const BorderSide( color: BrandColors.appRed, width: 2, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), borderSide: const BorderSide( color: BrandColors.luckyLime, width: 2, ), ), prefixIconConstraints: (widget._prefixCurrencySymbol) ? const BoxConstraints(minWidth: 0, minHeight: 0) : null, prefixIcon: (widget._prefixCurrencySymbol) ? Container( margin: const EdgeInsets.all(13), child: Text( widget._currencySymbol!, style: ThemeEndpoints.textFieldCurrencySymbolTextStyle(), ), ) : null, suffixIcon: (widget._obscurePasswordOption) ? GestureDetector( onTap: () { setState(() { _obscurePassword = !_obscurePassword; }); }, child: Container( margin: const EdgeInsets.all(13), child: _obscurePassword ? ThemeEndpoints.textFieldPasswordNotVisible() : ThemeEndpoints.textFieldPasswordVisible(), ), ) : null, ), ), ); } }
表单代码
class SignInForm extends ConsumerWidget { final FocusNode emailFocusNode; final FocusNode passwordFocusNode; SignInForm({ Key? key, required this.emailFocusNode, required this.passwordFocusNode, }) : super(key: key); final TextEditingController _email = TextEditingController(); final TextEditingController _password = TextEditingController(); static final GlobalKey<FormState> _signInFormKey = GlobalKey<FormState>(); @override Widget build(BuildContext context, WidgetRef ref) { return AutofillGroup( child: Form( key: _signInFormKey, child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( TextContent.of(context).authSignInText, style: ThemeEndpoints.primaryHeader(), ), const SizedBox(height: 16.0), CustomFormField( controller: _email, onChanged: null, prefixCurrencySymbol: false, currencySymbol: null, autoFillHints: const [AutofillHints.email], blockSystemKeyboard: false, width: 90.w, focusNode: emailFocusNode, keyboardType: TextInputType.emailAddress, inputAction: TextInputAction.next, label: TextContent.of(context).authFormEmailText, textCapitalization: TextCapitalization.none, validator: (email) => FormValidationUtility.emailFormValidator( context, email, ), obscurePasswordOption: false, saveAutofillData: false, passwordCreationField: false, ), const SizedBox(height: 16.0), CustomFormField( controller: _password, onChanged: null, prefixCurrencySymbol: false, currencySymbol: null, autoFillHints: const [AutofillHints.password], blockSystemKeyboard: false, width: 90.w, focusNode: passwordFocusNode, keyboardType: TextInputType.visiblePassword, inputAction: TextInputAction.done, label: TextContent.of(context).authFormPasswordText, textCapitalization: TextCapitalization.none, validator: (password) => FormValidationUtility.passwordGeneralValidator( context, password, ), obscurePasswordOption: true, saveAutofillData: false, passwordCreationField: false, ), const SizedBox(height: 64.0), CustomButton( buttonText: TextContent.of(context).authSignInText, width: 50.w, onPressed: () async => _onSignInPressed( context, ref, [emailFocusNode, passwordFocusNode], ), ), const SizedBox(height: 16.0), const SignUpInkwell(), const SizedBox(height: 16.0), const SignInOptionsPulldown(), ], ), ), ); } // On pressed button execution Future<void> _onSignInPressed( BuildContext context, WidgetRef ref, List<FocusNode> focusNodes, ) async { for (FocusNode node in focusNodes) { node.unfocus(); } if (_signInFormKey.currentState!.validate()) { await ref.read(authenticationEndpoints).signIn( context: context, email: _email.text.trim(), password: _password.text.trim(), ); } } }
从代码和问题现象来看,核心是不必要的Widget重建触发了资源占用飙升,以下是针对性修复步骤:
1. 避免传递匿名函数作为参数
你在SignInForm的build方法中直接传递匿名validator函数,比如:
validator: (email) => FormValidationUtility.emailFormValidator(context, email),
每次build都会生成新的函数实例,导致CustomFormField认为参数变化,触发重建。改为在SignInForm中定义静态或成员方法:
String? _emailValidator(String email) => FormValidationUtility.emailFormValidator(context, email); String? _passwordValidator(String password) => FormValidationUtility.passwordGeneralValidator(context, password);
然后传递_emailValidator、_passwordValidator作为参数,而非匿名函数。
2. 缓存Theme样式实例
如果ThemeEndpoints中的样式方法(如textFieldTextStyle())每次调用都返回新的TextStyle实例,会导致TextFormField的style参数变化,触发重建。修改为返回缓存的静态实例:
class ThemeEndpoints { static final TextStyle _textFieldTextStyle = TextStyle(...); static TextStyle textFieldTextStyle() => _textFieldTextStyle; // 其他样式同理处理 }
3. 修正FocusNode的dispose逻辑
CustomFormField中的FocusNode是从父组件SignInForm传递过来的,应该由创建它的父组件负责dispose,移除_CustomFormFieldState中的widget._formFocusNode.dispose()代码,避免导致父组件使用已释放的FocusNode。
4. 缩小ConsumerWidget的监听范围
作为ConsumerWidget,SignInForm默认会在全局Provider变化时重建。如果不需要监听所有Provider,使用select缩小监听范围:
@override Widget build(BuildContext context, WidgetRef ref) { // 只监听登录相关的必要状态,而非整个authenticationEndpoints final authState = ref.watch(authenticationEndpoints.select((state) => state.loginState)); return AutofillGroup(...); }
5. 用const优化无状态Widget
给不需要动态变化的Widget(如SizedBox、OutlineInputBorder)加上const修饰,减少不必要的Widget实例创建。
6. 包裹RepaintBoundary隔离重绘
如果表单区域频繁重绘,用RepaintBoundary包裹表单,避免影响页面其他区域:
return RepaintBoundary( child: AutofillGroup( child: Form(...), ), );
按以上步骤调整后,可大幅减少不必要的组件重建,降低资源占用。
内容的提问来源于stack exchange,提问作者RobbB

