存量 Flutter 项目鸿蒙化:模块化拆分与插件替换实战
本文介绍了将Flutter项目迁移至鸿蒙系统的完整方案。首先需要搭建开发环境,包括DevEco Studio、Flutter SDK和鸿蒙SDK的配置。接着对存量项目进行模块化拆分,降低耦合度,便于鸿蒙原生能力的集成。针对第三方插件不兼容问题,提供了存储插件、权限插件和相机插件的具体替换方案,通过MethodChannel实现Flutter与鸿蒙原生代码的交互。文章包含详细的代码示例和环境配置指南
欢迎大家加入[开源鸿蒙跨平台开发者社区](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 环境配置步骤
-
安装 DevEco Studio:下载后按照向导完成安装,首次启动时会自动下载鸿蒙 SDK(需勾选「HarmonyOS SDK」和「Flutter HarmonyOS Adapter」)。
-
配置 Flutter 环境:解压 Flutter SDK 后,在终端执行以下命令,确保 Flutter 识别鸿蒙环境:
bash
运行
# 检查 Flutter 环境(需包含 HarmonyOS 平台) flutter doctor -v # 若提示 "HarmonyOS toolchain - develop for HarmonyOS devices" 则配置成功 -
连接调试设备:
- 真机:开启「开发者模式」和「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
将依赖分为三类,整理成表格(示例):
| 依赖类型 | 插件示例 | 鸿蒙兼容性 | 处理方案 |
|---|---|---|---|
| 基础工具类 | fluttertoast、logger |
高(纯 Dart) | 直接复用 |
| 平台原生依赖类 | shared_preferences、dio |
中(需适配) | 替换为鸿蒙原生 API 或适配版 |
| 硬件交互类 | camera、location |
低(需封装) | 自定义 MethodChannel 调用 |
兼容性查询渠道:
- 优先查看插件官网 / Pub 页面:搜索「HarmonyOS」关键词(如 dio Pub 页)。
- 鸿蒙插件中心:HarmonyOS-Toolkit 提供部分适配插件。
- GitHub Issues:搜索插件仓库的「HarmonyOS」相关 Issues(如
flutter/plugins仓库)。
2.2.2 风险点梳理
- 架构耦合风险:若项目未模块化(如所有代码写在
main.dart或单一目录),需优先拆分,否则后续适配难以定位修改点。 - 原生代码依赖风险:若项目包含 Android/iOS 原生代码(如
android/app/src/main/java),需重新编写鸿蒙原生代码替换。 - UI 适配风险:鸿蒙设备屏幕尺寸多样(手机、平板、手表),需提前规划多设备 UI 适配方案。
3. 核心实战一:存量项目的模块化拆分
模块化拆分是存量项目鸿蒙化的基础前提—— 通过拆分降低耦合,使鸿蒙原生能力(如分布式数据管理、设备协同)可按需集成到独立模块,同时便于后续维护和复用。
3.1 模块化拆分的核心原则
- 单一职责原则:每个模块仅负责一类业务或功能(如「用户模块」仅处理登录、个人信息,「网络模块」仅处理请求)。
- 低耦合高内聚原则:模块间通过接口通信(如 EventBus、路由),避免直接依赖;模块内部逻辑高度关联。
- 可复用原则:公共功能(如网络、存储、工具类)抽为独立模块,供所有业务模块调用。
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 实现跨模块事件通知(如用户登录后通知商品模块刷新数据)。
-
添加依赖(
pubspec.yaml):yaml
dependencies: event_bus: ^2.0.0 # 最新稳定版 -
初始化 EventBus(
lib/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 {} -
发送事件(用户模块登录成功后,
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'); // 处理错误(如弹窗提示) } } -
监听事件(商品模块接收登录事件,
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 请求:
-
目录结构(
lib/common/network/):plaintext
network/ ├── dio_manager.dart # 单例管理 dio 实例 ├── api_exception.dart # 自定义异常(如网络错误、接口错误) └── request_interceptor.dart # 请求拦截器(添加 Token、日志) -
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'); } } } -
请求拦截器(添加 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_preferences、permission_handler)。需按「优先用鸿蒙适配插件 → 次用原生 API 封装 → 最后自定义实现」的优先级替换。
4.1 存储插件:shared_preferences → 鸿蒙 Preferences
shared_preferences 是 Flutter 常用的轻量存储插件,但不支持鸿蒙。鸿蒙提供原生 Preferences 组件,功能类似,需通过 MethodChannel 封装为 Flutter 可调用的接口。
4.1.1 鸿蒙原生封装(Java 代码)
-
创建 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(); } } } -
在 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');
参考文档:
- 鸿蒙 Preferences 官方文档:数据存储 - Preferences
- Flutter MethodChannel 文档:Platform Channels
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 封装
若某插件无鸿蒙适配版(如 camera、location),需完全自定义原生能力并通过 MethodChannel 暴露给 Flutter。以下以「相机拍照」为例,讲解完整流程。
4.3.1 鸿蒙原生相机能力封装(Java)
-
声明相机权限(在
config.json中添加):json
"requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "需要相机权限以拍摄头像", "usedScene": { "ability": "com.your.package.MainAbility", "when": "always" } } ] -
创建相机工具类(
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; } } /** * 获取照片保存路径

更多推荐





所有评论(0)