在 Flutter 开发中,表单输入(登录、注册、设置页)是高频场景。原生TextField存在样式配置繁琐、校验逻辑分散、交互反馈单一等问题。本文封装的CustomInputWidget整合 "统一样式 + 实时校验 + 输入格式化 + 交互反馈" 四大核心能力,支持手机号、密码、验证码等 10 + 场景,一行代码调用,覆盖 90%+ 表单需求,彻底减少重复编码!
一、核心优势(精准解决开发痛点)
✅ 样式统一:支持边框 / 下划线 / 无样式三种风格,颜色、圆角全局配置,无需重复编写样式代码✅ 实时校验:内置手机号、邮箱、密码等 6 大预设校验规则,支持自定义校验,错误即时反馈✅ 输入格式化:手机号 3-4-4 分隔、金额保留两位小数等自动处理,提升输入体验✅ 交互优化:密码可见切换、一键清除输入、图标点击事件,聚焦 / 错误状态自动高亮✅ 高扩展性:左侧图标、右侧自定义组件(验证码倒计时、密码眼睛)灵活嵌入,适配复杂场景✅ 深色模式适配:自动兼容亮色 / 深色主题,无需额外配置
二、核心配置速览(关键参数一目了然)
| 配置分类 | 核心参数 | 核心作用 |
|---|---|---|
| 必选配置 | controller、hintText |
输入控制器(外部管理生命周期)、占位提示文本 |
| 样式配置 | borderType、focusColor、borderRadius |
边框风格(边框 / 下划线 / 无)、聚焦高亮色、圆角半径(统一视觉风格) |
| 校验配置 | inputType、validator、autoValidate |
预设输入类型(手机号 / 密码等)、自定义校验规则、是否自动实时校验 |
| 交互配置 | isPassword、showClearBtn、onIconTap |
密码类型(自动管理可见性)、清除按钮显示、左侧图标点击事件 |
| 扩展配置 | prefixIcon、suffixWidget、formatter |
左侧图标、右侧自定义组件(倒计时等)、额外输入格式化器 |
| 适配配置 | adaptDarkMode、maxLines、enabled |
深色模式适配、输入框行数、是否启用输入 |
三、生产级完整代码(可直接复制,开箱即用)
dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// 输入框边框类型枚举(覆盖主流设计风格)
enum InputBorderType { outline, underline, none }
/// 预设输入类型枚举(覆盖高频表单场景)
enum InputType { normal, phone, email, password, code, idCard, number }
/// 通用表单输入组件(支持校验、格式化、自定义样式)
class CustomInputWidget extends StatefulWidget {
// 必选参数(核心依赖)
final TextEditingController controller; // 输入控制器(外部传入,需手动dispose)
final String hintText; // 占位提示文本
// 样式配置(统一视觉风格)
final InputBorderType borderType; // 边框风格(默认边框)
final TextStyle inputStyle; // 输入文本样式(默认16号黑色)
final TextStyle hintStyle; // 占位文本样式(默认16号灰色)
final Color focusColor; // 聚焦高亮色(默认蓝色)
final Color normalColor; // 普通状态色(默认灰色)
final Color errorColor; // 错误状态色(默认红色)
final double borderWidth; // 边框宽度(默认1px)
final double borderRadius; // 圆角半径(默认8px)
final EdgeInsetsGeometry padding; // 内边距(默认水平16、垂直14)
// 校验配置(实时反馈输入合法性)
final InputType inputType; // 预设输入类型(默认普通文本)
final String? errorText; // 外部传入错误提示(优先级高于内部校验)
final String? Function(String?)? validator; // 自定义校验规则(优先级高于预设)
final bool autoValidate; // 是否自动实时校验(默认true)
// 交互配置(提升用户操作体验)
final bool isPassword; // 是否为密码类型(默认false)
final bool showClearBtn; // 是否显示清除按钮(默认true)
final bool enabled; // 是否启用输入(默认true)
final bool readOnly; // 是否只读(默认false)
final int maxLength; // 最大输入长度(默认100)
final int maxLines; // 最大输入行数(默认1行)
// 扩展配置(适配复杂场景)
final Widget? prefixIcon; // 左侧图标(支持点击事件)
final VoidCallback? onIconTap; // 左侧图标点击回调
final Widget? suffixWidget; // 右侧自定义组件(如倒计时、密码眼睛)
final List<TextInputFormatter>? formatter; // 额外输入格式化器
final ValueChanged<String>? onChanged; // 输入变化回调
final VoidCallback? onEditingComplete; // 输入完成回调(回车触发)
// 适配配置(兼容多主题)
final bool adaptDarkMode; // 是否适配深色模式(默认true)
const CustomInputWidget({
super.key,
required this.controller,
required this.hintText,
this.borderType = InputBorderType.outline,
this.inputStyle = const TextStyle(fontSize: 16, color: Colors.black87),
this.hintStyle = const TextStyle(fontSize: 16, color: Colors.grey),
this.focusColor = Colors.blue,
this.normalColor = Colors.grey,
this.errorColor = Colors.redAccent,
this.borderWidth = 1.0,
this.borderRadius = 8.0,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
this.inputType = InputType.normal,
this.errorText,
this.validator,
this.autoValidate = true,
this.isPassword = false,
this.showClearBtn = true,
this.enabled = true,
this.readOnly = false,
this.maxLength = 100,
this.maxLines = 1,
this.prefixIcon,
this.onIconTap,
this.suffixWidget,
this.formatter,
this.onChanged,
this.onEditingComplete,
this.adaptDarkMode = true,
});
@override
State<CustomInputWidget> createState() => _CustomInputWidgetState();
}
class _CustomInputWidgetState extends State<CustomInputWidget> {
late FocusNode _focusNode; // 聚焦状态管理
bool _isFocused = false; // 是否聚焦
bool _showPassword = false; // 密码可见性状态
bool _showClearBtn = false; // 清除按钮显示状态
String? _currentErrorText; // 当前错误提示(内部校验+外部传入)
/// 预设输入格式化器(按输入类型自动适配)
List<TextInputFormatter> get _defaultFormatters {
switch (widget.inputType) {
case InputType.phone:
// 手机号:仅允许数字+3-4-4分隔+最大13位(含分隔符)
return [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(13),
_PhoneInputFormatter()
];
case InputType.code:
// 验证码:仅允许数字+最大6位
return [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)];
case InputType.idCard:
// 身份证:允许数字+Xx+最大18位
return [FilteringTextInputFormatter.allow(RegExp(r'[0-9Xx]')), LengthLimitingTextInputFormatter(18)];
case InputType.number:
// 数字:保留两位小数+自动补0
return [_NumberInputFormatter()];
default:
// 普通文本:仅限制最大长度
return [LengthLimitingTextInputFormatter(widget.maxLength)];
}
}
/// 预设校验规则(按输入类型自动校验)
String? _defaultValidator(String? value) {
if (value == null || value.trim().isEmpty) {
return "请输入${_getInputTypeName()}";
}
switch (widget.inputType) {
case InputType.phone:
// 手机号:纯数字长度11位
final purePhone = value.replaceAll(RegExp(r'\D'), '');
return purePhone.length != 11 ? "请输入11位有效手机号" : null;
case InputType.email:
// 邮箱:符合邮箱正则
final emailReg = RegExp(r'^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$');
return emailReg.hasMatch(value) ? null : "请输入有效邮箱地址";
case InputType.password:
// 密码:6-20位+含字母和数字
if (value.length < 6 || value.length > 20) return "密码长度为6-20位";
final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(value);
final hasNumber = RegExp(r'[0-9]').hasMatch(value);
return (hasLetter && hasNumber) ? null : "密码需包含字母和数字";
case InputType.code:
// 验证码:4-6位
return (value.length >= 4 && value.length <= 6) ? null : "请输入4-6位验证码";
case InputType.idCard:
// 身份证:18位+最后一位允许Xx
final idReg = RegExp(r'^\d{17}[\dXx]$');
return idReg.hasMatch(value) ? null : "请输入18位有效身份证号";
default:
return null;
}
}
/// 获取输入类型名称(用于错误提示)
String _getInputTypeName() {
switch (widget.inputType) {
case InputType.phone: return "手机号";
case InputType.email: return "邮箱";
case InputType.password: return "密码";
case InputType.code: return "验证码";
case InputType.idCard: return "身份证号";
case InputType.number: return "数字";
default: return "内容";
}
}
/// 输入校验(内部预设+外部自定义)
void _validateInput(String value) {
if (!widget.autoValidate) return;
// 外部错误提示优先级最高
if (widget.errorText != null) {
setState(() => _currentErrorText = widget.errorText);
return;
}
// 自定义校验优先级高于预设
String? error = widget.validator?.call(value) ?? _defaultValidator(value);
setState(() => _currentErrorText = error);
}
@override
void initState() {
super.initState();
// 初始化聚焦节点+监听聚焦状态
_focusNode = FocusNode()..addListener(() => setState(() => _isFocused = _focusNode.hasFocus));
// 监听输入变化:更新清除按钮状态+触发校验+外部回调
widget.controller.addListener(() {
final value = widget.controller.text;
setState(() => _showClearBtn = widget.showClearBtn && value.isNotEmpty && !widget.isPassword);
_validateInput(value);
widget.onChanged?.call(value);
});
// 初始校验
_validateInput(widget.controller.text);
}
@override
void didUpdateWidget(covariant CustomInputWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// 外部错误提示变化时更新
if (widget.errorText != oldWidget.errorText) {
setState(() => _currentErrorText = widget.errorText);
}
}
@override
void dispose() {
_focusNode.dispose(); // 释放聚焦节点,避免内存泄漏
super.dispose();
}
/// 深色模式颜色适配(统一处理颜色切换)
Color _adaptDarkMode(Color lightColor, Color darkColor) {
if (!widget.adaptDarkMode) return lightColor;
return MediaQuery.platformBrightnessOf(context) == Brightness.dark ? darkColor : lightColor;
}
/// 构建输入框边框(根据状态切换颜色)
InputBorder _buildBorder() {
// 颜色优先级:错误状态>聚焦状态>普通状态,同时适配深色模式
final currentColor = _currentErrorText != null
? _adaptDarkMode(widget.errorColor, Colors.red[400]!)
: (_isFocused ? _adaptDarkMode(widget.focusColor, Colors.blueAccent) : _adaptDarkMode(widget.normalColor, Colors.grey[600]!));
switch (widget.borderType) {
case InputBorderType.outline:
return OutlineInputBorder(
borderSide: BorderSide(color: currentColor, width: widget.borderWidth),
borderRadius: BorderRadius.circular(widget.borderRadius),
);
case InputBorderType.underline:
return UnderlineInputBorder(
borderSide: BorderSide(color: currentColor, width: widget.borderWidth),
);
case InputBorderType.none:
return InputBorder.none;
}
}
/// 构建右侧组件(密码眼睛/清除按钮/自定义组件)
Widget? _buildSuffixWidget() {
// 优先显示外部自定义右侧组件
if (widget.suffixWidget != null) return widget.suffixWidget;
// 密码类型:显示密码可见性切换按钮
if (widget.isPassword) {
return IconButton(
icon: Icon(
_showPassword ? Icons.visibility : Icons.visibility_off,
size: 20,
color: _adaptDarkMode(widget.normalColor, Colors.grey[400]!),
),
onPressed: () => setState(() => _showPassword = !_showPassword),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40),
);
}
// 普通文本:显示清除按钮(输入不为空时)
if (_showClearBtn) {
return IconButton(
icon: Icon(
Icons.clear,
size: 20,
color: _adaptDarkMode(widget.normalColor, Colors.grey[400]!),
),
onPressed: () => widget.controller.clear(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40),
);
}
return null;
}
/// 获取键盘类型(按输入类型自动适配)
TextInputType _getKeyboardType() {
switch (widget.inputType) {
case InputType.phone:
case InputType.code:
return TextInputType.phone;
case InputType.email:
return TextInputType.emailAddress;
case InputType.number:
return TextInputType.numberWithOptions(decimal: true);
default:
return TextInputType.text;
}
}
@override
Widget build(BuildContext context) {
// 合并格式化器:预设格式化器+外部自定义格式化器
final formatters = [..._defaultFormatters, if (widget.formatter != null) ...widget.formatter!];
// 适配深色模式的文本样式
final adaptedInputStyle = widget.inputStyle.copyWith(
color: _adaptDarkMode(widget.inputStyle.color!, Colors.white70),
);
final adaptedHintStyle = widget.hintStyle.copyWith(
color: _adaptDarkMode(widget.hintStyle.color!, Colors.grey[400]!),
);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: widget.controller,
focusNode: _focusNode,
style: adaptedInputStyle,
hintText: widget.hintText,
hintStyle: adaptedHintStyle,
obscureText: widget.isPassword && !_showPassword, // 密码可见性控制
enabled: widget.enabled,
readOnly: widget.readOnly,
maxLines: widget.maxLines,
inputFormatters: formatters,
keyboardType: _getKeyboardType(),
decoration: InputDecoration(
// 左侧图标(支持点击)
prefixIcon: widget.prefixIcon != null
? GestureDetector(
onTap: widget.onIconTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: widget.prefixIcon,
),
)
: null,
// 右侧组件(密码眼睛/清除按钮/自定义)
suffixIcon: _buildSuffixWidget(),
// 边框样式(统一构建,适配所有状态)
border: _buildBorder(),
focusedBorder: _buildBorder(),
enabledBorder: _buildBorder(),
disabledBorder: _buildBorder(),
errorBorder: _buildBorder(),
focusedErrorBorder: _buildBorder(),
// 内边距与密度
contentPadding: widget.padding,
isDense: true,
// 错误提示
errorText: _currentErrorText,
errorStyle: TextStyle(
fontSize: 12,
color: _adaptDarkMode(widget.errorColor, Colors.red[400]!),
),
errorMaxLines: 2,
),
onEditingComplete: widget.onEditingComplete,
),
],
);
}
}
/// 手机号格式化器(3-4-4分隔,如138-1234-5678)
class _PhoneInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final text = newValue.text.replaceAll(RegExp(r'\D'), ''); // 过滤非数字字符
final buffer = StringBuffer();
for (int i = 0; i < text.length; i++) {
buffer.write(text[i]);
// 第3位和第7位后添加分隔符(避免末尾添加)
if ((i == 2 || i == 6) && i != text.length - 1) {
buffer.write('-');
}
}
final value = buffer.toString();
return newValue.copyWith(
text: value,
selection: TextSelection.collapsed(offset: value.length),
);
}
}
/// 数字格式化器(保留两位小数,自动补0,如0.01、100.00)
class _NumberInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
String value = newValue.text;
// 仅允许数字和小数点
if (!RegExp(r'^[\d.]*$').hasMatch(value)) return oldValue;
// 禁止多个小数点
if (value.contains('.') && value.indexOf('.') != value.lastIndexOf('.')) return oldValue;
// 保留两位小数
if (value.contains('.')) {
final parts = value.split('.');
if (parts.length > 1 && parts[1].length > 2) {
value = '${parts[0]}.${parts[1].substring(0, 2)}';
}
}
// 开头补0(如.12→0.12)
if (value.startsWith('.')) value = '0$value';
return newValue.copyWith(
text: value,
selection: TextSelection.collapsed(offset: value.length),
);
}
}
四、四大高频场景落地示例(直接复制可用)
场景 1:登录页(手机号 + 密码 + 登录按钮联动)
适用场景:APP 登录页核心表单,支持手机号格式化、密码强度校验、登录按钮状态联动
dart
class LoginPage extends StatefulWidget {
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _pwdController = TextEditingController();
bool _isLoginEnabled = false; // 登录按钮是否可用
@override
void initState() {
super.initState();
// 监听输入变化,联动登录按钮状态
_phoneController.addListener(_checkLoginEnable);
_pwdController.addListener(_checkLoginEnable);
}
/// 校验输入合法性,控制登录按钮状态
void _checkLoginEnable() {
final phoneValid = _phoneController.text.replaceAll(RegExp(r'\D'), '').length == 11;
final pwdValid = _pwdController.text.length >= 6 && _pwdController.text.length <= 20;
setState(() => _isLoginEnabled = phoneValid && pwdValid);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("登录")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
children: [
// 手机号输入(下划线风格+图标)
CustomInputWidget(
controller: _phoneController,
hintText: "请输入手机号",
inputType: InputType.phone,
prefixIcon: const Icon(Icons.phone, color: Colors.blue),
borderType: InputBorderType.underline,
focusColor: Colors.blueAccent,
),
const SizedBox(height: 20),
// 密码输入(下划线风格+密码眼睛)
CustomInputWidget(
controller: _pwdController,
hintText: "请输入密码",
inputType: InputType.password,
isPassword: true,
prefixIcon: const Icon(Icons.lock, color: Colors.blue),
borderType: InputBorderType.underline,
focusColor: Colors.blueAccent,
// 自定义密码校验规则(示例:需含特殊字符)
validator: (value) {
if (value == null || value.isEmpty) return "请输入密码";
if (value.length < 6 || value.length > 20) return "密码长度为6-20位";
final hasSpecialChar = RegExp(r'[!@#$%^&*()]').hasMatch(value);
if (!hasSpecialChar) return "密码需包含字母、数字和特殊字符";
return null;
},
),
const SizedBox(height: 40),
// 登录按钮(联动输入状态)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoginEnabled
? () => debugPrint("执行登录:手机号=${_phoneController.text},密码=${_pwdController.text}")
: null,
style: ElevatedButton.styleFrom(
backgroundColor: _isLoginEnabled ? Colors.blue : Colors.grey[300],
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text("登录", style: TextStyle(fontSize: 16)),
),
),
],
),
),
);
}
@override
void dispose() {
// 释放控制器,避免内存泄漏
_phoneController.dispose();
_pwdController.dispose();
super.dispose();
}
}
场景 2:验证码输入(带倒计时 + 自定义右侧组件)
适用场景:手机号验证、找回密码、注册验证等需要短信验证码的场景
dart
class CodeVerificationPage extends StatefulWidget {
@override
State<CodeVerificationPage> createState() => _CodeVerificationPageState();
}
class _CodeVerificationPageState extends State<CodeVerificationPage> {
final TextEditingController _codeController = TextEditingController();
bool _isCounting = false; // 是否正在倒计时
int _countDown = 60; // 倒计时时长(60秒)
late Timer? _countDownTimer; // 倒计时定时器
/// 发送验证码(触发倒计时)
void _sendCode() {
setState(() => _isCounting = true);
// 模拟发送验证码接口调用
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("验证码已发送至手机号")));
// 启动倒计时
_countDownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
_countDown--;
if (_countDown <= 0) {
_isCounting = false;
_countDown = 60;
timer.cancel(); // 停止定时器
}
});
});
}
@override
void dispose() {
_codeController.dispose();
_countDownTimer?.cancel(); // 释放定时器
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("手机号验证")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
children: [
// 验证码输入(右侧倒计时按钮)
CustomInputWidget(
controller: _codeController,
hintText: "请输入4位验证码",
inputType: InputType.code,
prefixIcon: const Icon(Icons.sms, color: Colors.orangeAccent),
borderType: InputBorderType.outline,
borderRadius: 12,
focusColor: Colors.orangeAccent,
// 右侧自定义倒计时组件
suffixWidget: Padding(
padding: const EdgeInsets.only(right: 8),
child: TextButton(
onPressed: _isCounting ? null : _sendCode,
style: TextButton.styleFrom(
minimumSize: const Size(80, 40),
padding: EdgeInsets.zero,
),
child: Text(
_isCounting ? "$_countDown秒后重发" : "获取验证码",
style: TextStyle(
color: _isCounting ? Colors.grey : Colors.orangeAccent,
fontSize: 14,
),
),
),
),
),
const SizedBox(height: 30),
// 验证按钮(输入4位验证码后可用)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _codeController.text.length == 4
? () {
// 模拟验证逻辑
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("验证成功")));
Navigator.pop(context);
}
: null,
child: const Text("确认验证"),
),
),
],
),
),
);
}
}
场景 3:金额输入(带格式化 + 快捷金额选择)
适用场景:充值、支付、转账等需要输入金额的场景,支持快捷选择预设金额
dart
class RechargePage extends StatefulWidget {
@override
State<RechargePage> createState() => _RechargePageState();
}
class _RechargePageState extends State<RechargePage> {
final TextEditingController _amountController = TextEditingController();
final double _maxAmount = 10000.0; // 最大充值金额
/// 选择预设金额
void _selectPresetAmount(double amount) {
_amountController.text = amount.toString();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("余额充值")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("充值金额", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 12),
// 金额输入(数字格式化+图标)
CustomInputWidget(
controller: _amountController,
hintText: "最多可充值10000元",
inputType: InputType.number,
prefixIcon: const Icon(Icons.money, color: Colors.green),
borderType: InputBorderType.outline,
borderRadius: 12,
focusColor: Colors.green,
// 自定义金额校验规则
validator: (value) {
if (value == null || value.isEmpty) return "请输入充值金额";
final amount = double.tryParse(value) ?? 0;
if (amount < 0.01) return "最小充值金额0.01元";
if (amount > _maxAmount) return "最大充值金额10000元";
return null;
},
),
const SizedBox(height: 20),
// 预设金额快捷选择
const Text("常用金额", style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 8),
Wrap(
spacing: 12,
runSpacing: 12,
children: [100, 500, 1000, 2000, 5000, 10000]
.map((amount) => GestureDetector(
onTap: () => _selectPresetAmount(amount.toDouble()),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
color: _amountController.text == amount.toString() ? Colors.green[50] : Colors.white,
),
child: Text(
"¥$amount",
style: TextStyle(
color: _amountController.text == amount.toString() ? Colors.green : Colors.black87,
),
),
),
))
.toList(),
),
const Spacer(),
// 确认充值按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _amountController.text.isNotEmpty &&
double.parse(_amountController.text) >= 0.01 &&
double.parse(_amountController.text) <= _maxAmount
? () {
// 模拟充值逻辑
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("成功充值¥${_amountController.text}")),
);
Navigator.pop(context);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: const Text("确认充值", style: TextStyle(fontSize: 16)),
),
),
],
),
),
);
}
@override
void dispose() {
_amountController.dispose();
super.dispose();
}
}
场景 4:注册页(多表单组合 + 联动校验)
适用场景:用户注册页,包含手机号、验证码、密码、确认密码多字段联动
dart
class RegisterPage extends StatefulWidget {
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
final TextEditingController _pwdController = TextEditingController();
final TextEditingController _confirmPwdController = TextEditingController();
bool _isRegisterEnabled = false;
bool _isCounting = false;
int _countDown = 60;
@override
void initState() {
super.initState();
// 监听所有输入框变化,联动注册按钮状态
_phoneController.addListener(_checkRegisterEnable);
_codeController.addListener(_checkRegisterEnable);
_pwdController.addListener(_checkRegisterEnable);
_confirmPwdController.addListener(_checkRegisterEnable);
}
/// 校验所有字段合法性
void _checkRegisterEnable() {
final phoneValid = _phoneController.text.replaceAll(RegExp(r'\D'), '').length == 11;
final codeValid = _codeController.text.length == 4;
final pwdValid = _pwdController.text.length >= 6 && _pwdController.text.length <= 20;
final confirmPwdValid = _confirmPwdController.text == _pwdController.text;
setState(() => _isRegisterEnabled = phoneValid && codeValid && pwdValid && confirmPwdValid);
}
/// 发送验证码
void _sendCode() {
if (_phoneController.text.replaceAll(RegExp(r'\D'), '').length != 11) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("请输入有效手机号")));
return;
}
setState(() => _isCounting = true);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("验证码已发送")));
Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
_countDown--;
if (_countDown <= 0) {
_isCounting = false;
_countDown = 60;
timer.cancel();
}
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("用户注册")),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
children: [
CustomInputWidget(
controller: _phoneController,
hintText: "请输入手机号",
inputType: InputType.phone,
prefixIcon: const Icon(Icons.phone, color: Colors.blue),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: CustomInputWidget(
controller: _codeController,
hintText: "请输入验证码",
inputType: InputType.code,
prefixIcon: const Icon(Icons.sms, color: Colors.orange),
),
),
const SizedBox(width: 12),
TextButton(
onPressed: _isCounting ? null : _sendCode,
child: Text(_isCounting ? "$_countDown秒重发" : "获取验证码"),
),
],
),
const SizedBox(height: 16),
CustomInputWidget(
controller: _pwdController,
hintText: "请设置密码(6-20位,含字母和数字)",
inputType: InputType.password,
isPassword: true,
prefixIcon: const Icon(Icons.lock, color: Colors.grey),
),
const SizedBox(height: 16),
CustomInputWidget(
controller: _confirmPwdController,
hintText: "请确认密码",
inputType: InputType.password,
isPassword: true,
prefixIcon: const Icon(Icons.lock_outline, color: Colors.grey),
// 自定义确认密码校验
validator: (value) {
if (value == null || value.isEmpty) return "请确认密码";
if (value != _pwdController.text) return "两次密码输入不一致";
return null;
},
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isRegisterEnabled
? () => debugPrint("执行注册逻辑")
: null,
child: const Text("注册账号"),
),
),
],
),
),
);
}
@override
void dispose() {
_phoneController.dispose();
_codeController.dispose();
_pwdController.dispose();
_confirmPwdController.dispose();
super.dispose();
}
}
五、核心封装技巧(复用成熟设计思路)
- 分层校验设计:预设校验规则 + 外部自定义校验,自定义优先级更高,兼顾通用场景与个性化需求,减少重复编码。
- 输入格式化封装 :将手机号、金额等格式化逻辑封装为独立类,通过
TextInputFormatter统一接入,扩展新格式只需新增类。 - 状态联动管理:内部监听输入变化、聚焦状态、错误状态,自动切换 UI 展示(清除按钮、边框颜色、错误提示),无需外部手动管理。
- 插槽化扩展:支持左侧图标、右侧自定义组件,适配倒计时、密码眼睛等复杂场景,组件灵活性大幅提升。
- 深色模式统一适配 :通过
_adaptDarkMode方法统一处理颜色切换,所有可视化参数自动兼容亮色 / 深色主题,无需额外配置。 - 错误提示优先级:外部传入错误提示(如接口返回错误)优先级高于内部校验,支持业务错误与输入错误区分处理。
六、避坑指南(解决 90% 开发痛点)
- 控制器管理 :控制器需外部传入并手动
dispose,避免组件内部管理导致的内存泄漏;多表单场景建议用Form组件结合GlobalKey统一管理。 - 校验触发时机 :
autoValidate: false时,需手动调用_validateInput触发校验(如提交前);默认true时实时校验,适合即时反馈场景。 - 格式化冲突 :外部
formatter与预设格式化器合并,避免重复限制(如手机号已限制数字,无需额外添加FilteringTextInputFormatter.digitsOnly)。 - 密码状态管理 :
isPassword: true后,内部自动管理_showPassword状态,无需外部维护可见性变量,减少状态冗余。 - 错误提示显示:错误提示最大行数设为 2,避免文本过长导致 UI 错乱;外部错误提示需在接口请求失败后手动设置,请求成功后清空。
- 键盘类型适配 :输入类型与键盘类型自动关联(如手机号→电话键盘),无需手动设置
keyboardType,减少配置错误。
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。