Flutter for OpenHarmony 实战:打造功能完整的记账助手应用

摘要在这里插入图片描述

记账应用是移动应用中最实用的工具类应用之一。本文将详细介绍如何使用Flutter for OpenHarmony框架开发一款功能完整的记账助手应用。文章涵盖了数据模型设计、数据持久化方案、UI界面设计、收支统计等核心技术点。通过本文学习,读者将掌握Flutter在鸿蒙平台上开发工具类应用的完整流程,了解数据持久化和状态管理的最佳实践。


一、项目背景与功能概述

1.1 记账应用的需求分析

个人财务管理是现代生活中的重要组成部分,一款优秀的记账应用应该满足以下核心需求:

基础功能模块

  • 收入/支出记录添加
  • 交易历史查看
  • 余额实时统计
  • 分类管理
  • 数据持久化存储

用户体验要求

  • 简洁直观的操作界面
  • 快速添加记录
  • 清晰的数据展示
  • 数据安全保障

1.2 为什么选择Flutter for OpenHarmony

Flutter在开发工具类应用时具有独特优势:

开发效率高

  • 热重载功能加速UI调试
  • 丰富的Widget组件库
  • 声明式UI简化开发

跨平台一致性

  • 一套代码多端运行
  • 统一的用户体验
  • 降低维护成本

性能优异

  • 原生性能表现
  • 流畅的动画效果
  • 高效的渲染机制

1.3 核心功能规划

功能模块 具体功能
收支管理 添加收入/支出记录、删除记录
数据统计 总余额、总收入、总支出计算
分类管理 支出8个分类、收入5个分类
数据存储 本地持久化存储
界面展示 卡片式布局、列表展示

二、技术选型与架构设计

2.1 技术栈选择

数据持久化方案

  • shared_preferences:轻量级键值对存储
  • 适合小规模数据存储
  • 支持基本数据类型

日期格式化

  • intl包:国际化日期格式化
  • 支持多种日期格式

状态管理

  • StatefulWidget:基础状态管理
  • setState:简单直接的状态更新

2.2 应用架构设计

采用MVVM风格的分层架构:

表现层 (Presentation Layer)
├── HomePage (主页面)
├── BalanceCard (余额卡片)
├── TransactionList (交易列表)
└── AddTransactionDialog (添加对话框)

业务逻辑层 (Business Logic Layer)
├── _loadData (数据加载)
├── _saveData (数据保存)
├── _calculateTotals (统计计算)
└── _addTransaction (添加交易)

数据层 (Data Layer)
├── Transaction (数据模型)
├── SharedPreferences (存储接口)
└── JSON序列化 (数据转换)

2.3 数据流设计

在这里插入图片描述


三、数据模型设计

3.1 Transaction模型类设计

交易记录是应用的核心数据模型:

class Transaction {
  final String id;           // 唯一标识
  final String title;        // 标题
  final double amount;       // 金额
  final String category;     // 分类
  final bool isExpense;      // 是否为支出
  final DateTime date;       // 日期时间

  Transaction({
    required this.id,
    required this.title,
    required this.amount,
    required this.category,
    required this.isExpense,
    required this.date,
  });
}

3.2 JSON序列化实现

为了实现数据持久化,需要实现JSON序列化:

// 序列化方法
Map<String, dynamic> toJson() {
  return {
    'id': id,
    'title': title,
    'amount': amount,
    'category': category,
    'isExpense': isExpense,
    'date': date.millisecondsSinceEpoch,
  };
}

// 反序列化工厂方法
factory Transaction.fromJson(Map<String, dynamic> json) {
  return Transaction(
    id: json['id'] as String,
    title: json['title'] as String,
    amount: json['amount'] as double,
    category: json['category'] as String,
    isExpense: json['isExpense'] as bool,
    date: DateTime.fromMillisecondsSinceEpoch(json['date'] as int),
  );
}

3.3 分类数据设计

预定义分类列表,便于用户选择:

支出分类

final List<String> _expenseCategories = [
  '餐饮', '交通', '购物', '娱乐',
  '医疗', '教育', '住房', '其他'
];

在这里插入图片描述

收入分类

final List<String> _incomeCategories = [
  '工资', '奖金', '投资', '兼职', '其他'
];

在这里插入图片描述


四、UI界面设计与实现

4.1 整体布局结构

应用采用垂直滚动的单页面布局:

AppBar (顶部导航栏)
    ↓
BalanceCard (余额卡片)
    ↓
TransactionList (交易列表)
    ↓
FloatingActionButton (添加按钮)

4.2 余额卡片设计

余额卡片是应用的视觉焦点,展示关键财务信息:

Widget _buildBalanceCard() {
  return Container(
    margin: const EdgeInsets.all(16),
    padding: const EdgeInsets.all(20),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
        colors: [
          Theme.of(context).colorScheme.primary,
          Theme.of(context).colorScheme.primary.withAlpha(179),
        ],
      ),
      borderRadius: BorderRadius.circular(16),
      boxShadow: [
        BoxShadow(
          color: Theme.of(context).colorScheme.primary.withAlpha(77),
          blurRadius: 10,
          spreadRadius: 2,
        ),
      ],
    ),
    child: Column(
      children: [
        const Text('总余额'),
        Text($_totalBalance'),
        Row(
          children: [
            _buildSummaryItem('收入', $_totalIncome', Colors.green[300]!),
            _buildSummaryItem('支出', $_totalExpense', Colors.red[300]!),
          ],
        ),
      ],
    ),
  );
}

设计要点

  • 渐变背景增强视觉效果
  • 大字号突出显示余额
  • 收支对比一目了然
  • 圆角和阴影营造层次感

4.3 交易列表设计

使用ListView.builder构建高效的滚动列表:

ListView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: _transactions.length,
  itemBuilder: (context, index) {
    // 按日期降序排列
    final sortedTransactions = List.from(_transactions)
      ..sort((a, b) => b.date.compareTo(a.date));
    final sortedTransaction = sortedTransactions[index];

    return _buildTransactionCard(sortedTransaction);
  },
)

4.4 交易卡片组件

每条交易记录以卡片形式展示:

Widget _buildTransactionCard(Transaction transaction) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    elevation: 2,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    child: ListTile(
      leading: CircleAvatar(
        backgroundColor: transaction.isExpense
            ? Colors.red[100]
            : Colors.green[100],
        child: Icon(
          transaction.isExpense ? Icons.arrow_upward : Icons.arrow_downward,
          color: transaction.isExpense ? Colors.red : Colors.green,
        ),
      ),
      title: Text(transaction.title),
      subtitle: Text(
        '${transaction.category} · ${DateFormat('yyyy-MM-dd HH:mm').format(transaction.date)}',
      ),
      trailing: Column(
        children: [
          Text(
            '${transaction.isExpense ? '-' : '+'}¥${transaction.amount.toStringAsFixed(2)}',
            style: TextStyle(
              color: transaction.isExpense ? Colors.red : Colors.green,
            ),
          ),
          IconButton(
            icon: const Icon(Icons.delete_outline),
            onPressed: () => _deleteTransaction(transaction.id),
          ),
        ],
      ),
    ),
  );
}

设计特点

  • 圆形头像区分收支类型
  • 红绿色彩直观展示
  • 日期格式化显示
  • 快捷删除操作

[此处插入图片:记账应用界面截图]


五、数据持久化实现

5.1 SharedPreferences简介

SharedPreferences是Flutter平台上常用的轻量级存储方案:

特点

  • 键值对存储方式
  • 支持基本数据类型
  • 异步读写操作
  • 数据自动持久化

适用场景

  • 用户偏好设置
  • 小规模数据存储
  • 缓存临时数据

5.2 数据加载实现

Future<void> _loadData() async {
  final prefs = await SharedPreferences.getInstance();
  final transactionsJson = prefs.getStringList('transactions') ?? [];

  setState(() {
    _transactions = transactionsJson.map((json) {
      return Transaction.fromJson(
        jsonDecode(json) as Map<String, dynamic>
      );
    }).toList();
    _calculateTotals();
  });
}

加载流程

  1. 获取SharedPreferences实例
  2. 读取存储的JSON字符串列表
  3. 反序列化为Transaction对象
  4. 更新状态并计算统计

5.3 数据保存实现

Future<void> _saveData() async {
  final prefs = await SharedPreferences.getInstance();
  final transactionsJson = _transactions.map((t) {
    return jsonEncode(t.toJson());
  }).toList();

  await prefs.setStringList('transactions', transactionsJson);
}

保存流程

  1. 遍历交易列表
  2. 序列化为JSON字符串
  3. 保存到本地存储

5.4 统计计算逻辑

void _calculateTotals() {
  _totalIncome = 0;
  _totalExpense = 0;

  for (var transaction in _transactions) {
    if (transaction.isExpense) {
      _totalExpense += transaction.amount;
    } else {
      _totalIncome += transaction.amount;
    }
  }

  _totalBalance = _totalIncome - _totalExpense;
}

六、完整代码实现

6.1 添加记录对话框

