2.6 表单与输入处理

表单是 App 中最常见的交互场景之一。Flutter 提供了 Form + TextFormField 的声明式校验体系,配合 TextEditingController 和 FocusNode 实现完整的输入管理。


一、Form 与表单校验

1.1 基本表单

dart 复制代码
class RegisterPage extends StatefulWidget {
  @override
  State<RegisterPage> createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _confirmPasswordController = TextEditingController();
  bool _obscurePassword = true;

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      autovalidateMode: AutovalidateMode.onUserInteraction, // 用户交互后自动校验
      child: Column(
        children: [
          // 邮箱
          TextFormField(
            controller: _emailController,
            keyboardType: TextInputType.emailAddress,
            textInputAction: TextInputAction.next, // 键盘"下一个"按钮
            decoration: const InputDecoration(
              labelText: '邮箱',
              hintText: 'example@mail.com',
              prefixIcon: Icon(Icons.email_outlined),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) return '请输入邮箱';
              if (!RegExp(r'^[\w-\.]+@[\w-]+\.[a-zA-Z]{2,}$').hasMatch(value)) {
                return '邮箱格式不正确';
              }
              return null; // null 表示校验通过
            },
          ),
          const SizedBox(height: 16),

          // 密码
          TextFormField(
            controller: _passwordController,
            obscureText: _obscurePassword,
            textInputAction: TextInputAction.next,
            decoration: InputDecoration(
              labelText: '密码',
              prefixIcon: const Icon(Icons.lock_outlined),
              suffixIcon: IconButton(
                icon: Icon(_obscurePassword
                    ? Icons.visibility_off
                    : Icons.visibility),
                onPressed: () =>
                    setState(() => _obscurePassword = !_obscurePassword),
              ),
            ),
            validator: (value) {
              if (value == null || value.length < 8) return '密码至少 8 位';
              if (!RegExp(r'(?=.*[A-Z])(?=.*[0-9])').hasMatch(value)) {
                return '必须包含大写字母和数字';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),

          // 确认密码
          TextFormField(
            controller: _confirmPasswordController,
            obscureText: true,
            textInputAction: TextInputAction.done,
            decoration: const InputDecoration(
              labelText: '确认密码',
              prefixIcon: Icon(Icons.lock_outlined),
            ),
            validator: (value) {
              if (value != _passwordController.text) return '两次密码不一致';
              return null;
            },
          ),
          const SizedBox(height: 24),

          // 提交按钮
          SizedBox(
            width: double.infinity,
            child: ElevatedButton(
              onPressed: _submit,
              child: const Text('注册'),
            ),
          ),
        ],
      ),
    );
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      // 所有校验通过
      _formKey.currentState!.save(); // 触发 onSaved
      _performRegister();
    }
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    _confirmPasswordController.dispose();
    super.dispose();
  }
}

二、TextEditingController 进阶

dart 复制代码
class SearchField extends StatefulWidget { ... }

class _SearchFieldState extends State<SearchField> {
  final _controller = TextEditingController();

  @override
  void initState() {
    super.initState();

    // 监听文本变化
    _controller.addListener(() {
      print('当前文本: ${_controller.text}');
      print('光标位置: ${_controller.selection}');
    });
  }

  void _insertAtCursor(String text) {
    final cursorPos = _controller.selection.baseOffset;
    final currentText = _controller.text;
    _controller.text = currentText.substring(0, cursorPos) +
        text +
        currentText.substring(cursorPos);
    // 移动光标到插入内容之后
    _controller.selection = TextSelection.collapsed(
      offset: cursorPos + text.length,
    );
  }

  void _selectAll() {
    _controller.selection = TextSelection(
      baseOffset: 0,
      extentOffset: _controller.text.length,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

三、FocusNode(焦点管理)

dart 复制代码
class LoginForm extends StatefulWidget { ... }

class _LoginFormState extends State<LoginForm> {
  final _emailFocus = FocusNode();
  final _passwordFocus = FocusNode();

  @override
  void initState() {
    super.initState();
    // 监听焦点变化
    _emailFocus.addListener(() {
      if (!_emailFocus.hasFocus) {
        // 失去焦点时执行校验
        _validateEmail();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      TextFormField(
        focusNode: _emailFocus,
        textInputAction: TextInputAction.next,
        onFieldSubmitted: (_) {
          // 按"下一个"时跳到密码框
          FocusScope.of(context).requestFocus(_passwordFocus);
        },
      ),
      TextFormField(
        focusNode: _passwordFocus,
        textInputAction: TextInputAction.done,
        onFieldSubmitted: (_) {
          _passwordFocus.unfocus(); // 收起键盘
          _submit();
        },
      ),
      // 点击空白区域收起键盘
    ]);
  }

  @override
  void dispose() {
    _emailFocus.dispose();
    _passwordFocus.dispose();
    super.dispose();
  }
}

// 点击空白区域收起键盘(在 Scaffold 层处理)
GestureDetector(
  onTap: () => FocusScope.of(context).unfocus(),
  child: Scaffold(body: ...),
)

四、输入格式化(TextInputFormatter)

dart 复制代码
import 'package:flutter/services.dart';

TextFormField(
  keyboardType: TextInputType.number,
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,        // 仅允许数字
    LengthLimitingTextInputFormatter(11),           // 最多 11 位
    // 或自定义格式化器
    PhoneNumberFormatter(),
  ],
)

// 自定义手机号格式化器:138 0000 0000
class PhoneNumberFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final digits = newValue.text.replaceAll(RegExp(r'\D'), '');
    final buffer = StringBuffer();

    for (int i = 0; i < digits.length && i < 11; i++) {
      if (i == 3 || i == 7) buffer.write(' ');
      buffer.write(digits[i]);
    }

    final formatted = buffer.toString();
    return TextEditingValue(
      text: formatted,
      selection: TextSelection.collapsed(offset: formatted.length),
    );
  }
}

// 金额输入:最多两位小数
TextFormField(
  keyboardType: const TextInputType.numberWithOptions(decimal: true),
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')),
  ],
)

