目录

  1. 简介
  2. 核心概念
  3. 项目架构
  4. 组件详解
  5. 实现细节
  6. 使用示例
  7. 最佳实践
  8. 总结

简介

什么是 OpenHarmony?

OpenHarmony 是一个开放的、去中心化的金融生态系统,旨在为用户提供透明、安全且高效的财务管理解决方案。它强调用户对自己资产的完全控制权,并通过智能合约和区块链技术确保交易的安全性。

为什么选择 Flutter?

Flutter 是 Google 推出的跨平台 UI 框架,具有以下优势:

  • 高性能:使用 Dart 语言,编译为原生代码
  • 跨平台:一套代码可运行于 iOS、Android、Web 等多个平台
  • 丰富的组件库:Material Design 和 Cupertino 风格组件
  • 热重载:快速迭代开发体验
  • 活跃社区:大量第三方包和最佳实践

本文目标

本文将详细介绍如何使用 Flutter 构建一个功能完整的 OpenHarmony 钱包应用。


核心概念

1. 数据模型

交易模型(Transaction)

class Transaction {
  final String id;              // 唯一标识符
  final String title;           // 交易说明
  final double amount;          // 交易金额
  final DateTime date;          // 交易时间
  final TransactionType type;   // 交易类型(收入/支出)
  final String category;        // 分类标签

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

说明

  • id:使用时间戳确保唯一性,便于快速查询和删除
  • title:用户输入的交易描述,帮助回忆交易内容
  • amount:使用 double 类型存储金额,支持小数点
  • date:记录交易发生的精确时间
  • type:通过枚举区分收入和支出,类型安全
  • category:便于后续的分类统计和分析

交易类型枚举

enum TransactionType { income, expense }

说明:使用枚举而不是字符串,可以避免拼写错误,提高代码的类型安全性。


项目架构

整体结构

lib/
├── main.dart                    # 应用入口
├── openharmony_wallet_widget.dart        # OpenHarmony 钱包组件
└── models/                      # 数据模型(可选)

架构设计原则

  1. 单一职责原则:每个组件只负责一个功能
  2. 组件化:将 UI 拆分为可复用的小组件
  3. 状态管理:使用 StatefulWidget 管理钱包状态
  4. 数据驱动:UI 由数据驱动,数据变化自动更新 UI

组件详解

OpenHarmonyWallet 组件

这是整个应用的核心组件,继承自 StatefulWidget,用于管理钱包的所有状态和交互。

组件定义
class OpenHarmonyWallet extends StatefulWidget {
  final double initialBalance;
  final List<Transaction> transactions;
  final Function(Transaction)? onTransactionTap;

  const OpenMoneyWallet({
    Key? key,
    this.initialBalance = 0.0,
    this.transactions = const [],
    this.onTransactionTap,
  }) : super(key: key);

  
  State<OpenHarmonyWallet> createState() => _OpenHarmonyWalletState();
}

参数说明

  • initialBalance:初始余额,默认为 0.0
  • transactions:初始交易列表,默认为空列表
  • onTransactionTap:交易添加时的回调函数
状态管理
class _OpenHarmonyWalletState extends State<OpenHarmonyWallet> {
  late double _currentBalance;
  late List<Transaction> _transactions;

  
  void initState() {
    super.initState();
    _currentBalance = widget.initialBalance;
    _transactions = List.from(widget.transactions);
    _calculateBalance();
  }

说明

  • _currentBalance:实时余额,使用 late 关键字延迟初始化
  • _transactions:本地交易列表副本,避免直接修改 widget 参数
  • initState:在组件初始化时计算初始余额

核心方法

1. 余额计算
void _calculateBalance() {
  double balance = widget.initialBalance;
  for (var transaction in _transactions) {
    if (transaction.type == TransactionType.income) {
      balance += transaction.amount;
    } else {
      balance -= transaction.amount;
    }
  }
  setState(() {
    _currentBalance = balance;
  });
}

工作流程

