导语
输入框是 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.custom;customValidator参数已正确传入校验函数 |
| 焦点回调无响应 | 聚焦 / 失焦时未触发onFocusChanged |
需传入自定义FocusNode;确保未在外部手动移除焦点监听 |
| 错误提示重叠 | 错误文本与输入内容距离过近 | 无需手动调整,组件已设置errorStyle的height属性,保证间距合理 |
| 密码框显隐切换无效 | 点击图标后密码仍未显示 / 隐藏 | 确认输入框类型为InputType.password;未手动设置obscureText属性覆盖组件内部状态 |
总结
优化后的 InputWidget 不仅保留了原封装的核心功能,还通过自定义校验、焦点管理、样式适配等增强,覆盖了更多实际业务场景。其核心优势在于 "通用性强、配置灵活、体验优秀"------ 既减少了重复代码,又保证了样式和交互的一致性,是 Flutter 表单开发的必备组件。