引言
最近,隨着鴻蒙(HarmonyOS)操作系統的快速發展和生態的日益成熟,我們這些跨平台開發者面臨一個新問題:如何讓自己熟悉的框架,比如 Flutter,在鴻蒙上也能順暢運行。Flutter 憑藉其優秀的渲染性能和跨端一致性,依然是很多團隊的首選。但隨之而來的挑戰也很具體——如何將 Flutter 生態中那些好用的插件(尤其是 pub.dev 上的三方庫)平滑地遷移到鴻蒙平台。
flutter_native_splash 是 Flutter 中專門用來管理應用啓動屏(Splash Screen)的一個熱門庫。它通過自動生成代碼,幫我們省去了在各原生平台手動配置啓動畫面的麻煩。然而,由於鴻蒙獨特的系統架構和資源管理機制,這個庫並不能直接使用。啓動屏作為用户對應用的第一印象,它的體驗好壞直接影響用户感知。因此,解決它的適配問題成了我們無法迴避的一環。
在這篇文章裏,我想和大家深入聊聊 Flutter 插件在鴻蒙端適配的一般思路,並以 flutter_native_splash 為例,從技術原理、完整代碼實現、集成步驟,再到性能優化,提供一個完整的實戰方案。希望不僅幫你解決眼前的問題,更能讓你理解背後的邏輯,以後遇到其他插件適配時也能舉一反三。
技術分析:Flutter插件在鴻蒙上是如何適配的?
1. Flutter插件的三層架構
要適配一個Flutter插件,首先得清楚它的工作方式。一個典型的Flutter插件通常包含三層結構,這樣Dart代碼才能和原生平台“對話”:
- Dart層:這是我們最熟悉的一層,就是插件暴露給Flutter開發者的API接口。
- 平台通道層(Platform Channel):這是Flutter框架的通信橋樑。主要通過
MethodChannel實現,讓Dart代碼可以異步調用原生方法,並拿到返回結果。數據傳遞時會自動進行序列化和反序列化。 - 原生平台層:這才是插件的“實幹家”,包含了Android、iOS等各個平台的具體實現代碼,負責調用操作系統提供的原生API。
那麼,鴻蒙適配的核心任務是什麼? 其實就是在鴻蒙項目中,按照它的開發規範(比如使用ArkTS/ArkUI,適配對應API),重新實現上面的第三層——也就是原生平台層,並確保它能通過平台通道和Dart層正確通信。
2. 鴻蒙平台的特點與適配難點
鴻蒙和Android在設計理念上有不少區別,這些區別直接影響了我們的適配策略:
| 特性維度 | Android | 鴻蒙 (HarmonyOS) | 對適配的影響 |
|---|---|---|---|
| 資源管理 | 用XML文件在res/目錄下配置。 |
改用JSON格式描述資源(放在resources/base/等目錄),強調多設備適配。 |
需要把插件生成的圖片、顏色等資源轉換成鴻蒙認識的格式,並正確配置資源索引。 |
| UI框架 | 傳統的、基於View和ViewGroup的命令式UI。 |
基於ArkTS/ArkUI的聲明式UI,組件生命週期和佈局方式都變了。 | Android那套SplashActivity的視圖代碼沒法直接用了。我們需要用ArkUI組件(比如Image、Column)創建一個新的Page來當啓動頁。 |
| 應用模型 | 圍繞Activity、Service等組件構建。 |
變成了基於Ability(例如UIAbility、ExtensionAbility)的模型。 |
應用的啓動入口從Activity換成了UIAbility。啓動屏的邏輯需要整合到EntryAbility的創建和初始化階段裏。 |
| 線程模型 | 主線程(UI線程)配合Handler、Looper處理任務。 |
基於TaskDispatcher進行分佈式任務調度。 |
涉及到UI操作和異步任務時,得改用鴻蒙的MainTaskDispatcher和UITaskDispatcher。 |
3. flutter_native_splash 庫是怎麼工作的?
這個庫的核心可以看作一個構建階段工具加一套運行時協議。
-
構建時(代碼生成):
- 讀取
pubspec.yaml裏flutter_native_splash下的配置(比如背景色、圖片路徑、狀態欄樣式)。 -
然後根據這些配置,自動生成各個平台需要的原生資源文件。
- 在 Android 上,會生成
launch_background.xml,並修改styles.xml。 - 在 iOS 上,則生成
LaunchScreen.storyboard或修改Assets.xcassets。
- 在 Android 上,會生成
- 這一步一般通過Flutter的
flutter_gen或自定義的build.dart腳本來完成。
- 讀取
-
運行時(平台實現):
- 庫的Dart部分會在應用啓動時,通過
MethodChannel向原生端發送一個消息(比如‘remove’)。 - 原生端(Android的
SplashActivity或 iOS的AppDelegate)收到消息後,延遲移除啓動屏視圖,並顯示出Flutter引擎渲染的主頁。 - 這樣就保證了從原生啓動屏到Flutter頁面的平滑過渡,避免了中間白屏。
- 庫的Dart部分會在應用啓動時,通過
所以,我們在鴻蒙端要做什麼? 簡單説,就是模擬上述行為。我們需要在鴻蒙應用啓動時顯示一個自定義的啓動頁(用來替代原來自動生成的資源),然後在收到Flutter端的指令後,優雅地跳轉到Flutter主頁面。
具體實現與完整代碼
1. 核心思路
在鴻蒙這邊,我們打算創建一個自定義的SplashScreenAbility作為應用入口。它主要負責兩個頁面:
SplashPage:用ArkUI實現的啓動屏,用來展示logo或背景色。FlutterPage:承載Flutter引擎渲染內容的頁面。
同時,我們寫一個鴻蒙側的SplashScreenPlugin,讓它與Flutter側的MethodChannel通信,在合適的時機觸發從SplashPage到FlutterPage的跳轉。
2. 完整代碼實現
a. 鴻蒙側:SplashScreenAbility (入口Ability)
// entry/src/main/ets/entryability/SplashScreenAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
import { SplashScreenPlugin } from '../plugin/SplashScreenPlugin'; // 自定義插件
import { Logger } from '../utils/Logger';
const TAG: string = 'SplashScreenAbility';
const CHANNEL_NAME: string = 'splashscreen'; // 需要和Flutter側約定的通道名一致
export default class SplashScreenAbility extends UIAbility {
private splashPlugin: SplashScreenPlugin | null = null;
// Ability創建時的初始化
onCreate(want, launchParam) {
Logger.info(TAG, 'SplashScreenAbility onCreate');
// 1. 初始化與Flutter通信的插件
this.splashPlugin = new SplashScreenPlugin(this.context);
// 2. 註冊方法調用處理器
this.splashPlugin.registerMethodCallHandler((method: string, result: { success: (data?) => void, error: (code: string, message: string) => void }) => {
Logger.info(TAG, `收到Flutter端的方法調用: ${method}`);
switch (method) {
case 'show':
// 啓動時通常已顯示,這裏可以處理額外邏輯
result.success();
break;
case 'remove':
// Flutter請求移除啓動屏,通知Ability進行跳轉
this.handleRemoveSplash();
result.success();
break;
case 'getPlatformVersion':
result.success(`HarmonyOS ${window.processInfo?.versionName || 'Unknown'}`);
break;
default:
result.error('404', `方法 ${method} 未實現.`);
}
});
}
// 當Ability窗口創建時,加載啓動屏
onWindowStageCreate(windowStage: window.WindowStage): void {
Logger.info(TAG, 'SplashScreenAbility onWindowStageCreate');
windowStage.loadContent('pages/SplashPage', (err) => {
if (err.code) {
Logger.error(TAG, `加載SplashPage失敗. Code: ${err.code}, message: ${err.message}`);
return;
}
Logger.info(TAG, 'SplashPage加載成功.');
// 可選:設置一下窗口背景色,保持視覺統一
windowStage.getMainWindow().then((win) => {
win.setWindowBackgroundColor('#FFFFFF'); // 這裏顏色應該和啓動屏背景色一致
});
});
}
// 處理移除啓動屏的邏輯
private async handleRemoveSplash(): Promise<void> {
Logger.info(TAG, '開始移除啓動屏.');
try {
const windowStage = await window.WindowStage.getMainWindowStage();
// 跳轉到承載Flutter引擎的FlutterPage
windowStage.loadContent('pages/FlutterPage', (err) => {
if (err.code) {
Logger.error(TAG, `加載FlutterPage失敗. Code: ${err.code}, message: ${err.message}`);
// 降級處理:如果跳轉失敗,可以延遲重試
setTimeout(() => {
this.handleRemoveSplash();
}, 500);
} else {
Logger.info(TAG, '成功跳轉到FlutterPage.');
}
});
} catch (error) {
Logger.error(TAG, `獲取window stage時出錯: ${JSON.stringify(error)}`);
}
}
onDestroy() {
Logger.info(TAG, 'SplashScreenAbility onDestroy');
this.splashPlugin?.release();
}
}
b. 鴻蒙側:自定義通信插件 (SplashScreenPlugin)
// entry/src/main/ets/plugin/SplashScreenPlugin.ts
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';
import { Logger } from '../utils/Logger';
const TAG: string = 'SplashScreenPlugin';
// 這裏簡化模擬了MethodChannel的核心功能
export class SplashScreenPlugin {
private context: common.Context;
private methodCallHandler: ((method: string, result: MethodCallResult) => void) | null = null;
constructor(context: common.Context) {
this.context = context;
}
// 註冊來自Flutter端的方法調用處理器
registerMethodCallHandler(handler: (method: string, result: MethodCallResult) => void): void {
this.methodCallHandler = handler;
Logger.info(TAG, '方法調用處理器註冊成功.');
}
// 這個方法應由一個全局的、與Flutter C++層橋接的模塊來調用。
// 這裏為了簡化,假設橋接層在Flutter引擎初始化後,會調用這個方法來模擬Flutter側的invokeMethod。
simulateMethodCallFromFlutter(method: string): Promise<any> {
return new Promise((resolve, reject) => {
if (!this.methodCallHandler) {
reject(new Error('尚未註冊方法調用處理器.'));
return;
}
Logger.debug(TAG, `模擬Flutter端調用: ${method}`);
this.methodCallHandler(method, {
success: (data) => resolve(data),
error: (code: string, message: string) => reject(new Error(`[$code] $message`))
});
});
}
release(): void {
this.methodCallHandler = null;
Logger.info(TAG, '插件資源已釋放.');
}
}
export interface MethodCallResult {
success: (data?: any) => void;
error: (code: string, message: string) => void;
}
c. 鴻蒙側:啓動屏UI頁面 (SplashPage)
<!-- entry/src/main/resources/base/profile/main_pages.json -->
{
"src": [
"pages/SplashPage",
"pages/FlutterPage"
]
}
<!-- entry/src/main/ets/pages/SplashPage.hml -->
<div class="container">
<!-- 根據實際設計調整,這裏展示一個居中logo -->
<image src="/common/splash_logo.png" class="splash-image"></image>
<!-- 可選:添加應用名稱或其他元素 -->
<text class="app-name">我的Flutter應用</text>
</div>
/* entry/src/main/ets/pages/SplashPage.css */
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: #2196F3; /* 這個顏色應該和pubspec.yaml裏配置的背景色保持一致 */
}
.splash-image {
width: 120px;
height: 120px;
object-fit: contain;
}
.app-name {
margin-top: 20px;
font-size: 18fp;
color: #FFFFFF;
font-weight: 500;
}
d. Flutter側:Dart接口適配
我們需要創建一個專門用於鴻蒙的Dart插件包,或者修改flutter_native_splash庫,讓它能條件化地導入我們的實現。
// lib/harmony_splash.dart
import 'dart:async';
import 'package:flutter/services.dart';
class HarmonyNativeSplash {
static const MethodChannel _channel =
const MethodChannel('splashscreen'); // 與鴻蒙側通道名一致
static Future<void> show() async {
try {
await _channel.invokeMethod('show');
} on PlatformException catch (e) {
print("顯示啓動屏失敗: '${e.message}'.");
}
}
static Future<void> remove() async {
try {
await _channel.invokeMethod('remove');
} on PlatformException catch (e) {
print("移除啓動屏失敗: '${e.message}'.");
}
}
static Future<String?> getPlatformVersion() async {
try {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
} on PlatformException catch (e) {
print("獲取系統版本失敗: '${e.message}'.");
return null;
}
}
}
在Flutter應用的主文件中使用:
// lib/main.dart
import 'package:flutter/material.dart';
import 'harmony_splash.dart'; // 導入我們自定義的鴻蒙適配層
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 在runApp之前,可以調用show(鴻蒙端可能默認已經顯示了)
// HarmonyNativeSplash.show();
runApp(MyApp());
// 在Flutter首幀渲染完成後,請求移除原生啓動屏
WidgetsBinding.instance.addPostFrameCallback((_) async {
// 加一個短暫的延遲,讓過渡更平滑
await Future.delayed(const Duration(milliseconds: 300));
await HarmonyNativeSplash.remove();
});
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter on HarmonyOS',
home: HomePage(),
);
}
}
集成步驟與性能優化建議
1. 詳細集成步驟
- 環境準備:確保 DevEco Studio、HarmonyOS SDK 已安裝,並配置好 Flutter for HarmonyOS 的開發環境(主要是 OpenHarmony 上的 Flutter 運行時)。
- 創建HarmonyOS工程:用 DevEco Studio 新建一個空的 HarmonyOS 應用項目。
- 集成Flutter模塊:把你的 Flutter 項目以 Har 包或模塊的形式集成到鴻蒙工程裏。這一步通常需要把 Flutter 的構建產物(比如
libflutter.so,app.so, 各種資源)放到鴻蒙項目的指定目錄。 - 替換入口Ability:把鴻蒙工程裏默認的
EntryAbility換成我們剛實現的SplashScreenAbility(記得修改module.json5中的srcEntry配置)。 - 實現插件通信層:把上面的
SplashScreenPlugin代碼集成到項目中,並確保 Flutter 引擎初始化後,Dart層和鴻蒙原生層能通過MethodChannel正確連接上。這部分可能需要修改 Flutter 引擎在鴻蒙端的集成層代碼(C++ 或 ArkTS)。 - 資源配置:把設計好的啓動屏圖片(比如
splash_logo.png)放到entry/src/main/resources/base/media/目錄下,並在SplashPage.css中正確引用。 - 配置Flutter側:在 Flutter 項目的
pubspec.yaml裏,移除或條件化原來的flutter_native_splash配置,引入或編寫我們自定義的harmony_splash.dart插件邏輯。 - 構建與調試:用 DevEco Studio 編譯並運行鴻蒙應用,仔細觀察整個啓動流程。
2. 調試方法與常見問題
- 善用日誌:充分利用鴻蒙的
HiLog或你自己的Logger,在SplashScreenAbility和SplashScreenPlugin的關鍵節點打上日誌,確認生命週期順序和MethodChannel調用是否成功。 -
頁面不跳轉怎麼辦?
- 檢查一下,Dart 和 ArkTS 兩邊的
MethodChannel名字是不是一模一樣。 - 確認
handleRemoveSplash方法裏獲取WindowStage的邏輯在當前 Ability 上下文中是否有效。 - 看看
FlutterPage有沒有在main_pages.json里正確配置。
- 檢查一下,Dart 和 ArkTS 兩邊的
-
啓動屏樣式不對?
- 核對
SplashPage.css裏的背景色和 Flutter 項目原來的配置是否一致。 - 檢查圖片資源的路徑和格式鴻蒙是否支持。
- 核對
3. 性能優化建議
-
啓動時間優化:
- 保持SplashPage簡單:千萬別在啓動頁做耗時操作(比如網絡請求、複雜計算)。只放必要的圖片和樣式就好。
- 預加載Flutter引擎:可以在顯示
SplashPage的同時,在後台異步初始化 Flutter 引擎裏那些非 UI 相關的模塊。 - 優化圖片資源:對啓動屏圖片進行無損壓縮,並準備合適分辨率的版本(
hdpi,xhdpi等),避免因圖片解碼拖慢首屏顯示。
-
內存與視覺過渡優化:
- 及時釋放資源:跳轉到
FlutterPage後,確保SplashPage的 UI 組件和相關資源能被及時回收。 - 追求平滑過渡:在 Flutter 側調用
remove之後,可以給初始的 Flutter 頁面設置一個和啓動屏背景色相同的背景,或者在鴻蒙側做一個簡單的漸隱動畫,避免視覺上的生硬切換。
- 及時釋放資源:跳轉到
-
性能數據對比參考:
你可以通過系統工具或自己打點,來量化一下適配前後的效果。下面是個示例:指標 適配前 (無啓動屏/白屏) 適配後 (自定義鴻蒙啓動屏) 優化説明 首次啓動到首幀顯示(ms) ~1200ms (主要是Flutter引擎初始化耗時) ~400ms 鴻蒙原生頁面幾乎瞬間展示,掩蓋了大部分Flutter引擎的初始化時間。 啓動屏顯示總時長(ms) N/A ~1500ms 從顯示SplashPage到跳轉FlutterPage的總時間,包含了用户能感知到的啓動屏展示和隱藏過程。 UI線程阻塞風險 低(因為沒複雜的原生UI) 低(ArkUI聲明式,且頁面很簡單) 關鍵是要保證SplashPage的UI複雜度足夠低。
總結
這篇文章我們詳細討論瞭如何將 Flutter 生態插件——特別是 flutter_native_splash 這個啓動屏庫——適配到鴻蒙平台。我們首先分析了 Flutter 插件的分層架構和鴻蒙系統特性的差異,明確了適配工作的核心就是 重寫原生平台層的實現。
通過具體的代碼實例,我們展示瞭如何構建一個定製的 SplashScreenAbility 來管理啓動生命週期,如何用 ArkUI 創建啓動頁面,以及如何通過模擬 MethodChannel 的通信機制,在 Dart 和鴻蒙原生代碼之間協調,實現啓動屏的定時移除。希望不僅提供了“怎麼做”的步驟,也講清楚了“為什麼這麼做”的道理。
此外,我們還給出了從環境準備到調試的完整實踐路徑,並提供了一些切實可行的性能優化建議,目標是幫助大家打造啓動更快、體驗更流暢的鴻蒙 Flutter 應用。
這次適配實踐其實揭示了一個通用模式:對於大多數 Flutter 插件,只要搞清楚它的 Dart 接口和原生平台功能的邊界,並深入理解鴻蒙對應的 API 和能力(比如 UI、網絡、存儲等),都可以按照這種 “通信橋接 + 原生實現” 的思路來完成遷移。隨着 Flutter for HarmonyOS 的不斷成熟,未來這類適配工作肯定會越來越標準化,甚至自動化,但掌握其底層原理,永遠是我們開發者應對新技術挑戰最可靠的武器。