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/cat_provider.dart';
import '../../models/expense_record.dart';

Provider管理数据状态,screenutil处理屏幕适配。

intl用于日期格式化。


有状态组件

支出页面需要维护表单状态:

dart 复制代码
class AddExpenseScreen extends StatefulWidget {
  const AddExpenseScreen({super.key});

  @override
  State<AddExpenseScreen> createState() => _AddExpenseScreenState();
}

不需要传入catId,可以在页面内选择。

StatefulWidget用于管理输入状态。


状态变量定义

State类中声明变量:

dart 复制代码
class _AddExpenseScreenState extends State<AddExpenseScreen> {
  final _formKey = GlobalKey<FormState>();
  final _titleController = TextEditingController();
  final _amountController = TextEditingController();
  final _notesController = TextEditingController();

  ExpenseCategory _category = ExpenseCategory.food;
  DateTime _date = DateTime.now();
  String? _selectedCatId;

默认类别是食品,日期是今天。

_selectedCatId为null表示不关联猫咪。


资源清理

dispose中释放控制器:

dart 复制代码
  @override
  void dispose() {
    _titleController.dispose();
    _amountController.dispose();
    _notesController.dispose();
    super.dispose();
  }

三个TextEditingController都要释放。

这是Flutter开发的基本规范。


页面结构

build方法构建UI:

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监听猫咪列表变化。

Form包裹表单用于统一验证。


类别选择

用ChoiceChip展示类别选项:

dart 复制代码
              Text('支出类别', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
              SizedBox(height: 8.h),
              Wrap(
                spacing: 8.w,
                runSpacing: 8.h,
                children: ExpenseCategory.values.map((cat) {
                  return ChoiceChip(
                    label: Text(_getCategoryString(cat)),
                    selected: _category == cat,
                    onSelected: (selected) {
                      if (selected) setState(() => _category = cat);
                    },
                    selectedColor: Colors.orange[100],
                  );
                }).toList(),
              ),
              SizedBox(height: 16.h),

Wrap让Chip自动换行排列。

选中的Chip用橙色背景突出显示。


名称输入

必填的支出名称:

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 复制代码
              TextFormField(
                controller: _amountController,
                keyboardType: TextInputType.number,
                decoration: const InputDecoration(
                  labelText: '金额 *',
                  border: OutlineInputBorder(),
                  prefixIcon: Icon(Icons.attach_money),
                  prefixText: '¥ ',
                ),
                validator: (value) {
                  if (value?.isEmpty ?? true) return '请输入金额';
                  if (double.tryParse(value!) == null) return '请输入有效金额';
                  return null;
                },
              ),
              SizedBox(height: 16.h),

keyboardType设为number弹出数字键盘。

prefixText显示人民币符号。


日期选择

点击选择支出日期:

dart 复制代码
              InkWell(
                onTap: () => _selectDate(context),
                child: InputDecorator(
                  decoration: const InputDecoration(
                    labelText: '日期',
                    border: OutlineInputBorder(),
                    prefixIcon: Icon(Icons.calendar_today),
                  ),
                  child: Text(DateFormat('yyyy-MM-dd').format(_date)),
                ),
              ),
              SizedBox(height: 16.h),

InputDecorator让显示样式与输入框一致。

点击整个区域触发日期选择。


猫咪关联

下拉选择关联的猫咪:

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),
              ],

只有当有猫咪时才显示这个选项。

可以选择不关联任何猫咪。


备注输入

可选的备注字段:

dart 复制代码
              TextFormField(
                controller: _notesController,
                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: _saveExpense,
                  child: const Text('保存'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

按钮撑满宽度,视觉上更突出。

点击触发保存逻辑。


类别转换方法

枚举转中文:

dart 复制代码
  String _getCategoryString(ExpenseCategory category) {
    switch (category) {
      case ExpenseCategory.food: return '食品';
      case ExpenseCategory.medical: return '医疗';
      case ExpenseCategory.grooming: return '美容';
      case ExpenseCategory.toys: return '玩具';
      case ExpenseCategory.supplies: return '用品';
      case ExpenseCategory.other: return '其他';
    }
  }

switch语句处理每种类别的显示文本。

六种类别覆盖常见的养猫支出。


日期选择方法

弹出日期选择器:

dart 复制代码
  Future<void> _selectDate(BuildContext context) async {
    final picked = await showDatePicker(
      context: context,
      initialDate: _date,
      firstDate: DateTime(2020),
      lastDate: DateTime.now(),
    );
    if (picked != null) setState(() => _date = picked);
  }

lastDate设为今天,不能选择未来的日期。

选择后更新状态刷新UI。


保存支出逻辑

验证并保存数据:

dart 复制代码
  void _saveExpense() {
    if (_formKey.currentState!.validate()) {
      final record = ExpenseRecord(
        catId: _selectedCatId,
        category: _category,
        title: _titleController.text,
        amount: double.parse(_amountController.text),
        date: _date,
        notes: _notesController.text.isEmpty ? null : _notesController.text,
      );

      context.read<CatProvider>().addExpenseRecord(record);
      Navigator.pop(context);
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('支出记录添加成功!')),
      );
    }
  }
}

