欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。

1. 引言

随着鸿蒙(HarmonyOS)生态的快速发展,越来越多企业开始将存量跨平台项目迁移至鸿蒙系统,以享受其分布式能力、低时延和多设备协同的优势。对于基于 Flutter 开发的存量项目,鸿蒙化过程中面临两大核心挑战:项目架构耦合度高(难以单独适配鸿蒙原生能力)和第三方插件不兼容(多数 Flutter 插件仅支持 Android/iOS,缺乏鸿蒙适配)。

本文以实战为核心,从「环境准备→模块化拆分→插件替换→适配调试→打包部署」全流程,讲解存量 Flutter 项目的鸿蒙化改造方案。文中包含大量可直接复用的代码片段、官方文档链接和工具配置指南,适用于有 Flutter 开发经验且需落地鸿蒙迁移的开发者。

阅读指南

  • 若您是鸿蒙新手:建议先阅读「2. 前置准备」搭建环境,再逐步推进核心模块。
  • 若您聚焦特定问题:可直接跳转至对应章节(如插件替换见「4. 核心实战二」)。
  • 所有代码已在鸿蒙 4.0 设备上验证,依赖版本均为当前最新稳定版。

2. 前置准备:环境搭建与项目评估

在启动鸿蒙化改造前,需完成开发环境搭建和存量项目评估,避免后续因环境问题或信息差导致返工。

2.1 开发环境搭建

鸿蒙化 Flutter 项目需同时配置 Flutter 环境 和 鸿蒙开发环境,两者通过 Flutter HarmonyOS Adapter 实现联动。

2.1.1 基础工具清单
工具 / 环境 版本要求 下载链接
DevEco Studio 4.0+(推荐 4.1 Beta) 鸿蒙开发者官网
Flutter SDK 3.16+ Flutter 官网
HarmonyOS SDK API Version 9+ 集成于 DevEco Studio(自动下载)
鸿蒙模拟器 / 真机 API Version 9+ 模拟器下载指南
JDK 17+ Oracle JDK 官网
2.1.2 环境配置步骤
  1. 安装 DevEco Studio:下载后按照向导完成安装,首次启动时会自动下载鸿蒙 SDK(需勾选「HarmonyOS SDK」和「Flutter HarmonyOS Adapter」)。

  2. 配置 Flutter 环境:解压 Flutter SDK 后,在终端执行以下命令,确保 Flutter 识别鸿蒙环境:

    bash

    运行

    # 检查 Flutter 环境(需包含 HarmonyOS 平台)
    flutter doctor -v
    # 若提示 "HarmonyOS toolchain - develop for HarmonyOS devices" 则配置成功
    
  3. 连接调试设备

    • 真机:开启「开发者模式」和「USB 调试」,通过 USB 连接电脑,在 DevEco Studio 中选择设备(Tools → Device Manager)。
    • 模拟器:在 DevEco Studio 中创建鸿蒙模拟器(Tools → Device Manager → New Emulator),推荐选择「Phone 设备」(如 P50 Pro)。

2.2 存量项目评估

评估的核心目标是梳理依赖风险明确改造范围,避免盲目拆分导致工作量激增。

2.2.1 依赖分析(关键步骤)

使用 Flutter 官方工具 pub deps 分析项目依赖,重点关注第三方插件的鸿蒙兼容性:

bash

运行

# 在 Flutter 项目根目录执行,生成依赖树
pub deps --no-dev --style=list

将依赖分为三类,整理成表格(示例):

依赖类型 插件示例 鸿蒙兼容性 处理方案
基础工具类 fluttertoastlogger 高(纯 Dart) 直接复用
平台原生依赖类 shared_preferencesdio 中(需适配) 替换为鸿蒙原生 API 或适配版
硬件交互类 cameralocation 低(需封装) 自定义 MethodChannel 调用

兼容性查询渠道

  • 优先查看插件官网 / Pub 页面:搜索「HarmonyOS」关键词(如 dio Pub 页)。
  • 鸿蒙插件中心:HarmonyOS-Toolkit 提供部分适配插件。
  • GitHub Issues:搜索插件仓库的「HarmonyOS」相关 Issues(如 flutter/plugins 仓库)。
2.2.2 风险点梳理
  1. 架构耦合风险:若项目未模块化(如所有代码写在 main.dart 或单一目录),需优先拆分,否则后续适配难以定位修改点。
  2. 原生代码依赖风险:若项目包含 Android/iOS 原生代码(如 android/app/src/main/java),需重新编写鸿蒙原生代码替换。
  3. UI 适配风险:鸿蒙设备屏幕尺寸多样(手机、平板、手表),需提前规划多设备 UI 适配方案。

3. 核心实战一:存量项目的模块化拆分

模块化拆分是存量项目鸿蒙化的基础前提—— 通过拆分降低耦合,使鸿蒙原生能力(如分布式数据管理、设备协同)可按需集成到独立模块,同时便于后续维护和复用。

