Flutter `shared_preferences` 三方库在 OpenHarmony 平台的适配实践

Flutter shared_preferences 三方库在 OpenHarmony 平台的适配实践

摘要

在将 Flutter 应用迁移到 OpenHarmony 平台时,数据持久化是首先要解决的挑战之一。shared_preferences 作为 Flutter 生态中最常用的轻量级键值存储插件,其官方版本并未覆盖 OpenHarmony。本文将分享我们如何从原理出发,一步步为 shared_preferences 实现鸿蒙端的原生支持。内容涵盖适配背后的原因、整体架构设计、详细的代码实现、以及在实际集成中可能遇到的问题与优化建议,希望能为类似插件迁移提供一份实用的参考。


一、 为什么需要适配?

Flutter 的"一次编写,多端部署"能力,很大程度上依赖于各平台(Android/iOS)提供的原生接口实现。像 shared_preferences 这样的插件,在 Android 端调用的是 SharedPreferences API,在 iOS 端则是 NSUserDefaults。然而,OpenHarmony 作为一套全新的操作系统,并不在 Flutter 官方原生支持的范围之内。因此,任何基于平台通道(Platform Channel)的 Flutter 插件,在鸿蒙上都无法直接运行。

如果我们希望 Flutter 应用能在 OpenHarmony 上正常使用 shared_preferences,就必须为它实现一个鸿蒙专属的"后端"。这个后端需要负责通过 Flutter 的平台通道与 Dart 层通信,并调用 OpenHarmony 系统本身的持久化存储接口来完成数据读写。下面,我们就来详细说说具体的实现过程。

二、 适配思路与整体架构

1. Flutter 插件是如何工作的?

一个典型的 Flutter 插件分为三层:

  • Dart 层 :面向开发者,提供简洁的 API(例如 SharedPreferences.getInstance())。
  • 平台层:分别在 Android(Java/Kotlin)和 iOS(Objective-C/Swift)上实现数据存取的具体逻辑。
  • 通信层 :双方通过 MethodChannel 进行异步消息传递。Dart 层发起调用并传递参数,平台层执行本地操作后,再将结果返回。
2. OpenHarmony 提供了哪种存储方案?

OpenHarmony 的持久化方案有好几种,对于 shared_preferences 这种简单的键值对存储,最匹配的是 首选项(Preferences) 子系统。

  • 特点 :它采用 KV 模型,数据以文件形式存储在应用沙箱内,通过 Preferences 类进行访问。内部实现了内存缓存和异步回写,兼顾了性能和数据安全。
3. 我们的适配方案

核心目标很明确:在 OpenHarmony 端(使用 ArkTS)构建一个功能对等的平台层实现。整个过程可以分解为几个步骤:

  1. 搭建鸿蒙模块 :在插件工程中,新建 OpenHarmony 平台目录(ohos/)。
  2. 实现核心类:创建一个 ArkTS 类,作为插件在鸿蒙端的入口。
  3. 调用 Preferences API :在这个类中,利用 @ohos.data.preferences 包的能力,实现 getAllsetStringremoveclear 等核心方法。
  4. 注册通道 :在插件初始化时,将这个实现类注册到 Flutter Engine 的 MethodChannel 上,完成 Dart 与鸿蒙的桥接。

三、 具体代码实现

接下来,我们从环境搭建开始,看看关键的代码应该如何编写。

1. 准备工程结构

首先,你需要获取插件源码,并建立适配所需的目录。

bash 复制代码
# 1. 克隆官方插件仓库(或从 pub.dev 下载指定版本源码)
git clone https://github.com/flutter/packages.git
cd packages/packages/shared_preferences

# 2. 查看标准结构(通常会看到 android/, ios/, lib/ 等目录)
ls -la

# 3. 创建 OpenHarmony 平台的实现目录
mkdir -p ohos/src/main/ets/com/example/sharedpreferencesohos

随后,在 ohos 目录下创建模块配置文件 package.json

json 复制代码
{
  "license": "BSD-3-Clause",
  "types": "./index.d.ts",
  "name": "@flutter/shared_preferences_ohos",
  "ohos": {
    "org": "flutter",
    "directoryLevel": "module",
    "buildTool": "hvigor"
  },
  "description": "OHOS implementation of the shared_preferences plugin",
  "repository": "https://github.com/flutter/packages/tree/main/packages/shared_preferences",
  "version": "2.2.2+ohos",
  "main": "./src/main/ets/MainAbility/SharedPreferencesOhos.ts",
  "types": "./src/main/ets/MainAbility/SharedPreferencesOhos.ts",
  "dependencies": {
    "@ohos/data.preferences": "1.0.0"
  }
}
2. 鸿蒙端核心实现类

在刚创建的目录下,新建 SharedPreferencesOhos.ets 文件:

typescript 复制代码
// SharedPreferencesOhos.ets
import preferences from '@ohos.data.preferences';
import { BusinessError } from '@ohos.base';
import common from '@ohos.app.ability.common';

// 一个简单的管理器,确保每个配置文件只对应一个Preferences实例
class PreferencesManager {
  private static instance: PreferencesManager;
  private preferencesMap: Map<string, preferences.Preferences> = new Map();

  static getInstance(): PreferencesManager {
    if (!PreferencesManager.instance) {
      PreferencesManager.instance = new PreferencesManager();
    }
    return PreferencesManager.instance;
  }

  async getPreferences(context: common.Context, name: string): Promise<preferences.Preferences> {
    let prefs = this.preferencesMap.get(name);
    if (!prefs) {
      try {
        // 文件会存储在 /data/app/.../files/<name>.preferences
        prefs = await preferences.getPreferences(context, name);
        this.preferencesMap.set(name, prefs);
        console.info(`[SharedPreferencesOhos] Successfully loaded preferences: ${name}`);
      } catch (err) {
        const error = err as BusinessError;
        console.error(`[SharedPreferencesOhos] Failed to get preferences ${name}: ${error.code}, ${error.message}`);
        throw new Error(`Unable to get preferences: ${error.message}`);
      }
    }
    return prefs;
  }

  removePreferences(name: string): void {
    this.preferencesMap.delete(name);
  }
}

// Flutter平台通道的具体实现
export class SharedPreferencesOhos {
  private static CHANNEL_NAME = 'plugins.flutter.io/shared_preferences';
  private manager: PreferencesManager = PreferencesManager.getInstance();
  private context: common.Context | undefined = undefined;

  // 需要从Ability注入Context
  setContext(context: common.Context): void {
    this.context = context;
    console.info('[SharedPreferencesOhos] Context injected.');
  }

  // 主入口:注册方法通道处理器
  registerMethodCallHandler(): void {
    if (!this.context) {
      console.error('[SharedPreferencesOhos] Context is not set. Cannot register handler.');
      return;
    }
    // 注意:这里假设Flutter for OHOS引擎提供了全局的`flutter`对象来注册通道。
    // 实际集成方式可能因桥接方案而异,此处展示核心逻辑。
    const methodChannel = flutter.engine.MethodChannel(this.CHANNEL_NAME);
    methodChannel.setMethodCallHandler(this.handleMethodCall.bind(this));
    console.info(`[SharedPreferencesOhos] Method channel '${this.CHANNEL_NAME}' registered.`);
  }

  private async handleMethodCall(call: flutter.engine.MethodCall): Promise<any> {
    if (!this.context) {
      return Promise.reject(new Error('Context not available'));
    }

    const args = call.arguments;
    const prefsName = 'FlutterSharedPreferences'; // 采用与Android端相同的固定文件名

    try {
      const prefs = await this.manager.getPreferences(this.context, prefsName);

      switch (call.method) {
        case 'getAll':
          const allEntries: Record<string, any> = {};
          const keys = await prefs.getAllKeys();
          for (const key of keys) {
            const value = await prefs.get(key, '');
            // 这里需要将值包装成Flutter Dart层期望的特定格式
            allEntries[key] = this._wrapValue(value);
          }
          return allEntries;

        case 'setBool':
        case 'setInt':
        case 'setDouble':
        case 'setString':
        case 'setStringList':
          const key = args['key'] as string;
          const value = args['value'];
          await prefs.put(key, value);
          await prefs.flush(); // 确保数据写入磁盘
          return true;

        case 'remove':
          const removeKey = args['key'] as string;
          await prefs.delete(removeKey);
          await prefs.flush();
          return true;

        case 'clear':
          await prefs.clear();
          await prefs.flush();
          // 清理内存中的实例
          this.manager.removePreferences(prefsName);
          return true;

        default:
          console.warn(`[SharedPreferencesOhos] Unknown method: ${call.method}`);
          throw new Error(`Method '${call.method}' not implemented`);
      }
    } catch (err) {
      const error = err as BusinessError | Error;
      console.error(`[SharedPreferencesOhos] Error in method ${call.method}:`, error.message);
      // 抛出异常后,Dart侧会收到PlatformException
      throw error;
    }
  }

  // 辅助方法:将原始值转换为Flutter约定的格式(此处为简化示例,实际类型处理需更细致)
  private _wrapValue(rawValue: preferences.ValueType): any {
    if (typeof rawValue === 'boolean') {
      return { 'bool': rawValue };
    } else if (typeof rawValue === 'number') {
      // 注意:Preferences存储不区分整型和浮点,都为number。可能需要额外约定。
      return { 'double': rawValue };
    } else if (typeof rawValue === 'string') {
      return { 'string': rawValue };
    } else if (Array.isArray(rawValue)) {
      return { 'stringList': rawValue };
    }
    return { 'string': String(rawValue) }; // 默认处理
  }
}
3. 在鸿蒙应用入口初始化插件

最后,别忘了在你的鸿蒙主 Ability(例如 EntryAbility.ets)中初始化插件:

typescript 复制代码
// EntryAbility.ets (节选)
import { SharedPreferencesOhos } from '../sharedpreferencesohos/SharedPreferencesOhos';
import AbilityStage from '@ohos.app.ability.AbilityStage';

let spOhos: SharedPreferencesOhos | undefined = undefined;

export default class EntryAbility extends Ability {
  onCreate(want, launchParam) {
    console.info('[EntryAbility] onCreate');
    // 初始化插件
    spOhos = new SharedPreferencesOhos();
    spOhos.setContext(this.context);
    // 注册方法通道
    spOhos.registerMethodCallHandler();
  }

  // ... 其他生命周期方法
}

四、 集成与调试

1. 在 Flutter 项目中集成

由于修改了原生代码,你需要使用本地路径依赖的方式来引用我们适配后的插件。

在 Flutter 项目的 pubspec.yaml 中:

yaml 复制代码
dependencies:
  shared_preferences:
    path: /path/to/your/adapted/packages/packages/shared_preferences
2. 调试技巧
  1. 查看日志 :充分利用代码中的 console.info/error 输出,在 DevEco Studio 的 HiLog 或终端里观察插件运行状态。

  2. 检查数据文件 :适配成功后,数据文件会保存在应用沙箱内。可以通过 hdc shell 连接到设备,查看 /data/app/.../files/FlutterSharedPreferences.preferences 文件是否存在,内容是否正确。

  3. 编写简单测试 :在 Flutter 侧写一个测试页面,验证基本功能。

    dart 复制代码
    Future<void> testSharedPreferences() async {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('demo_key', 'Hello OpenHarmony!');
      final value = prefs.getString('demo_key');
      print('读取到的值: $value'); // 确认日志输出正确
      assert(value == 'Hello OpenHarmony!');
    }

五、 一些优化点和注意事项

1. 性能优化建议
  • 批量写入shared_preferences 每次 set 操作都会触发一次平台通信和文件写入。如果遇到高频写入场景,建议在 Dart 层自己做批量合并,减少通信和 I/O 开销。
  • 管理内存:我们目前的实现用了一个简单的 Map 做内存缓存。在应用内存吃紧时,可以考虑引入 LRU 机制,或者提供手动释放不常用配置文件的接口。
  • 确保持久化preferences.flush() 是异步的,但会等待写盘完成。在应用退出前,要确保所有 flush 操作都已完成,避免数据丢失。
2. 需要留意的细节
  • 数据类型映射 :这是最容易出错的地方。OpenHarmony Preferences 支持 stringnumberbooleanArray<string>,而 Flutter 侧有 intdouble 之分(在鸿蒙端都对应 number)。必须在 _wrapValue 和 Dart 层的解析逻辑中做好精细的类型转换与约定。
  • 线程安全 :虽然 OpenHarmony 的 Preferences API 本身是线程安全的,但我们封装的 PreferencesManager 仍需考虑多线程同时访问的情况。
  • 版本兼容 :关注 OpenHarmony SDK 及 @ohos.data.preferences API 的版本更新,适配代码可能需要随之调整。

六、 写在最后

通过以上步骤,我们系统地完成了 shared_preferences 插件在 OpenHarmony 平台上的适配。这个过程不仅涉及 Flutter 插件机制的深入理解,也要求对鸿蒙系统的存储 API 有实际的应用经验。

成功的适配离不开三点:

  1. 理解通信协议:准确把握 Flutter MethodChannel 的数据格式和调用约定。
  2. 用好原生能力:熟练掌握 OpenHarmony 相关子系统(如 Preferences)的接口特性。
  3. 处理兼容细节:耐心解决好数据类型、文件路径、生命周期等跨平台带来的差异。

这个方案也为其他 Flutter 插件(比如 path_providersqflite)迁移到 OpenHarmony 提供了一个可行的思路和代码参考。随着 OpenHarmony 生态的逐步成熟,这类跨平台适配工作将成为连接 Flutter 开发与鸿蒙生态的重要一环。希望我们的实践能给你带来实实在在的帮助。

相关推荐
月光下的丝瓜15 小时前
Flutter 国内安装指南
前端·flutter
TrisighT1 天前
我用 AI 逆向了 ArkTS @Builder 的编译产物,看完再也不敢乱写嵌套了
ai编程·harmonyos·arkts
看谷秀3 天前
鸿蒙-part3-arkts下
arkts
TrisighT3 天前
ArkTS 的 @BuilderParam 你八成只用了皮毛——那个尾随闭包写法差点被我当 bug 删了
harmonyos·arkts·arkui
恋猫de小郭3 天前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
张风捷特烈3 天前
Flutter 类库大揭秘#02 | path_provider 各平台实现
前端·flutter
TT_Close4 天前
别劝退了!5秒搞定 Flutter 鸿蒙 FVM 起跑线
flutter·harmonyos·visual studio code
TrisighT4 天前
ArkTS 列表滚动时为什么会闪现旧数据?我扒了 LazyForEach 的复用逻辑
harmonyos·arkts·arkui
你听得到114 天前
用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
前端·flutter·性能优化
TrisighT5 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui