Flutter 通用输入框封装实战:带校验 / 清除 / 密码切换的 InputWidget

导语

输入框是 Flutter 表单开发的核心组件,但原生 TextField 存在样式不统一、校验逻辑重复、功能单一等问题 ------ 每次开发都要配置边框、校验规则、清除按钮,效率低下且易出错。本文基于原封装思路,优化通用输入框 InputWidget,强化校验灵活性、样式适配性和交互体验,集成输入校验、一键清除、密码显隐切换、焦点管理等核心功能,实现 "一次封装,全域复用",适配手机号、邮箱、密码等 90% 以上的输入场景!

一、核心需求升级拆解

在原需求基础上优化扩展,覆盖更多实际业务场景:✅ 支持手机号 / 邮箱 / 密码 / 普通文本等多类型校验,支持自定义校验规则✅ 输入内容一键清除,密码框显隐切换,交互反馈更清晰✅ 自定义提示文本、图标、颜色、圆角,适配不同设计风格✅ 支持焦点监听、最大长度限制、必填校验,满足表单规范✅ 统一输入框样式,适配暗黑模式,避免重复配置✅ 错误提示灵活控制,支持外部传入和内部校验双重模式

二、完整代码实现(带优化注释)

dart

复制代码
import 'package:flutter/material.dart';

/// 输入框类型枚举(语义化区分输入场景)
enum InputType {
  text, // 普通文本
  password, // 密码(自动隐藏+显隐切换)
  phone, // 手机号(内置正则校验)
  email, // 邮箱(内置正则校验)
  custom, // 自定义校验(外部传入校验规则)
}

/// 通用输入框组件(增强版:支持多类型校验、自定义样式、灵活交互)
class InputWidget extends StatefulWidget {
  // 必选参数
  final String hintText; // 输入提示文本
  final InputType inputType; // 输入框类型
  final ValueChanged<String> onChanged; // 输入变化回调

  // 可选参数(带合理默认值,降低使用成本)
  final Icon? prefixIcon; // 左侧图标
  final String? initialValue; // 初始值
  final bool isRequired; // 是否必填(默认false)
  final String? errorText; // 外部传入错误提示(优先级高于内部校验)
  final double borderRadius; // 圆角(默认8px,符合设计规范)
  final int? maxLength; // 最大输入长度
  final FocusNode? focusNode; // 焦点节点(用于表单焦点管理)
  final ValueChanged<bool>? onFocusChanged; // 焦点变化回调
  final Color? borderColor; // 边框颜色(默认灰色)
  final Color? focusBorderColor; // 聚焦时边框颜色(默认蓝色)
  final Color? textColor; // 输入文本颜色(默认黑色)
  final double fontSize; // 字体大小(默认16px)
  final bool enableClear; // 是否启用清除按钮(默认true)
  final String? Function(String value)? customValidator; // 自定义校验规则(InputType.custom时生效)

  const InputWidget({
    super.key,
    required this.hintText,
    required this.inputType,
    required this.onChanged,
    this.prefixIcon,
    this.initialValue,
    this.isRequired = false,
    this.errorText,
    this.borderRadius = 8.0,
    this.maxLength,
    this.focusNode,
    this.onFocusChanged,
    this.borderColor = Colors.grey,
    this.focusBorderColor = Colors.blue,
    this.textColor = Colors.black87,
    this.fontSize = 16.0,
    this.enableClear = true,
    this.customValidator,
  });

  @override
  State<InputWidget> createState() => _InputWidgetState();
}

class _InputWidgetState extends State<InputWidget> {
  late TextEditingController _controller; // 输入控制器
  bool _obscureText = true; // 密码是否隐藏
  bool _showClear = false; // 是否显示清除按钮
  bool _hasFocus = false; // 是否获取焦点

  @override
  void initState() {
    super.initState();
    // 初始化控制器,设置初始值
    _controller = TextEditingController(text: widget.initialValue);
    // 监听输入变化,控制清除按钮显示
    _controller.addListener(_onInputChanged);
    // 监听焦点变化,触发回调
    widget.focusNode?.addListener(_onFocusChanged);
    // 初始状态判断:有初始值则显示清除按钮
    _showClear = _controller.text.isNotEmpty;
  }

  @override
  void dispose() {
    // 释放资源,避免内存泄漏
    _controller.removeListener(_onInputChanged);
    widget.focusNode?.removeListener(_onFocusChanged);
    _controller.dispose();
    super.dispose();
  }

  /// 输入内容变化监听
  void _onInputChanged() {
    setState(() {
      _showClear = widget.enableClear && _controller.text.isNotEmpty;
    });
    // 触发外部输入回调
    widget.onChanged(_controller.text);
  }

  /// 焦点变化监听
  void _onFocusChanged() {
    setState(() {
      _hasFocus = widget.focusNode?.hasFocus ?? false;
    });
    // 触发外部焦点回调
    widget.onFocusChanged?.call(_hasFocus);
  }

  /// 内置校验规则(手机号/邮箱)
  String? _internalValidate(String value) {
    // 必填校验(优先级最高)
    if (widget.isRequired && value.isEmpty) {
      return '此项不能为空';
    }
    // 手机号校验
    if (widget.inputType == InputType.phone && value.isNotEmpty) {
      if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
        return '请输入正确的手机号';
      }
    }
    // 邮箱校验
    if (widget.inputType == InputType.email && value.isNotEmpty) {
      if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
        return '请输入正确的邮箱';
      }
    }
    // 自定义校验(InputType.custom时生效)
    if (widget.inputType == InputType.custom && widget.customValidator != null) {
      return widget.customValidator!(value);
    }
    return null;
  }

  /// 统一校验入口(外部错误提示优先级高于内部校验)
  String? _validateInput() {
    // 外部传入errorText则直接使用,否则执行内部校验
    return widget.errorText ?? _internalValidate(_controller.text);
  }

  /// 构建右侧图标(清除按钮/密码显隐切换)
  Widget? _buildSuffixIcon() {
    // 密码框:显隐切换图标
    if (widget.inputType == InputType.password) {
      return IconButton(
        icon: Icon(
          _obscureText ? Icons.visibility_off : Icons.visibility,
          color: Colors.grey,
          size: 20,
        ),
        onPressed: () {
          setState(() {
            _obscureText = !_obscureText;
          });
        },
        padding: EdgeInsets.zero,
        constraints: const BoxConstraints(minWidth: 40),
      );
    }
    // 普通输入框:清除按钮(需启用且有输入内容)
    if (widget.enableClear && _showClear) {
      return IconButton(
        icon: const Icon(Icons.clear, color: Colors.grey, size: 20),
        onPressed: () {
          _controller.clear();
          _onInputChanged(); // 触发输入变化回调
        },
        padding: EdgeInsets.zero,
        constraints: const BoxConstraints(minWidth: 40),
      );
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    // 输入框样式配置(统一风格,支持自定义颜色)
    InputDecoration inputDecoration = InputDecoration(
      hintText: widget.hintText,
      hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: widget.fontSize),
      prefixIcon: widget.prefixIcon,
      suffixIcon: _buildSuffixIcon(),
      // 正常状态边框
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(widget.borderRadius),
        borderSide: BorderSide(color: widget.borderColor!, width: 1.0),
      ),
      // 未聚焦状态边框
      enabledBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(widget.borderRadius),
        borderSide: BorderSide(color: widget.borderColor!, width: 1.0),
      ),
      // 聚焦状态边框
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(widget.borderRadius),
        borderSide: BorderSide(color: widget.focusBorderColor!, width: 1.5),
      ),
      // 错误状态边框
      errorBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(widget.borderRadius),
        borderSide: const BorderSide(color: Colors.red, width: 1.0),
      ),
      // 聚焦错误状态边框
      focusedErrorBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(widget.borderRadius),
        borderSide: const BorderSide(color: Colors.red, width: 1.5),
      ),
      errorText: _validateInput(),
      errorStyle: const TextStyle(fontSize: 12, height: 1.2),
      counterText: '', // 隐藏默认长度提示
      contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
      filled: true,
      fillColor: Colors.transparent,
    );

    return TextField(
      controller: _controller,
      focusNode: widget.focusNode,
      obscureText: widget.inputType == InputType.password && _obscureText,
      // 根据输入类型配置键盘
      keyboardType: widget.inputType == InputType.phone
          ? TextInputType.phone
          : widget.inputType == InputType.email
              ? TextInputType.emailAddress
              : TextInputType.text,
      maxLength: widget.maxLength,
      style: TextStyle(
        fontSize: widget.fontSize,
        color: widget.textColor,
        height: 1.4,
      ),
      decoration: inputDecoration,
      // 禁止输入时的样式
      enabled: true,
      cursorColor: widget.focusBorderColor,
      cursorWidth: 2.0,
      cursorRadius: Radius.circular(1.0),
    );
  }
}

三、丰富使用示例(覆盖全场景)

dart

复制代码
class InputWidgetDemo extends StatelessWidget {
  const InputWidgetDemo({super.key});

  @override
  Widget build(BuildContext context) {
    // 焦点管理(用于表单提交时自动聚焦错误输入框)
    final phoneFocus = FocusNode();
    final passwordFocus = FocusNode();
    final customFocus = FocusNode();

    return Scaffold(
      appBar: AppBar(title: const Text('通用输入框示例')),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
        child: Column(
          children: [
            // 1. 手机号输入框(必填+内置校验)
            InputWidget(
              hintText: '请输入手机号',
              inputType: InputType.phone,
              prefixIcon: const Icon(Icons.phone, color: Colors.blue),
              isRequired: true,
              focusNode: phoneFocus,
              onFocusChanged: (hasFocus) {
                debugPrint('手机号输入框焦点:$hasFocus');
              },
              onChanged: (value) => debugPrint('手机号:$value'),
            ),
            const SizedBox(height: 12),

            // 2. 密码输入框(最大长度+显隐切换)
            InputWidget(
              hintText: '请输入密码(6-16位)',
              inputType: InputType.password,
              prefixIcon: const Icon(Icons.lock, color: Colors.grey),
              isRequired: true,
              maxLength: 16,
              focusNode: passwordFocus,
              borderColor: Colors.grey.shade300,
              focusBorderColor: Colors.green,
              onChanged: (value) => debugPrint('密码:$value'),
            ),
            const SizedBox(height: 12),

            // 3. 邮箱输入框(外部错误提示)
            InputWidget(
              hintText: '请输入邮箱',
              inputType: InputType.email,
              prefixIcon: const Icon(Icons.email, color: Colors.orange),
              errorText: '邮箱格式错误', // 外部传入错误提示(例如接口返回的错误)
              onChanged: (value) => debugPrint('邮箱:$value'),
            ),
            const SizedBox(height: 12),

            // 4. 普通文本输入框(禁用清除按钮)
            InputWidget(
              hintText: '请输入昵称',
              inputType: InputType.text,
              prefixIcon: const Icon(Icons.person, color: Colors.purple),
              enableClear: false,
              textColor: Colors.purple.shade700,
              onChanged: (value) => debugPrint('昵称:$value'),
            ),
            const SizedBox(height: 12),

            // 5. 自定义校验输入框(例如验证码)
            InputWidget(
              hintText: '请输入6位验证码',
              inputType: InputType.custom,
              prefixIcon: const Icon(Icons.verified_user, color: Colors.red),
              isRequired: true,
              maxLength: 6,
              focusNode: customFocus,
              customValidator: (value) {
                if (value.length != 6) {
                  return '请输入6位验证码';
                }
                if (!RegExp(r'^\d{6}$').hasMatch(value)) {
                  return '验证码仅支持数字';
                }
                return null;
              },
              onChanged: (value) => debugPrint('验证码:$value'),
            ),
          ],
        ),
      ),
    );
  }
}

四、优化亮点与核心技巧

1. 功能增强(解决原封装痛点)

  • 自定义校验支持 :新增InputType.custom类型,通过customValidator传入自定义校验规则,适配验证码、身份证等特殊场景。
  • 焦点管理优化 :支持焦点监听回调onFocusChanged,便于表单提交时自动聚焦错误输入框,提升用户体验。
  • 样式全自定义:可配置边框颜色、聚焦颜色、文本颜色等,适配不同设计风格,无需修改组件源码。
  • 错误提示灵活 :支持外部传入errorText(如接口返回错误)和内部校验双重模式,优先级清晰。

2. 交互体验升级

  • 清除按钮优化 :添加enableClear开关,支持禁用清除功能;图标尺寸和间距优化,视觉更协调。
  • 焦点样式强化:聚焦时边框加粗(1.5px),错误状态显示红色边框,视觉反馈更清晰。
  • 输入细节优化:配置光标颜色、宽度和圆角,统一输入文本行高,提升视觉一致性。

3. 封装核心方法论

  • 状态内聚:密码显隐、清除按钮显示、焦点状态等均封装在组件内部,外部无需关心实现细节,仅需通过参数配置。
  • 校验分层:内置常用校验规则(手机号 / 邮箱),同时支持自定义校验,兼顾通用性和灵活性。
  • 样式统一:固定内边距、圆角默认值,提供颜色配置接口,既保证样式一致性,又支持个性化调整。
  • 生命周期安全 :及时释放TextEditingController和焦点监听,避免内存泄漏;初始化时处理初始值,状态更稳定。

五、避坑指南(开发者必看)

常见问题 表现形式 解决方案
清除按钮不显示 输入内容后仍无清除图标 检查enableClear是否为true;确认输入框类型不是密码框(密码框优先显示显隐切换图标)
自定义校验不生效 输入内容后未触发自定义校验 确保输入框类型设为InputType.customcustomValidator参数已正确传入校验函数
焦点回调无响应 聚焦 / 失焦时未触发onFocusChanged 需传入自定义FocusNode;确保未在外部手动移除焦点监听
错误提示重叠 错误文本与输入内容距离过近 无需手动调整,组件已设置errorStyleheight属性,保证间距合理
密码框显隐切换无效 点击图标后密码仍未显示 / 隐藏 确认输入框类型为InputType.password;未手动设置obscureText属性覆盖组件内部状态

总结

优化后的 InputWidget 不仅保留了原封装的核心功能,还通过自定义校验、焦点管理、样式适配等增强,覆盖了更多实际业务场景。其核心优势在于 "通用性强、配置灵活、体验优秀"------ 既减少了重复代码,又保证了样式和交互的一致性,是 Flutter 表单开发的必备组件。

相关推荐
wearegogog12314 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars14 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤14 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·14 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°14 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
qq_4198540515 小时前
CSS动效
前端·javascript·css
烛阴15 小时前
3D字体TextGeometry
前端·webgl·three.js
桜吹雪15 小时前
markstream-vue实战踩坑笔记
前端
云诗卡达16 小时前
Flutter安卓APP接入极光推送和本地通知
android·flutter
南村群童欺我老无力.16 小时前
Flutter应用鸿蒙迁移实战:性能优化与渐进式迁移指南
javascript·flutter·ci/cd·华为·性能优化·typescript·harmonyos