第3章:基础组件 —— 3.5 输入框及表单

72 阅读6分钟

3.5 输入框及表单

📚 章节概览

输入框和表单是用户交互的核心组件。Flutter 提供了强大的输入处理能力,本章节将学习:

  • TextField - 基本输入框
  • TextEditingController - 控制器
  • FocusNode - 焦点管理
  • InputDecoration - 装饰器
  • 键盘类型 - keyboardType
  • Form - 表单组件
  • TextFormField - 表单输入框
  • 表单验证 - validator

🎯 核心知识点

1. TextField

TextField 是 Flutter 中用于文本输入的基本组件。

基本用法
TextField(
  decoration: InputDecoration(
    labelText: '用户名',
    hintText: '请输入用户名',
  ),
)
常用属性
属性类型说明
controllerTextEditingController?控制器
focusNodeFocusNode?焦点节点
decorationInputDecoration?装饰器
keyboardTypeTextInputType?键盘类型
textInputActionTextInputAction?键盘动作按钮
styleTextStyle?文本样式
textAlignTextAlign文本对齐
autofocusbool自动获取焦点
obscureTextbool隐藏文本(密码)
maxLinesint?最大行数
maxLengthint?最大长度
enabledbool?是否启用
onChangedValueChanged?内容改变回调
onSubmittedValueChanged?提交回调
inputFormattersList?输入格式化器

2️⃣ TextEditingController

TextEditingController 用于控制TextField的文本内容。

基本用法

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final TextEditingController _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    // 设置初始值
    _controller.text = 'Hello';
    
    // 监听输入变化
    _controller.addListener(() {
      print('当前输入:${_controller.text}');
    });
  }

  @override
  void dispose() {
    // 释放控制器
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
    );
  }
}

常用方法

方法说明
text获取/设置文本内容
clear()清空文本
selection获取/设置选中文本
addListener()添加监听器
removeListener()移除监听器
dispose()释放资源

选中文本

// 全选
_controller.selection = TextSelection(
  baseOffset: 0,
  extentOffset: _controller.text.length,
);

// 选中部分文本
_controller.selection = TextSelection(
  baseOffset: 0,
  extentOffset: 5,
);

3️⃣ FocusNode - 焦点管理

FocusNode 用于控制 TextField 的焦点状态。

基本用法

class FocusExample extends StatefulWidget {
  @override
  _FocusExampleState createState() => _FocusExampleState();
}

class _FocusExampleState extends State<FocusExample> {
  final FocusNode _focusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    // 监听焦点变化
    _focusNode.addListener(() {
      print('焦点状态:${_focusNode.hasFocus}');
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _focusNode,
          decoration: InputDecoration(labelText: '输入框'),
        ),
        ElevatedButton(
          onPressed: () {
            // 请求焦点
            FocusScope.of(context).requestFocus(_focusNode);
          },
          child: Text('获取焦点'),
        ),
        ElevatedButton(
          onPressed: () {
            // 失去焦点
            _focusNode.unfocus();
          },
          child: Text('失去焦点'),
        ),
      ],
    );
  }
}

焦点操作

方法说明
requestFocus()请求焦点
unfocus()失去焦点
hasFocus是否有焦点
nextFocus()移到下一个焦点
previousFocus()移到上一个焦点

4️⃣ InputDecoration - 装饰器

InputDecoration 用于装饰 TextField 的外观。

完整示例