3.1 模块化拆分的核心原则

  1. 单一职责原则:每个模块仅负责一类业务或功能(如「用户模块」仅处理登录、个人信息,「网络模块」仅处理请求)。
  2. 低耦合高内聚原则:模块间通过接口通信(如 EventBus、路由),避免直接依赖;模块内部逻辑高度关联。
  3. 可复用原则:公共功能(如网络、存储、工具类)抽为独立模块,供所有业务模块调用。

3.2 基于业务维度的拆分实践

以典型电商 Flutter 项目为例,按业务拆分后的目录结构如下(重点关注 lib/modules 和 lib/common):

plaintext

lib/
├── main.dart                # 入口文件(初始化路由、全局配置)
├── router/                  # 路由配置(统一管理页面跳转)
│   └── app_router.dart
├── modules/                 # 业务模块(按功能拆分)
│   ├── user/                # 用户模块(登录、个人中心)
│   │   ├── user.dart        # 模块入口(暴露对外接口)
│   │   ├── view/            # 页面(UI 代码)
│   │   │   ├── login_page.dart
│   │   │   └── profile_page.dart
│   │   ├── viewModel/       # 状态管理(业务逻辑)
│   │   │   └── user_view_model.dart
│   │   ├── model/           # 数据模型(实体类)
│   │   │   └── user_model.dart
│   │   └── api/             # 接口请求(模块内专属 API)
│   │       └── user_api.dart
│   ├── goods/               # 商品模块(列表、详情)
│   │   ├── goods.dart
│   │   ├── view/
│   │   ├── viewModel/
│   │   ├── model/
│   │   └── api/
│   └── order/               # 订单模块(下单、订单列表)
│       └── ...(结构同 user/goods)
└── common/                  # 公共模块(所有业务模块可复用)
    ├── network/             # 网络请求(dio 封装)
    ├── storage/             # 存储(鸿蒙 Preferences 封装)
    ├── utils/               # 工具类(日期、加密、屏幕适配)
    ├── widgets/             # 公共组件(按钮、列表项)
    └── constants/           # 常量(接口地址、颜色、字体)
3.2.1 模块间通信:EventBus 示例

业务模块间需避免直接依赖,通过 event_bus 实现跨模块事件通知(如用户登录后通知商品模块刷新数据)。

  1. 添加依赖pubspec.yaml):

    yaml

    dependencies:
      event_bus: ^2.0.0  # 最新稳定版
    
  2. 初始化 EventBuslib/common/utils/event_bus.dart):

    dart

    import 'package:event_bus/event_bus.dart';
    
    // 全局单例 EventBus
    final EventBus eventBus = EventBus();
    
    // 定义事件类(按业务场景划分)
    // 1. 用户登录事件
    class UserLoginEvent {
      final String userId;    // 用户ID
      final String token;     // 登录令牌
      UserLoginEvent({required this.userId, required this.token});
    }
    
    // 2. 用户退出事件
    class UserLogoutEvent {}
    
  3. 发送事件(用户模块登录成功后,lib/modules/user/viewModel/user_view_model.dart):

    dart

    import 'package:your_project/common/utils/event_bus.dart';
    
    Future<void> login(String username, String password) async {
      try {
        // 1. 调用登录接口
        final response = await UserApi.login(username, password);
        final userId = response['userId'];
        final token = response['token'];
    
        // 2. 存储 Token(后续讲鸿蒙存储)
        await OhosPreferences.putString('user_token', token);
    
        // 3. 发送登录成功事件(通知其他模块)
        eventBus.fire(UserLoginEvent(userId: userId, token: token));
    
        // 4. 跳转首页
        AppRouter.pushNamed('/home');
      } catch (e) {
        print('登录失败:$e');
        // 处理错误(如弹窗提示)
      }
    }
    
  4. 监听事件(商品模块接收登录事件,lib/modules/goods/viewModel/goods_view_model.dart):

    dart

    import 'package:flutter/material.dart';
    import 'package:your_project/common/utils/event_bus.dart';
    
    class GoodsViewModel with ChangeNotifier {
      List<GoodsModel> _goodsList = [];
      List<GoodsModel> get goodsList => _goodsList;
    
      // 事件订阅对象(用于取消订阅)
      late final StreamSubscription _loginSubscription;
      late final StreamSubscription _logoutSubscription;
    
      GoodsViewModel() {
        // 初始化时监听事件
        _listenEvents();
      }
    
      // 监听登录/退出事件
      void _listenEvents() {
        // 登录成功:刷新商品列表(携带用户ID)
        _loginSubscription = eventBus.on<UserLoginEvent>().listen((event) {
          _fetchGoodsList(event.userId); // 调用接口获取用户专属商品
        });
    
        // 退出登录:清空商品列表
        _logoutSubscription = eventBus.on<UserLogoutEvent>().listen((_) {
          _goodsList.clear();
          notifyListeners();
        });
      }
    
      // 调用商品列表接口
      Future<void> _fetchGoodsList(String userId) async {
        // 实现逻辑(后续结合鸿蒙网络模块)
      }
    
      // 页面销毁时取消订阅(避免内存泄漏)
      void dispose() {
        _loginSubscription.cancel();
        _logoutSubscription.cancel();
        super.dispose();
      }
    }
    

