欢迎加入开源鸿蒙跨平台社区: 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 实时预览 效果展示
在这里插入图片描述

运行到鸿蒙虚拟设备中效果展示
在这里插入图片描述

功能代码实现

电话号码提取工具类实现

在本项目中,我们实现了一个模拟的 dlibphonenumber 工具类,用于电话号码的解析、验证和提取。这个工具类提供了与真实库类似的功能接口,便于后续扩展。

核心实现代码

// 模拟 dlibphonenumber 库的实现
class PhoneNumber {
  final String nationalNumber;
  final String countryCode;

  PhoneNumber({required this.nationalNumber, required this.countryCode});

  
  String toString() {
    return '+$countryCode $nationalNumber';
  }
}

class PhoneNumberUtil {
  Future<PhoneNumber> parse(String phoneNumber, String countryCode) async {
    // 简单的解析实现
    return PhoneNumber(
      nationalNumber: phoneNumber.replaceAll(RegExp(r'\D'), ''),
      countryCode: countryCode,
    );
  }

  Future<bool> isValidNumber(PhoneNumber phoneNumber) async {
    // 简单的验证逻辑:检查是否是11位数字
    return phoneNumber.nationalNumber.length == 11;
  }

  // 从文本中提取电话号码
  List<String> extractPhoneNumbers(String text) {
    // 使用正则表达式提取电话号码
    // 支持多种格式的电话号码
    final RegExp phoneRegex = RegExp(
      r'1[3-9]\d{9}|\\+?861[3-9]\d{9}',
      caseSensitive: false,
    );

    final matches = phoneRegex.allMatches(text);
    return matches.map((match) => match.group(0)!).toList();
  }
}

设计说明与使用方法

  • PhoneNumber 类:用于存储解析后的电话号码信息,包含国家代码和电话号码两个属性,并提供了 toString() 方法用于格式化显示
  • PhoneNumberUtil 类:提供电话号码的解析、验证和提取功能
    • parse 方法:将输入的电话号码字符串解析为 PhoneNumber 对象,去除非数字字符
    • isValidNumber 方法:验证电话号码是否有效,这里简单检查是否是11位数字
    • extractPhoneNumbers 方法:从文本中提取电话号码,支持多种格式

开发注意事项

  • 该实现是一个简化版本,仅用于演示目的
  • 正则表达式模式支持中国大陆手机号码格式,包括带+86前缀的情况
  • 在实际应用中,可能需要根据不同国家和地区的电话号码格式调整正则表达式

电话号码提取组件设计与实现

PhoneNumberExtractorWidget 是整个应用的核心组件,它实现了用户界面交互和电话号码提取逻辑的整合。

核心实现代码

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

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

  
  State<PhoneNumberExtractorWidget> createState() => _PhoneNumberExtractorWidgetState();
}

class _PhoneNumberExtractorWidgetState extends State<PhoneNumberExtractorWidget> {
  final TextEditingController _textController = TextEditingController();
  List<String> _extractedNumbers = [];
  bool _isLoading = false;

  // 示例文本
  final List<String> _exampleTexts = [
    '联系我:13800138000,或者发邮件到test@example.com',
    '客服电话:13912345678,工作时间:9:00-18:00',
    '紧急联系人:15012345678 和 18612345678',
    '请拨打 +8613800138000 联系我们的客服团队',
  ];

  void _extractPhoneNumbers() {
    if (_textController.text.isEmpty) {
      setState(() {
        _extractedNumbers = [];
      });
      return;
    }

    setState(() {
      _isLoading = true;
    });

    // 模拟异步操作
    Future.delayed(const Duration(milliseconds: 500), () {
      final phoneUtil = PhoneNumberUtil();
      final numbers = phoneUtil.extractPhoneNumbers(_textController.text);

      setState(() {
        _extractedNumbers = numbers;
        _isLoading = false;
      });
    });
  }

  void _selectExampleText(String text) {
    setState(() {
      _textController.text = text;
      _extractedNumbers = [];
    });
  }