  1. 从初始余额开始
  2. 遍历所有交易记录
  3. 根据交易类型(收入/支出)更新余额
  4. 调用 setState() 触发 UI 重新构建
2. 添加交易
void addTransaction(Transaction transaction) {
  setState(() {
    _transactions.insert(0, transaction);
    _calculateBalance();
  });
}

说明

  • insert(0, ...) 将新交易插入列表头部,最新交易显示在最上面
  • 自动重新计算余额
  • 触发 UI 更新
3. 删除交易
void removeTransaction(String transactionId) {
  setState(() {
    _transactions.removeWhere((t) => t.id == transactionId);
    _calculateBalance();
  });
}
4. 分类统计
Map<String, double> getCategoryStats() {
  Map<String, double> stats = {};
  for (var transaction in _transactions) {
    if (transaction.type == TransactionType.expense) {
      stats[transaction.category] =
          (stats[transaction.category] ?? 0) + transaction.amount;
    }
  }
  return stats;
}

工作原理

  • 只统计支出交易
  • 按分类汇总金额
  • 使用 ?? 操作符处理首次出现的分类

实现细节

UI 构建

1. 余额卡片
Widget _buildBalanceCard() {
  return Container(
    margin: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.blue.shade400, Colors.blue.shade800],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      borderRadius: BorderRadius.circular(20),
      boxShadow: [
        BoxShadow(
          color: Colors.blue.withOpacity(0.3),
          blurRadius: 10,
          offset: const Offset(0, 5),
        ),
      ],
    ),
    padding: const EdgeInsets.all(24),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'OpenHarmony 钱包',
          style: Theme.of(context).textTheme.titleMedium?.copyWith(
                color: Colors.white70,
                fontWeight: FontWeight.w500,
              ),
        ),
        const SizedBox(height: 12),
        Text(
          ${_currentBalance.toStringAsFixed(2)}',
          style: Theme.of(context).textTheme.displayMedium?.copyWith(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
        ),
      ],
    ),
  );
}

设计特点

  • 渐变背景:从浅蓝到深蓝,视觉效果现代
  • 圆角:borderRadius: BorderRadius.circular(20) 提供柔和边界
  • 阴影效果:增加深度感和立体感
  • 响应式文本:使用 Theme.of(context) 获取主题文本样式

在这里插入图片描述

2. 快速操作按钮
Widget _buildActionButtons() {
  return Padding(
    padding: const EdgeInsets.symmetric(horizontal: 16),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _buildActionButton(
          icon: Icons.arrow_downward,
          label: '收入',
          color: Colors.green,
          onTap: () => _showTransactionDialog(TransactionType.income),
        ),
        _buildActionButton(
          icon: Icons.arrow_upward,
          label: '支出',
          color: Colors.red,
          onTap: () => _showTransactionDialog(TransactionType.expense),
        ),
        _buildActionButton(
          icon: Icons.pie_chart,
          label: '统计',
          color: Colors.orange,
          onTap: _showStatisticsDialog,
        ),
      ],
    ),
  );
}

说明

  • 三个主要操作:添加收入、添加支出、查看统计
  • 使用不同颜色区分不同操作
  • 图标清晰直观

在这里插入图片描述

3. 单个操作按钮
Widget _buildActionButton({
  required IconData icon,
  required String label,
  required Color color,
  required VoidCallback onTap,
}) {
  return GestureDetector(
    onTap: onTap,
    child: Column(
      children: [
        Container(
          width: 56,
          height: 56,
          decoration: BoxDecoration(
            color: color.withOpacity(0.1),
            borderRadius: BorderRadius.circular(12),
          ),
          child: Icon(icon, color: color, size: 28),
        ),
        const SizedBox(height: 8),
        Text(
          label,
          style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
        ),
      ],
    ),
  );
}

设计亮点

  • GestureDetector 处理点击事件
  • 半透明背景色 color.withOpacity(0.1) 创建视觉层次
  • 固定尺寸 56x56 遵循 Material Design 规范
4. 交易历史列表
Widget _buildTransactionHistory() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: Text(
          '交易历史',
          style: Theme.of(context).textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
        ),
      ),
      const SizedBox(height: 12),
      if (_transactions.isEmpty)
        Padding(
          padding: const EdgeInsets.all(32),
          child: Center(
            child: Text(
              '暂无交易记录',
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    color: Colors.grey,
                  ),
            ),
          ),
        )
      else
        ListView.builder(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          itemCount: _transactions.length,
          itemBuilder: (context, index) {
            return _buildTransactionItem(_transactions[index]);
          },
        ),
    ],
  );
}

关键点

  • 使用 ListView.builder 高效渲染大列表
  • shrinkWrap: true 让列表适应内容高度
  • NeverScrollableScrollPhysics 禁用列表自身滚动
5. 单个交易项
Widget _buildTransactionItem(Transaction transaction) {
  final isIncome = transaction.type == TransactionType.income;
  final color = isIncome ? Colors.green : Colors.red;
  final sign = isIncome ? '+' : '-';

  return Container(
    margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: Colors.grey.shade50,
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: Colors.grey.shade200),
    ),
    child: Row(
      children: [
        Container(
          width: 40,
          height: 40,
          decoration: BoxDecoration(
            color: color.withOpacity(0.1),
            borderRadius: BorderRadius.circular(8),
          ),
          child: Icon(
            isIncome ? Icons.arrow_downward : Icons.arrow_upward,
            color: color,
            size: 20,
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                transaction.title,
                style: const TextStyle(
                  fontWeight: FontWeight.w600,
                  fontSize: 14,
                ),
              ),
              Text(
                transaction.category,
                style: TextStyle(
                  color: Colors.grey.shade600,
                  fontSize: 12,
                ),
              ),
            ],
          ),
        ),
        Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            Text(
              '$sign¥${transaction.amount.toStringAsFixed(2)}',
              style: TextStyle(
                color: color,
                fontWeight: FontWeight.bold,
                fontSize: 14,
              ),
            ),
            Text(
              _formatDate(transaction.date),
              style: TextStyle(
                color: Colors.grey.shade600,
                fontSize: 11,
              ),
            ),
          ],
        ),
        const SizedBox(width: 8),
        GestureDetector(
          onTap: () => removeTransaction(transaction.id),
          child: Icon(
            Icons.close,
            color: Colors.grey.shade400,
            size: 18,
          ),
        ),
      ],
    ),
  );
}

布局分析

  • 左侧图标:直观显示收入/支出
  • 中间内容:交易标题和分类
  • 右侧金额:显示金额和时间
  • 删除按钮:快速删除交易
    在这里插入图片描述

交互功能

1. 添加交易对话框
void _showTransactionDialog(TransactionType type) {
  final titleController = TextEditingController();
  final amountController = TextEditingController();
  String selectedCategory = '其他';

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text(type == TransactionType.income ? '添加收入' : '添加支出'),
      content: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: titleController,
              decoration: const InputDecoration(
                labelText: '交易说明',
                hintText: '请输入交易说明',
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: amountController,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(
                labelText: '金额',
                hintText: '请输入金额',
                prefixText: '¥',
              ),
            ),
            const SizedBox(height: 12),
            DropdownButton<String>(
              value: selectedCategory,
              isExpanded: true,
              items: _getCategoryList(type)
                  .map((cat) => DropdownMenuItem(
                        value: cat,
                        child: Text(cat),
                      ))
                  .toList(),
              onChanged: (value) {
                if (value != null) {
                  selectedCategory = value;
                }
              },
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            if (titleController.text.isNotEmpty &&
                amountController.text.isNotEmpty) {
              final transaction = Transaction(
                id: DateTime.now().millisecondsSinceEpoch.toString(),
                title: titleController.text,
                amount: double.parse(amountController.text),
                date: DateTime.now(),
                type: type,
                category: selectedCategory,
              );
              addTransaction(transaction);
              widget.onTransactionTap?.call(transaction);
              Navigator.pop(context);
            }
          },
          child: const Text('确认'),
        ),
      ],
    ),
  );
}

工作流程

  1. 初始化文本控制器和分类变量
  2. 显示模态对话框
  3. 用户输入交易说明、金额和分类
  4. 验证必填字段不为空
  5. 创建 Transaction 对象
  6. 调用 addTransaction() 并触发回调
  7. 关闭对话框

