每天一个Flutter开发小项目 (6) : 表单与验证的专业实践 - 构建预约应用

发布于:2025-02-28 ⋅ 阅读:(16) ⋅ 点赞:(0)

引言

再次欢迎来到 每天一个Flutter开发小项目 系列博客!在前五篇博客中,我们一路从 Flutter 的基础组件、布局,到进阶的状态管理、导航路由,相信您已逐步掌握了构建现代 Flutter 应用的关键技能。

在实际应用开发中,表单 (Form) 是用户交互的核心组成部分。无论是用户注册、信息填写,还是数据录入,表单都扮演着至关重要的角色。而表单验证 (Form Validation) 则是确保用户输入数据有效性、提升应用健壮性的关键环节。一个专业级的 Flutter 应用,必须具备完善的表单处理和验证机制。

今天,我们将聚焦 Flutter 表单与验证的专业实践,并构建一个生活中常见的 预约应用,让您在实战中掌握 Flutter 表单的精髓。

通过本篇博客,您将深入学习:

  • Flutter 表单核心组件: 熟练掌握 FormFormFieldTextFormField 等表单核心 Widget 的使用方法。
  • Flutter 表单验证机制: 深入理解 Flutter 表单验证的工作原理,掌握 GlobalKey<FormState>FormStatevalidatoronSaved 等关键概念。
  • 自定义表单验证规则: 学习如何根据实际需求,灵活定制各种表单验证规则,例如,必填验证、邮箱格式验证、日期格式验证等。
  • 优雅的用户体验: 学习如何在表单验证中提供友好的错误提示,提升用户体验。
  • 更强大的用户交互: 通过构建预约应用,掌握处理用户输入、表单提交、数据处理等用户交互流程。
  • 专业技能跃升: 从简单的页面展示到复杂的表单交互,让您的 Flutter 开发能力更上一层楼。

项目简介: 预约应用

我们的预约应用将围绕以下核心功能展开:

  • 预约信息填写: 用户可以在表单中填写预约信息,包括姓名、邮箱、预约日期、预约时间、预约服务等。
  • 表单验证: 对用户填写的预约信息进行验证,确保数据格式和完整性符合要求。
  • 预约提交: 用户提交预约表单后,应用能够处理表单数据,并给出预约成功的提示 (本篇博客重点在于表单与验证,预约提交后的数据处理和后台交互暂不涉及)。
  • 友好的错误提示: 当表单验证失败时,应用能够清晰地提示用户错误信息,引导用户正确填写表单。

通过构建预约应用,我们将重点实践:

  • Flutter 表单构建: 使用 FormTextFormField 等 Widget 构建预约表单界面。
  • 表单验证实现: 应用 Flutter 表单验证机制,实现各种表单验证规则。
  • 用户输入处理: 处理用户在表单中的输入操作,获取表单数据。
  • 提升用户体验: 设计用户友好的表单界面和错误提示。

Flutter 表单验证核心概念解析

在开始构建预约应用之前,我们先来深入理解 Flutter 表单验证的核心概念,为后续的实战打下坚实基础。

  • Form Widget: Form Widget 是 Flutter 中用于组织和管理表单的核心容器 Widget。它提供了表单验证、表单提交等功能。一个表单通常由一个 Form Widget 包裹,内部包含多个表单项 (例如,TextFormField, Checkbox, DropdownButton 等)。
  • FormField Widget: FormField Widget 是表单项的基类,用于表示表单中的一个输入字段。例如,TextFormField (文本输入框)、CheckboxFormField (复选框)、DropdownButtonFormField (下拉菜单) 等都是 FormField 的子类。 FormField 负责处理用户输入、验证输入数据、以及在表单状态改变时通知 Form Widget。
  • TextFormField Widget: TextFormField Widget 是最常用的表单项,用于接收用户文本输入。它继承自 FormField,并提供了丰富的属性和方法,例如,decoration (设置输入框样式)、validator (设置验证器)、onSaved (设置保存回调) 等。
  • GlobalKey: GlobalKey<FormState> 是用于访问 Form Widget 的 FormState 的全局 Key。 FormState 对象包含了表单的当前状态,例如,表单是否有效、表单数据等。 我们可以通过 GlobalKey<FormState> 访问 FormState 对象,并调用 FormState 提供的方法,例如,validate() (验证表单)、save() (保存表单数据)、reset() (重置表单) 等。
  • FormState: FormState 是与 Form Widget 关联的状态对象,它包含了表单的当前状态和方法。 FormState 对象通常通过 GlobalKey<FormState> 访问。
  • validator 属性: FormField (例如,TextFormField) 提供了 validator 属性,用于设置表单项的验证器validator 属性接收一个函数,该函数接收表单项的当前值作为参数,并返回一个错误提示字符串 (如果验证失败) 或 null (如果验证成功)。
  • onSaved 属性: FormField (例如,TextFormField) 提供了 onSaved 属性,用于设置表单项的保存回调onSaved 属性接收一个函数,该函数在表单验证成功后,点击提交按钮时被调用,用于保存表单项的值。

Flutter 表单验证的工作流程

Flutter 表单验证的工作流程大致如下:

  1. 创建 GlobalKey<FormState>: 在 State 类中创建一个 GlobalKey<FormState> 对象,用于关联 Form Widget。
  2. 关联 GlobalKey<FormState>Form Widget: 将创建的 GlobalKey<FormState> 对象赋值给 Form Widget 的 key 属性。
  3. FormField 设置 validator: 为每个需要验证的 FormField (例如,TextFormField) 设置 validator 属性,定义验证规则。
  4. 调用 FormState.validate() 验证表单: 在表单提交时,通过 _formKey.currentState.validate() 调用 FormStatevalidate() 方法,触发表单验证。 validate() 方法会遍历表单中的所有 FormField,并调用每个 FormFieldvalidator 方法进行验证。
  5. 处理验证结果: validate() 方法返回一个布尔值,true 表示表单验证成功,false 表示表单验证失败。 如果验证失败,validate() 方法会自动调用 FormFieldsetState() 方法,触发 UI 重新构建,显示错误提示信息 (错误提示信息由 validator 方法返回的字符串决定)。
  6. 调用 FormState.save() 保存表单数据: 如果表单验证成功,可以调用 _formKey.currentState.save() 方法,触发表单数据保存。 save() 方法会遍历表单中的所有 FormField,并调用每个 FormFieldonSaved 方法。

实战步骤: 构建预约应用

接下来,我们将一步步使用 Flutter 表单与验证构建我们的预约应用。

步骤 1: 创建新的 Flutter 项目

首先,创建一个新的 Flutter 项目,命名为 booking_app.

步骤 2: 定义预约数据模型 (Booking)

我们需要定义一个 Booking 类来表示预约数据,包含姓名、邮箱、日期、时间、服务等信息。

创建 lib/models/booking.dart 文件,定义 Booking 类:

class Booking {
  String name; //  姓名
  String email; //  邮箱
  DateTime date; //  预约日期
  TimeOfDay time; //  预约时间
  String service; //  预约服务

  Booking({
    required this.name,
    required this.email,
    required this.date,
    required this.time,
    required this.service,
  });
}

代码解释:

  • Booking: 定义了 Booking 类,包含 name (姓名), email (邮箱), date (预约日期), time (预约时间), service (预约服务) 等属性。
  • DateTime date: 使用 DateTime 类型表示预约日期。
  • TimeOfDay time: 使用 TimeOfDay 类型表示预约时间。

步骤 3: 创建预约表单页面 (BookingFormScreen)

创建 lib/screens/booking_form_screen.dart 文件,定义 BookingFormScreen Widget,用于构建预约表单界面。

创建 lib/screens/booking_form_screen.dart 文件:

import 'package:flutter/material.dart';

import '../models/booking.dart'; // 导入 Booking 类

class BookingFormScreen extends StatefulWidget {
  static const routeName = '/booking-form'; // 定义命名路由名称

  const BookingFormScreen({super.key});

  
  State<BookingFormScreen> createState() => _BookingFormScreenState();
}

class _BookingFormScreenState extends State<BookingFormScreen> {
  final _formKey = GlobalKey<FormState>(); //  创建 GlobalKey<FormState>
  final _nameController = TextEditingController(); // 姓名输入框控制器
  final _emailController = TextEditingController(); // 邮箱输入框控制器
  DateTime? _selectedDate; //  选中的日期
  TimeOfDay? _selectedTime; //  选中的时间
  String? _selectedService; //  选中的服务

  final List<String> _services = ['理发', 'SPA', '按摩', '美甲']; //  服务列表