3.3 基于功能维度的拆分实践

公共功能(如网络、存储)需抽为独立模块,避免重复开发。以网络模块为例,封装鸿蒙适配的 dio 请求:

  1. 目录结构lib/common/network/):

    plaintext

    network/
    ├── dio_manager.dart       # 单例管理 dio 实例
    ├── api_exception.dart     # 自定义异常(如网络错误、接口错误)
    └── request_interceptor.dart # 请求拦截器(添加 Token、日志)
    
  2. dio 初始化(鸿蒙适配)dio_manager.dart):

    dart

    import 'dart:io';
    import 'package:dio/dio.dart';
    import 'package:dio/io.dart';
    import 'package:your_project/common/storage/ohos_preferences.dart';
    import 'api_exception.dart';
    import 'request_interceptor.dart';
    
    class DioManager {
      // 单例模式(懒加载)
      static DioManager? _instance;
      static DioManager get instance => _instance ??= DioManager._();
    
      Dio? _dio;
      Dio get dio => _dio ??= _initDio();
    
      // 私有构造函数
      DioManager._();
    
      // 初始化 dio(关键:配置鸿蒙网络适配器)
      Dio _initDio() {
        final options = BaseOptions(
          baseUrl: Constants.baseUrl, // 从常量模块获取接口基地址
          connectTimeout: const Duration(seconds: 5),
          receiveTimeout: const Duration(seconds: 3),
          responseType: ResponseType.json,
        );
    
        final dio = Dio(options);
    
        // 1. 添加鸿蒙网络适配器(解决默认适配器不兼容问题)
        if (Platform.isHarmonyOS) {
          dio.httpClientAdapter = IOHttpClientAdapter(
            createHttpClient: () {
              final client = HttpClient();
              // 鸿蒙网络配置(如允许 http 请求,需在 config.json 中配置权限)
              client.badCertificateCallback = (cert, host, port) => true;
              return client;
            },
          );
        }
    
        // 2. 添加请求拦截器(添加 Token、日志)
        dio.interceptors.add(RequestInterceptor());
    
        // 3. 添加响应拦截器(统一处理错误)
        dio.interceptors.add(InterceptorsWrapper(
          onResponse: (response, handler) {
            final code = response.data['code'];
            final message = response.data['message'];
            // 接口错误(如 Token 过期、参数错误)
            if (code != 200) {
              handler.reject(DioError(
                requestOptions: response.requestOptions,
                error: ApiException(code: code, message: message),
              ));
              return;
            }
            handler.next(response);
          },
        ));
    
        return dio;
      }
    
      // 封装 GET 请求
      Future<T> get<T>(
        String path, {
        Map<String, dynamic>? queryParameters,
        Options? options,
      }) async {
        try {
          final response = await dio.get(
            path,
            queryParameters: queryParameters,
            options: options,
          );
          return response.data['data'] as T;
        } catch (e) {
          // 统一异常处理(如弹窗提示)
          _handleError(e);
          rethrow; // 抛出异常,让业务层处理
        }
      }
    
      // 封装 POST 请求(类似 GET)
      Future<T> post<T>(...) async { ... }
    
      // 异常处理
      void _handleError(dynamic e) {
        if (e is DioError) {
          if (e.error is ApiException) {
            // 接口错误(如 "账号不存在")
            print('接口错误:${(e.error as ApiException).message}');
          } else {
            // 网络错误(如无网、超时)
            print('网络错误:${e.message}');
          }
        } else {
          // 其他错误
          print('未知错误:$e');
        }
      }
    }
    
  3. 请求拦截器(添加 Token)request_interceptor.dart):

    dart

    import 'package:dio/dio.dart';
    import 'package:your_project/common/storage/ohos_preferences.dart';
    
    class RequestInterceptor extends Interceptor {
      @override
      void onRequest(
        RequestOptions options,
        RequestInterceptorHandler handler,
      ) async {
        // 1. 从鸿蒙存储中获取 Token
        final token = await OhosPreferences.getString('user_token');
        // 2. 添加 Token 到请求头
        if (token.isNotEmpty) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        // 3. 继续执行请求
        handler.next(options);
      }
    }
    

3.4 拆分后的项目构建配置

模块化后需调整 pubspec.yaml,确保各模块依赖正确:

yaml

name: your_project_name
description: 存量 Flutter 项目鸿蒙化示例
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'
  flutter: '>=3.16.0'

dependencies:
  flutter:
    sdk: flutter

  # 基础依赖
  event_bus: ^2.0.0          # 模块通信
  dio: ^5.4.0                # 网络请求
  provider: ^6.1.1           # 状态管理
  flutter_screenutil: ^5.9.0 # 屏幕适配(可选,辅助鸿蒙多设备适配)

  # 鸿蒙原生能力依赖(后续插件替换会用到)
  flutter_ohos_plugin: ^1.0.0 # 鸿蒙基础插件(如权限、存储)

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0      # 代码规范检查

flutter:
  uses-material-design: true

  # 鸿蒙资源配置(需在 config.json 中同步配置)
  ohos:
    config:
      deviceType: [phone, tablet] # 支持的设备类型
      permissions:
        - ohos.permission.INTERNET # 网络权限(必须配置)
        - ohos.permission.READ_USER_STORAGE # 存储权限(按需配置)

4. 核心实战二:关键插件的鸿蒙化替换

存量 Flutter 项目依赖的第三方插件中,约 60% 不支持鸿蒙(如 shared_preferencespermission_handler)。需按「优先用鸿蒙适配插件 → 次用原生 API 封装 → 最后自定义实现」的优先级替换。

4.1 存储插件:shared_preferences → 鸿蒙 Preferences

shared_preferences 是 Flutter 常用的轻量存储插件,但不支持鸿蒙。鸿蒙提供原生 Preferences 组件,功能类似,需通过 MethodChannel 封装为 Flutter 可调用的接口。

4.1.1 鸿蒙原生封装(Java 代码)
  1. 创建 Preferences 工具类(路径:ohos/mainability/src/main/java/com/your/package/utils/PreferencesUtil.java):

    java

    运行

    package com.your.package.utils;
    
    import ohos.data.preferences.Preferences;
    import ohos.app.Context;
    import java.util.Optional;
    
    /**
     * 鸿蒙 Preferences 工具类(封装存储、读取、删除操作)
     */
    public class PreferencesUtil {
        // 存储文件名(唯一标识)
        private static final String PREF_NAME = "flutter_ohos_storage";
        private static Preferences preferences;
    
        /**
         * 初始化 Preferences(需在 Ability 启动时调用)
         * @param context 上下文(如 MainAbility)
         */
        public static void init(Context context) {
            if (preferences == null) {
                preferences = context.getPreferences(PREF_NAME);
            }
        }
    
        /**
         * 存储 String 类型数据
         * @param key 键
         * @param value 值
         */
        public static void putString(String key, String value) {
            if (preferences != null) {
                // 存储后需调用 flush() 确保数据持久化
                preferences.putString(key, value).flush();
            }
        }
    
        /**
         * 读取 String 类型数据
         * @param key 键
         * @param defaultValue 默认值(当 key 不存在时返回)
         * @return 存储的值或默认值
         */
        public static String getString(String key, String defaultValue) {
            if (preferences != null) {
                Optional<String> value = preferences.getString(key);
                return value.orElse(defaultValue);
            }
            return defaultValue;
        }
    
        /**
         * 删除指定键的数据
         * @param key 键
         */
        public static void remove(String key) {
            if (preferences != null) {
                preferences.delete(key).flush();
            }
        }
    
        /**
         * 清空所有存储数据
         */
        public static void clear() {
            if (preferences != null) {
                preferences.clear().flush();
            }
        }
    }
    
  2. 在 MainAbility 中初始化(路径:ohos/mainability/src/main/java/com/your/package/MainAbility.java):

    java

    运行

    package com.your.package;
    
    import com.your.package.utils.PreferencesUtil;
    import ohos.aafwk.ability.Ability;
    import ohos.aafwk.content.Intent;
    import io.flutter.embedding.ohos.FlutterAbility;
    import io.flutter.plugin.common.MethodChannel;
    
    /**
     * 鸿蒙主 Ability(Flutter 入口)
     */
    public class MainAbility extends FlutterAbility {
        // MethodChannel 名称(需与 Flutter 端一致)
        private static final String CHANNEL_NAME = "com.your.package/preferences";
    
        @Override
        public void onStart(Intent intent) {
            super.onStart(intent);
            // 1. 初始化 Preferences(必须在使用前调用)
            PreferencesUtil.init(this);
            // 2. 注册 MethodChannel(实现 Flutter 与原生通信)
            registerMethodChannel();
        }
    
        /**
         * 注册 MethodChannel,处理 Flutter 端的调用
         */
        private void registerMethodChannel() {
            new MethodChannel(
                getFlutterEngine().getDartExecutor().getBinaryMessenger(),
                CHANNEL_NAME
            ).setMethodCallHandler((call, result) -> {
                // 根据方法名分发处理逻辑
                switch (call.method) {
                    case "putString":
                        handlePutString(call, result);
                        break;
                    case "getString":
                        handleGetString(call, result);
                        break;
                    case "remove":
                        handleRemove(call, result);
                        break;
                    case "clear":
                        handleClear(result);
                        break;
                    default:
                        // 未实现的方法
                        result.notImplemented();
                        break;
                }
            });
        }
    
        // 处理 Flutter 端的 putString 调用
        private void handlePutString(MethodCall call, MethodChannel.Result result) {
            String key = call.argument("key");
            String value = call.argument("value");
            if (key == null || value == null) {
                result.error("INVALID_PARAM", "key 或 value 为空", null);
                return;
            }
            PreferencesUtil.putString(key, value);
            result.success(null); // 成功返回 null
        }
    
        // 处理 Flutter 端的 getString 调用
        private void handleGetString(MethodCall call, MethodChannel.Result result) {
            String key = call.argument("key");
            String defaultValue = call.argument("defaultValue");
            if (key == null) {
                result.error("INVALID_PARAM", "key 为空", null);
                return;
            }
            String value = PreferencesUtil.getString(key, defaultValue);
            result.success(value); // 返回存储的值
        }
    
        // 处理 remove 调用(类似 putString)
        private void handleRemove(MethodCall call, MethodChannel.Result result) {
            String key = call.argument("key");
            if (key == null) {
                result.error("INVALID_PARAM", "key 为空", null);
                return;
            }
            PreferencesUtil.remove(key);
            result.success(null);
        }
    
        // 处理 clear 调用
        private void handleClear(MethodChannel.Result result) {
            PreferencesUtil.clear();
            result.success(null);
        }
    }
    
