前言:
这一讲进入交互部分了,在已经可以监督AI进行UI开发的情况下,每个UI中的Widget如何与用户交互,就是这一部分要说的。看完之后,对于交互页面如何与AI交互,你就心里有数了。
一、总览
本讲聚焦 Flutter 中用户输入采集和基础交互响应的核心能力,是实现 App 与人交互的基础(比如登录页、表单页、设置页等场景)。通过学习 TextField(文本输入框)、各类按钮、焦点管理和表单校验,你将掌握:
- 如何收集用户输入的文本信息(如账号、密码、备注)
- 如何响应用户的点击/触摸操作(按钮点击)
- 如何控制输入行为(如焦点切换、输入限制)
- 如何验证用户输入的合法性(如手机号格式、密码长度) 看看有哪些内容
- 事件入口:所有用户交互(输入、点击)最终都会被 Flutter 输入系统捕获;
- 状态管理:TextField 的文本状态由 Controller 独立管理(分离 UI 和数据),后面会有单独的一讲讲这个;
- 职责分离:焦点由 FocusManager 统一管控,按钮负责分发点击事件,表单负责校验输入合法性;
- 闭环逻辑:交互事件 → 系统处理 → 业务校验 → UI 反馈,形成完整的交互闭环。
二、核心知识
2.1 TextField 文本输入框
核心属性说明
| 属性名 | 作用 | 常用值/示例 |
|---|---|---|
| controller | 管理输入文本(获取/设置/监听) | TextEditingController() |
| decoration | 输入框样式(提示文字、边框、图标) | InputDecoration(hintText: "请输入手机号") |
| onChanged | 输入内容变化时的监听回调 | (value) => print("输入了:$value") |
| keyboardType | 键盘类型(数字/文本/邮箱) | TextInputType.number |
| obscureText | 是否隐藏输入(密码框) | true/false |
| focusNode | 关联焦点节点(控制焦点) | FocusNode() |
基础案例
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: TextFieldDemo(),);
}
}
class TextFieldDemo extends StatefulWidget {
const TextFieldDemo({super.key});
@override
State<TextFieldDemo> createState() => _TextFieldDemoState();
}
class _TextFieldDemoState extends State<TextFieldDemo> {
// 1. 定义控制器(核心:管理输入文本)
final TextEditingController _phoneController = TextEditingController();
// 定义焦点节点
final FocusNode _phoneFocusNode = FocusNode();
@override
void initState() {
super.initState();
// 2. 监听输入内容变化
_phoneController.addListener(() {
print("实时输入:${_phoneController.text}");
});
// 监听焦点变化
_phoneFocusNode.addListener(() {
print("焦点状态:${_phoneFocusNode.hasFocus}");
});
}
@override
void dispose() {
// 3. 销毁控制器(避免内存泄漏,必须做!)
_phoneController.dispose();
_phoneFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("TextField 示例")),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
controller: _phoneController, // 绑定控制器
focusNode: _phoneFocusNode, // 绑定焦点
decoration: const InputDecoration(
hintText: "请输入手机号", // 提示文字
labelText: "手机号", // 标签文字
prefixIcon: Icon(Icons.phone), // 左侧图标
border: OutlineInputBorder(), // 带边框样式
),
keyboardType: TextInputType.number, // 数字键盘
maxLength: 11, // 最大输入长度
onChanged: (value) {
// 另一种监听方式(单次变化回调)
print("onChanged:$value");
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// 获取输入内容
String phone = _phoneController.text;
print("最终输入:$phone");
// 清空输入
_phoneController.clear();
// 失去焦点
_phoneFocusNode.unfocus();
},
child: const Text("获取输入内容"),
)
],
),
),
);
}
}
注意事项
- 必须销毁 Controller:Controller 持有 State 引用,不 dispose 会导致内存泄漏;注意观察输入值的监听,这部分内容与交互强关联,在后面会有单独的讲解。
- 监听方式选择:
addListener是全局监听(适合实时响应),onChanged是单次回调(适合简单逻辑); - 焦点管理:通过 FocusNode 主动控制焦点(如点击按钮让输入框失去焦点)。
2.2 按钮(ElevatedButton/TextButton/IconButton)
核心属性说明
| 按钮类型 | 核心属性 | 适用场景 |
|---|---|---|
| ElevatedButton | onPressed(点击回调)、child(按钮内容)、style(样式) | 主要操作按钮(提交、确认、下一步) |
| TextButton | 同 ElevatedButton(无背景色,仅文字) | 次要操作(取消、返回、查看详情) |
| IconButton | icon(图标)、onPressed、iconSize(图标大小) | 图标类操作(收藏、删除、分享) |
基础案例
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ButtonsDemo(),);
}
}
class ButtonsDemo extends StatelessWidget {
const ButtonsDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("按钮示例")),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// 1. 主要按钮(带背景)
ElevatedButton(
onPressed: () {
print("提交按钮被点击");
// 业务逻辑:提交表单、跳转页面等
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // 背景色
padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 15), // 内边距
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // 圆角
),
),
child: const Text(
"提交",
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
const SizedBox(height: 20),
// 2. 文本按钮(无背景)
TextButton(
onPressed: () {
print("取消按钮被点击");
},
child: const Text(
"取消",
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
const SizedBox(height: 20),
// 3. 图标按钮
IconButton(
icon: const Icon(Icons.favorite_border, size: 30),
color: Colors.red,
onPressed: () {
print("收藏按钮被点击");
},
),
// 4. 图标+文字按钮(常用组合)
ElevatedButton.icon(
onPressed: () {
print("登录按钮被点击");
},
icon: const Icon(Icons.login, color: Colors.white),
label: const Text("登录", style: TextStyle(color: Colors.white)),
)
],
),
),
);
}
}
注意事项
- onPressed 为 null 时按钮禁用:如
onPressed: null,按钮会变成灰色不可点击; - 样式统一:建议通过 Theme 统一按钮样式,避免页面样式混乱;
- IconButton 点击区域:默认点击区域比图标大,可通过
padding调整。
2.3 焦点管理与输入控制
核心概念
- FocusManager:全局焦点管理器,管理所有焦点节点;
- FocusNode:单个输入框的焦点节点(关联 TextField);
- FocusScope:焦点作用域,批量管理多个焦点节点。
基础案例(焦点切换)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "登录页面示例",
theme: ThemeData(primarySwatch: Colors.blue),
home: const LoginPage(),
);
}
}
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
// 1. 定义核心对象
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final FocusNode _phoneFocus = FocusNode();
final FocusNode _passwordFocus = FocusNode();
bool _isObscure = true; // 密码是否隐藏
@override
void dispose() {
// 2. 销毁资源(必做)
_phoneController.dispose();
_passwordController.dispose();
_phoneFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
// 3. 登录提交逻辑
void _login() {
if (_formKey.currentState!.validate()) {
// 隐藏键盘(失去焦点)
FocusScope.of(context).unfocus();
// 模拟登录请求(实际开发中替换为网络请求)
String phone = _phoneController.text;
String password = _passwordController.text;
print("开始登录:手机号=$phone,密码=$password");
// 交互反馈
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("登录成功!正在跳转..."),
backgroundColor: Colors.green,
),
);
// 模拟跳转首页(延迟2秒)
Future.delayed(const Duration(seconds: 2), () {
// 实际开发中替换为 Navigator.push 跳转
print("跳转到首页");
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("用户登录"),
centerTitle: true,
),
body: SingleChildScrollView( // 防止键盘弹出时溢出
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 标题
const Text(
"欢迎登录",
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 40),
// 手机号输入框
TextFormField(
controller: _phoneController,
focusNode: _phoneFocus,
decoration: const InputDecoration(
hintText: "请输入手机号",
labelText: "手机号",
prefixIcon: Icon(Icons.phone_android),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
// 回车切换到密码框
FocusScope.of(context).requestFocus(_passwordFocus);
},
validator: (value) {
if (value == null || value.isEmpty) {
return "手机号不能为空";
}
RegExp phoneReg = RegExp(r'^1[3-9]\d{9}$');
if (!phoneReg.hasMatch(value)) {
return "请输入有效的手机号";
}
return null;
},
),
const SizedBox(height: 20),
// 密码输入框
TextFormField(
controller: _passwordController,
focusNode: _passwordFocus,
decoration: InputDecoration(
hintText: "请输入密码",
labelText: "密码",
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
// 显示/隐藏密码
icon: Icon(_isObscure ? Icons.visibility_off : Icons.visibility),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
obscureText: _isObscure,
textInputAction: TextInputAction.done,
onFieldSubmitted: (value) => _login(), // 回车登录
validator: (value) {
if (value == null || value.isEmpty) {
return "密码不能为空";
}
if (value.length < 6 || value.length > 20) {
return "密码长度需在6-20位之间";
}
return null;
},
),
const SizedBox(height: 30),
// 登录按钮(ElevatedButton)
ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
),
child: const Text(
"登录",
style: TextStyle(fontSize: 18),
),
),
const SizedBox(height: 20),
// 辅助按钮(TextButton)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
print("跳转到注册页面");
},
child: const Text("还没有账号?立即注册"),
),
TextButton(
onPressed: () {
print("跳转到忘记密码页面");
},
child: const Text("忘记密码?"),
),
],
),
const SizedBox(height: 40),
// 其他登录方式(IconButton)
const Text("其他登录方式"),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.wechat, size: 40, color: Colors.green),
onPressed: () {
print("微信登录");
},
),
const SizedBox(width: 40),
IconButton(
icon: const Icon(Icons.facebook, size: 40, color: Colors.blue),
onPressed: () {
print("QQ登录");
},
),
],
)
],
),
),
),
);
}
}
注意事项
- 焦点与键盘:输入框获取焦点会自动弹出键盘,失去焦点会收起键盘;
- TextInputAction:通过
textInputAction定义回车按钮的行为(next/done/search 等); - 避免内存泄漏:FocusNode 必须在 dispose 中销毁。
2.4 表单基础校验逻辑
核心组件
- Form:表单容器,管理多个 FormField,提供统一校验/重置;
- TextFormField:带校验功能的 TextField(继承自 FormField);
- GlobalKey :表单的全局 key,用于触发校验/重置。
GlobalKey:Flutter 中的全局键,作用是跨组件树找到对应的 Widget 实例,并获取其状态(State),和普通的Key不同,GlobalKey是全局唯一的,能突破组件层级限制访问目标组件。<FormState>是GlobalKey的泛型限定,指定这个全局键专门绑定Form组件的状态类(FormState是Form组件的状态管理类,包含表单验证、重置、保存等核心方法)。
核心属性(TextFormField)
| 属性 | 作用 |
|---|---|
| validator | 校验函数(返回 null 表示校验通过,返回字符串表示错误提示) |
| autovalidateMode | 校验时机(onUserInteraction:用户交互时校验;onSubmit:提交时校验) |
基础案例
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FormDemo(),);
}
}
class FormDemo extends StatefulWidget {
const FormDemo({super.key});
@override
State<FormDemo> createState() => _FormDemoState();
}
class _FormDemoState extends State<FormDemo> {
// 1. 定义表单全局key(核心:触发校验)
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
void dispose() {
_phoneController.dispose();
_passwordController.dispose();
super.dispose();
}
// 2. 提交表单逻辑
void _submitForm() {
// 触发表单校验
if (_formKey.currentState!.validate()) {
// 校验通过,处理业务逻辑
print("手机号:${_phoneController.text},密码:${_passwordController.text}");
// 重置表单
_formKey.currentState!.reset();
// 提示用户
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("表单提交成功!")),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("表单校验示例")),
body: Padding(
padding: const EdgeInsets.all(20),
// 3. 表单容器
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, // 用户交互时自动校验
child: Column(
children: [
// 手机号输入框(带校验)
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(
hintText: "请输入手机号",
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
// 4. 手机号校验逻辑
validator: (value) {
if (value == null || value.isEmpty) {
return "手机号不能为空";
} else if (value.length != 11) {
return "请输入11位手机号";
}
// 校验通过返回null
return null;
},
),
const SizedBox(height: 20),
// 密码输入框(带校验)
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
hintText: "请输入密码",
border: OutlineInputBorder(),
),
obscureText: true,
// 密码校验逻辑
validator: (value) {
if (value == null || value.isEmpty) {
return "密码不能为空";
} else if (value.length < 6) {
return "密码长度不能少于6位";
}
return null;
},
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50), // 宽度充满
),
child: const Text("提交表单"),
)
],
),
),
),
);
}
}
注意事项
-
AutovalidateMode 选择:
AutovalidateMode.disabled:仅手动调用 validate 时校验(默认);AutovalidateMode.onUserInteraction:用户输入/点击时校验(推荐);AutovalidateMode.always:每次重建都校验(不推荐,性能差);
-
校验函数规则:返回 null = 校验通过,返回字符串 = 校验失败(字符串为错误提示);
-
FormState 操作:通过
_formKey.currentState可以调用validate()(校验)、reset()(重置)。
三、应用案例(登录页面)
实现一个完整的登录页面(包含:文本输入、按钮点击、焦点管理、表单校验、交互反馈)。
完整代码
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "登录页面示例",
theme: ThemeData(primarySwatch: Colors.blue),
home: const LoginPage(),
);
}
}
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
// 1. 定义核心对象
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final FocusNode _phoneFocus = FocusNode();
final FocusNode _passwordFocus = FocusNode();
bool _isObscure = true; // 密码是否隐藏
@override
void dispose() {
// 2. 销毁资源(必做)
_phoneController.dispose();
_passwordController.dispose();
_phoneFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
// 3. 登录提交逻辑
void _login() {
if (_formKey.currentState!.validate()) {
// 隐藏键盘(失去焦点)
FocusScope.of(context).unfocus();
// 模拟登录请求(实际开发中替换为网络请求)
String phone = _phoneController.text;
String password = _passwordController.text;
print("开始登录:手机号=$phone,密码=$password");
// 交互反馈
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("登录成功!正在跳转..."),
backgroundColor: Colors.green,
),
);
// 模拟跳转首页(延迟2秒)
Future.delayed(const Duration(seconds: 2), () {
// 实际开发中替换为 Navigator.push 跳转
print("跳转到首页");
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("用户登录"),
centerTitle: true,
),
body: SingleChildScrollView( // 防止键盘弹出时溢出
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 标题
const Text(
"欢迎登录",
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 40),
// 手机号输入框
TextFormField(
controller: _phoneController,
focusNode: _phoneFocus,
decoration: const InputDecoration(
hintText: "请输入手机号",
labelText: "手机号",
prefixIcon: Icon(Icons.phone_android),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
onSubmitted: (value) {
// 回车切换到密码框
FocusScope.of(context).requestFocus(_passwordFocus);
},
validator: (value) {
if (value == null || value.isEmpty) {
return "手机号不能为空";
}
RegExp phoneReg = RegExp(r'^1[3-9]\d{9}$');
if (!phoneReg.hasMatch(value)) {
return "请输入有效的手机号";
}
return null;
},
),
const SizedBox(height: 20),
// 密码输入框
TextFormField(
controller: _passwordController,
focusNode: _passwordFocus,
decoration: InputDecoration(
hintText: "请输入密码",
labelText: "密码",
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
// 显示/隐藏密码
icon: Icon(_isObscure ? Icons.visibility_off : Icons.visibility),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
obscureText: _isObscure,
textInputAction: TextInputAction.done,
onSubmitted: (value) => _login(), // 回车登录
validator: (value) {
if (value == null || value.isEmpty) {
return "密码不能为空";
}
if (value.length < 6 || value.length > 20) {
return "密码长度需在6-20位之间";
}
return null;
},
),
const SizedBox(height: 30),
// 登录按钮(ElevatedButton)
ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
),
child: const Text(
"登录",
style: TextStyle(fontSize: 18),
),
),
const SizedBox(height: 20),
// 辅助按钮(TextButton)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
print("跳转到注册页面");
},
child: const Text("还没有账号?立即注册"),
),
TextButton(
onPressed: () {
print("跳转到忘记密码页面");
},
child: const Text("忘记密码?"),
),
],
),
const SizedBox(height: 40),
// 其他登录方式(IconButton)
const Text("其他登录方式"),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.wechat, size: 40, color: Colors.green),
onPressed: () {
print("微信登录");
},
),
const SizedBox(width: 40),
IconButton(
icon: const Icon(Icons.qq_chat, size: 40, color: Colors.blue),
onPressed: () {
print("QQ登录");
},
),
],
)
],
),
),
),
);
}
}
功能说明
-
输入功能:手机号(数字键盘、格式校验)、密码(隐藏/显示切换、长度校验);
-
焦点管理:手机号回车切换到密码框,密码回车触发登录,登录后隐藏键盘;
-
交互反馈:校验失败显示错误提示,登录成功显示 SnackBar 提示;
-
按钮类型:
- ElevatedButton:核心登录按钮;
- TextButton:注册/忘记密码辅助按钮;
- IconButton:微信/FaceBook 快捷登录按钮;
-
表单校验:实时校验(用户输入时),提交时统一校验,确保输入合法。
-
交互闭环:用户输入 → 焦点控制 → 表单校验 → 按钮触发业务逻辑 → UI 反馈,是所有交互页面的通用逻辑;
-
性能与内存:FocusNode、TextEditingController 等对象必须在 dispose 中销毁,避免内存泄漏。