  void _submitForm() { //  提交表单的方法
    if (_formKey.currentState!.validate()) { //  验证表单
      _formKey.currentState!.save(); //  保存表单数据

      final booking = Booking( //  创建 Booking 对象
        name: _nameController.text,
        email: _emailController.text,
        date: _selectedDate!,
        time: _selectedTime!,
        service: _selectedService!,
      );

      //  TODO:  处理预约数据 (例如,发送到后台服务器)
      print('预约信息:'); //  打印预约信息到控制台,仅为演示
      print('姓名: ${booking.name}');
      print('邮箱: ${booking.email}');
      print('日期: ${_selectedDate!.toLocal()}'); //  转换为本地时间格式
      print('时间: ${_selectedTime!.format(context)}'); //  格式化时间
      print('服务: ${booking.service}');

      //  显示预约成功提示 (SnackBar)
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('预约成功!'),
          duration: Duration(seconds: 2),
        ),
      );
    }
  }

  Future<void> _selectDate(BuildContext context) async { //  选择日期的方法
    final DateTime? pickedDate = await showDatePicker(
      context: context,
      initialDate: DateTime.now(),
      firstDate: DateTime.now(),
      lastDate: DateTime(DateTime.now().year + 1),
    );
    if (pickedDate != null && pickedDate != _selectedDate) { //  判断是否选择了日期
      setState(() {
        _selectedDate = pickedDate;
      });
    }
  }

  Future<void> _selectTime(BuildContext context) async { // 选择时间的方法
    final TimeOfDay? pickedTime = await showTimePicker(
      context: context,
      initialTime: TimeOfDay.now(),
    );
    if (pickedTime != null && pickedTime != _selectedTime) { //  判断是否选择了时间
      setState(() {
        _selectedTime = pickedTime;
      });
    }
  }

  
  void dispose() { // 释放控制器
    _nameController.dispose();
    _emailController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('预约服务'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form( //  使用 Form Widget 包裹表单
          key: _formKey, //  关联 GlobalKey<FormState>
          child: Column( //  表单内容使用 Column 垂直布局
            children: <Widget>[
              TextFormField( // 姓名输入框
                decoration: const InputDecoration(labelText: '姓名 *'),
                controller: _nameController,
                validator: (value) { //  添加验证器
                  if (value == null || value.isEmpty) { //  必填验证
                    return '姓名不能为空';
                  }
                  return null; //  验证通过,返回 null
                },
                onSaved: (value) { //  添加保存回调
                  //  保存姓名 (本例中无需额外保存,直接使用 controller.text)
                },
              ),
              TextFormField( //  邮箱输入框
                decoration: const InputDecoration(labelText: '邮箱 *'),
                controller: _emailController,
                keyboardType: TextInputType.emailAddress, //  设置键盘类型为邮箱
                validator: (value) { //  添加验证器
                  if (value == null || value.isEmpty) { //  必填验证
                    return '邮箱不能为空';
                  }
                  if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') //  邮箱格式验证
                          .hasMatch(value)) {
                    return '邮箱格式不正确';
                  }
                  return null; //  验证通过,返回 null
                },
                onSaved: (value) { //  添加保存回调
                  //  保存邮箱 (本例中无需额外保存,直接使用 controller.text)
                },
              ),
              Row( //  日期和时间选择器 Row 水平布局
                children: <Widget>[
                  Expanded( //  日期选择器 Expanded 占据剩余空间
                    child: Row( //  日期选择器 Row
                      children: <Widget>[
                        Text(_selectedDate == null //  显示选中的日期或默认提示
                            ? '请选择日期 *'
                            : '已选日期:${_selectedDate!.toLocal().toString().split(' ')[0]}'), //  只显示年月日
                        IconButton( //  日期选择按钮
                          icon: const Icon(Icons.calendar_today),
                          onPressed: () => _selectDate(context), //  绑定选择日期方法
                        ),
                      ],
                    ),
                  ),
                  Expanded( // 时间选择器 Expanded 占据剩余空间
                    child: Row( //  时间选择器 Row
                      children: <Widget>[
                        Text(_selectedTime == null //  显示选中的时间或默认提示
                            ? '请选择时间 *'
                            : '已选时间:${_selectedTime!.format(context)}'), //  格式化显示时间
                        IconButton( //  时间选择按钮
                          icon: const Icon(Icons.access_time),
                          onPressed: () => _selectTime(context), // 绑定选择时间方法
                        ),
                      ],
                    ),
                  ),
                ],
              ),
              DropdownButtonFormField<String>( //  服务选择下拉菜单
                decoration: const InputDecoration(labelText: '选择服务 *'),
                value: _selectedService, //  当前选中的服务
                items: _services.map((service) => DropdownMenuItem<String>( //  构建下拉菜单项
                      value: service,
                      child: Text(service),
                    )).toList(),
                onChanged: (value) { //  下拉菜单值改变回调
                  setState(() {
                    _selectedService = value;
                  });
                },
                validator: (value) { //  添加验证器
                  if (value == null || value.isEmpty) { //  必填验证
                    return '请选择服务';
                  }
                  return null; //  验证通过,返回 null
                },
                onSaved: (value) { //  添加保存回调
                  //  保存服务 (本例中无需额外保存,直接使用 _selectedService)
                },
              ),
              const SizedBox(height: 20),
              ElevatedButton( //  提交按钮
                onPressed: _submitForm, //  绑定提交表单方法
                child: const Text('提交预约'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

代码解释:

  • final _formKey = GlobalKey<FormState>();: 创建 GlobalKey<FormState> 对象 _formKey,用于关联 Form Widget。
  • TextEditingController: 为姓名和邮箱输入框创建 TextEditingController,方便获取输入框的值。
  • DateTime? _selectedDate;TimeOfDay? _selectedTime;: 定义变量存储选中的日期和时间。
  • String? _selectedService;: 定义变量存储选中的服务。
  • _services: 定义服务列表,用于下拉菜单选项。
  • _submitForm() 方法: 提交表单的方法。
    • if (_formKey.currentState!.validate()) { ... }: 调用 _formKey.currentState!.validate() 验证表单validate() 方法会返回 true (验证通过) 或 false (验证失败)。 只有当验证通过时,才会执行 if 语句块内的代码。
    • _formKey.currentState!.save();: 调用 _formKey.currentState!.save() 保存表单数据save() 方法会触发所有 FormFieldonSaved 回调。
    • 创建 Booking 对象: 使用输入框的值和选择的日期、时间、服务创建 Booking 对象。
    • TODO: 处理预约数据: // TODO: 处理预约数据 (例如,发送到后台服务器): 此处为占位代码,表示后续需要将预约数据发送到后台服务器进行处理。 本篇博客重点在于表单与验证,数据提交后的处理暂不涉及。
    • 打印预约信息到控制台: 打印预约信息到控制台,仅为演示。
    • 显示预约成功提示 (SnackBar): 使用 ScaffoldMessenger.of(context).showSnackBar(...) 显示一个 SnackBar,提示用户预约成功。
  • _selectDate(BuildContext context) 方法: 弹出 DatePicker 选择日期。
  • _selectTime(BuildContext context) 方法: 弹出 TimePicker 选择时间。
  • dispose() 方法: 释放控制器,防止内存泄漏。
  • Form(...): 使用 Form Widget 包裹表单,并使用 key: _formKey 关联 GlobalKey<FormState> _formKey
  • TextFormField(...) (姓名和邮箱): 使用 TextFormField 构建姓名和邮箱输入框。
    • decoration: InputDecoration(...): 设置输入框样式,显示 labelText
    • validator: (value) { ... }: 添加 validator 属性,定义验证规则
      • 姓名 validator: 进行必填验证,如果姓名为空,返回错误提示字符串 '姓名不能为空',否则返回 null (验证通过)。
      • 邮箱 validator: 进行必填验证邮箱格式验证。 首先进行必填验证,如果邮箱为空,返回错误提示字符串 '邮箱不能为空'。 然后使用 RegExp 进行邮箱格式验证,如果邮箱格式不正确,返回错误提示字符串 '邮箱格式不正确',否则返回 null (验证通过)。
    • onSaved: (value) { ... }: 添加 onSaved 属性,设置保存回调。 本例中无需额外保存,直接使用 controller.text 获取输入框的值。 onSaved 回调函数可以留空。
  • Row(...) (日期和时间选择器): 使用 Row 水平布局日期和时间选择器。
    • Expanded(...): 使用 Expanded 使日期和时间选择器平分 Row 的宽度。
    • IconButton(...): 使用 IconButton 触发日期和时间选择器。
    • Text(...): 使用 Text 显示选中的日期和时间,或者默认提示信息。
  • DropdownButtonFormField<String>(...) (服务选择下拉菜单): 使用 DropdownButtonFormField 构建服务选择下拉菜单。
    • decoration: InputDecoration(...): 设置下拉菜单样式,显示 labelText
    • value: _selectedService: 设置下拉菜单的当前选中值。
    • items: _services.map(...).toList(): 使用 _services 列表构建下拉菜单选项。
    • onChanged: (value) { ... }: 下拉菜单值改变回调,更新 _selectedService 变量。
    • validator: (value) { ... }: 添加 validator 属性,进行必填验证。 如果未选择服务,返回错误提示字符串 '请选择服务',否则返回 null (验证通过)。
    • onSaved: (value) { ... }: 添加 onSaved 属性,设置保存回调。 本例中无需额外保存,直接使用 _selectedService 获取选中的服务。 onSaved 回调函数可以留空。
  • ElevatedButton(...) (提交按钮): 使用 ElevatedButton 构建提交按钮,并绑定 _submitForm 方法。

步骤 4: 配置命名路由 (main.dart)

main.dart 文件中配置预约表单页面的命名路由。

修改 main.dart 文件如下:

import 'package:flutter/material.dart';

import './screens/booking_form_screen.dart'; // 导入预约表单页

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Booking App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      initialRoute: BookingFormScreen.routeName, //  设置初始路由为预约表单页
      routes: { //  配置命名路由
        BookingFormScreen.routeName: (context) => const BookingFormScreen(), // 预约表单页路由
      },
    );
  }
}

代码解释:

  • import './screens/booking_form_screen.dart';: 导入预约表单页 BookingFormScreen
  • initialRoute: BookingFormScreen.routeName: 设置初始路由为预约表单页的命名路由 BookingFormScreen.routeName,应用启动时首先显示预约表单页。
  • routes: { ... }: 配置命名路由,将路由名称 BookingFormScreen.routeNameBookingFormScreen Widget 关联起来。

步骤 5: 运行应用,体验专业表单与验证

重新运行应用 (或热重启), 现在,您将体验到使用 Flutter 表单与验证构建的预约应用。

  • 预约表单界面: 应用启动后,首先显示预约表单页面,包含姓名、邮箱、日期、时间、服务等输入项,UI 简洁清晰。
  • 表单验证: 尝试提交表单,如果表单验证失败 (例如,姓名为空、邮箱格式错误、日期/时间未选择、服务未选择),应用会在相应的输入框下方显示错误提示信息,引导用户正确填写表单。
  • 表单提交成功提示: 填写完整的、格式正确的表单信息,点击 “提交预约” 按钮,如果表单验证通过,应用会在底部显示一个 SnackBar,提示 “预约成功!”,并在控制台打印预约信息。
  • 专业的表单交互体验: 整个表单填写、验证、提交流程流畅自然,用户体验专业。

Flutter 表单与验证优势展现 - 用户交互更专业

在这个预约应用项目中,我们深入体验了 Flutter 表单与验证的专业优势:

  • 声明式表单构建: 使用 Flutter 的表单 Widget (例如,Form, TextFormField, DropdownButtonFormField),可以简洁、高效地构建各种复杂的表单界面。
  • 强大的表单验证机制: Flutter 提供了完善的表单验证机制,可以方便地实现各种表单验证规则,确保用户输入数据的有效性。
  • 用户友好的错误提示: Flutter 表单验证可以自动显示错误提示信息,引导用户正确填写表单,提升用户体验。
  • 易于扩展和定制: Flutter 表单验证机制具有良好的扩展性和定制性,可以根据实际需求灵活定制表单和验证规则。

总结

恭喜您完成了 每天一个Flutter开发小项目 系列的第六篇博客的学习! 我们成功地构建了一个实用的预约应用,并深入学习了 Flutter 中专业的表单与验证实践。 通过这个项目,您不仅掌握了 Flutter 表单构建和验证的核心技术,更重要的是,学会了如何使用 Flutter 构建用户交互更专业、用户体验更友好的应用。

掌握表单与验证是 Flutter 应用开发中不可或缺的重要技能,是构建各种用户交互型应用的基础。 Flutter 提供的表单与验证机制强大而灵活,合理运用可以极大地提升 Flutter 应用的专业性和用户体验。

在接下来的 每天一个Flutter开发小项目 系列博客中,我们将继续深入探索 Flutter 的更多高级特性和实战技巧,构建更复杂、更实用的 Flutter 应用,敬请期待! 如果您在学习过程中遇到任何问题,欢迎在评论区留言交流。