Flutter 三方库鸿蒙适配实战:从原理到实践
引言:鸿蒙适配,为何成为新课题?
鸿蒙操作系统发展势头很猛,市场份额也在快速扩大。越来越多的开发者开始面临一个新任务:把现有的 Flutter 应用迁移到鸿蒙平台。根据华为官方数据,截至 2024 年,鸿蒙生态设备数量已经超过了 8 亿台,覆盖了手机、平板、智能穿戴、汽车座舱等各种设备。Flutter 作为谷歌主推的跨平台 UI 框架,其"一次编写,到处运行"的承诺深入人心,但这主要针对的是 Android 和 iOS。当我们真要把 Flutter 应用部署到鸿蒙上时,一个棘手的问题就浮出了水面:那些依赖原生平台能力的三方库,绝大部分都没法直接用了。
问题的根源在于,大约有 70% 的 Flutter 三方库是通过 Platform Channel 机制去调用原生 API 的,而这些原生实现在鸿蒙平台上压根不存在。本文将以一个典型的网络请求库改造为例,从技术原理讲到具体的代码实现,为你梳理一套可行的鸿蒙适配方案。无论你是打算迁移现有项目,还是想开发一个同时支持多平台的新应用,相信这篇文章都能给你带来一些实用的参考。
第一章:摸清原理,适配才不盲目
1.1 搞懂 Flutter 的 Platform Channel 机制
Flutter 和原生平台之间的"对话",主要靠 Platform Channel 来完成。这套机制本质上是客户端-服务端架构,理解它是进行鸿蒙适配的第一步:
dart
// Flutter 端 Platform Channel 基础架构示例
import 'package:flutter/services.dart';
abstract class PlatformChannel {
// MethodChannel 用于调用方法
final MethodChannel _methodChannel = const MethodChannel('com.example/network');
// EventChannel 用于监听事件流(比如网络状态变化)
final EventChannel _eventChannel = const EventChannel('com.example/network_events');
// BasicMessageChannel 用于传递基础消息
final BasicMessageChannel<String> _messageChannel =
const BasicMessageChannel<String>('com.example/messages', StringCodec());
// 通信数据的序列化格式
static const StandardMethodCodec _codec = StandardMethodCodec();
// 封装平台方法调用
Future<T?> invokeMethod<T>(String method, [dynamic arguments]) async {
try {
return await _methodChannel.invokeMethod<T>(method, arguments);
} on PlatformException catch (e) {
print('平台方法调用失败: ${e.message}');
rethrow;
}
}
}
通信流程可以拆解为下面几步:
- 编码 :Flutter 端把 Dart 对象通过
StandardMethodCodec序列化成二进制数据。 - 传输:二进制数据经由底层引擎(Skia+Darwin)传递到原生平台。
- 解码:原生平台接收数据,并反序列化成自己平台的对象。
- 执行:原生平台找到对应的方法执行,并生成结果。
- 回调:结果再按原路返回,最终抵达 Flutter 端。
1.2 Android 与鸿蒙,到底有哪些不同?
要把 Android 的插件搬到鸿蒙,得先知道两者的差异在哪里。下面这张表格对比了几个关键维度:
| 特性维度 | Android 实现 | 鸿蒙实现 | 适配影响 |
|---|---|---|---|
| 线程模型 | Looper/Handler + Binder 线程池 | 分布式任务调度 + ArkTS 协程 | 线程管理和消息传递的逻辑需要重写 |
| JNI/NAPI | Java Native Interface,支持反射调用 | Native API (NAPI),基于 C++ 接口,无反射 | 插件接口需要重新设计 |
| 系统服务 | Android Framework API | Ability Kit + 分布式服务 | 所有涉及系统调用的地方都要适配 |
| 资源管理 | Resources + AssetManager | ResourceManager + 统一资源 | 资源访问的路径和方式都变了 |
| 网络栈 | OkHttp/HttpURLConnection | ArkTS Networking Kit | 网络库几乎需要完全重写 |
| 存储系统 | SharedPreferences + SQLite | Preferences + RDB | 数据持久化方案得调整 |
1.3 适配鸿蒙,我们可以怎么干?
基于上面的差异,我们总结了三种适配策略:
策略一:条件编译 + 平台抽象层 这是最清晰的隔离方式。通过一个抽象层,把平台相关的具体实现隐藏起来。
dart
// 平台检测与条件编译
enum PlatformType { android, ios, harmony }
PlatformType get currentPlatform {
if (Platform.isAndroid) return PlatformType.android;
if (Platform.isIOS) return PlatformType.ios;
// 可以通过特定的 API 来检测鸿蒙
return PlatformType.harmony;
}
// 平台抽象接口
abstract class NetworkAdapter {
Future<Response> get(String url, Map<String, String> headers);
Future<Response> post(String url, dynamic body, Map<String, String> headers);
factory NetworkAdapter.create() {
switch (currentPlatform) {
case PlatformType.harmony:
return HarmonyNetworkAdapter(); // 鸿蒙实现
default:
return DefaultNetworkAdapter(); // Android/iOS 实现
}
}
}
策略二:插件接口统一化 为不同的平台定义一套统一的原生接口规范。这样,同一套 Dart 代码就能对接不同平台的原生实现了,差异被规范"消化"掉了。
策略三:渐进式迁移 对于功能复杂的插件,不要想着一步到位。可以先把核心功能适配好,让应用跑起来,那些高级功能可以放到后续版本中逐步添加。
第二章:动手实战:改造一个网络请求库
2.1 搭好项目架子,选好技术方案
在动手写代码之前,先把项目结构规划清楚。我们采用"平台抽象层"的策略,目录结构大致如下:
flutter_harmony_network/
├── lib/ # Flutter Dart 代码
│ ├── src/
│ │ ├── adapter/ # 适配器抽象与实现
│ │ │ ├── network_adapter.dart
│ │ │ ├── android_adapter.dart
│ │ │ └── harmony_adapter.dart
│ │ ├── channel/ # 平台通道封装
│ │ │ └── harmony_channel.dart
│ │ └── utils/
│ │ └── platform_detector.dart
│ ├── http_client.dart # 主接口类
│ └── exceptions.dart
├── harmony/ # 鸿蒙原生端代码
│ ├── entry/
│ │ └── src/main/
│ │ ├── ets/ # ArkTS 代码
│ │ │ ├── NetworkService.ets
│ │ │ └── ChannelManager.ets
│ │ └── cpp/ # C++ 原生代码(如需要)
│ │ └── native_network.cpp
│ └── build.gradle
└── pubspec.yaml
2.2 Flutter 端的完整实现
2.2.1 主 HTTP 客户端类
这是给业务代码调用的入口,它内部会根据平台选择对应的适配器。
dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'src/adapter/network_adapter.dart';
import 'src/utils/platform_detector.dart';
import 'exceptions.dart';
/// 封装 HTTP 响应
class HttpResponse {
final int statusCode;
final Map<String, String> headers;
final dynamic body;
final String? error;
HttpResponse({
required this.statusCode,
required this.headers,
required this.body,
this.error,
});
bool get isSuccess => statusCode >= 200 && statusCode < 300;
Map<String, dynamic> toJson() => {
'statusCode': statusCode,
'headers': headers,
'body': body,
'error': error,
};
factory HttpResponse.fromJson(Map<String, dynamic> json) {
return HttpResponse(
statusCode: json['statusCode'],
headers: Map<String, String>.from(json['headers']),
body: json['body'],
error: json['error'],
);
}
}
/// 主 HTTP 客户端(单例)
class HarmonyHttpClient {
static final HarmonyHttpClient _instance = HarmonyHttpClient._internal();
late final NetworkAdapter _adapter;
factory HarmonyHttpClient() => _instance;
HarmonyHttpClient._internal() {
_adapter = NetworkAdapter.create(); // 工厂方法创建对应平台的适配器
_initialize();
}
Future<void> _initialize() async {
try {
await _adapter.initialize();
print('HarmonyHttpClient 初始化成功');
} catch (e) {
print('HarmonyHttpClient 初始化失败: $e');
throw NetworkException('客户端初始化失败', e);
}
}
/// 发起 GET 请求
Future<HttpResponse> get(
String url, {
Map<String, String> headers = const {},
int timeoutSeconds = 30,
}) async {
return await _executeWithTimeout(
() => _adapter.get(url, headers),
timeoutSeconds,
);
}
/// 发起 POST 请求
Future<HttpResponse> post(
String url, {
dynamic body,
Map<String, String> headers = const {},
int timeoutSeconds = 30,
}) async {
final updatedHeaders = Map<String, String>.from(headers);
// 如果是 Map 且未指定 Content-Type,默认用 JSON
if (body is Map && !updatedHeaders.containsKey('Content-Type')) {
updatedHeaders['Content-Type'] = 'application/json';
}
return await _executeWithTimeout(
() => _adapter.post(url, body, updatedHeaders),
timeoutSeconds,
);
}
/// 统一的带超时处理
Future<HttpResponse> _executeWithTimeout(
Future<HttpResponse> Function() request,
int timeoutSeconds,
) async {
try {
return await request().timeout(Duration(seconds: timeoutSeconds));
} on TimeoutException {
throw NetworkException('请求超时 ($timeoutSeconds 秒)');
} on PlatformException catch (e) {
throw NetworkException('平台异常: ${e.message}', e);
} catch (e) {
throw NetworkException('请求失败', e);
}
}
/// 批量 GET 请求(简单并发控制)
Future<List<HttpResponse>> batchGet(
List<String> urls, {
Map<String, String> headers = const {},
int maxConcurrent = 3,
}) async {
final responses = <HttpResponse>[];
final semaphore = StreamController<void>();
int active = 0;
for (final url in urls) {
// 控制并发数
while (active >= maxConcurrent) {
await semaphore.stream.first;
}
active++;
// 使用 unawaited 防止阻塞循环
unawaited(
get(url, headers: headers).then((response) {
responses.add(response);
active--;
if (!semaphore.isClosed) semaphore.add(null);
}).catchError((e) {
active--;
if (!semaphore.isClosed) semaphore.add(null);
// 记录错误但继续其他请求
responses.add(HttpResponse(
statusCode: 0,
headers: {},
body: null,
error: e.toString(),
));
})
);
}
// 等待所有进行中的请求完成
while (active > 0) {
await semaphore.stream.first;
}
await semaphore.close();
return responses;
}
}
2.2.2 鸿蒙平台适配器的具体实现
这是核心,负责和鸿蒙原生侧通信。
dart
// lib/src/adapter/harmony_adapter.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import '../channel/harmony_channel.dart';
import 'network_adapter.dart';
class HarmonyNetworkAdapter implements NetworkAdapter {
late final HarmonyChannel _channel;
final StreamController<NetworkEvent> _eventController =
StreamController<NetworkEvent>.broadcast();
@override
Future<void> initialize() async {
_channel = HarmonyChannel();
await _channel.initialize();
// 监听来自原生端的网络状态变化事件
_channel.onNetworkStateChanged.listen((state) {
_eventController.add(NetworkEvent(
type: NetworkEventType.stateChanged,
data: {'state': state},
));
});
}
@override
Future<HttpResponse> get(String url, Map<String, String> headers) async {
try {
final result = await _channel.invokeMethod('get', {
'url': url,
'headers': headers,
});
return _parseResponse(result);
} on PlatformException catch (e) {
throw NetworkException(
'鸿蒙平台GET请求失败',
e,
extra: {'url': url, 'code': e.code},
);
}
}
@override
Future<HttpResponse> post(
String url,
dynamic body,
Map<String, String> headers,
) async {
try {
String bodyString;
if (body is Map || body is List) {
bodyString = json.encode(body);
} else {
bodyString = body.toString();
}
final result = await _channel.invokeMethod('post', {
'url': url,
'headers': headers,
'body': bodyString,
});
return _parseResponse(result);
} on PlatformException catch (e) {
throw NetworkException(
'鸿蒙平台POST请求失败',
e,
extra: {'url': url, 'code': e.code},
);
}
}
// 解析原生端返回的响应数据
HttpResponse _parseResponse(dynamic result) {
if (result is Map) {
return HttpResponse(
statusCode: result['statusCode'] ?? 0,
headers: Map<String, String>.from(result['headers'] ?? {}),
body: result['body'],
error: result['error'],
);
}
throw const FormatException('响应格式错误');
}
@override
Stream<NetworkEvent> get onEvent => _eventController.stream;
@override
Future<void> dispose() async {
await _eventController.close();
await _channel.dispose();
}
}
// 定义网络相关事件
enum NetworkEventType {
stateChanged,
requestStarted,
requestCompleted,
errorOccurred,
}
class NetworkEvent {
final NetworkEventType type;
final Map<String, dynamic> data;
NetworkEvent({required this.type, required this.data});
}
2.2.3 在 Widget 中使用的示例
看看上面写的库,在界面里该怎么用。
dart
import 'package:flutter/material.dart';
import 'package:flutter_harmony_network/http_client.dart';
class NetworkDemoPage extends StatefulWidget {
const NetworkDemoPage({super.key});
@override
State<NetworkDemoPage> createState() => _NetworkDemoPageState();
}
class _NetworkDemoPageState extends State<NetworkDemoPage> {
final HarmonyHttpClient _client = HarmonyHttpClient();
HttpResponse? _response;
bool _isLoading = false;
String _error = '';
Future<void> _fetchData() async {
setState(() {
_isLoading = true;
_error = '';
});
try {
final response = await _client.get(
'https://jsonplaceholder.typicode.com/posts/1',
headers: {
'User-Agent': 'Flutter-Harmony-Demo',
'Accept': 'application/json',
},
timeoutSeconds: 10,
);
setState(() {
_response = response;
_isLoading = false;
});
if (!response.isSuccess) {
setState(() {
_error = '请求失败: ${response.statusCode}';
});
}
} on NetworkException catch (e) {
setState(() {
_isLoading = false;
_error = '网络异常: ${e.message}';
});
} catch (e) {
setState(() {
_isLoading = false;
_error = '未知错误: $e';
});
}
}
Future<void> _postData() async {
setState(() {
_isLoading = true;
_error = '';
});
try {
final response = await _client.post(
'https://jsonplaceholder.typicode.com/posts',
body: {
'title': 'Harmony POST Test',
'body': 'This is a test from Flutter Harmony adapter',
'userId': 1,
},
);
setState(() {
_response = response;
_isLoading = false;
});
if (!response.isSuccess) {
setState(() {
_error = 'POST请求失败: ${response.statusCode}';
});
}
} catch (e) {
setState(() {
_isLoading = false;
_error = 'POST错误: $e';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('鸿蒙网络适配演示'),
backgroundColor: Colors.blue[800],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: _isLoading ? null : _fetchData,
icon: const Icon(Icons.download),
label: const Text('GET请求'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
),
ElevatedButton.icon(
onPressed: _isLoading ? null : _postData,
icon: const Icon(Icons.upload),
label: const Text('POST请求'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
),
],
),
const SizedBox(height: 20),
// 状态和结果显示卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 状态指示器
Row(
children: [
Icon(
_isLoading ? Icons.refresh : Icons.check_circle,
color: _isLoading ? Colors.blue : Colors.green,
),
const SizedBox(width: 8),
Text(
_isLoading ? '请求中...' : '就绪',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _isLoading ? Colors.blue : Colors.green,
),
),
],
),
// 错误信息展示
if (_error.isNotEmpty) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red),
),
child: Row(
children: [
const Icon(Icons.error, color: Colors.red),
const SizedBox(width: 8),
Expanded(
child: Text(_error, style: const TextStyle(color: Colors.red)),
),
],
),
),
],
// 响应详情展示
if (_response != null) ...[
const SizedBox(height: 16),
const Text('响应详情:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('状态码: ${_response!.statusCode}'),
Text('成功: ${_response!.isSuccess}'),
const SizedBox(height: 8),
const Text('响应体:'),
Container(
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(4),
),
child: SelectableText(
_response!.body?.toString() ?? '空响应',
style: const TextStyle(fontFamily: 'monospace'),
),
),
],
),
),
],
],
),
),
),
],
),
),
);
}
}
2.3 鸿蒙原生端的实现(ArkTS)
2.3.1 网络服务层
这是真正执行网络请求的地方,使用鸿蒙官方的 @ohos.net.http 能力。
typescript
// entry/src/main/ets/NetworkService.ets
import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';
import { logger } from './Logger';
export class NetworkService {
private static instance: NetworkService;
private httpClient: http.HttpClient;
private constructor() {
this.httpClient = http.createHttp();
}
public static getInstance(): NetworkService {
if (!NetworkService.instance) {
NetworkService.instance = new NetworkService();
}
return NetworkService.instance;
}
// GET 请求实现
async get(url: string, headers: Record<string, string>): Promise<any> {
logger.info(`Harmony GET请求: ${url}`);
try {
const options: http.HttpRequestOptions = {
method: http.RequestMethod.GET,
header: headers,
readTimeout: 30000,
connectTimeout: 30000,
};
const response = await this.httpClient.request(url, options);
if (response.responseCode === 200) {
const result = response.result.toString();
return {
statusCode: response.responseCode,
headers: response.header,
body: JSON.parse(result),
error: null
};
} else {
return {
statusCode: response.responseCode,
headers: response.header,
body: null,
error: `HTTP ${response.responseCode}`
};
}
} catch (error) {
logger.error(`GET请求失败: ${JSON.stringify(error)}`);
throw new BusinessError({
code: 500,
message: `网络请求失败: ${error.message}`
});
}
}
// POST 请求实现
async post(url: string, headers: Record<string, string>, body: string): Promise<any> {
logger.info(`Harmony POST请求: ${url}`);
try {
const options: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
header: {
...headers,
'Content-Type': headers['Content-Type'] || 'application/json'
},
extraData: body,
readTimeout: 30000,
connectTimeout: 30000,
};
const response = await this.httpClient.request(url, options);
if (response.responseCode >= 200 && response.responseCode < 300) {
const result = response.result.toString();
return {
statusCode: response.responseCode,
headers: response.header,
body: result ? JSON.parse(result) : null,
error: null
};
} else {
return {
statusCode: response.responseCode,
headers: response.header,
body: null,
error: `HTTP ${response.responseCode}`
};
}
} catch (error) {
logger.error(`POST请求失败: ${JSON.stringify(error)}`);
throw new BusinessError({
code: 500,
message: `网络请求失败: ${error.message}`
});
}
}
destroy(): void {
this.httpClient.destroy();
logger.info('NetworkService 已销毁');
}
}
2.3.2 Channel 管理器
负责与 Flutter 端的 Platform Channel 对接,是通信的桥梁。
typescript
// entry/src/main/ets/ChannelManager.ets
import { BusinessError } from '@ohos.base';
import { NetworkService } from './NetworkService';
import { logger } from './Logger';
export class ChannelManager {
private networkService: NetworkService;
// 假设这里已经注入了 Flutter 的 Channel 对象
private methodChannel: any;
constructor() {
this.networkService = NetworkService.getInstance();
this._setupChannels();
}
private _setupChannels(): void {
// 这里需要根据鸿蒙 Flutter 插件的实际 API 来注册 MethodChannel
// 例如: