Flutter for OpenHarmony 实战之基础组件:第八篇 TextField 输入框与表单
用户输入是 App 交互的重要环节。本文作为基础组件系列的终章,将深入解析 Flutter 中的 TextField 和 TextFormField,涵盖内容获取、表单校验、键盘遮挡优化以及如何封装一套鸿蒙风格的通用输入组件。

Flutter for OpenHarmony 实战之基础组件:第八篇 TextField 输入框与表单
前言
从登录注册到搜索商品,输入框无处不在。
相比于展示类组件,TextField 明显复杂得多。你需要管理光标、处理软键盘弹起、实时监听内容变化、校验格式合法性…
本文你将学到:
- 控制器 (Controller) 的生命周期管理
- 实现“点击眼睛显示/隐藏密码”
- Form 表单的一键校验技巧
- 解决“键盘遮挡输入框”的经典方案
- 实战:封装现代化的登录输入组件
一、TextField 基础用法

1.1 最简单的输入框
TextField(
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入手机号/邮箱',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(), // 边框样式
),
onChanged: (text) {
print('当前输入: $text');
},
)
1.2 获取与控制输入 (TextEditingController)
想要获取输入框的内容,或者设置默认值,必须使用 TextEditingController。
class LoginDemo extends StatefulWidget {
const LoginDemo({super.key});
State<LoginDemo> createState() => _LoginDemoState();
}
class _LoginDemoState extends State<LoginDemo> {
// 1. 创建控制器
final TextEditingController _usernameController = TextEditingController();
void initState() {
super.initState();
// 2. 设置默认值 (可选)
_usernameController.text = "admin";
}
void dispose() {
// 3. ⭐️ 务必销毁,防止内存泄漏
_usernameController.dispose();
super.dispose();
}
void _login() {
// 4. 获取内容
final username = _usernameController.text;
print('登录用户名: $username');
}
Widget build(BuildContext context) {
return TextField(
controller: _usernameController, // 绑定
);
}
}
1.3 密码框与显示/隐藏
通过 obscureText 属性控制是否隐藏内容(变为星号或圆点)。
bool _isObscure = true;
TextField(
obscureText: _isObscure, // 控制密码显示
decoration: InputDecoration(
labelText: '密码',
// 后缀图标:眼睛按钮
suffixIcon: IconButton(
icon: Icon(_isObscure ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
),
)
二、Form 表单与校验
如果一个页面有多个输入框(如注册页),一个个去判断太麻烦了。Flutter 提供了 Form 和 TextFormField 来批量管理。

2.1 核心组件
- Form: 容器,通过
GlobalKey管理状态。 - TextFormField: 也就是
TextField的加强版,增加了validator和onSaved回调。
2.2 实战:带校验的注册表单
class RegisterForm extends StatefulWidget {
const RegisterForm({super.key});
State<RegisterForm> createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
// 1. 创建 GlobalKey
final _formKey = GlobalKey<FormState>();
String _email = '';
String _password = '';
void _submit() {
// 2. 调用 validate() 触发所有子项校验
if (_formKey.currentState!.validate()) {
// 3. 校验通过,保存数据
_formKey.currentState!.save();
print('注册信息: $_email, $_password');
// TODO: 发起注册请求
}
}
Widget build(BuildContext context) {
return Form(
key: _formKey, // 绑定 Key
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: '邮箱'),
// 校验逻辑
validator: (value) {
if (value == null || value.isEmpty) {
return '邮箱不能为空';
}
if (!value.contains('@')) {
return '请输入有效的邮箱地址';
}
return null; // 返回 null 表示通过
},
onSaved: (val) => _email = val!,
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(labelText: '密码'),
validator: (value) {
if (value == null || value.length < 6) {
return '密码长度不能少于6位';
}
return null;
},
onSaved: (val) => _password = val!,
obscureText: true,
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _submit,
child: const Text('立即注册'),
),
],
),
);
}
}
三、常见问题解决方案
3.1 键盘遮挡输入框

当在底部输入时,软键盘弹起会盖住输入框。
方案 A: Scaffold(resizeToAvoidBottomInset: true) (默认开启)。这会让 Body 自动变矮。
方案 B: 将页面包裹在 SingleChildScrollView 或 ListView 中。当键盘弹起,页面可滚动。
import 'package:flutter/material.dart';
class KeyboardOcclusionPage extends StatefulWidget {
const KeyboardOcclusionPage({super.key});
State<KeyboardOcclusionPage> createState() => _KeyboardOcclusionPageState();
}
class _KeyboardOcclusionPageState extends State<KeyboardOcclusionPage> {
bool _useFix = false; // 是否开启修复方案
Widget build(BuildContext context) {
return Scaffold(
// 核心差异点 1: 反例时关闭自动避让,模拟遮挡效果
// resizeToAvoidBottomInset 默认为 true。
// 设为 false 时,键盘弹起不仅不会把页面顶上去,还会直接盖住底部内容。
resizeToAvoidBottomInset: _useFix,
appBar: AppBar(
title: const Text('场景1:键盘遮挡问题'),
backgroundColor: _useFix ? Colors.green[100] : Colors.red[100],
actions: [
Row(
children: [
const Text('启用修复'),
Switch(
value: _useFix,
onChanged: (v) {
FocusScope.of(context).unfocus(); // 切换时先收起键盘
setState(() => _useFix = v);
},
),
],
),
const SizedBox(width: 12),
],
),
// 核心差异点 2: 正例使用 SingleChildScrollView
body: _useFix ? _buildScrollView() : _buildFixedColumn(),
);
}
// 反例:固定布局,无滚动,且 resizeToAvoidBottomInset = false
Widget _buildFixedColumn() {
return Container(
width: double.infinity,
color: Colors.red[50],
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.end, // 把内容顶到底部,确保键盘能遮挡它
children: [
const Spacer(),
const Icon(Icons.warning_amber_rounded,
size: 80, color: Colors.orange),
const SizedBox(height: 16),
const Text(
'【反面教材】\n\n1. resizeToAvoidBottomInset = false\n2. 没有 ScrollView\n\n👉 点击下方输入框,键盘将弹起并直接无情地【遮挡】住它。\n你看不到正在输入什么。',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold, color: Colors.red),
),
const SizedBox(height: 50),
const TextField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
labelText: '我是底部的输入框 (一点就废)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 30), // 留点底部边距
],
),
);
}
// 正例:滚动视图 + 自动避让
Widget _buildScrollView() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 模拟很多内容撑开高度
...List.generate(
3,
(i) => Container(
height: 150,
margin: const EdgeInsets.only(bottom: 16),
color: Colors.blue[100 * (i % 3 + 1)],
alignment: Alignment.center,
child: Text('占位内容 $i'),
)),
const SizedBox(height: 20),
const Text(
'【正面教材】\n\n1. resizeToAvoidBottomInset = true (默认)\n2. 包裹 SingleChildScrollView\n\n👉 点击下方输入框,页面会自动顶起,\n且你可以自由滑动查看所有内容。',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.green, fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 20),
const TextField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
labelText: '安全输入框 (自动避让)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 300), // 故意加长底部,测试滚动
],
),
);
}
}
3.2 点击空白处收起键盘
这在 iOS 和鸿蒙上是符合用户直觉的体验。
import 'package:flutter/material.dart';
class KeyboardDismissPage extends StatefulWidget {
const KeyboardDismissPage({super.key});
State<KeyboardDismissPage> createState() => _KeyboardDismissPageState();
}
class _KeyboardDismissPageState extends State<KeyboardDismissPage> {
bool _enableDismiss = false;
Widget build(BuildContext context) {
// 构造内部页面内容
Widget bodyContent = Scaffold(
appBar: AppBar(
title: const Text('场景2:点击收起键盘'),
backgroundColor: _enableDismiss ? Colors.green[100] : Colors.red[100],
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('启用“点击空白收起”'),
const SizedBox(width: 8),
Switch(
value: _enableDismiss,
onChanged: (v) => setState(() => _enableDismiss = v),
),
],
),
const Divider(),
const SizedBox(height: 40),
Text(
_enableDismiss
? '【正面教材】\n\n已包裹 GestureDetector。\n👉 现在点击下方输入框唤起键盘,\n然后点击任意【空白区域】,键盘会自动收起。'
: '【反面教材】\n\n未处理点击事件。\n👉 点击下方输入框唤起键盘后...\n尝试狂点空白处,键盘会纹丝不动。\n(只能无奈地去点键盘上小小的完成键)',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _enableDismiss ? Colors.green : Colors.red,
),
),
const SizedBox(height: 40),
const TextField(
autofocus: true,
decoration: InputDecoration(
hintText: '点我尝试唤起键盘...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.keyboard),
),
),
],
),
),
);
// 核心差异:正例包裹 GestureDetector
if (_enableDismiss) {
return GestureDetector(
behavior:
HitTestBehavior.translucent, // 💡 只有设为 translucent,才能捕捉透明区域的点击
onTap: () {
debugPrint('触发全局点击,收起键盘');
// 收起键盘的核心代码
FocusScope.of(context).unfocus();
},
child: bodyContent,
);
} else {
return bodyContent;
}
}
}
四、鸿蒙实战:封装通用输入组件