validate触发表单验证,通过后才保存。

SnackBar给用户一个成功的反馈。


数据模型

ExpenseRecord的结构:

dart 复制代码
class ExpenseRecord {
  final String id;
  final String? catId;
  final ExpenseCategory category;
  final String title;
  final double amount;
  final DateTime date;
  final String? notes;
}

catId和notes是可选字段。

amount用double存储金额。

类别枚举:

dart 复制代码
enum ExpenseCategory {
  food,
  medical,
  grooming,
  toys,
  supplies,
  other,
}

六种类别覆盖养猫的主要支出。

枚举比字符串更类型安全。


金额验证详解

两步验证逻辑:

dart 复制代码
validator: (value) {
  if (value?.isEmpty ?? true) return '请输入金额';
  if (double.tryParse(value!) == null) return '请输入有效金额';
  return null;
},

先检查是否为空。

再检查是否是有效的数字。

tryParse的作用:

dart 复制代码
double.tryParse(value!)

尝试将字符串转为double。

转换失败返回null,不会抛异常。


InputDecoration详解

输入框装饰的各个属性:

dart 复制代码
InputDecoration(
  labelText: '金额 *',        // 标签文字
  border: OutlineInputBorder(), // 边框样式
  prefixIcon: Icon(...),      // 前缀图标
  prefixText: '¥ ',           // 前缀文字
  hintText: '如:猫粮',        // 提示文字
)

labelText在输入时会浮动到上方。

prefixText显示在输入内容前面。


ChoiceChip使用

选择类型的Chip:

dart 复制代码
ChoiceChip(
  label: Text(_getCategoryString(cat)),
  selected: _category == cat,
  onSelected: (selected) {
    if (selected) setState(() => _category = cat);
  },
  selectedColor: Colors.orange[100],
)

selected控制是否选中状态。

onSelected在点击时触发。


Wrap布局

让子组件自动换行:

dart 复制代码
Wrap(
  spacing: 8.w,      // 水平间距
  runSpacing: 8.h,   // 垂直间距
  children: [...],
)

spacing是同一行内的间距。

runSpacing是行与行之间的间距。


条件渲染

只在有猫咪时显示选择框:

dart 复制代码
if (cats.isNotEmpty) ...[
  DropdownButtonFormField(...),
  SizedBox(height: 16.h),
],

展开运算符...让条件渲染更简洁。

没有猫咪时这部分不会渲染。


Provider读写

读取猫咪列表:

dart 复制代码
final cats = context.watch<CatProvider>().cats;

watch会在数据变化时触发重建。

保存支出记录:

dart 复制代码
context.read<CatProvider>().addExpenseRecord(record);

read不会触发重建,适合在回调中使用。


表单验证流程

触发验证:

dart 复制代码
if (_formKey.currentState!.validate()) {
  // 验证通过,执行保存
}

validate方法会触发所有字段的验证。

只有全部通过才返回true。


小结

添加支出页面涵盖了这些知识点:

  • 表单验证和数据保存
  • ChoiceChip类别选择
  • 金额输入和验证
  • 日期选择器使用

这些技巧在其他表单页面也能复用,是Flutter开发的基础技能。


欢迎加入OpenHarmony跨平台开发社区,一起交流Flutter开发经验:

https://openharmonycrossplatform.csdn.net

相关推荐
console.log('npc')2 小时前
vue2 使用高德接口查询天气
前端·vue.js
天马37982 小时前
Canvas 倾斜矩形绘制波浪效果
开发语言·前端·javascript
天天向上10243 小时前
vue3 实现el-table 部分行不让勾选
前端·javascript·vue.js
qx093 小时前
esm模块与commonjs模块相互调用的方法
开发语言·前端·javascript
2601_949613023 小时前
flutter_for_openharmony家庭药箱管理app实战+药品分类实现
大数据·数据库·flutter
摘星编程3 小时前
在OpenHarmony上用React Native:SectionList吸顶分组标题
javascript·react native·react.js
Mr Xu_4 小时前
前端实战:基于Element Plus的CustomTable表格组件封装与应用
前端·javascript·vue.js·elementui
0思必得04 小时前
[Web自动化] 爬虫之API请求
前端·爬虫·python·selenium·自动化
混迹在开发队伍里的伪开发4 小时前
css的var用法,定义属性,全局使用
前端·css