
养猫需要记住的事情太多了,喂食、驱虫、疫苗、美容...稍不注意就会忘记。今天我们来实现一个添加提醒的功能,帮助铲屎官们管理各种待办事项。
功能规划
添加提醒页面需要支持:
- 选择提醒类型(喂食、用药、疫苗等)
- 输入提醒标题和描述
- 设置提醒时间
- 选择重复周期
- 关联特定猫咪
这些功能组合起来,就能满足日常的提醒需求了。
依赖引入
先把需要的包导入:
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../providers/reminder_provider.dart';
import '../../providers/cat_provider.dart';
import '../../models/reminder_model.dart';
这个页面需要用到两个Provider,一个管理提醒,一个获取猫咪列表。
intl用于格式化日期时间的显示。
有状态组件
添加页面需要维护表单状态:
dart
class AddReminderScreen extends StatefulWidget {
const AddReminderScreen({super.key});
@override
State<AddReminderScreen> createState() => _AddReminderScreenState();
}
这个页面不需要传入catId,因为可以在页面内选择关联的猫咪。
StatefulWidget用于管理表单的各种状态。
状态变量定义
State类中声明需要的变量:
dart
class _AddReminderScreenState extends State<AddReminderScreen> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
ReminderType _type = ReminderType.feeding;
RepeatType _repeatType = RepeatType.none;
DateTime _dateTime = DateTime.now().add(const Duration(hours: 1));
String? _selectedCatId;
默认提醒时间是一小时后,这样比较合理。
_selectedCatId可以为null,表示不关联任何猫咪。
资源清理
dispose中释放控制器:
dart
@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
TextEditingController使用完要释放。
这是Flutter开发的基本规范。
页面结构搭建
build方法构建整体布局:
dart
@override
Widget build(BuildContext context) {
final cats = context.watch<CatProvider>().cats;
return Scaffold(
appBar: AppBar(title: const Text('添加提醒')),
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
context.watch监听CatProvider,猫咪列表变化时自动刷新。
Form包裹表单内容,用于统一验证。
提醒类型选择
用ChoiceChip展示类型选项:
dart
Text('提醒类型', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
SizedBox(height: 8.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: ReminderType.values.map((type) {
return ChoiceChip(
label: Text(_getTypeString(type)),
selected: _type == type,
onSelected: (selected) {
if (selected) setState(() => _type = type);
},
selectedColor: Colors.orange[100],
);
}).toList(),
),
SizedBox(height: 16.h),
Wrap组件让Chip自动换行,适应不同屏幕。
runSpacing控制行与行之间的间距。
标题输入框
必填的标题字段:
dart
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '提醒标题 *',
hintText: '如:喂食提醒',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.title),
),
validator: (value) => value?.isEmpty ?? true ? '请输入标题' : null,
),
SizedBox(height: 16.h),
hintText给出输入示例,帮助用户理解。
validator验证非空,星号表示必填。
猫咪关联选择
下拉选择关联的猫咪:
dart
if (cats.isNotEmpty) ...[
DropdownButtonFormField<String?>(
value: _selectedCatId,
decoration: const InputDecoration(
labelText: '关联猫咪 (选填)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.pets),
),
items: [
const DropdownMenuItem(value: null, child: Text('不关联')),
...cats.map((cat) => DropdownMenuItem(value: cat.id, child: Text(cat.name))),
],
onChanged: (value) => setState(() => _selectedCatId = value),
),
SizedBox(height: 16.h),
],
只有当有猫咪时才显示这个选择框。
第一个选项是"不关联",value为null。
时间选择器
点击选择提醒时间:
dart
InkWell(
onTap: () => _selectDateTime(context),
child: InputDecorator(
decoration: const InputDecoration(
labelText: '提醒时间 *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.access_time),
),
child: Text(DateFormat('yyyy-MM-dd HH:mm').format(_dateTime)),
),
),
SizedBox(height: 16.h),
InputDecorator让显示样式与输入框一致。
点击整个区域都能触发时间选择。
重复周期选择
设置提醒的重复规则:
dart
DropdownButtonFormField<RepeatType>(
value: _repeatType,
decoration: const InputDecoration(
labelText: '重复',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.repeat),
),
items: RepeatType.values.map((type) {
return DropdownMenuItem(value: type, child: Text(_getRepeatString(type)));
}).toList(),
onChanged: (value) => setState(() => _repeatType = value!),
),
SizedBox(height: 16.h),
支持不重复、每天、每周、每月四种选项。
下拉框比ChoiceChip更适合这种场景。
备注输入
可选的描述字段:
dart
TextFormField(
controller: _descriptionController,
maxLines: 2,
decoration: const InputDecoration(
labelText: '备注 (选填)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.note),
),
),
SizedBox(height: 32.h),
maxLines: 2让输入框有两行高度。
备注是选填的,不需要验证。
保存按钮
底部的提交按钮:
dart
SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
onPressed: _saveReminder,
child: const Text('保存'),
),
),
],
),
),
),
);
}
按钮撑满宽度,视觉上更突出。
点击触发保存逻辑。
类型转换方法
提醒类型转中文:
dart
String _getTypeString(ReminderType type) {
switch (type) {
case ReminderType.feeding: return '喂食';
case ReminderType.medication: return '用药';
case ReminderType.vaccination: return '疫苗';
case ReminderType.deworming: return '驱虫';
case ReminderType.grooming: return '美容';
case ReminderType.veterinary: return '就医';
case ReminderType.other: return '其他';
}
}
switch语句处理每种类型的显示文本。
涵盖了养猫常见的各种提醒场景。
重复类型转中文:
dart
String _getRepeatString(RepeatType type) {
switch (type) {
case RepeatType.none: return '不重复';
case RepeatType.daily: return '每天';
case RepeatType.weekly: return '每周';
case RepeatType.monthly: return '每月';
}
}
四种重复选项基本能满足日常需求。
不重复适合一次性的提醒。
日期时间选择
分两步选择日期和时间:
dart
Future<void> _selectDateTime(BuildContext context) async {
final date = await showDatePicker(
context: context,
initialDate: _dateTime,
firstDate: DateTime.now(),
lastDate: DateTime(2100),
);
if (date != null) {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(_dateTime),
);
if (time != null) {
setState(() {
_dateTime = DateTime(date.year, date.month, date.day, time.hour, time.minute);
});
}
}
}
firstDate设为当前时间,不能设置过去的提醒。
日期和时间分开选择,用户体验更好。
保存提醒逻辑
验证并保存数据:
dart
void _saveReminder() {
if (_formKey.currentState!.validate()) {
final reminder = ReminderModel(
catId: _selectedCatId,
type: _type,
title: _titleController.text,
description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
dateTime: _dateTime,
repeatType: _repeatType,
);
context.read<ReminderProvider>().addReminder(reminder);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('提醒添加成功!')),
);
}
}
}
validate触发表单验证,通过后才保存。
SnackBar给用户一个成功的反馈。
数据模型设计
ReminderModel的结构:
dart
class ReminderModel {
final String id;
final String? catId;
final ReminderType type;
final String title;
final String? description;
final DateTime dateTime;
final RepeatType repeatType;
final bool isCompleted;
}
catId和description是可选字段。
isCompleted用于标记提醒是否已完成。
枚举类型定义
提醒类型枚举:
dart
enum ReminderType {
feeding,
medication,
vaccination,
deworming,
grooming,
veterinary,
other,
}
枚举让类型更加类型安全。
比用字符串更不容易出错。
重复类型枚举:
dart
enum RepeatType {
none,
daily,
weekly,
monthly,
}
四种重复周期覆盖常见场景。
none表示一次性提醒。
Provider使用
读取猫咪列表:
dart
final cats = context.watch<CatProvider>().cats;
watch会在数据变化时触发重建。
这样新添加的猫咪也能出现在选择列表中。
保存提醒:
dart
context.read<ReminderProvider>().addReminder(reminder);
read不会触发重建,适合在回调中使用。
addReminder方法会通知所有监听者。
条件渲染技巧
只在有猫咪时显示选择框:
dart
if (cats.isNotEmpty) ...[
DropdownButtonFormField(...),
SizedBox(height: 16.h),
],
展开运算符...让条件渲染更简洁。
如果没有猫咪,这部分UI就不会显示。
ChoiceChip vs Dropdown
什么时候用ChoiceChip:
dart
Wrap(
children: ReminderType.values.map((type) {
return ChoiceChip(
label: Text(_getTypeString(type)),
selected: _type == type,
...
);
}).toList(),
)
选项较多且需要一目了然时用ChoiceChip。
用户可以直接看到所有选项。
什么时候用Dropdown:
dart
DropdownButtonFormField<RepeatType>(
value: _repeatType,
items: RepeatType.values.map((type) {
return DropdownMenuItem(...);
}).toList(),
...
)
选项较少或不需要同时展示时用Dropdown。
节省屏幕空间。
表单验证要点
必填字段的验证:
dart
validator: (value) => value?.isEmpty ?? true ? '请输入标题' : null,
返回字符串表示验证失败,显示错误信息。
返回null表示验证通过。
触发验证:
dart
if (_formKey.currentState!.validate()) {
// 验证通过,执行保存
}
validate方法会触发所有字段的验证。
只有全部通过才返回true。
小结
添加提醒页面涵盖了这些知识点:
- 多种表单组件的使用
- 日期时间选择器
- 条件渲染和状态管理
- 表单验证和数据保存
这些技巧在其他表单页面也能复用,是Flutter开发的基础技能。
欢迎加入OpenHarmony跨平台开发社区,一起交流Flutter开发经验: