🚀运行效果展示

在这里插入图片描述

在这里插入图片描述

Flutter框架跨平台鸿蒙开发——小票管家APP开发流程

📝 前言

随着移动应用开发技术的快速发展,跨平台开发框架已成为移动开发领域的重要趋势。Flutter作为Google推出的开源UI工具包,凭借其"一次编写,多平台运行"的特性,在跨平台开发领域占据了重要地位。本文将详细介绍如何使用Flutter框架开发一款跨平台的小票管家APP,并成功运行在鸿蒙系统上。

📱 应用介绍

功能概述

小票管家APP是一款用于管理个人消费小票的移动应用,主要功能包括:

  • 📋 小票列表管理:展示所有小票,支持下拉刷新和左滑删除
  • 添加小票:通过表单输入小票信息
  • 📊 小票详情:查看小票的详细信息
  • 🏷️ 分类管理:支持多种小票分类
  • 💵 金额统计:实时显示总消费金额

技术栈

技术 版本 用途
Flutter 3.6.2+ 跨平台UI框架
Dart 3.0.0+ 开发语言
sqflite 2.4.1 本地数据库
path_provider 2.1.5 文件路径管理
intl 0.19.0 国际化和格式化
flutter_slidable 3.1.2 滑动操作组件

🏗️ 开发流程

1. 项目初始化

首先,我们需要创建一个Flutter项目,并添加必要的依赖。

# 创建Flutter项目
flutter create flutter_text

# 添加依赖
flutter pub add sqflite path_provider intl flutter_slidable

2. 项目结构设计

遵循Flutter最佳实践,我们采用清晰的分层架构:

lib/
├── models/          # 数据模型
│   └── receipt.dart
├── services/        # 业务逻辑和数据持久化
│   └── receipt_service.dart
├── screens/         # 页面组件
│   ├── receipt_list_screen.dart
│   ├── add_receipt_screen.dart
│   └── receipt_detail_screen.dart
└── main.dart        # 应用入口

3. 核心功能实现

3.1 数据模型设计

首先,我们定义小票的数据模型:

/// 小票数据模型
class Receipt {
  /// 小票唯一标识符
  final String id;
  
  /// 商家名称
  final String merchantName;
  
  /// 消费金额
  final double amount;
  
  /// 消费日期
  final DateTime date;
  
  /// 小票分类
  final String category;
  
  /// 小票描述
  final String description;
  
  /// 小票图片路径 (可选)
  final String? imagePath;

  /// 创建小票实例
  Receipt({
    String? id,
    required this.merchantName,
    required this.amount,
    DateTime? date,
    required this.category,
    required this.description,
    this.imagePath,
  })  : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
        date = date ?? DateTime.now();

  /// 从Map转换为Receipt实例
  factory Receipt.fromMap(Map<String, dynamic> map) {
    return Receipt(
      id: map['id'],
      merchantName: map['merchantName'],
      amount: map['amount'],
      date: DateTime.parse(map['date']),
      category: map['category'],
      description: map['description'],
      imagePath: map['imagePath'],
    );
  }

  /// 转换为Map,用于数据持久化
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'merchantName': merchantName,
      'amount': amount,
      'date': date.toIso8601String(),
      'category': category,
      'description': description,
      'imagePath': imagePath,
    };
  }
}
3.2 数据持久化服务

接下来,我们实现小票数据的持久化服务:

import 'dart:async';
import 'package:sqflite/sqflite.dart';
import '../models/receipt.dart';

/// 小票数据持久化服务
class ReceiptService {
  static final ReceiptService _instance = ReceiptService._internal();
  factory ReceiptService() => _instance;
  
  ReceiptService._internal();
  
  static Database? _database;
  