4.1.2 Flutter 端封装(Dart 代码)

创建 OhosPreferences 类,封装 MethodChannel 调用,让业务模块无需关心原生实现:

dart

// 路径:lib/common/storage/ohos_preferences.dart
import 'package:flutter/services.dart';

/**
 * 鸿蒙 Preferences 封装(对外提供统一存储接口)
 */
class OhosPreferences {
  // MethodChannel 名称(必须与原生端一致)
  static const MethodChannel _channel =
      MethodChannel('com.your.package/preferences');

  /**
   * 存储 String 类型数据
   * @param key 存储键
   * @param value 存储值
   * @throws PlatformException 当参数无效或原生调用失败时抛出
   */
  static Future<void> putString(String key, String value) async {
    try {
      await _channel.invokeMethod('putString', {
        'key': key,
        'value': value,
      });
    } on PlatformException catch (e) {
      throw Exception('存储失败:${e.message}');
    }
  }

  /**
   * 读取 String 类型数据
   * @param key 存储键
   * @param defaultValue 默认值(key 不存在时返回)
   * @return 存储的值或默认值
   * @throws PlatformException 当参数无效或原生调用失败时抛出
   */
  static Future<String> getString(String key, {String defaultValue = ''}) async {
    try {
      final result = await _channel.invokeMethod('getString', {
        'key': key,
        'defaultValue': defaultValue,
      });
      return result as String;
    } on PlatformException catch (e) {
      throw Exception('读取失败:${e.message}');
    }
  }

  /**
   * 删除指定键的数据
   * @param key 存储键
   * @throws PlatformException 当参数无效或原生调用失败时抛出
   */
  static Future<void> remove(String key) async {
    try {
      await _channel.invokeMethod('remove', {'key': key});
    } on PlatformException catch (e) {
      throw Exception('删除失败:${e.message}');
    }
  }

  /**
   * 清空所有存储数据
   * @throws PlatformException 当原生调用失败时抛出
   */
  static Future<void> clear() async {
    try {
      await _channel.invokeMethod('clear');
    } on PlatformException catch (e) {
      throw Exception('清空失败:${e.message}');
    }
  }
}
4.1.3 业务模块使用示例

在用户模块中存储 / 读取用户 Token:

dart

// 存储 Token(登录成功后)
await OhosPreferences.putString('user_token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');

// 读取 Token(请求接口前)
final token = await OhosPreferences.getString('user_token');
if (token.isEmpty) {
  // 未登录,跳转登录页
  AppRouter.pushNamed('/login');
}

// 退出登录时删除 Token
await OhosPreferences.remove('user_token');

参考文档

4.2 权限插件:permission_handler → 鸿蒙权限 API

permission_handler 不支持鸿蒙,需通过鸿蒙原生 ohos.security.permission 框架申请权限(如相机、存储、定位)。以下以「存储权限」为例,讲解适配过程。

4.2.1 鸿蒙权限配置(config.json)

首先在鸿蒙项目的 config.json 中声明所需权限(路径:ohos/mainability/src/main/config.json):

json

{
  "app": {
    "bundleName": "com.your.package",
    "vendor": "your_company",
    "version": {
      "code": 1000000,
      "name": "1.0.0"
    }
  },
  "deviceConfig": {},
  "module": {
    "package": "com.your.package",
    "name": ".MainAbility",
    "mainAbility": "com.your.package.MainAbility",
    "deviceType": ["phone", "tablet"],
    "abilities": [
      {
        "name": "com.your.package.MainAbility",
        "type": "page",
        "launchType": "standard"
      }
    ],
    // 声明权限(重点)
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_USER_STORAGE",
        "reason": "需要读取存储以加载用户头像",
        "usedScene": {
          "ability": "com.your.package.MainAbility",
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.WRITE_USER_STORAGE",
        "reason": "需要写入存储以保存用户头像",
        "usedScene": {
          "ability": "com.your.package.MainAbility",
          "when": "always"
        }
      }
    ]
  }
}
4.2.2 鸿蒙原生权限工具类(Java)

创建 PermissionUtil 封装权限申请逻辑(路径:ohos/mainability/src/main/java/com/your/package/utils/PermissionUtil.java):

java

运行

package com.your.package.utils;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.security.permission.PermissionRequest;
import java.util.ArrayList;
import java.util.List;

/**
 * 鸿蒙权限工具类(申请、检查权限)
 */
public class PermissionUtil {
    // 权限请求码(唯一标识一次请求)
    public static final int REQUEST_CODE_STORAGE = 1001;

    /**
     * 检查权限是否已授予
     * @param ability 上下文(如 MainAbility)
     * @param permission 权限名(如 ohos.permission.READ_USER_STORAGE)
     * @return true:已授予;false:未授予
     */
    public static boolean checkPermission(Ability ability, String permission) {
        return ability.verifySelfPermission(permission) == 0;
    }

    /**
     * 检查多个权限是否已授予
     * @param ability 上下文
     * @param permissions 权限列表
     * @return 未授予的权限列表(为空则全部授予)
     */
    public static List<String> checkPermissions(Ability ability, List<String> permissions) {
        List<String> deniedPermissions = new ArrayList<>();
        for (String permission : permissions) {
            if (!checkPermission(ability, permission)) {
                deniedPermissions.add(permission);
            }
        }
        return deniedPermissions;
    }

    /**
     * 申请单个权限
     * @param ability 上下文
     * @param permission 权限名
     * @param requestCode 请求码
     */
    public static void requestPermission(Ability ability, String permission, int requestCode) {
        List<String> permissions = new ArrayList<>();
        permissions.add(permission);
        requestPermissions(ability, permissions, requestCode);
    }

    /**
     * 申请多个权限
     * @param ability 上下文
     * @param permissions 权限列表
     * @param requestCode 请求码
     */
    public static void requestPermissions(Ability ability, List<String> permissions, int requestCode) {
        // 过滤已授予的权限,只申请未授予的
        List<String> deniedPermissions = checkPermissions(ability, permissions);
        if (deniedPermissions.isEmpty()) {
            return; // 所有权限已授予,无需申请
        }

        // 构建权限请求
        PermissionRequest request = new PermissionRequest.Builder()
                .addPermissions(deniedPermissions.toArray(new String[0]))
                .setRequestCode(requestCode)
                .build();

        // 发起权限申请
        ability.requestPermissionsFromUser(request);
    }

    /**
     * 处理权限申请结果(需在 Ability 的 onRequestPermissionsFromUserResult 中调用)
     * @param requestCode 请求码
     * @param permissions 申请的权限列表
     * @param grantResults 授权结果(0:授予,-1:拒绝)
     * @param callback 结果回调(通知 Flutter 端)
     */
    public static void onPermissionResult(
            int requestCode,
            String[] permissions,
            int[] grantResults,
            PermissionResultCallback callback
    ) {
        if (permissions == null || grantResults == null || permissions.length != grantResults.length) {
            callback.onResult(requestCode, false);
            return;
        }

        // 判断是否所有申请的权限都已授予
        boolean allGranted = true;
        for (int result : grantResults) {
            if (result != 0) {
                allGranted = false;
                break;
            }
        }

        callback.onResult(requestCode, allGranted);
    }

    /**
     * 权限申请结果回调接口(用于通知 Flutter 端)
     */
    public interface PermissionResultCallback {
        void onResult(int requestCode, boolean isAllGranted);
    }
}
4.2.3 Flutter 端权限封装(Dart)

创建 OhosPermission 类,通过 MethodChannel 调用原生权限接口:

dart

// 路径:lib/common/utils/ohos_permission.dart
import 'package:flutter/services.dart';

/**
 * 鸿蒙权限封装(支持存储、相机、定位等权限申请)
 */
class OhosPermission {
  // MethodChannel 名称(与原生端一致)
  static const MethodChannel _channel =
      MethodChannel('com.your.package/permission');

  // 权限常量(与鸿蒙权限名对应)
  static const String storageRead = 'ohos.permission.READ_USER_STORAGE';
  static const String storageWrite = 'ohos.permission.WRITE_USER_STORAGE';
  static const String camera = 'ohos.permission.CAMERA';
  static const String location = 'ohos.permission.LOCATION';

  // 请求码(与原生端一致)
  static const int requestCodeStorage = 1001;
  static const int requestCodeCamera = 1002;

  /**
   * 检查单个权限是否已授予
   * @param permission 权限名(如 OhosPermission.storageRead)
   * @return true:已授予;false:未授予
   */
  static Future<bool> checkPermission(String permission) async {
    try {
      final result = await _channel.invokeMethod('checkPermission', {
        'permission': permission,
      });
      return result as bool;
    } on PlatformException catch (e) {
      print('检查权限失败:${e.message}');
      return false;
    }
  }

  /**
   * 申请存储权限(读写)
   * @return true:所有权限已授予;false:至少一个权限被拒绝
   */
  static Future<bool> requestStoragePermission() async {
    try {
      final result = await _channel.invokeMethod('requestPermissions', {
        'permissions': [storageRead, storageWrite],
        'requestCode': requestCodeStorage,
      });
      return result as bool;
    } on PlatformException catch (e) {
      print('申请存储权限失败:${e.message}');
      return false;
    }
  }

