Flutter for OpenHarmony 猫咪管家App实战 - 添加提醒实现

养猫需要记住的事情太多了,喂食、驱虫、疫苗、美容...稍不注意就会忘记。今天我们来实现一个添加提醒的功能,帮助铲屎官们管理各种待办事项。


功能规划

添加提醒页面需要支持:

  • 选择提醒类型(喂食、用药、疫苗等)
  • 输入提醒标题和描述
  • 设置提醒时间
  • 选择重复周期
  • 关联特定猫咪

这些功能组合起来,就能满足日常的提醒需求了。


依赖引入

先把需要的包导入:

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:

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开发经验:

https://openharmonycrossplatform.csdn.net

相关推荐
Yolanda942 小时前
【项目经验】vue h5移动端禁止缩放
前端·javascript·vue.js
时光慢煮2 小时前
【Flutter × OpenHarmony】跨端开发实现全局Toast提示卡片
flutter·华为·开源·openharmony
IT陈图图2 小时前
Flutter × OpenHarmony 混合布局实战:在一个容器中优雅组合列表与网格
flutter·鸿蒙·openharmony
广州华水科技3 小时前
单北斗GNSS形变监测一体机在基础设施安全中的应用与技术优势
前端
EndingCoder3 小时前
案例研究:从 JavaScript 迁移到 TypeScript
开发语言·前端·javascript·性能优化·typescript
2603_949462104 小时前
Flutter for OpenHarmony社团管理App实战:意见反馈实现
android·flutter
阿珊和她的猫5 小时前
React 路由:构建单页面应用的导航系统
前端·react.js·状态模式
Amumu121385 小时前
Vue脚手架(二)
前端·javascript·vue.js
花间相见5 小时前
【LangChain】—— Prompt、Model、Chain与多模型执行链
前端·langchain·prompt