TextField(
  decoration: InputDecoration(
    // 标签
    labelText: '用户名',
    labelStyle: TextStyle(color: Colors.blue),
    
    // 提示文本
    hintText: '请输入用户名',
    hintStyle: TextStyle(color: Colors.grey),
    
    // 帮助文本
    helperText: '用于登录的用户名',
    
    // 错误提示
    errorText: '用户名不能为空',
    
    // 图标
    icon: Icon(Icons.person),        // 输入框外的图标
    prefixIcon: Icon(Icons.person),  // 输入框内左侧图标
    suffixIcon: Icon(Icons.clear),   // 输入框内右侧图标
    
    // 前缀/后缀文本
    prefix: Text('Mr.'),
    suffix: Text('@gmail.com'),
    
    // 背景填充
    filled: true,
    fillColor: Colors.grey[200],
    
    // 边框
    border: OutlineInputBorder(),
    enabledBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.grey),
    ),
    focusedBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.blue, width: 2),
    ),
    errorBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.red),
    ),
  ),
)

边框样式

// 1. 默认边框(下划线)
InputDecoration(
  border: UnderlineInputBorder(),
)

// 2. 轮廓边框
InputDecoration(
  border: OutlineInputBorder(),
)

// 3. 圆角边框
InputDecoration(
  border: OutlineInputBorder(
    borderRadius: BorderRadius.circular(12),
  ),
)

// 4. 无边框
InputDecoration(
  border: InputBorder.none,
)

5️⃣ 键盘类型

通过 keyboardType 设置不同的键盘类型。

类型说明键盘显示
TextInputType.text默认文本标准键盘
TextInputType.number数字数字键盘
TextInputType.phone电话电话号码键盘
TextInputType.emailAddress邮箱带@的键盘
TextInputType.url网址带/的键盘
TextInputType.datetime日期时间带-和:的键盘
TextInputType.multiline多行支持换行

示例

// 数字键盘
TextField(
  keyboardType: TextInputType.number,
  decoration: InputDecoration(labelText: '年龄'),
)

// 邮箱键盘
TextField(
  keyboardType: TextInputType.emailAddress,
  decoration: InputDecoration(labelText: '邮箱'),
)

// 多行输入
TextField(
  keyboardType: TextInputType.multiline,
  maxLines: 3,
  decoration: InputDecoration(labelText: '备注'),
)

6️⃣ 输入格式化

使用 inputFormatters 限制输入内容。

常用格式化器

// 1. 只允许数字
TextField(
  inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)

// 2. 限制长度
TextField(
  inputFormatters: [LengthLimitingTextInputFormatter(10)],
)

// 3. 自定义正则
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')),
  ],
)

// 4. 组合多个格式化器
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
    LengthLimitingTextInputFormatter(11),
  ],
)

7️⃣ Form - 表单

Form 组件用于表单验证和统一管理。

基本用法

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(labelText: '用户名'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入用户名';
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                // 验证通过
                print('表单验证通过');
              }
            },
            child: Text('提交'),
          ),
        ],
      ),
    );
  }
}

Form 属性

属性类型说明
keyGlobalKey全局键
autovalidateModeAutovalidateMode自动验证模式
onChangedVoidCallback?内容改变回调

AutovalidateMode

// 不自动验证(默认)
Form(
  autovalidateMode: AutovalidateMode.disabled,
)

// 用户交互后验证
Form(
  autovalidateMode: AutovalidateMode.onUserInteraction,
)

// 始终验证
Form(
  autovalidateMode: AutovalidateMode.always,
)

8️⃣ TextFormField

TextFormFieldTextField 的Form版本,支持验证。

基本用法

TextFormField(
  decoration: InputDecoration(labelText: '用户名'),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '用户名不能为空';
    }
    if (value.length < 3) {
      return '用户名至少3个字符';
    }
    return null;  // 验证通过返回null
  },
  onSaved: (value) {
    // 保存数据
    print('保存:$value');
  },
)

验证器返回值

  • null:验证通过
  • String:验证失败,返回错误提示

9️⃣ FormState

通过 GlobalKey 获取 FormState 进行操作。

常用方法

方法说明
validate()验证所有字段
save()保存所有字段
reset()重置所有字段

