Flutter for OpenHarmony 实战:消费记录 - 简单记账功能
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
前言:跨生态开发的新机遇
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。
Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。将现有的Flutter应用适配到鸿蒙,听起来像是一个“跨界”任务,但它本质上是一次有价值的技术拓展:让产品触达更多用户,也让技术栈覆盖更广。
不过,这条路走起来并不像听起来那么简单。Flutter和鸿蒙,从底层的架构到上层的工具链,都有着各自的设计逻辑。会遇到一些具体的问题:代码如何组织?原有的功能在鸿蒙上如何实现?那些平台特有的能力该怎么调用?更实际的是,从编译打包到上架部署,整个流程都需要重新摸索。
这篇文章想做的,就是把这些我们趟过的路、踩过的坑,清晰地摊开给你看。我们不会只停留在“怎么做”,还会聊到“为什么得这么做”,以及“如果出了问题该往哪想”。这更像是一份实战笔记,源自真实的项目经验,聚焦于那些真正卡住过我们的环节。
无论你是在为一个成熟产品寻找新的落地平台,还是从一开始就希望构建能面向多端的应用,这里的思路和解决方案都能提供直接的参考。理解了两套体系之间的异同,掌握了关键的衔接技术,不仅能完成这次迁移,更能积累起应对未来技术变化的能力。
混合工程结构深度解析
项目目录架构
当Flutter项目集成鸿蒙支持后,典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构:
my_flutter_harmony_app/
├── lib/ # Flutter业务代码(基本不变)
│ ├── main.dart # 应用入口
│ ├── home_page.dart # 首页
│ └── utils/
│ └── platform_utils.dart # 平台工具类
├── pubspec.yaml # Flutter依赖配置
├── ohos/ # 鸿蒙原生层(核心适配区)
│ ├── entry/ # 主模块
│ │ └── src/main/
│ │ ├── ets/ # ArkTS代码
│ │ │ ├── MainAbility/
│ │ │ │ ├── MainAbility.ts # 主Ability
│ │ │ │ └── MainAbilityContext.ts
│ │ │ └── pages/
│ │ │ ├── Index.ets # 主页面
│ │ │ └── Splash.ets # 启动页
│ │ ├── resources/ # 鸿蒙资源文件
│ │ │ ├── base/
│ │ │ │ ├── element/ # 字符串等
│ │ │ │ ├── media/ # 图片资源
│ │ │ │ └── profile/ # 配置文件
│ │ │ └── en_US/ # 英文资源
│ │ └── config.json # 应用核心配置
│ ├── ohos_test/ # 测试模块
│ ├── build-profile.json5 # 构建配置
│ └── oh-package.json5 # 鸿蒙依赖管理
└── README.md
展示效果图片
flutter 实时预览 效果展示
运行到鸿蒙虚拟设备中效果展示
目录
功能代码实现
核心组件开发实现
1. 消费记录数据模型 (Expense)
实现分析:
消费记录数据模型是整个应用的基础,用于存储和管理消费信息。采用简单的类结构设计,包含必要的字段。
代码实现:
class Expense {
final String id;
final String title;
final double amount;
final DateTime date;
final String category;
Expense({
required this.id,
required this.title,
required this.amount,
required this.date,
required this.category,
});
}
List<String> expenseCategories = [
'餐饮',
'交通',
'购物',
'娱乐',
'医疗',
'其他',
];
使用方法:
- 用于创建新的消费记录对象
- 作为列表展示的数据类型
- 传递给各个组件进行操作
开发注意点:
id字段使用时间戳生成,确保唯一性- 金额类型使用
double,方便计算总和 - 类别使用预定义列表,确保数据一致性
2. 添加消费表单 (AddExpenseForm)
实现分析:
添加消费表单是用户交互的核心组件,用于收集用户输入的消费信息。采用状态管理和表单验证确保数据的有效性。
代码实现:
class AddExpenseForm extends StatefulWidget {
final Function(Expense) onAddExpense;
const AddExpenseForm({Key? key, required this.onAddExpense}) : super(key: key);
_AddExpenseFormState createState() => _AddExpenseFormState();
}
class _AddExpenseFormState extends State<AddExpenseForm> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _amountController = TextEditingController();
String _selectedCategory = expenseCategories[0];
DateTime _selectedDate = DateTime.now();
void dispose() {
_titleController.dispose();
_amountController.dispose();
super.dispose();
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
final expense = Expense(
id: DateTime.now().toString(),
title: _titleController.text,
amount: double.parse(_amountController.text),
date: _selectedDate,
category: _selectedCategory,
);
widget.onAddExpense(expense);
_resetForm();
}
}
void _resetForm() {
_titleController.clear();
_amountController.clear();
setState(() {
_selectedCategory = expenseCategories[0];
_selectedDate = DateTime.now();
});
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
);
if (picked != null && picked != _selectedDate) {
setState(() {
_selectedDate = picked;
});
}
}
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
Text(
'添加消费记录',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16),
TextFormField(
controller: _titleController,
decoration: InputDecoration(
labelText: '消费标题',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入消费标题';
}
return null;
},
),
SizedBox(height: 12),
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: '消费金额',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixText: '¥',
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入消费金额';
}
if (double.tryParse(value) == null) {
return '请输入有效的金额';
}
return null;
},
),
SizedBox(height: 12),
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedCategory,
decoration: InputDecoration(
labelText: '消费类别',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
items: expenseCategories.map((category) {
return DropdownMenuItem(
value: category,
child: Text(category),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedCategory = value!;
});
},
),
),
SizedBox(width: 12),
Expanded(
child: TextFormField(
readOnly: true,
decoration: InputDecoration(
labelText: '消费日期',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
suffixIcon: Icon(Icons.calendar_today),
),
onTap: () => _selectDate(context),
controller: TextEditingController(
text: '${_selectedDate.year}-${_selectedDate.month.toString().padLeft(2, '0')}-${_selectedDate.day.toString().padLeft(2, '0')}',
),
),
),
],
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'添加记录',
style: TextStyle(fontSize: 16),
),
),
],
),
),
),
);
}
}
使用方法:
- 在父组件中引入并传递
onAddExpense回调函数 - 当用户填写表单并提交后,会调用回调函数添加新的消费记录
开发注意点:
- 使用
GlobalKey<FormState>进行表单验证 - 控制器使用后要及时
dispose(),避免内存泄漏 - 日期选择器的使用需要处理异步操作
- 表单重置功能确保用户体验流畅
3. 消费记录列表 (ExpenseList)
实现分析:
消费记录列表用于展示所有的消费记录,支持点击查看详情和删除操作。采用 ListView.builder 优化性能,支持空状态显示。
代码实现:
class ExpenseList extends StatelessWidget {
final List<Expense> expenses;
final Function(Expense) onExpenseTap;
final Function(Expense) onExpenseDelete;
const ExpenseList({
Key? key,
required this.expenses,
required this.onExpenseTap,
required this.onExpenseDelete,
}) : super(key: key);
Widget build(BuildContext context) {
if (expenses.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long, size: 64, color: Colors.grey[400]),
SizedBox(height: 16),
Text('暂无消费记录', style: TextStyle(color: Colors.grey[600])),
],
),
);
}
return ListView.builder(
itemCount: expenses.length,
itemBuilder: (context, index) {
final expense = expenses[index];
return GestureDetector(
onTap: () => onExpenseTap(expense),
child: Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
expense.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Row(
children: [
Chip(
label: Text(expense.category),
labelStyle: TextStyle(fontSize: 12),
backgroundColor: Colors.blue[100],
padding: EdgeInsets.symmetric(horizontal: 8),
),
SizedBox(width: 8),
Text(
'${expense.date.year}-${expense.date.month.toString().padLeft(2, '0')}-${expense.date.day.toString().padLeft(2, '0')}',
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
],
),
Row(
children: [
Text(
'¥${expense.amount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
SizedBox(width: 16),
IconButton(
onPressed: () => onExpenseDelete(expense),
icon: Icon(Icons.delete, color: Colors.red[400]),
),
],
),
],
),
),
),
);
},
);
}
}
使用方法:
- 在父组件中引入并传递
expenses列表和回调函数 - 点击列表项会触发
onExpenseTap查看详情 - 点击删除图标会触发
onExpenseDelete删除记录
开发注意点:
- 使用
ListView.builder提高性能,尤其在记录较多时 - 实现空状态界面,提升用户体验
- 使用
GestureDetector实现点击事件 - 金额显示使用
toStringAsFixed(2)确保格式统一
4. 主页面 (MyHomePage)
实现分析:
主页面是应用的核心容器,整合了所有组件,管理应用状态,实现数据流转和业务逻辑。
代码实现:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Expense> _expenses = [];
void _addExpense(Expense expense) {
setState(() {
_expenses.add(expense);
});
}
void _deleteExpense(Expense expense) {
setState(() {
_expenses.remove(expense);
});
}
void _onExpenseTap(Expense expense) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(expense.title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('金额: ¥${expense.amount.toStringAsFixed(2)}'),
Text('类别: ${expense.category}'),
Text('日期: ${expense.date.year}-${expense.date.month.toString().padLeft(2, '0')}-${expense.date.day.toString().padLeft(2, '0')}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('关闭'),
),
],
);
},
);
}
double _calculateTotalExpenses() {
return _expenses.fold(0, (sum, expense) => sum + expense.amount);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Colors.blue,
),
body: SafeArea(
child: Column(
children: [
Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'总消费',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
'¥${_calculateTotalExpenses().toStringAsFixed(2)}',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
],
),
),
AddExpenseForm(onAddExpense: _addExpense),
Expanded(
child: ExpenseList(
expenses: _expenses,
onExpenseTap: _onExpenseTap,
onExpenseDelete: _deleteExpense,
),
),
],
),
),
);
}
}
使用方法:
- 作为应用的首页,集成了所有功能组件
- 显示总消费金额
- 管理消费记录的添加、删除和查看
开发注意点:
- 使用
setState管理状态变化 - 使用
SafeArea确保内容不被系统 UI 遮挡 - 使用
Expanded确保列表能够滚动 - 使用
fold方法计算总金额,代码简洁高效
应用入口 (main.dart)
实现分析:
应用入口文件,配置应用主题和启动页面。
代码实现:
import 'package:flutter/material.dart';
import 'expenses/expense_model.dart';
import 'expenses/expense_list.dart';
import 'expenses/add_expense_form.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter for openHarmony',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const MyHomePage(title: 'Flutter for openHarmony'),
);
}
}
使用方法:
- 应用的启动点,无需直接调用
开发注意点:
- 配置主题时可根据需要调整颜色和样式
- 设置
debugShowCheckedModeBanner: false移除调试横幅
开发中容易遇到的问题
1. 表单验证问题
问题描述:
在开发添加消费表单时,用户可能输入无效的金额或空值,导致应用崩溃。
解决方案:
- 使用
TextFormField的validator属性进行表单验证 - 对金额输入进行类型检查,确保可以转换为
double类型 - 使用
tryParse而不是parse,避免异常
代码示例:
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入消费金额';
}
if (double.tryParse(value) == null) {
return '请输入有效的金额';
}
return null;
},
2. 状态管理问题
问题描述:
添加或删除消费记录后,UI 没有及时更新。
解决方案:
- 使用
setState方法更新状态,触发 UI 重建 - 确保状态更新操作在正确的位置调用
代码示例:
void _addExpense(Expense expense) {
setState(() {
_expenses.add(expense);
});
}
3. 内存泄漏问题
问题描述:
长期使用应用后,可能出现内存占用增加的情况。
解决方案:
- 在
StatefulWidget的dispose方法中释放控制器资源 - 避免在构建方法中创建不必要的对象
代码示例:
void dispose() {
_titleController.dispose();
_amountController.dispose();
super.dispose();
}
4. 日期格式化问题
问题描述:
日期显示格式不一致,月份和日期可能显示为单个数字。
解决方案:
- 使用
toString().padLeft(2, '0')确保月份和日期始终显示为两位数 - 统一日期格式化逻辑,确保所有地方显示一致
代码示例:
'${expense.date.year}-${expense.date.month.toString().padLeft(2, '0')}-${expense.date.day.toString().padLeft(2, '0')}'
5. 空状态处理问题
问题描述:
当没有消费记录时,列表显示空白,用户体验不佳。
解决方案:
- 检查列表是否为空,显示友好的空状态提示
- 添加图标和提示文字,引导用户添加第一条记录
代码示例:
if (expenses.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long, size: 64, color: Colors.grey[400]),
SizedBox(height: 16),
Text('暂无消费记录', style: TextStyle(color: Colors.grey[600])),
],
),
);
}
总结开发中用到的技术点
1. Flutter 基础组件
- Scaffold:应用的基本结构,包含 appBar 和 body
- AppBar:顶部导航栏,显示应用标题
- Column:垂直布局组件,用于组织页面元素
- Row:水平布局组件,用于排列子元素
- Card:卡片式容器,提供阴影和圆角效果
- TextFormField:带验证的文本输入框
- DropdownButtonFormField:带表单验证的下拉选择框
- ElevatedButton:带背景色的按钮
- IconButton:图标按钮
- ListView.builder:高效的列表构建器,适合动态数据
- AlertDialog:弹出对话框,用于显示详情
- Chip:小标签组件,用于显示类别
2. 状态管理
- setState:Flutter 内置的状态管理方法,用于更新 UI
- StatefulWidget:有状态的组件,用于管理可变状态
- StatelessWidget:无状态的组件,用于展示不变数据
3. 表单处理
- GlobalKey:表单状态管理,用于验证和提交
- TextEditingController:文本输入控制器,用于获取和设置输入值
- 表单验证:使用 validator 函数验证输入数据
4. 数据处理
- 数据模型:使用类定义数据结构
- 列表操作:使用
add、remove等方法操作列表 - 折叠计算:使用
fold方法计算总金额 - 日期处理:使用
DateTime类和日期选择器
5. UI 设计与用户体验
- 响应式布局:使用
Expanded、SizedBox等组件实现灵活布局 - 安全区域:使用
SafeArea避免内容被系统 UI 遮挡 - 空状态处理:显示友好的空状态提示
- 视觉反馈:使用颜色、字体大小和权重区分重要信息
- 交互反馈:点击事件和对话框的使用
6. 代码优化
- 控制器管理:及时释放控制器资源,避免内存泄漏
- 性能优化:使用
ListView.builder提高列表性能 - 代码组织:将功能拆分为多个组件,提高代码可读性和可维护性
- 常量定义:使用预定义列表存储类别,确保数据一致性
7. 鸿蒙适配
- 项目结构:集成鸿蒙支持后的项目结构包含
ohos目录 - 跨平台兼容:代码设计考虑跨平台兼容性,确保在鸿蒙设备上正常运行
- 资源管理:鸿蒙资源文件的组织和使用
通过以上技术点的应用,我们成功实现了一个功能完整、用户体验良好的消费记录应用,并且确保了其在鸿蒙平台上的正常运行。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)