  /// 获取数据库实例
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }
  
  /// 初始化数据库
  Future<Database> _initDatabase() async {
    String databasesPath = await getDatabasesPath();
    String path = '$databasesPath/receipts.db';
    
    return await openDatabase(
      path,
      version: 1,
      onCreate: _onCreate,
    );
  }
  
  /// 创建数据库表
  Future<void> _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE receipts (
        id TEXT PRIMARY KEY,
        merchantName TEXT NOT NULL,
        amount REAL NOT NULL,
        date TEXT NOT NULL,
        category TEXT NOT NULL,
        description TEXT NOT NULL,
        imagePath TEXT
      )
    ''');
  }
  
  /// 添加小票
  Future<void> addReceipt(Receipt receipt) async {
    final db = await database;
    await db.insert('receipts', receipt.toMap());
  }
  
  /// 获取所有小票
  Future<List<Receipt>> getAllReceipts() async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query('receipts', orderBy: 'date DESC');
    
    return List.generate(maps.length, (i) {
      return Receipt.fromMap(maps[i]);
    });
  }
  
  /// 其他CRUD方法...
}
3.3 页面实现
3.3.1 小票列表页面
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:intl/intl.dart';
import '../models/receipt.dart';
import '../services/receipt_service.dart';

/// 小票列表页面
class ReceiptListScreen extends StatefulWidget {
  const ReceiptListScreen({super.key});

  
  State<ReceiptListScreen> createState() => _ReceiptListScreenState();
}

class _ReceiptListScreenState extends State<ReceiptListScreen> {
  final ReceiptService _receiptService = ReceiptService();
  List<Receipt> _receipts = [];
  bool _isLoading = true;
  double _totalAmount = 0.0;
  
  static final DateFormat _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
  static final NumberFormat _currencyFormat = NumberFormat.currency(locale: 'zh_CN', symbol: '¥');

  
  void initState() {
    super.initState();
    _loadReceipts();
  }

  /// 加载小票数据
  Future<void> _loadReceipts() async {
    setState(() {
      _isLoading = true;
    });
    
    try {
      final receipts = await _receiptService.getAllReceipts();
      final totalAmount = await _receiptService.getTotalAmount();
      
      setState(() {
        _receipts = receipts;
        _totalAmount = totalAmount;
      });
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('加载小票数据失败: $e')),
        );
      }
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('小票管家'),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 16.0),
            child: Text(
              '总计: ${_currencyFormat.format(_totalAmount)}',
              style: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
            ),
          ),
        ],
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : RefreshIndicator(
              onRefresh: _loadReceipts,
              child: _receipts.isEmpty
                  ? const Center(child: Text('暂无小票数据'))
                  : ListView.builder(
                      padding: const EdgeInsets.all(8.0),
                      itemCount: _receipts.length,
                      itemBuilder: (context, index) {
                        final receipt = _receipts[index];
                        return Slidable(
                          key: ValueKey(receipt.id),
                          endActionPane: ActionPane(
                            motion: const ScrollMotion(),
                            children: [
                              SlidableAction(
                                onPressed: (context) => _deleteReceipt(receipt.id),
                                backgroundColor: Colors.red,
                                foregroundColor: Colors.white,
                                icon: Icons.delete,
                                label: '删除',
                              ),
                            ],
                          ),
                          child: Card(
                            elevation: 2.0,
                            margin: const EdgeInsets.symmetric(vertical: 8.0),
                            child: ListTile(
                              leading: Container(
                                width: 50.0,
                                height: 50.0,
                                decoration: BoxDecoration(
                                  color: Colors.blue,
                                  borderRadius: BorderRadius.circular(8.0),
                                ),
                                child: const Icon(Icons.receipt_long, color: Colors.white),
                              ),
                              title: Text(receipt.merchantName),
                              subtitle: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text('${receipt.category}${_dateFormat.format(receipt.date)}'),
                                  if (receipt.description.isNotEmpty) ...[
                                    const SizedBox(height: 4.0),
                                    Text(receipt.description, overflow: TextOverflow.ellipsis),
                                  ],
                                ],
                              ),
                              trailing: Text(
                                _currencyFormat.format(receipt.amount),
                                style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
                              ),
                              onTap: () => _navigateToReceiptDetail(receipt),
                            ),
                          ),
                        );
                      },
                    ),
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _navigateToAddReceipt,
        tooltip: '添加小票',
        child: const Icon(Icons.add),
      ),
    );
  }
  
  /// 其他方法...
}
3.3.2 添加小票页面
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/receipt.dart';
import '../services/receipt_service.dart';

/// 添加小票页面
class AddReceiptScreen extends StatefulWidget {
  const AddReceiptScreen({super.key});

  
  State<AddReceiptScreen> createState() => _AddReceiptScreenState();
}

class _AddReceiptScreenState extends State<AddReceiptScreen> {
  final ReceiptService _receiptService = ReceiptService();
  final _formKey = GlobalKey<FormState>();
  
  String _merchantName = '';
  double _amount = 0.0;
  DateTime _date = DateTime.now();
  String _category = '餐饮';
  String _description = '';
  
  final List<String> _categories = [
    '餐饮', '购物', '交通', '娱乐', '医疗', '教育', '其他',
  ];
  
  final DateFormat _dateFormat = DateFormat('yyyy-MM-dd HH:mm');

  /// 提交表单,添加小票
  Future<void> _submitForm() async {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      
      final receipt = Receipt(
        merchantName: _merchantName,
        amount: _amount,
        date: _date,
        category: _category,
        description: _description,
      );
      
      try {
        await _receiptService.addReceipt(receipt);
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('小票添加成功')),
        );
        Navigator.pop(context);
      } catch (e) {
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('添加小票失败')),
        );
      }
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('添加小票')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              /// 商家名称字段
              TextFormField(
                decoration: const InputDecoration(
                  labelText: '商家名称',
                  border: OutlineInputBorder(),
                  prefixIcon: Icon(Icons.store),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入商家名称';
                  }
                  return null;
                },
                onSaved: (value) {
                  _merchantName = value!;
                },
              ),
              const SizedBox(height: 16.0),
              
              /// 消费金额字段
              TextFormField(
                decoration: const InputDecoration(
                  labelText: '消费金额',
                  border: OutlineInputBorder(),
                  prefixIcon: Icon(Icons.attach_money),
                ),
                keyboardType: TextInputType.numberWithOptions(decimal: true),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入消费金额';
                  }
                  final double? amount = double.tryParse(value);
                  if (amount == null || amount <= 0) {
                    return '请输入有效的消费金额';
                  }
                  return null;
                },
                onSaved: (value) {
                  _amount = double.parse(value!);
                },
              ),
              const SizedBox(height: 16.0),
              
              /// 其他表单字段...
              
              /// 提交按钮
              SizedBox(
                width: double.infinity,
                height: 50.0,
                child: ElevatedButton(
                  onPressed: _submitForm,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                    foregroundColor: Colors.white,
                  ),
                  child: const Text('保存小票', style: TextStyle(fontSize: 18.0)),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
  
  /// 其他方法...
}
3.3 页面导航

最后,我们实现应用的主入口和页面导航:

import 'package:flutter/material.dart';
import 'screens/receipt_list_screen.dart';

/// 小票管家APP主入口
void main() {
  runApp(const ReceiptManagerApp());
}

/// 小票管家APP
class ReceiptManagerApp extends StatelessWidget {
  /// 创建小票管家APP实例
  const ReceiptManagerApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '小票管家',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const ReceiptListScreen(),
      debugShowCheckedModeBanner: false,
    );
  }
}

🚀 鸿蒙平台适配

1. 环境准备

在进行鸿蒙开发前,我们需要安装必要的工具:

  • DevEco Studio:鸿蒙应用开发IDE
  • Flutter SDK:Flutter开发工具包
  • HUAWEI DevEco Device Tool:设备调试工具

2. 配置鸿蒙支持

Flutter 3.0+ 已原生支持鸿蒙系统,我们只需在项目中添加鸿蒙平台配置:

# 初始化鸿蒙模块
flutter create --platforms=ohos .

3. 构建鸿蒙应用

使用以下命令构建鸿蒙应用:

# 构建鸿蒙应用
flutter build ohos

# 运行鸿蒙应用
flutter run -d ohos

📊 应用架构流程图

应用启动

加载小票数据

数据加载成功?

显示小票列表

显示空状态

点击添加按钮

打开添加小票页面

填写小票信息

保存小票

更新小票列表

点击小票项

打开小票详情页面

左滑小票项

点击删除按钮

删除小票

更新小票列表

🧪 测试与调试

1. 单元测试

我们可以使用Flutter的测试框架进行单元测试:

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_text/models/receipt.dart';

void main() {
  test('Receipt model should be created correctly', () {
    final receipt = Receipt(
      merchantName: 'Test Merchant',
      amount: 100.0,
      category: '餐饮',
      description: 'Test Description',
    );
    
    expect(receipt.merchantName, 'Test Merchant');
    expect(receipt.amount, 100.0);
    expect(receipt.category, '餐饮');
    expect(receipt.description, 'Test Description');
  });
  
  test('Receipt should be convertible to Map', () {
    final receipt = Receipt(
      merchantName: 'Test Merchant',
      amount: 100.0,
      category: '餐饮',
      description: 'Test Description',
    );
    
    final map = receipt.toMap();
    
    expect(map['merchantName'], 'Test Merchant');
    expect(map['amount'], 100.0);
    expect(map['category'], '餐饮');
    expect(map['description'], 'Test Description');
  });
}

2. 集成测试

对于集成测试,我们可以使用Flutter的集成测试框架:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_text/main.dart';

void main() {
  testWidgets('Add receipt test', (WidgetTester tester) async {
    // 启动应用
    await tester.pumpWidget(const ReceiptManagerApp());
    
    // 点击添加按钮
    await tester.tap(find.byIcon(Icons.add));
    await tester.pumpAndSettle();
    
    // 填写表单
    await tester.enterText(find.widgetWithText(TextFormField, '商家名称'), '测试商家');
    await tester.enterText(find.widgetWithText(TextFormField, '消费金额'), '50.0');
    await tester.tap(find.text('保存小票'));
    await tester.pumpAndSettle();
    
    // 验证小票是否添加成功
    expect(find.text('测试商家'), findsOneWidget);
    expect(find.text('¥50.00'), findsOneWidget);
  });
}

📝 总结

通过本文的介绍,我们详细了解了如何使用Flutter框架开发一款跨平台的小票管家APP,并成功适配鸿蒙系统。主要实现了以下功能:

  1. 数据模型设计:定义了小票的基本属性和方法
  2. 数据持久化:使用SQLite数据库存储小票数据
  3. UI界面实现:包含小票列表、添加小票和小票详情页面
  4. 鸿蒙平台适配:实现了Flutter应用在鸿蒙系统上的运行

Flutter框架的跨平台特性使得我们可以只编写一套代码,就能在包括鸿蒙在内的多个平台上运行。这大大提高了开发效率,降低了维护成本。

未来,我们可以进一步优化应用,添加更多功能:

  • 🔍 小票搜索:支持按商家名称、日期范围搜索
  • 📊 消费统计:提供消费趋势分析和图表展示
  • ☁️ 云同步:支持多设备数据同步
  • 📸 OCR识别:支持扫描小票自动识别信息

通过不断优化和扩展,小票管家APP可以更好地满足用户的需求,成为一款功能强大、易用的个人消费管理工具。

📚 参考资料

  1. Flutter官方文档
  2. 鸿蒙开发者文档
  3. sqflite插件文档
  4. Flutter跨平台开发最佳实践

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