完整示例

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

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

  void _submitForm() {
    // 1. 验证
    if (_formKey.currentState!.validate()) {
      // 2. 保存
      _formKey.currentState!.save();
      
      // 3. 提交数据
      print('用户名:${_usernameController.text}');
      print('密码:${_passwordController.text}');
    }
  }

  void _resetForm() {
    // 重置表单
    _formKey.currentState!.reset();
    _usernameController.clear();
    _passwordController.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      autovalidateMode: AutovalidateMode.onUserInteraction,
      child: Column(
        children: [
          TextFormField(
            controller: _usernameController,
            decoration: InputDecoration(
              labelText: '用户名',
              prefixIcon: Icon(Icons.person),
            ),
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return '用户名不能为空';
              }
              return null;
            },
          ),
          TextFormField(
            controller: _passwordController,
            decoration: InputDecoration(
              labelText: '密码',
              prefixIcon: Icon(Icons.lock),
            ),
            obscureText: true,
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return '密码不能为空';
              }
              if (value.length < 6) {
                return '密码不能少于6位';
              }
              return null;
            },
          ),
          Row(
            children: [
              ElevatedButton(
                onPressed: _resetForm,
                child: Text('重置'),
              ),
              ElevatedButton(
                onPressed: _submitForm,
                child: Text('提交'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

💡 最佳实践

1. 总是释放控制器

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

2. 使用 GlobalKey 获取 FormState

final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

// 不推荐:通过 context 获取(可能找不到)
Form.of(context).validate();

// 推荐:通过 GlobalKey 获取
_formKey.currentState!.validate();

3. 自动验证模式

// 推荐:用户交互后才验证
Form(
  autovalidateMode: AutovalidateMode.onUserInteraction,
)

// 不推荐:始终验证(用户还没输入就显示错误)
Form(
  autovalidateMode: AutovalidateMode.always,
)

4. 监听输入变化

// 方法1:使用 controller.addListener
_controller.addListener(() {
  print(_controller.text);
});

// 方法2:使用 onChanged
TextField(
  onChanged: (value) {
    print(value);
  },
)

5. 常见验证规则

// 邮箱验证
String? validateEmail(String? value) {
  if (value == null || value.isEmpty) {
    return '邮箱不能为空';
  }
  final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
  if (!emailRegex.hasMatch(value)) {
    return '请输入有效的邮箱地址';
  }
  return null;
}

// 手机号验证
String? validatePhone(String? value) {
  if (value == null || value.isEmpty) {
    return '手机号不能为空';
  }
  final phoneRegex = RegExp(r'^1[3-9]\d{9}$');
  if (!phoneRegex.hasMatch(value)) {
    return '请输入有效的手机号';
  }
  return null;
}

// 密码强度验证
String? validatePassword(String? value) {
  if (value == null || value.isEmpty) {
    return '密码不能为空';
  }
  if (value.length < 8) {
    return '密码不能少于8位';
  }
  if (!RegExp(r'[A-Z]').hasMatch(value)) {
    return '密码必须包含大写字母';
  }
  if (!RegExp(r'[a-z]').hasMatch(value)) {
    return '密码必须包含小写字母';
  }
  if (!RegExp(r'[0-9]').hasMatch(value)) {
    return '密码必须包含数字';
  }
  return null;
}

🤔 常见问题(FAQ)

Q1: TextField 和 TextFormField 有什么区别?

A:

特性TextFieldTextFormField
用途基础输入框表单输入框
验证需要手动实现内置 validator
使用场景单独使用配合 Form 使用
继承关系StatefulWidgetFormField
// TextField:手动验证
TextField(
  onChanged: (value) {
    if (value.isEmpty) {
      // 手动显示错误
    }
  },
)

// TextFormField:自动验证
TextFormField(
  validator: (value) {
    if (value?.isEmpty ?? true) {
      return '不能为空';  // 自动显示错误
    }
    return null;
  },
)

Q2: 如何实现"显示/隐藏密码"功能?

A:

class PasswordField extends StatefulWidget {
  @override
  _PasswordFieldState createState() => _PasswordFieldState();
}

class _PasswordFieldState extends State<PasswordField> {
  bool _obscureText = true;

  @override
  Widget build(BuildContext context) {
    return TextField(
      obscureText: _obscureText,
      decoration: InputDecoration(
        labelText: '密码',
        suffixIcon: IconButton(
          icon: Icon(
            _obscureText ? Icons.visibility : Icons.visibility_off,
          ),
          onPressed: () {
            setState(() {
              _obscureText = !_obscureText;
            });
          },
        ),
      ),
    );
  }
}

Q3: 如何限制只能输入数字?

A: 三种方法:

// 方法1:设置数字键盘
TextField(
  keyboardType: TextInputType.number,
)

// 方法2:使用 inputFormatters
TextField(
  inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)

// 方法3:在 validator 中检查
TextFormField(
  validator: (value) {
    if (value != null && !RegExp(r'^\d+$').hasMatch(value)) {
      return '只能输入数字';
    }
    return null;
  },
)

Q4: 如何实现"确认密码"验证?

A:

class PasswordForm extends StatefulWidget {
  @override
  _PasswordFormState createState() => _PasswordFormState();
}

class _PasswordFormState extends State<PasswordForm> {
  final TextEditingController _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextFormField(
          controller: _passwordController,
          decoration: InputDecoration(labelText: '密码'),
          obscureText: true,
          validator: (value) {
            if (value == null || value.length < 6) {
              return '密码不能少于6位';
            }
            return null;
          },
        ),
        TextFormField(
          decoration: InputDecoration(labelText: '确认密码'),
          obscureText: true,
          validator: (value) {
            if (value != _passwordController.text) {
              return '两次密码输入不一致';
            }
            return null;
          },
        ),
      ],
    );
  }
}

Q5: 如何实现实时搜索?

A:

class SearchField extends StatefulWidget {
  @override
  _SearchFieldState createState() => _SearchFieldState();
}

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

  @override
  void initState() {
    super.initState();
    _controller.addListener(_onSearchChanged);
  }

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

  void _onSearchChanged() {
    // 防抖:用户停止输入500ms后才执行搜索
    if (_debounce?.isActive ?? false) _debounce!.cancel();
    _debounce = Timer(const Duration(milliseconds: 500), () {
      // 执行搜索
      print('搜索:${_controller.text}');
    });
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      decoration: InputDecoration(
        labelText: '搜索',
        prefixIcon: Icon(Icons.search),
        suffixIcon: IconButton(
          icon: Icon(Icons.clear),
          onPressed: () {
            _controller.clear();
          },
        ),
      ),
    );
  }
}