  /**
   * 申请相机权限
   * @return true:已授予;false:被拒绝
   */
  static Future<bool> requestCameraPermission() async {
    try {
      final result = await _channel.invokeMethod('requestPermissions', {
        'permissions': [camera],
        'requestCode': requestCodeCamera,
      });
      return result as bool;
    } on PlatformException catch (e) {
      print('申请相机权限失败:${e.message}');
      return false;
    }
  }
}
4.2.4 原生端注册 MethodChannel(MainAbility.java)

在 MainAbility 中添加权限相关的 MethodChannel 处理逻辑(延续 4.1.1 中的 registerMethodChannel 方法):

java

运行

// 在 registerMethodChannel() 中添加以下 switch case
case "checkPermission":
    handleCheckPermission(call, result);
    break;
case "requestPermissions":
    handleRequestPermissions(call, result);
    break;

// 新增处理方法
private void handleCheckPermission(MethodCall call, MethodChannel.Result result) {
    String permission = call.argument("permission");
    if (permission == null) {
        result.error("INVALID_PARAM", "权限名为空", null);
        return;
    }
    boolean isGranted = PermissionUtil.checkPermission(this, permission);
    result.success(isGranted);
}

private void handleRequestPermissions(MethodCall call, MethodChannel.Result result) {
    List<String> permissions = call.argument("permissions");
    int requestCode = call.argument("requestCode");
    if (permissions == null || permissions.isEmpty()) {
        result.error("INVALID_PARAM", "权限列表为空", null);
        return;
    }

    // 存储权限申请结果的回调(需在 MainAbility 中保存)
    permissionResultCallback = (code, isAllGranted) -> {
        if (code == requestCode) {
            result.success(isAllGranted);
        }
    };

    // 发起权限申请
    PermissionUtil.requestPermissions(this, permissions, requestCode);
}

// 声明回调变量(MainAbility 类成员)
private PermissionUtil.PermissionResultCallback permissionResultCallback;

// 重写 onRequestPermissionsFromUserResult 处理权限申请结果
@Override
public void onRequestPermissionsFromUserResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsFromUserResult(requestCode, permissions, grantResults);
    if (permissionResultCallback != null) {
        PermissionUtil.onPermissionResult(
            requestCode,
            permissions,
            grantResults,
            permissionResultCallback
        );
        // 处理后清空回调,避免重复调用
        permissionResultCallback = null;
    }
}
4.2.5 业务模块使用示例

在用户模块中,加载用户头像前先申请存储权限:

dart

// 路径:lib/modules/user/viewModel/user_view_model.dart
Future<void> loadUserAvatar() async {
  // 1. 检查存储权限是否已授予
  final hasStoragePermission = await OhosPermission.checkPermission(OhosPermission.storageRead);
  if (!hasStoragePermission) {
    // 2. 未授予,申请权限
    final isGranted = await OhosPermission.requestStoragePermission();
    if (!isGranted) {
      // 3. 权限被拒绝,提示用户
      showToast('需要存储权限才能加载头像,请在设置中开启');
      return;
    }
  }

  // 4. 权限已授予,加载头像(从本地存储或网络)
  final avatarPath = await _getAvatarPathFromStorage();
  if (avatarPath.isNotEmpty) {
    userAvatar.value = FileImage(File(avatarPath));
  } else {
    // 本地无头像,从网络下载
    await _downloadAvatar();
  }
}

参考文档

4.3 无适配插件的替代方案:自定义 MethodChannel 封装

若某插件无鸿蒙适配版(如 cameralocation),需完全自定义原生能力并通过 MethodChannel 暴露给 Flutter。以下以「相机拍照」为例,讲解完整流程。

