欢迎加入开源鸿蒙跨平台社区: 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. 表单验证问题

问题描述
在开发添加消费表单时,用户可能输入无效的金额或空值,导致应用崩溃。

解决方案

  • 使用 TextFormFieldvalidator 属性进行表单验证
  • 对金额输入进行类型检查,确保可以转换为 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. 内存泄漏问题

问题描述
长期使用应用后,可能出现内存占用增加的情况。

解决方案

  • StatefulWidgetdispose 方法中释放控制器资源
  • 避免在构建方法中创建不必要的对象

代码示例


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. 数据处理

  • 数据模型:使用类定义数据结构
  • 列表操作:使用 addremove 等方法操作列表
  • 折叠计算:使用 fold 方法计算总金额
  • 日期处理:使用 DateTime 类和日期选择器

5. UI 设计与用户体验

  • 响应式布局:使用 ExpandedSizedBox 等组件实现灵活布局
  • 安全区域:使用 SafeArea 避免内容被系统 UI 遮挡
  • 空状态处理:显示友好的空状态提示
  • 视觉反馈:使用颜色、字体大小和权重区分重要信息
  • 交互反馈:点击事件和对话框的使用

6. 代码优化

  • 控制器管理:及时释放控制器资源,避免内存泄漏
  • 性能优化:使用 ListView.builder 提高列表性能
  • 代码组织:将功能拆分为多个组件,提高代码可读性和可维护性
  • 常量定义:使用预定义列表存储类别,确保数据一致性

7. 鸿蒙适配

  • 项目结构:集成鸿蒙支持后的项目结构包含 ohos 目录
  • 跨平台兼容:代码设计考虑跨平台兼容性,确保在鸿蒙设备上正常运行
  • 资源管理:鸿蒙资源文件的组织和使用

通过以上技术点的应用,我们成功实现了一个功能完整、用户体验良好的消费记录应用,并且确保了其在鸿蒙平台上的正常运行。

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