🎯 跟着做练习

练习1:实现一个注册表单

目标: 创建注册表单,包含用户名、邮箱、密码、确认密码

步骤:

  1. 使用 FormTextFormField
  2. 实现表单验证
  3. 提交时显示输入内容
💡 查看答案
class RegisterForm extends StatefulWidget {
  const RegisterForm({super.key});

  @override
  State<RegisterForm> createState() => _RegisterFormState();
}

class _RegisterFormState extends State<RegisterForm> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

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

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('注册信息'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('用户名:${_usernameController.text}'),
              Text('邮箱:${_emailController.text}'),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('确定'),
            ),
          ],
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      autovalidateMode: AutovalidateMode.onUserInteraction,
      child: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          TextFormField(
            controller: _usernameController,
            decoration: const InputDecoration(
              labelText: '用户名',
              hintText: '请输入用户名',
              prefixIcon: Icon(Icons.person),
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return '用户名不能为空';
              }
              if (value.length < 3) {
                return '用户名至少3个字符';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          TextFormField(
            controller: _emailController,
            keyboardType: TextInputType.emailAddress,
            decoration: const InputDecoration(
              labelText: '邮箱',
              hintText: '请输入邮箱',
              prefixIcon: Icon(Icons.email),
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return '邮箱不能为空';
              }
              final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
              if (!emailRegex.hasMatch(value)) {
                return '请输入有效的邮箱地址';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          TextFormField(
            controller: _passwordController,
            obscureText: true,
            decoration: const InputDecoration(
              labelText: '密码',
              hintText: '请输入密码',
              prefixIcon: Icon(Icons.lock),
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return '密码不能为空';
              }
              if (value.length < 6) {
                return '密码不能少于6位';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          TextFormField(
            obscureText: true,
            decoration: const InputDecoration(
              labelText: '确认密码',
              hintText: '请再次输入密码',
              prefixIcon: Icon(Icons.lock_outline),
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value != _passwordController.text) {
                return '两次密码输入不一致';
              }
              return null;
            },
          ),
          const SizedBox(height: 24),
          ElevatedButton(
            onPressed: _submitForm,
            child: const Padding(
              padding: EdgeInsets.all(12),
              child: Text('注册', style: TextStyle(fontSize: 16)),
            ),
          ),
        ],
      ),
    );
  }
}