  void _clearText() {
    setState(() {
      _textController.clear();
      _extractedNumbers = [];
    });
  }

  
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            '从文本中提取电话号码',
            style: TextStyle(
              fontSize: 20.0,
              fontWeight: FontWeight.bold,
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 20.0),
          TextField(
            controller: _textController,
            decoration: InputDecoration(
              labelText: '请输入包含电话号码的文本',
              border: const OutlineInputBorder(),
              suffixIcon: IconButton(
                icon: const Icon(Icons.clear),
                onPressed: _clearText,
              ),
              alignLabelWithHint: true,
            ),
            maxLines: 4,
            keyboardType: TextInputType.multiline,
          ),
          const SizedBox(height: 20.0),
          ElevatedButton(
            onPressed: _isLoading ? null : _extractPhoneNumbers,
            style: ElevatedButton.styleFrom(
              padding: const EdgeInsets.symmetric(vertical: 15.0),
            ),
            child: _isLoading
                ? const SizedBox(
                    height: 20.0,
                    width: 20.0,
                    child: CircularProgressIndicator(
                      strokeWidth: 2.0,
                      valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                    ),
                  )
                : const Text('提取电话号码'),
          ),
          const SizedBox(height: 20.0),
          if (_extractedNumbers.isNotEmpty)
            Container(
              padding: const EdgeInsets.all(15.0),
              decoration: BoxDecoration(
                color: Colors.blue.shade50,
                borderRadius: BorderRadius.circular(8.0),
                border: Border.all(color: Colors.blue.shade200),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    '提取到的电话号码:',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16.0,
                    ),
                  ),
                  const SizedBox(height: 10.0),
                  ..._extractedNumbers.map((number) {
                    return Container(
                      margin: const EdgeInsets.only(bottom: 8.0),
                      padding: const EdgeInsets.all(10.0),
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(4.0),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.grey.shade200,
                            blurRadius: 2.0,
                            offset: const Offset(0, 1),
                          ),
                        ],
                      ),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Text(number),
                          IconButton(
                            icon: const Icon(Icons.copy, size: 16.0),
                            onPressed: () {
                              // 复制到剪贴板的功能
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(
                                  content: Text('电话号码已复制到剪贴板'),
                                  duration: Duration(seconds: 1),
                                ),
                              );
                            },
                          ),
                        ],
                      ),
                    );
                  }).toList(),
                ],
              ),
            ),
          if (_extractedNumbers.isEmpty && _textController.text.isNotEmpty)
            Container(
              padding: const EdgeInsets.all(15.0),
              decoration: BoxDecoration(
                color: Colors.grey.shade50,
                borderRadius: BorderRadius.circular(8.0),
                border: Border.all(color: Colors.grey.shade200),
              ),
              child: const Text(
                '未提取到电话号码',
                textAlign: TextAlign.center,
                style: TextStyle(color: Colors.grey),
              ),
            ),
          const SizedBox(height: 30.0),
          const Text(
            '示例文本:',
            style: TextStyle(
              fontSize: 16.0,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 10.0),
          Wrap(
            spacing: 10.0,
            runSpacing: 10.0,
            children: _exampleTexts.map((text) {
              return ElevatedButton(
                onPressed: () => _selectExampleText(text),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.grey.shade100,
                  foregroundColor: Colors.black,
                  padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 8.0),
                  textStyle: const TextStyle(fontSize: 12.0),
                ),
                child: Text(
                  text.length > 20 ? '${text.substring(0, 20)}...' : text,
                  maxLines: 1,
                ),
              );
            }).toList(),
          ),
        ],
      ),
    );
  }
}

组件设计说明

  • 状态管理:使用 StatefulWidget 管理组件状态,包括文本输入、提取结果和加载状态
  • 输入处理:提供多行 TextField 用于输入包含电话号码的文本,支持清空操作
  • 提取逻辑:调用 PhoneNumberUtilextractPhoneNumbers 方法提取电话号码,模拟异步操作
  • 结果展示:根据提取结果显示不同的界面,提取到号码时显示列表,未提取到号码时显示提示信息
  • 用户体验:添加了加载指示器、示例文本选择等交互元素,提升用户体验

使用方法

  1. 在需要使用电话号码提取功能的地方导入组件:

    import 'phone_number_extractor_widget.dart';
    
  2. 在布局中添加组件:

    const PhoneNumberExtractorWidget()
    

开发注意事项

  • 组件使用了 Future.delayed 模拟异步操作,实际应用中可能需要根据具体需求调整
  • 复制到剪贴板的功能目前只是显示提示,实际应用中需要实现真正的剪贴板操作
  • 示例文本提供了多种电话号码格式,便于用户测试
  • 注意在 dispose 方法中释放 TextEditingController,避免内存泄漏

应用入口与集成

main.dart 文件中,我们集成了 PhoneNumberExtractorWidget 组件,构建了完整的应用。

核心实现代码

import 'package:flutter/material.dart';
import 'phone_number_extractor_widget.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'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const PhoneNumberExtractorWidget(),
          ],
        ),
      ),
    );
  }
}

