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

引言

再次欢迎来到 每天一个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 类:

dart 复制代码
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 文件:

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

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

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

  const BookingFormScreen({super.key});

  @override
  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;
      });
    }
  }

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

  @override
  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 文件如下:

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});

  @override
  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 应用,敬请期待! 如果您在学习过程中遇到任何问题,欢迎在评论区留言交流。

相关推荐
我爱喝伊利7 分钟前
C#中使用System.Net库实现自动发送邮件功能
开发语言·c#
七公子7739 分钟前
网络协议 HTTP、HTTPS、HTTP/1.1、HTTP/2 对比分析
前端·网络·网络协议·http
勘察加熊人1 小时前
angular日历
前端·javascript·angular.js
Min_nna1 小时前
web前端初学Angular由浅入深上手开发项目
前端·typescript·angular.js
NoneCoder1 小时前
JavaScript系列(86)--现代构建工具详解
开发语言·javascript·rust
Zhen (Evan) Wang1 小时前
C#中提供的多种集合类以及适用场景
开发语言·c#
weixin_444009001 小时前
浏览器JS打不上断点,一点就跳到其他文件里。浏览器控制台 js打断点,指定的位置打不上断点,一打就跳到其他地方了。
开发语言·javascript·ecmascript
Ama_tor1 小时前
网页制作10-html,css,javascript初认识の适用XHTML
javascript·css·html
郑祎亦1 小时前
Java 关键字 volatile
java·开发语言·jvm
程序员SKY1 小时前
JavaScript 系列之:垃圾回收机制
javascript