练习2:实现一个搜索框

目标: 实现带清空按钮和实时搜索的搜索框

步骤:

  1. 使用 TextEditingController
  2. 添加清空按钮
  3. 实现防抖搜索
💡 查看答案
import 'dart:async';

class SearchBox extends StatefulWidget {
  const SearchBox({super.key});

  @override
  State<SearchBox> createState() => _SearchBoxState();
}

class _SearchBoxState extends State<SearchBox> {
  final TextEditingController _controller = TextEditingController();
  Timer? _debounce;
  List<String> _results = [];
  bool _isSearching = false;

  @override
  void initState() {
    super.initState();
    _controller.addListener(_onSearchChanged);
  }

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

  void _onSearchChanged() {
    if (_debounce?.isActive ?? false) _debounce!.cancel();
    
    setState(() {
      _isSearching = true;
    });

    _debounce = Timer(const Duration(milliseconds: 500), () {
      // 模拟搜索
      _performSearch(_controller.text);
    });
  }

  void _performSearch(String query) {
    if (query.isEmpty) {
      setState(() {
        _results = [];
        _isSearching = false;
      });
      return;
    }

    // 模拟搜索结果
    final allItems = [
      'Flutter',
      'Dart',
      'Widget',
      'StatelessWidget',
      'StatefulWidget',
      'Material',
      'Cupertino',
    ];

    setState(() {
      _results = allItems
          .where((item) => item.toLowerCase().contains(query.toLowerCase()))
          .toList();
      _isSearching = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _controller,
          decoration: InputDecoration(
            labelText: '搜索',
            hintText: '输入关键词搜索',
            prefixIcon: const Icon(Icons.search),
            suffixIcon: _controller.text.isNotEmpty
                ? IconButton(
                    icon: const Icon(Icons.clear),
                    onPressed: () {
                      _controller.clear();
                    },
                  )
                : null,
            border: const OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 16),
        if (_isSearching)
          const CircularProgressIndicator()
        else if (_results.isNotEmpty)
          ...results.map((result) => ListTile(
                title: Text(result),
                onTap: () {
                  print('选中:$result');
                },
              ))
        else if (_controller.text.isNotEmpty)
          const Text('无搜索结果'),
      ],
    );
  }
}

📋 小结

核心要点

组件用途关键特性
TextField基础输入框controller, decoration, keyboardType
TextEditingController控制器text, selection, addListener
FocusNode焦点管理hasFocus, requestFocus, unfocus
InputDecoration装饰器labelText, hintText, prefixIcon, border
Form表单validate, save, reset
TextFormField表单输入框validator, onSaved

记忆技巧

  1. 控制器三步曲:创建 → 使用 → 释放
  2. 焦点管理:FocusNode + FocusScope
  3. 表单验证:Form + TextFormField + GlobalKey
  4. 验证器返回值:null = 通过,String = 错误信息

🔗 相关资源