// 身份证号
TextFormField(
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[0-9Xx]')),
    LengthLimitingTextInputFormatter(18),
  ],
)

五、键盘处理

dart 复制代码
// 键盘弹出时自动滚动
Scaffold(
  // resizeToAvoidBottomInset 默认 true,Scaffold 会自动缩减
  resizeToAvoidBottomInset: true,
  body: SingleChildScrollView(
    // 确保输入框在键盘弹出时可见
    reverse: false,
    child: Padding(
      padding: EdgeInsets.only(
        bottom: MediaQuery.of(context).viewInsets.bottom,
      ),
      child: _buildForm(),
    ),
  ),
)

// 监听键盘高度变化
Widget build(BuildContext context) {
  final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
  final isKeyboardVisible = keyboardHeight > 0;

  return AnimatedPadding(
    duration: const Duration(milliseconds: 200),
    padding: EdgeInsets.only(bottom: isKeyboardVisible ? keyboardHeight : 0),
    child: _buildContent(),
  );
}

六、搜索防抖(Debounce)

dart 复制代码
class SearchPage extends StatefulWidget { ... }

class _SearchPageState extends State<SearchPage> {
  final _controller = TextEditingController();
  Timer? _debounceTimer;
  List<Product> _results = [];

  void _onSearchChanged(String query) {
    _debounceTimer?.cancel(); // 取消前一次
    _debounceTimer = Timer(
      const Duration(milliseconds: 500), // 500ms 防抖
      () => _performSearch(query),
    );
  }

  Future<void> _performSearch(String query) async {
    if (query.trim().isEmpty) {
      setState(() => _results = []);
      return;
    }
    final results = await SearchService.search(query);
    if (mounted) setState(() => _results = results);
  }

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      TextField(
        controller: _controller,
        onChanged: _onSearchChanged,
        decoration: InputDecoration(
          hintText: '搜索商品...',
          prefixIcon: const Icon(Icons.search),
          suffixIcon: _controller.text.isNotEmpty
              ? IconButton(
                  icon: const Icon(Icons.clear),
                  onPressed: () {
                    _controller.clear();
                    _onSearchChanged('');
                  },
                )
              : null,
        ),
      ),
      Expanded(child: SearchResultList(results: _results)),
    ]);
  }

  @override
  void dispose() {
    _debounceTimer?.cancel();
    _controller.dispose();
    super.dispose();
  }
}

小结

功能 核心 API
表单校验 Form + GlobalKey<FormState> + validator
文本控制 TextEditingController(读取/设置文本、光标位置)
焦点管理 FocusNode + FocusScope(跳转/收起键盘)
输入格式化 TextInputFormatter(数字/手机号/金额)
键盘适配 MediaQuery.viewInsets.bottom
搜索防抖 Timer + 500ms 延迟

👉 下一节:2.7 列表与滚动性能优化

相关推荐
AI_零食2 小时前
开源鸿蒙跨平台Flutter开发:脑筋急转弯应用开发文档
flutter·华为·开源·harmonyos·鸿蒙
2301_822703203 小时前
Flutter 框架跨平台鸿蒙开发 - 家庭时间胶囊应用
算法·flutter·华为·图形渲染·harmonyos·鸿蒙
提子拌饭1333 小时前
Flutter 框架跨平台鸿蒙开发 - 声音风景分享应用
flutter·华为·harmonyos·鸿蒙·风景
独特的螺狮粉3 小时前
开源鸿蒙跨平台Flutter开发:超市购物清单应用
flutter·华为·开源·harmonyos·鸿蒙
2301_822703203 小时前
成语小词典:鸿蒙Flutter实现的成语查询与管理应用
算法·flutter·华为·开源·图形渲染·harmonyos
2301_822703204 小时前
Flutter 框架跨平台鸿蒙开发 - 智能植物生长记录应用
算法·flutter·华为·harmonyos·鸿蒙
世人万千丶4 小时前
开源鸿蒙跨平台Flutter开发:成语接龙游戏应用
学习·flutter·游戏·华为·开源·harmonyos·鸿蒙
浮芷.4 小时前
开源鸿蒙跨平台Flutter开发:校园闲置物品交换应用
科技·flutter·华为·开源·ar·harmonyos·鸿蒙
李李李勃谦5 小时前
Flutter 框架跨平台鸿蒙开发 - 手工技能学习
学习·flutter·华为·harmonyos