为了让 APP 风格统一,我们封装一个样式的 InputWidget。
import 'package:flutter/material.dart';
class CustomInputPage extends StatelessWidget {
const CustomInputPage({super.key});
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: Colors.grey[100], // 浅灰背景突出卡片
appBar: AppBar(title: const Text('实战:封装通用输入组件')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const Text('以下使用了自定义的 ModernTextField 组件',
style: TextStyle(color: Colors.grey)),
const SizedBox(height: 32),
const ModernTextField(
hint: '请输入账号',
icon: Icons.person,
),
const SizedBox(height: 20),
const ModernTextField(
hint: '请输入密码',
icon: Icons.lock,
isPassword: true,
),
const SizedBox(height: 48),
SizedBox(
width: double.infinity,
height: 50,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.blue, Colors.purple]),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
offset: const Offset(0, 4),
blurRadius: 10,
)
],
),
child: MaterialButton(
onPressed: () {},
child: const Text('登录',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
),
),
),
],
),
),
),
);
}
}
/// 封装的现代化输入框组件
class ModernTextField extends StatelessWidget {
final TextEditingController? controller;
final String hint;
final IconData? icon;
final bool isPassword;
const ModernTextField({
super.key,
this.controller,
required this.hint,
this.icon,
this.isPassword = false,
});
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
// 增加投影,提升立体感
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 4),
blurRadius: 10,
),
],
),
child: TextField(
controller: controller,
obscureText: isPassword,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(color: Colors.grey[400]),
prefixIcon:
icon != null ? Icon(icon, color: Colors.blueAccent) : null,
border: InputBorder.none, // 去掉默认下划线
contentPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
),
),
);
}
}
五、阶段总结
至此,《Flutter for OpenHarmony 实战之基础组件》 系列的前 8 篇基础中的基础已经讲解完毕!但这仅仅是开始。
我们已经掌握了:
- Container: 布局的基石。
- Row/Column: 线性排列的艺术。
- Stack: 这就是层叠(Z轴)。
- Text: 信息的传递者。
- Image: 颜值即正义。
- ListView: 动态的长河。
- Button: 交互的起点。
- TextField: 数据的入口。
掌握了这 8 大组件,你已经能画出大部分 UI 界面了。但要构建一个完整的、有灵魂的 App,我们还需要搭建页面的骨架(Scaffold)、与用户进行对话(Dialog)、以及更复杂的导航结构(Tabs)。
下一篇预告
接下来的第九篇,我们将进入**“中级组件篇”**,解决页面的结构问题:
《Flutter for OpenHarmony 实战之基础组件:第九篇 Scaffold 与 AppBar 页面骨架》
我们将学习如何像搭积木一样,快速构建出具备导航栏、侧边栏和悬浮按钮的标准 Material 页面。
🚀 感谢你的陪伴! 愿你在鸿蒙跨平台开发的道路上越走越远,打造出令人惊艳的应用。
🌐 欢迎加入开源鸿蒙跨平台社区:开源鸿蒙跨平台开发者社区
更多推荐



所有评论(0)