集成说明

  • 导入组件:在 main.dart 中导入 phone_number_extractor_widget.dart
  • 添加到布局:将 PhoneNumberExtractorWidget 添加到 MyHomePage 的布局中
  • 居中显示:使用 CenterColumn 确保组件在屏幕中居中显示
  • 主题配置:使用 ThemeData 配置应用主题,启用 Material 3

开发注意事项

  • 移除了默认的计数器功能,专注于电话号码提取功能
  • 使用 debugShowCheckedModeBanner: false 移除调试横幅,提供更干净的界面
  • 保持布局简洁,让用户专注于电话号码提取功能

本次开发中容易遇到的问题

  1. 电话号码提取的准确性

    • 问题:不同国家和地区的电话号码格式差异很大,单一的正则表达式可能无法覆盖所有情况
    • 解决方案:根据目标用户群体,调整正则表达式以支持特定地区的电话号码格式,或使用更复杂的解析逻辑
  2. 异步操作的处理

    • 问题:在提取电话号码时,如果文本较长,可能会导致UI卡顿
    • 解决方案:使用 Future.delayedcompute 函数将耗时操作移至后台线程,避免阻塞UI线程
  3. 剪贴板操作的跨平台兼容性

    • 问题:不同平台的剪贴板API可能存在差异
    • 解决方案:使用 Flutter 的 clipboard 包,它提供了跨平台的剪贴板操作接口
  4. Flutter for OpenHarmony 的兼容性

    • 问题:在将Flutter应用适配到OpenHarmony平台时,可能会遇到平台特定的API差异
    • 解决方案:使用平台无关的Flutter API,避免直接调用平台特定的功能,确保跨平台兼容性
  5. UI响应式设计

    • 问题:在不同尺寸的设备上,UI布局可能会出现问题
    • 解决方案:使用Flutter的响应式布局组件,如 ExpandedFlexible 等,确保在不同设备上都能正常显示
  6. 内存管理

    • 问题:在频繁的状态更新和文本输入中,可能会出现内存泄漏
    • 解决方案:正确使用 dispose 方法释放资源,特别是 TextEditingController 等需要手动管理的对象
  7. 正则表达式性能

    • 问题:复杂的正则表达式在处理长文本时可能会影响性能
    • 解决方案:优化正则表达式,避免过度复杂的模式,必要时考虑使用更高效的字符串处理方法

总结本次开发中用到的技术点

  1. Flutter 组件化开发

    • 将电话号码提取功能封装成独立的 PhoneNumberExtractorWidget 组件,提高代码复用性和可维护性
    • 使用 StatefulWidgetStatelessWidget 构建不同类型的组件
  2. 异步编程

    • 使用 Future.delayed 模拟异步操作,确保UI响应流畅
    • 实现了加载状态的管理,提升用户体验
  3. 正则表达式

    • 使用正则表达式从文本中提取电话号码,支持多种格式
    • 优化正则表达式模式,提高匹配准确性
  4. 状态管理

    • 使用 Flutter 的 setState 管理组件状态,包括文本输入、提取结果和加载状态
    • 合理组织状态变量,确保状态更新的一致性
  5. 用户交互设计

    • 添加了加载指示器、示例文本选择、复制功能等交互元素
    • 实现了响应式按钮状态,在加载过程中禁用提取按钮
  6. UI 布局

    • 使用 ColumnTextFieldElevatedButtonWrap 等 Flutter 组件构建美观的用户界面
    • 应用了 Material 3 设计规范,提供现代化的视觉效果
  7. 错误处理

    • 对空输入情况进行了处理,显示合理的提示信息
    • 实现了未提取到电话号码时的友好提示
  8. OpenHarmony 适配

    • 确保代码在 OpenHarmony 平台上正常运行,实现跨平台兼容
    • 遵循 OpenHarmony 的应用结构规范
  9. 工具类设计

    • 实现了模拟的 PhoneNumberUtil 工具类,提供电话号码解析、验证和提取功能
    • 设计了清晰的类结构和方法接口,便于后续扩展
  10. 代码组织

    • 将功能模块拆分为不同的文件,提高代码的可读性和可维护性
    • 遵循 Flutter 的代码风格和最佳实践

通过本项目的开发,我们不仅实现了一个功能完整的电话号码提取工具,还掌握了 Flutter for OpenHarmony 开发的核心技术点,为跨生态应用开发积累了宝贵经验。

Logo

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

更多推荐