void _showAddTransactionDialog() {
  bool isExpense = true;
  final titleController = TextEditingController();
  final amountController = TextEditingController();
  String selectedCategory = _expenseCategories[0];

  showDialog(
    context: context,
    builder: (context) {
      return StatefulBuilder(
        builder: (context, setDialogState) {
          return AlertDialog(
            title: const Text('添加记录'),
            content: SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  // 收支类型切换
                  SegmentedButton<bool>(
                    segments: const [
                      ButtonSegment(
                        value: true,
                        label: Text('支出'),
                        icon: Icon(Icons.arrow_upward),
                      ),
                      ButtonSegment(
                        value: false,
                        label: Text('收入'),
                        icon: Icon(Icons.arrow_downward),
                      ),
                    ],
                    selected: {isExpense},
                    onSelectionChanged: (Set<bool> newSelection) {
                      setDialogState(() {
                        isExpense = newSelection.first;
                        selectedCategory = isExpense
                            ? _expenseCategories[0]
                            : _incomeCategories[0];
                      });
                    },
                  ),
                  const SizedBox(height: 16),

                  // 标题输入框
                  TextField(
                    controller: titleController,
                    decoration: const InputDecoration(
                      labelText: '标题',
                      border: OutlineInputBorder(),
                      prefixIcon: Icon(Icons.edit),
                    ),
                  ),
                  const SizedBox(height: 16),

                  // 金额输入框
                  TextField(
                    controller: amountController,
                    decoration: const InputDecoration(
                      labelText: '金额',
                      border: OutlineInputBorder(),
                      prefixIcon: Icon(Icons.attach_money),
                    ),
                    keyboardType: TextInputType.number,
                  ),
                  const SizedBox(height: 16),

                  // 分类选择器
                  DropdownButtonFormField<String>(
                    value: selectedCategory,
                    decoration: const InputDecoration(
                      labelText: '分类',
                      border: OutlineInputBorder(),
                      prefixIcon: Icon(Icons.category),
                    ),
                    items: (isExpense ? _expenseCategories : _incomeCategories)
                        .map((category) {
                      return DropdownMenuItem(
                        value: category,
                        child: Text(category),
                      );
                    }).toList(),
                    onChanged: (value) {
                      if (value != null) {
                        setDialogState(() {
                          selectedCategory = value;
                        });
                      }
                    },
                  ),
                ],
              ),
            ),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('取消'),
              ),
              ElevatedButton(
                onPressed: () {
                  // 表单验证
                  if (titleController.text.isEmpty ||
                      amountController.text.isEmpty) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('请填写完整信息')),
                    );
                    return;
                  }

                  final amount = double.tryParse(amountController.text) ?? 0;
                  if (amount <= 0) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('请输入有效金额')),
                    );
                    return;
                  }

                  // 创建交易记录
                  final transaction = Transaction(
                    id: DateTime.now().millisecondsSinceEpoch.toString(),
                    title: titleController.text,
                    amount: amount,
                    category: selectedCategory,
                    isExpense: isExpense,
                    date: DateTime.now(),
                  );

                  _addTransaction(transaction);
                  Navigator.pop(context);
                },
                child: const Text('保存'),
              ),
            ],
          );
        },
      );
    },
  );
}

6.2 空状态处理

当没有交易记录时显示友好的空状态提示:

child: _transactions.isEmpty
    ? const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.receipt_long, size: 64, color: Colors.grey),
            SizedBox(height: 16),
            Text(
              '暂无记录',
              style: TextStyle(fontSize: 16, color: Colors.grey),
            ),
            SizedBox(height: 8),
            Text(
              '点击右下角按钮添加第一笔记录',
              style: TextStyle(fontSize: 14, color: Colors.grey),
            ),
          ],
        ),
      )
    : ListView.builder(...)

6.3 清空确认对话框

void _showClearDialog() {
  if (_transactions.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('暂无记录可清空')),
    );
    return;
  }

  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: const Text('确认清空'),
        content: const Text('确定要清空所有记录吗?此操作不可恢复。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              setState(() {
                _transactions.clear();
                _calculateTotals();
              });
              _saveData();
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('已清空所有记录')),
              );
            },
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            child: const Text('清空'),
          ),
        ],
      );
    },
  );
}

在这里插入图片描述


七、运行效果与测试

7.1 项目运行命令

cd E:\HarmonyOS\oh.code\expense_tracker
flutter run -d ohos

7.2 功能测试清单

基础功能测试

  • 添加支出记录是否正常
  • 添加收入记录是否正常
  • 删除记录功能是否正常
  • 余额统计是否准确

UI交互测试

  • 空状态提示是否显示
  • 收支切换是否流畅
  • 表单验证是否生效
  • 日期格式是否正确

数据持久化测试

  • 重启应用后数据是否保留
  • 添加记录后是否正确保存
  • 删除记录后是否正确更新

7.3 性能考虑

ListView优化

  • 使用ListView.builder懒加载
  • 避免不必要的widget重建

数据计算优化

  • 只在数据变化时计算统计
  • 缓存计算结果

八、总结

本文详细介绍了使用Flutter for OpenHarmony开发记账助手应用的完整过程,涵盖了以下核心技术点:

  1. 数据模型设计:定义Transaction类,实现JSON序列化
  2. 数据持久化:使用SharedPreferences实现本地存储
  3. UI设计:卡片式布局、列表展示、对话框交互
  4. 状态管理:使用setState管理应用状态
  5. 数据统计:实现收支计算和余额统计

这个项目展示了Flutter在工具类应用开发中的完整流程,代码结构清晰,功能完整。读者可以基于此项目添加更多功能,如:

  • 图表可视化展示
  • 数据导出功能
  • 搜索和筛选功能
  • 月度报表统计
  • 预算管理功能

通过本文的学习,读者应该能够独立开发类似的工具类应用,掌握Flutter在鸿蒙平台上的数据持久化和状态管理技巧。


欢迎加入开源鸿蒙跨平台社区: 开源鸿蒙跨平台开发者社区

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