4.3.1 鸿蒙原生相机能力封装(Java)
  1. 声明相机权限(在 config.json 中添加):

    json

    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "需要相机权限以拍摄头像",
        "usedScene": {
          "ability": "com.your.package.MainAbility",
          "when": "always"
        }
      }
    ]
    
  2. 创建相机工具类CameraUtil.java):

    java

    运行

    package com.your.package.utils;
    
    import ohos.aafwk.ability.Ability;
    import ohos.aafwk.content.Intent;
    import ohos.agp.utils.LayoutAlignment;
    import ohos.agp.window.dialog.ToastDialog;
    import ohos.media.camera.CameraKit;
    import ohos.media.camera.device.CameraDevice;
    import ohos.media.camera.device.CameraInput;
    import ohos.media.camera.device.CaptureConfig;
    import ohos.media.camera.device.CaptureSession;
    import ohos.media.camera.device.PhotoOutput;
    import ohos.media.camera.params.MetadataObject;
    import ohos.media.image.Image;
    import ohos.media.image.ImageReceiver;
    import ohos.media.image.ImageSource;
    import ohos.media.image.common.PixelFormat;
    import ohos.media.image.common.Size;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    
    /**
     * 鸿蒙相机工具类(拍照功能)
     */
    public class CameraUtil {
        private Ability ability;
        private CameraDevice cameraDevice;
        private CaptureSession captureSession;
        private ImageReceiver imageReceiver;
        private String photoSavePath; // 照片保存路径
        private CameraCallback callback; // 拍照结果回调
    
        // 相机回调接口(通知 Flutter 端)
        public interface CameraCallback {
            void onPhotoTaken(String photoPath); // 拍照成功
            void onError(String errorMsg);       // 拍照失败
        }
    
        public CameraUtil(Ability ability, CameraCallback callback) {
            this.ability = ability;
            this.callback = callback;
            // 初始化照片保存路径(鸿蒙应用沙箱路径)
            this.photoSavePath = ability.getContext().getFilesDir() + "/avatar.jpg";
        }
    
        /**
         * 初始化相机并开始预览(简化版,实际需处理预览界面)
         */
        public void initCamera() {
            try {
                // 1. 获取 CameraKit 实例
                CameraKit cameraKit = CameraKit.getInstance(ability.getContext());
                if (cameraKit == null) {
                    callback.onError("获取相机实例失败");
                    return;
                }
    
                // 2. 打开默认相机(后置相机)
                String cameraId = cameraKit.getCameraIds().get(0);
                cameraKit.createCameraDevice(cameraId, new CameraDevice.StateCallback() {
                    @Override
                    public void onOpened(CameraDevice camera) {
                        cameraDevice = camera;
                        // 3. 初始化 ImageReceiver(接收拍照结果)
                        initImageReceiver();
                        // 4. 创建捕获会话(预览+拍照)
                        createCaptureSession();
                    }
    
                    @Override
                    public void onError(CameraDevice camera, int errorCode) {
                        callback.onError("相机打开失败,错误码:" + errorCode);
                    }
                }, null);
            } catch (Exception e) {
                callback.onError("相机初始化失败:" + e.getMessage());
            }
        }
    
        /**
         * 初始化 ImageReceiver(接收拍照后的图像数据)
         */
        private void initImageReceiver() {
            // 图像尺寸(480x640)和格式(JPEG)
            imageReceiver = ImageReceiver.create(480, 640, PixelFormat.JPEG, 1);
            // 设置图像接收回调
            imageReceiver.setImageArrivalListener(image -> {
                // 保存图像到本地
                saveImageToFile(image);
                // 关闭图像
                image.close();
            });
        }
    
        /**
         * 创建捕获会话(关联相机输入和图像输出)
         */
        private void createCaptureSession() {
            try {
                // 1. 创建相机输入
                CameraInput cameraInput = CameraInput.create(cameraDevice);
    
                // 2. 创建照片输出(关联 ImageReceiver)
                PhotoOutput photoOutput = PhotoOutput.create(imageReceiver.getReceivingSurface(), "photoOutput");
    
                // 3. 构建捕获会话
                CaptureSession.Config sessionConfig = new CaptureSession.Config.Builder()
                        .addInput(cameraInput)
                        .addOutput(photoOutput)
                        .build();
    
                // 4. 创建捕获会话
                cameraDevice.createCaptureSession(sessionConfig, new CaptureSession.StateCallback() {
                    @Override
                    public void onConfigured(CaptureSession session) {
                        captureSession = session;
                        // 会话创建成功,可开始预览(此处简化,实际需添加预览输出)
                    }
    
                    @Override
                    public void onConfigureFailed(CaptureSession session, int errorCode) {
                        callback.onError("捕获会话创建失败,错误码:" + errorCode);
                    }
                }, null);
            } catch (Exception e) {
                callback.onError("创建捕获会话失败:" + e.getMessage());
            }
        }
    
        /**
         * 拍照
         */
        public void takePhoto() {
            if (captureSession == null || cameraDevice == null) {
                callback.onError("相机未初始化完成");
                return;
            }
    
            try {
                // 构建拍照配置
                CaptureConfig captureConfig = new CaptureConfig.Builder()
                        .addOutput(cameraDevice.getOutput("photoOutput"))
                        .build();
    
                // 发起拍照请求
                captureSession.capture(captureConfig, null);
            } catch (Exception e) {
                callback.onError("拍照失败:" + e.getMessage());
            }
        }
    
        /**
         * 将图像数据保存到本地文件
         */
        private void saveImageToFile(Image image) {
            ByteBuffer buffer = image.getComponent(Image.ComponentType.JPEG).getBuffer();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
    
            try (FileOutputStream fos = new FileOutputStream(photoSavePath)) {
                fos.write(data);
                fos.flush();
                // 保存成功,通知回调
                callback.onPhotoTaken(photoSavePath);
            } catch (IOException e) {
                callback.onError("照片保存失败:" + e.getMessage());
            }
        }
    
        /**
         * 释放相机资源
         */
        public void release() {
            if (captureSession != null) {
                captureSession.close();
                captureSession = null;
            }
            if (cameraDevice != null) {
                cameraDevice.close();
                cameraDevice = null;
            }
            if (imageReceiver != null) {
                imageReceiver.release();
                imageReceiver = null;
            }
        }
    
        /**
         * 获取照片保存路径
    

Logo

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

更多推荐