You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter表单提交后自定义TextField验证状态不更新求助

Flutter自定义TextField提交后错误状态不更新的解决方案

问题根源

  1. 表单提交后,Form组件会保留字段的错误状态,但自定义组件内部的_isValid、_isTouched状态未与Form的验证流程同步
  2. 当前逻辑中,仅在获取焦点时重置_isTouched,但表单提交后用户可能直接修改内容不触发焦点变化,导致错误状态持续显示
  3. 输入变化时的验证逻辑未考虑表单已提交的场景,无法及时清除错误标记

修复方案

以下是修改后的完整代码,核心调整点包括:

  • 新增_formSubmitted变量跟踪表单提交状态
  • 优化输入、焦点变化时的验证逻辑,确保表单提交后输入修正能实时更新错误状态
  • 同步外部自定义validator的错误状态到组件内部
import 'package:flutter/material.dart';

class CustomTextField extends StatefulWidget {
  final String label;
  final TextEditingController? controller;
  final String? initialValue;
  final String? Function(String?)? validator;
  final void Function(String)? onChanged;
  final String inputType; // 'text', 'email', 'phone', etc.
  final bool isMandatory;

  const CustomTextField({
    super.key,
    required this.label,
    this.controller,
    this.initialValue,
    this.validator,
    this.onChanged,
    this.inputType = 'text',
    this.isMandatory = false,
  });

  @override
  _CustomTextFieldState createState() => _CustomTextFieldState();
}

class _CustomTextFieldState extends State<CustomTextField> {
  bool _isValid = true;
  bool _isTouched = false;
  bool _formSubmitted = false; // 新增:跟踪表单是否提交过
  late FocusNode _focusNode;

  @override
  void initState() {
    super.initState();
    _focusNode = FocusNode();
    _focusNode.addListener(_onFocusChange);
  }

  @override
  void dispose() {
    _focusNode.removeListener(_onFocusChange);
    _focusNode.dispose();
    super.dispose();
  }

  // 供父组件调用:标记表单已提交
  void markFormSubmitted() {
    setState(() {
      _formSubmitted = true;
      _validate(widget.controller?.text ?? '');
    });
  }

  void _onFocusChange() {
    if (_focusNode.hasFocus) {
      // 仅在表单未提交时,获取焦点才重置状态
      if (!_formSubmitted) {
        setState(() {
          _isTouched = false;
          _isValid = true;
        });
      }
    } else {
      _validate(widget.controller?.text ?? '');
    }
  }

  void _validate(String value) {
    setState(() {
      if (widget.isMandatory) {
        switch (widget.inputType) {
          case 'email':
            _isValid = _isValidEmail(value);
            break;
          case 'phone':
            _isValid = _isValidPhone(value);
            break;
          case 'number':
            _isValid = _isValidNumber(value);
            break;
          default:
            _isValid = value.isNotEmpty;
        }
      } else {
        _isValid = true;
      }
      // 表单提交后,输入变化直接标记为已触碰
      if (_formSubmitted) _isTouched = true;
    });
  }

  bool _isValidEmail(String email) {
    final regex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
    return regex.hasMatch(email);
  }

  bool _isValidPhone(String phone) {
    final regex = RegExp(r'^\+?[0-9]{10,15}$');
    return regex.hasMatch(phone);
  }

  bool _isValidNumber(String number) {
    final regex = RegExp(r'^\d+(\.\d{1,2})?$');
    return regex.hasMatch(number);
  }

  bool isValid() {
    return !_isTouched || _isValid;
  }

  String? _internalValidator(String? value) {
    _validate(value ?? '');
    // 仅在已触碰或表单已提交时,才返回错误提示
    if (!_isValid && (_isTouched || _formSubmitted)) {
      switch (widget.inputType) {
        case 'email':
          return 'Inserisci un indirizzo email valido.';
        case 'phone':
          return 'Inserisci un numero di telefono valido.';
        case 'number':
          return 'Inserisci un numero valido.';
        default:
          return 'Il campo è obbligatorio.';
      }
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        TextFormField(
          controller: widget.controller,
          focusNode: _focusNode,
          onChanged: (value) {
            _validate(value);
            widget.onChanged?.call(value);
          },
          keyboardType: _getKeyboardType(widget.inputType),
          decoration: InputDecoration(
            labelText: widget.label,
            labelStyle: const TextStyle(color: Colors.black),
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8),
              borderSide: BorderSide(color: Colors.grey.shade300),
            ),
            enabledBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8),
              borderSide: BorderSide(
                color: (widget.isMandatory && !_isValid && (_isTouched || _formSubmitted))
                    ? Colors.red
                    : Colors.grey.shade300,
              ),
            ),
            focusedBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8),
              borderSide: BorderSide(
                color: (widget.isMandatory && !_isValid && (_isTouched || _formSubmitted))
                    ? Colors.red
                    : Colors.blue,
                width: 2,
              ),
            ),
            contentPadding:
                const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
            // 显示错误提示
            errorText: (!_isValid && (_isTouched || _formSubmitted))
                ? _internalValidator(widget.controller?.text)
                : null,
          ),
          validator: (value) {
            if (widget.validator != null) {
              final externalError = widget.validator!(value);
              setState(() {
                _isValid = externalError == null;
                if (externalError != null) _isTouched = true;
              });
              return externalError;
            }
            return _internalValidator(value);
          },
        ),
        const SizedBox(height: 4),
      ],
    );
  }

  TextInputType _getKeyboardType(String type) {
    switch (type) {
      case 'email':
        return TextInputType.emailAddress;
      case 'phone':
        return TextInputType.phone;
      case 'number':
        return TextInputType.number;
      default:
        return TextInputType.text;
    }
  }
}

父组件使用示例

在表单提交时,调用每个CustomTextField的markFormSubmitted方法触发全局验证:

final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final GlobalKey<_CustomTextFieldState> _emailFieldKey = GlobalKey();

void _submitForm() {
  // 标记所有字段为表单已提交状态
  _emailFieldKey.currentState?.markFormSubmitted();
  // 其他字段同理

  if (_formKey.currentState!.validate()) {
    // 执行表单提交逻辑
  }
}

内容的提问来源于stack exchange,提问作者sai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 00:03:12