1. 项目概述:Flutter跨平台鸿蒙购物清单应用
购物清单应用是日常生活中非常实用的工具类应用,能够帮助用户高效管理购物计划、精确计算购物预算。本项目使用Flutter框架开发,实现了完整的商品管理、分类筛选、价格计算等功能,特别针对鸿蒙系统进行了适配优化。
作为一名有多年Flutter开发经验的工程师,我发现购物类应用虽然看似简单,但要开发出一个体验优秀、功能完善的产品,需要考虑很多细节。比如商品价格的精确计算、分类筛选的交互设计、数据持久化的稳定性等。这个项目不仅适合Flutter初学者学习基础开发流程,也包含了许多进阶技巧,值得有一定经验的开发者参考。
2. 环境准备与项目搭建
2.1 Flutter开发环境配置
首先需要确保你的开发环境已经正确配置了Flutter SDK。我推荐使用Flutter 3.41.9版本,这个版本对鸿蒙系统的兼容性较好。安装完成后,可以通过以下命令检查环境:
flutter doctor如果要在鸿蒙设备上运行,还需要安装鸿蒙开发工具链。目前Flutter官方还没有正式支持鸿蒙OS,但可以通过以下方式实现兼容:
- 使用鸿蒙的Java UI框架作为Flutter的渲染后端
- 通过FFI调用鸿蒙的Native API
- 使用鸿蒙的分布式能力扩展应用功能
2.2 项目初始化
创建一个新的Flutter项目:
flutter create shopping_list_harmony cd shopping_list_harmony然后添加项目所需的主要依赖:
dependencies: flutter: sdk: flutter shared_preferences: ^2.2.2 # 用于本地数据存储 intl: ^0.18.1 # 国际化支持 provider: ^6.0.5 # 状态管理3. 核心功能实现
3.1 数据模型设计
购物清单应用的核心是商品数据模型,我们需要设计一个能够完整描述商品信息的类:
class ShoppingItem { final String id; final String name; final String category; final double price; final double quantity; final String unit; bool isPurchased; final DateTime addedDate; ShoppingItem({ required this.id, required this.name, required this.category, required this.price, required this.quantity, required this.unit, this.isPurchased = false, required this.addedDate, }); double get totalPrice => price * quantity; Map<String, dynamic> toJson() { return { 'id': id, 'name': name, 'category': category, 'price': price, 'quantity': quantity, 'unit': unit, 'isPurchased': isPurchased, 'addedDate': addedDate.toIso8601String(), }; } factory ShoppingItem.fromJson(Map<String, dynamic> json) { return ShoppingItem( id: json['id'], name: json['name'], category: json['category'], price: json['price'], quantity: json['quantity'], unit: json['unit'], isPurchased: json['isPurchased'] ?? false, addedDate: DateTime.parse(json['addedDate']), ); } }这个模型包含了商品的基本信息,以及购买状态、添加时间等元数据。特别注意price和quantity都使用double类型,以支持小数数量的商品(如0.5公斤)。
3.2 状态管理与数据持久化
购物清单应用需要管理商品列表的状态,并在应用关闭后能够恢复数据。我们使用shared_preferences插件实现本地存储:
class ShoppingListProvider with ChangeNotifier { List<ShoppingItem> _items = []; List<ShoppingItem> get items => _items; Future<void> loadItems() async { final prefs = await SharedPreferences.getInstance(); final itemsJson = prefs.getStringList('shopping_items') ?? []; _items = itemsJson .map((json) => ShoppingItem.fromJson(jsonDecode(json))) .toList(); notifyListeners(); } Future<void> saveItems() async { final prefs = await SharedPreferences.getInstance(); final itemsJson = _items.map((item) => jsonEncode(item.toJson())).toList(); await prefs.setStringList('shopping_items', itemsJson); } void addItem(ShoppingItem item) { _items.add(item); saveItems(); notifyListeners(); } void togglePurchased(String id) { final index = _items.indexWhere((item) => item.id == id); if (index != -1) { _items[index].isPurchased = !_items[index].isPurchased; saveItems(); notifyListeners(); } } void removeItem(String id) { _items.removeWhere((item) => item.id == id); saveItems(); notifyListeners(); } void clearPurchased() { _items.removeWhere((item) => item.isPurchased); saveItems(); notifyListeners(); } }在实际开发中,我发现直接使用SharedPreferences存储大量数据可能会导致性能问题。当商品数量超过100时,建议考虑使用SQLite或Hive等更专业的本地存储方案。
3.3 用户界面实现
3.3.1 主界面布局
主界面采用经典的Material Design布局,包含以下几个部分:
- 顶部的金额汇总卡片
- 中部的分类筛选栏
- 底部的商品列表
- 右下角的添加商品按钮
class ShoppingListScreen extends StatelessWidget { @override Widget build(BuildContext context) { final provider = Provider.of<ShoppingListProvider>(context); return Scaffold( appBar: AppBar( title: Text('购物清单'), actions: [ IconButton( icon: Icon(Icons.delete_sweep), onPressed: () => _showClearDialog(context), ), ], ), body: Column( children: [ _buildSummaryCard(provider), _buildCategoryFilter(provider), Expanded( child: provider.items.isEmpty ? _buildEmptyState() : _buildItemList(provider), ), ], ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () => _showAddItemDialog(context), ), ); } }3.3.2 金额汇总卡片
金额汇总卡片显示待购金额、已购金额和待购商品数量,帮助用户快速了解购物情况:
Widget _buildSummaryCard(ShoppingListProvider provider) { final totalPrice = provider.items .where((item) => !item.isPurchased) .fold(0.0, (sum, item) => sum + item.totalPrice); final purchasedPrice = provider.items .where((item) => item.isPurchased) .fold(0.0, (sum, item) => sum + item.totalPrice); final unpurchasedCount = provider.items .where((item) => !item.isPurchased) .length; return Card( margin: EdgeInsets.all(16), elevation: 4, child: Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue.shade400, Colors.blue.shade600], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('待购金额', style: TextStyle(color: Colors.white70)), Text( '¥${totalPrice.toStringAsFixed(2)}', style: TextStyle( color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold, ), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('待购 $unpurchasedCount 件', style: TextStyle(color: Colors.white70)), Text('已购 ¥${purchasedPrice.toStringAsFixed(2)}', style: TextStyle(color: Colors.white70)), ], ), ], ), ), ); }3.3.3 商品列表项
每个商品项显示商品名称、分类、数量、单价和小计,并提供复选框标记购买状态:
Widget _buildItemCard(ShoppingItem item, ShoppingListProvider provider) { return Card( margin: EdgeInsets.only(bottom: 12), child: ListTile( leading: Checkbox( value: item.isPurchased, onChanged: (value) => provider.togglePurchased(item.id), ), title: Text( item.name, style: TextStyle( fontWeight: FontWeight.bold, decoration: item.isPurchased ? TextDecoration.lineThrough : null, color: item.isPurchased ? Colors.grey : null, ), ), subtitle: Text( '${item.category} · ${item.quantity}${item.unit} × ¥${item.price.toStringAsFixed(2)}', ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '¥${item.totalPrice.toStringAsFixed(2)}', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: item.isPurchased ? Colors.grey : Colors.blue.shade700, ), ), IconButton( icon: Icon(Icons.delete_outline, size: 20), onPressed: () => provider.removeItem(item.id), ), ], ), ), ); }4. 鸿蒙系统适配与优化
4.1 鸿蒙特性集成
虽然Flutter应用在鸿蒙系统上可以正常运行,但要充分发挥鸿蒙的特性,需要进行一些特殊适配:
- 分布式能力:通过鸿蒙的分布式软总线,可以实现手机和平板之间的购物清单同步
- 原子化服务:将常用功能如"快速添加商品"作为鸿蒙的原子化服务提供
- 卡片功能:开发鸿蒙服务卡片,在桌面显示待购商品数量和金额
4.2 性能优化建议
在鸿蒙设备上运行Flutter应用时,可以采取以下优化措施:
- 减少Widget重建:使用const构造函数和Provider的select方法
- 列表性能优化:使用ListView.builder并设置合适的cacheExtent
- 图片资源优化:使用.9图适配不同屏幕尺寸
- 内存管理:及时释放不用的资源,避免内存泄漏
5. 项目扩展与进阶功能
5.1 购物历史记录
扩展数据模型,记录每次购物的情况:
class ShoppingHistory { final String id; final DateTime date; final List<ShoppingItem> items; final double totalAmount; final String? notes; ShoppingHistory({ required this.id, required this.date, required this.items, required this.totalAmount, this.notes, }); }5.2 预算管理功能
添加预算管理模块,帮助用户控制消费:
class BudgetManager { double monthlyBudget = 1000.0; double getRemainingBudget(double spent) { return monthlyBudget - spent; } double getBudgetProgress(double spent) { return (spent / monthlyBudget).clamp(0.0, 1.0); } String getBudgetStatus(double spent) { final progress = getBudgetProgress(spent); if (progress >= 1.0) return '预算已超支'; if (progress >= 0.9) return '预算即将用完'; if (progress >= 0.7) return '预算使用较多'; return '预算充足'; } }5.3 条码扫描功能
集成条码扫描功能,快速添加商品:
import 'package:mobile_scanner/mobile_scanner.dart'; class BarcodeScannerPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('扫描条码')), body: MobileScanner( onDetect: (capture) { final barcode = capture.barcodes.first.rawValue; if (barcode != null) { Navigator.pop(context, barcode); } }, ), ); } }6. 测试与调试
6.1 单元测试示例
编写单元测试验证核心逻辑:
void main() { group('ShoppingItem', () { test('totalPrice calculation', () { final item = ShoppingItem( id: '1', name: 'Apple', category: 'Fruit', price: 5.0, quantity: 3.0, unit: '个', addedDate: DateTime.now(), ); expect(item.totalPrice, 15.0); }); }); group('ShoppingListProvider', () { late ShoppingListProvider provider; setUp(() { provider = ShoppingListProvider(); }); test('add and remove item', () { final item = ShoppingItem(...); provider.addItem(item); expect(provider.items.length, 1); provider.removeItem(item.id); expect(provider.items.length, 0); }); }); }6.2 Widget测试
测试UI组件的交互行为:
void main() { testWidgets('Add item dialog test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Builder( builder: (context) => TextButton( child: Text('Add'), onPressed: () => showDialog( context: context, builder: (_) => AddItemDialog(), ), ), ), ), ), ); await tester.tap(find.text('Add')); await tester.pumpAndSettle(); expect(find.text('添加商品'), findsOneWidget); }); }7. 项目部署与发布
7.1 鸿蒙应用打包
虽然Flutter官方尚未直接支持鸿蒙应用打包,但可以通过以下步骤实现:
- 使用Flutter构建Android APK
- 使用鸿蒙的APK转换工具将Android应用转换为鸿蒙应用
- 添加鸿蒙特有的元数据和权限
- 使用鸿蒙的签名工具对应用进行签名
7.2 应用商店发布
将打包好的应用发布到华为应用市场:
- 注册华为开发者账号
- 准备应用元数据(图标、截图、描述等)
- 提交应用审核
- 通过后即可上架
8. 常见问题与解决方案
8.1 数据同步问题
问题:在多设备间同步购物清单时可能出现冲突解决方案:
- 使用时间戳作为最后修改时间的标记
- 实现冲突解决策略(如最后修改优先)
- 提供手动解决冲突的界面
8.2 性能问题
问题:商品数量多时界面卡顿解决方案:
- 实现分页加载
- 使用Isolate处理大量计算
- 优化列表项的构建方式
8.3 鸿蒙兼容性问题
问题:某些Flutter插件在鸿蒙上无法正常工作解决方案:
- 检查插件是否依赖Android特定API
- 寻找替代插件或自行实现功能
- 通过MethodChannel调用鸿蒙原生API
9. 项目总结与经验分享
通过这个购物清单应用的开发,我总结了以下几点经验:
状态管理:对于中等复杂度的应用,Provider是一个简单有效的选择。但对于更复杂的场景,考虑使用Riverpod或Bloc。
数据持久化:SharedPreferences适合存储简单数据,但复杂数据结构建议使用Hive或SQLite。
鸿蒙适配:目前Flutter在鸿蒙上的支持还在完善中,需要针对性地解决一些兼容性问题。
性能优化:列表性能是关键,务必使用ListView.builder并合理设置cacheExtent。
测试覆盖:良好的测试覆盖率能大大减少后期维护成本,特别是核心业务逻辑。
这个项目还有很多可以扩展的方向,比如:
- 添加云端同步功能
- 实现智能推荐商品
- 集成支付功能
- 开发手表版应用
希望这个教程能帮助你掌握Flutter跨平台开发的基本流程,并为鸿蒙应用开发打下基础。在实际开发中,最重要的是保持代码的可维护性和可扩展性,这样才能应对不断变化的需求。