在这里插入图片描述

2. 统计对话框
void _showStatisticsDialog() {
  final stats = getCategoryStats();

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('支出统计'),
      content: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: stats.entries
              .map((entry) => Padding(
                    padding: const EdgeInsets.symmetric(vertical: 8),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(entry.key),
                        Text(
                          ${entry.value.toStringAsFixed(2)}',
                          style: const TextStyle(fontWeight: FontWeight.bold),
                        ),
                      ],
                    ),
                  ))
              .toList(),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('关闭'),
        ),
      ],
    ),
  );
}

说明

  • 调用 getCategoryStats() 获取分类统计数据
  • 使用 map() 将统计数据转换为 UI 组件
  • 显示分类和对应的总支出

在这里插入图片描述

在这里插入图片描述


使用示例

在主应用中集成 OpenMoneyWallet

import 'package:flutter/material.dart';
import 'openmoney_widget.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'OpenMoney 钱包',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('OpenHarmony 钱包'),
        elevation: 0,
      ),
      body: OpenHarmonyWallet(
        initialBalance: 5000.0,
        transactions: [
          Transaction(
            id: '1',
            title: '月薪',
            amount: 5000.0,
            date: DateTime.now().subtract(const Duration(days: 5)),
            type: TransactionType.income,
            category: '工资',
          ),
          Transaction(
            id: '2',
            title: '午餐',
            amount: 35.0,
            date: DateTime.now().subtract(const Duration(hours: 2)),
            type: TransactionType.expense,
            category: '食物',
          ),
        ],
        onTransactionTap: (transaction) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('添加交易: ${transaction.title}')),
          );
        },
      ),
    );
  }
}

说明

  • 初始余额设置为 5000 元
  • 预加载两条示例交易
  • 通过 onTransactionTap 回调处理交易添加事件

最佳实践

1. 状态管理

问题:随着应用复杂度增加,状态管理变得困难

解决方案:使用 Provider 包进行全局状态管理

class WalletProvider extends ChangeNotifier {
  double _balance = 0.0;
  List<Transaction> _transactions = [];

  void addTransaction(Transaction transaction) {
    _transactions.add(transaction);
    _updateBalance();
    notifyListeners();
  }

  void _updateBalance() {
    // 计算逻辑
  }
}

2. 数据持久化

问题:应用关闭后数据丢失

解决方案:使用 shared_preferenceshive

import 'package:shared_preferences/shared_preferences.dart';

Future<void> saveBalance(double balance) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setDouble('balance', balance);
}

Future<double> loadBalance() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getDouble('balance') ?? 0.0;
}

3. 错误处理

问题:金额输入可能无效

解决方案

try {
  final amount = double.parse(amountController.text);
  if (amount <= 0) {
    throw FormatException('金额必须大于 0');
  }
} on FormatException catch (e) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('输入错误: ${e.message}')),
  );
}

4. 性能优化

问题:大量交易记录导致列表卡顿

解决方案:使用分页加载

class PaginatedTransactionList {
  static const pageSize = 20;
  int _currentPage = 0;

  List<Transaction> getNextPage(List<Transaction> allTransactions) {
    final start = _currentPage * pageSize;
    final end = (start + pageSize).clamp(0, allTransactions.length);
    _currentPage++;
    return allTransactions.sublist(start, end);
  }
}

5. 安全性考虑

问题:敏感财务数据需要保护

解决方案:使用加密存储

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

const storage = FlutterSecureStorage();

Future<void> saveEncryptedBalance(double balance) async {
  await storage.write(
    key: 'balance',
    value: balance.toString(),
  );
}


总结

核心要点

  1. 数据模型:清晰的数据结构是应用的基础
  2. 组件化:将 UI 拆分为可复用的小组件
  3. 状态管理:使用 setState() 或 Provider 管理状态
  4. 用户交互:通过对话框和按钮提供直观的交互
  5. 数据分析:支持分类统计和数据可视化
    欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
Logo

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

更多推荐