博客 / 詳情

返回

HarmonyOS適配 Flutter `flutter_native_splash` 庫:原理、實現與性能優化

引言

最近,隨着鴻蒙(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框架 傳統的、基於ViewViewGroup的命令式UI。 基於ArkTS/ArkUI的聲明式UI,組件生命週期和佈局方式都變了。 Android那套SplashActivity的視圖代碼沒法直接用了。我們需要用ArkUI組件(比如ImageColumn)創建一個新的Page來當啓動頁。
應用模型 圍繞ActivityService等組件構建。 變成了基於Ability(例如UIAbilityExtensionAbility)的模型。 應用的啓動入口從Activity換成了UIAbility。啓動屏的邏輯需要整合到EntryAbility的創建和初始化階段裏。
線程模型 主線程(UI線程)配合HandlerLooper處理任務。 基於TaskDispatcher進行分佈式任務調度。 涉及到UI操作和異步任務時,得改用鴻蒙的MainTaskDispatcherUITaskDispatcher

3. flutter_native_splash 庫是怎麼工作的?

這個庫的核心可以看作一個構建階段工具加一套運行時協議

  1. 構建時(代碼生成)

    • 讀取pubspec.yamlflutter_native_splash下的配置(比如背景色、圖片路徑、狀態欄樣式)。
    • 然後根據這些配置,自動生成各個平台需要的原生資源文件。

      • Android 上,會生成launch_background.xml,並修改styles.xml
      • iOS 上,則生成LaunchScreen.storyboard或修改Assets.xcassets
    • 這一步一般通過Flutter的flutter_gen或自定義的build.dart腳本來完成。
  2. 運行時(平台實現)

    • 庫的Dart部分會在應用啓動時,通過MethodChannel向原生端發送一個消息(比如 ‘remove’)。
    • 原生端(Android的SplashActivity或 iOS的AppDelegate)收到消息後,延遲移除啓動屏視圖,並顯示出Flutter引擎渲染的主頁。
    • 這樣就保證了從原生啓動屏到Flutter頁面的平滑過渡,避免了中間白屏。

所以,我們在鴻蒙端要做什麼? 簡單説,就是模擬上述行為。我們需要在鴻蒙應用啓動時顯示一個自定義的啓動頁(用來替代原來自動生成的資源),然後在收到Flutter端的指令後,優雅地跳轉到Flutter主頁面。

具體實現與完整代碼

1. 核心思路

在鴻蒙這邊,我們打算創建一個自定義的SplashScreenAbility作為應用入口。它主要負責兩個頁面:

  • SplashPage:用ArkUI實現的啓動屏,用來展示logo或背景色。
  • FlutterPage:承載Flutter引擎渲染內容的頁面。

同時,我們寫一個鴻蒙側的SplashScreenPlugin,讓它與Flutter側的MethodChannel通信,在合適的時機觸發從SplashPageFlutterPage的跳轉。

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. 詳細集成步驟

  1. 環境準備:確保 DevEco Studio、HarmonyOS SDK 已安裝,並配置好 Flutter for HarmonyOS 的開發環境(主要是 OpenHarmony 上的 Flutter 運行時)。
  2. 創建HarmonyOS工程:用 DevEco Studio 新建一個空的 HarmonyOS 應用項目。
  3. 集成Flutter模塊:把你的 Flutter 項目以 Har 包或模塊的形式集成到鴻蒙工程裏。這一步通常需要把 Flutter 的構建產物(比如 libflutter.soapp.so, 各種資源)放到鴻蒙項目的指定目錄。
  4. 替換入口Ability:把鴻蒙工程裏默認的 EntryAbility 換成我們剛實現的 SplashScreenAbility(記得修改 module.json5 中的 srcEntry 配置)。
  5. 實現插件通信層:把上面的 SplashScreenPlugin 代碼集成到項目中,並確保 Flutter 引擎初始化後,Dart層和鴻蒙原生層能通過 MethodChannel 正確連接上。這部分可能需要修改 Flutter 引擎在鴻蒙端的集成層代碼(C++ 或 ArkTS)。
  6. 資源配置:把設計好的啓動屏圖片(比如 splash_logo.png)放到 entry/src/main/resources/base/media/ 目錄下,並在 SplashPage.css 中正確引用。
  7. 配置Flutter側:在 Flutter 項目的 pubspec.yaml 裏,移除或條件化原來的 flutter_native_splash 配置,引入或編寫我們自定義的 harmony_splash.dart 插件邏輯。
  8. 構建與調試:用 DevEco Studio 編譯並運行鴻蒙應用,仔細觀察整個啓動流程。

2. 調試方法與常見問題

  • 善用日誌:充分利用鴻蒙的 HiLog 或你自己的 Logger,在 SplashScreenAbilitySplashScreenPlugin 的關鍵節點打上日誌,確認生命週期順序和 MethodChannel 調用是否成功。
  • 頁面不跳轉怎麼辦?

    • 檢查一下,Dart 和 ArkTS 兩邊的 MethodChannel 名字是不是一模一樣。
    • 確認 handleRemoveSplash 方法裏獲取 WindowStage 的邏輯在當前 Ability 上下文中是否有效。
    • 看看 FlutterPage 有沒有在 main_pages.json 里正確配置。
  • 啓動屏樣式不對?

    • 核對 SplashPage.css 裏的背景色和 Flutter 項目原來的配置是否一致。
    • 檢查圖片資源的路徑和格式鴻蒙是否支持。

3. 性能優化建議

  1. 啓動時間優化

    • 保持SplashPage簡單:千萬別在啓動頁做耗時操作(比如網絡請求、複雜計算)。只放必要的圖片和樣式就好。
    • 預加載Flutter引擎:可以在顯示 SplashPage 的同時,在後台異步初始化 Flutter 引擎裏那些非 UI 相關的模塊。
    • 優化圖片資源:對啓動屏圖片進行無損壓縮,並準備合適分辨率的版本(hdpixhdpi等),避免因圖片解碼拖慢首屏顯示。
  2. 內存與視覺過渡優化

    • 及時釋放資源:跳轉到 FlutterPage 後,確保 SplashPage 的 UI 組件和相關資源能被及時回收。
    • 追求平滑過渡:在 Flutter 側調用 remove 之後,可以給初始的 Flutter 頁面設置一個和啓動屏背景色相同的背景,或者在鴻蒙側做一個簡單的漸隱動畫,避免視覺上的生硬切換。
  3. 性能數據對比參考
    你可以通過系統工具或自己打點,來量化一下適配前後的效果。下面是個示例:

    指標 適配前 (無啓動屏/白屏) 適配後 (自定義鴻蒙啓動屏) 優化説明
    首次啓動到首幀顯示(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 的不斷成熟,未來這類適配工作肯定會越來越標準化,甚至自動化,但掌握其底層原理,永遠是我們開發者應對新技術挑戰最可靠的武器。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.