第四讲 用户输入与基础交互

0 阅读8分钟

前言:

这一讲进入交互部分了,在已经可以监督AI进行UI开发的情况下,每个UI中的Widget如何与用户交互,就是这一部分要说的。看完之后,对于交互页面如何与AI交互,你就心里有数了。

一、总览

本讲聚焦 Flutter 中用户输入采集基础交互响应的核心能力,是实现 App 与人交互的基础(比如登录页、表单页、设置页等场景)。通过学习 TextField(文本输入框)、各类按钮、焦点管理和表单校验,你将掌握:

  • 如何收集用户输入的文本信息(如账号、密码、备注)
  • 如何响应用户的点击/触摸操作(按钮点击)
  • 如何控制输入行为(如焦点切换、输入限制)
  • 如何验证用户输入的合法性(如手机号格式、密码长度) 看看有哪些内容

image.png

  • 事件入口:所有用户交互(输入、点击)最终都会被 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)

核心属性说明
按钮类型核心属性适用场景
ElevatedButtononPressed(点击回调)、child(按钮内容)、style(样式)主要操作按钮(提交、确认、下一步)
TextButton同 ElevatedButton(无背景色,仅文字)次要操作(取消、返回、查看详情)
IconButtonicon(图标)、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 组件的状态类(FormStateForm 组件的状态管理类,包含表单验证、重置、保存等核心方法)。
核心属性(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登录");
                    },
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

功能说明

  1. 输入功能:手机号(数字键盘、格式校验)、密码(隐藏/显示切换、长度校验);

  2. 焦点管理:手机号回车切换到密码框,密码回车触发登录,登录后隐藏键盘;

  3. 交互反馈:校验失败显示错误提示,登录成功显示 SnackBar 提示;

  4. 按钮类型

    1. ElevatedButton:核心登录按钮;
    2. TextButton:注册/忘记密码辅助按钮;
    3. IconButton:微信/FaceBook 快捷登录按钮;
  5. 表单校验:实时校验(用户输入时),提交时统一校验,确保输入合法。

  6. 交互闭环:用户输入 → 焦点控制 → 表单校验 → 按钮触发业务逻辑 → UI 反馈,是所有交互页面的通用逻辑;

  7. 性能与内存:FocusNode、TextEditingController 等对象必须在 dispose 中销毁,避免内存泄漏。