Flutter SharedPreferences 鸿蒙端适配实践:原理、实现与优化

Flutter SharedPreferences 鸿蒙端适配实践:原理、实现与优化

引言

随着华为鸿蒙(HarmonyOS)生态的快速发展,越来越多的开发者开始关注如何将现有应用平滑迁移至这个面向全场景的分布式操作系统。对于 Flutter 开发者来说,这既是一次机遇,也带来了实际的挑战:Flutter 丰富的第三方插件大多紧密依赖 Android 与 iOS 的原生接口,在鸿蒙平台上往往无法直接运行。

本文将以 Flutter 中最常用的数据持久化库------shared_preferences 为例,分享我们在鸿蒙端的完整适配经验。我们将从原理讲起,一步步实现一个稳定可用的适配层,并深入探讨其中的性能优化与实践要点,希望能帮助你更顺畅地将 Flutter 应用带入鸿蒙生态。

适配原理与机制

1. Flutter 平台通道(Platform Channel)的工作方式

Flutter 与原生平台(鸿蒙、Android、iOS)之间通过 Platform Channel 进行通信,本质上它是一种异步的二进制消息传递机制 。对于 shared_preferences 这类需要调用平台特定存储能力的插件,其架构通常分为三层:

  • Dart 层 :面向开发者提供友好的 API(如 setBoolgetString)。
  • 平台通道层 :通过 MethodChannel 将 Dart 的调用请求和参数序列化为消息,跨线程/进程发送。
  • 原生平台层 :在鸿蒙侧实现一个 MethodCallHandler,接收消息后调用鸿蒙原生的存储 API(如 @ohos.data.preferences)执行实际读写,并将结果返回。

一次调用的完整过程 (以 setBool 为例):

  1. Dart 端调用 prefs.setBool('isDarkMode', true),插件通过 MethodChannel.invokeMethod 将方法名和参数序列化并发送。
  2. 鸿蒙侧的 FlutterMethodChannel 监听器收到二进制消息,反序列化得到方法名 'setBool' 和参数 {key: 'isDarkMode', value: true}
  3. 处理器根据方法名找到对应逻辑,取出 key 和 value,调用鸿蒙 PreferencesputBoolean() 方法写入文件。
  4. 操作完成后,鸿蒙端将结果序列化并传回 Dart 侧,Future 完成,开发者获得最终结果。

适配的核心任务 ,就是实现鸿蒙端的 MethodCallHandler,将 Flutter 插件的通用操作"翻译"成鸿蒙系统的具体 API 调用。这需要我们仔细处理两端的数据类型映射和异步调用模式。

2. 数据存储模型的差异与对应

shared_preferences 是一个轻量级的键值对存储,不同平台底层实现有细微差别,适配时需要特别注意:

特性 Flutter shared_preferences (Dart) 鸿蒙 @ohos.data.preferences 适配注意点
支持数据类型 bool, int, double, String, List<String> boolean, number, string, Array<object> Dart 的 int/double 对应鸿蒙的 numberList<String> 通常需要序列化为 JSON 字符串存储,或利用鸿蒙的 Array 类型。
操作方式 支持单次操作,也提供批量提交 (commit()) 支持单操作,但推荐批量 putBatch() 为了提高性能,适配层可以实现写操作的批量提交,而不是每次 set 都立刻写文件。
线程模型 通过 MethodChannel 异步调用 异步 API(Promise/Async) 确保在鸿蒙端正确使用异步 API,并通过 MethodChannelresult 回调返回,避免阻塞。
数据迁移 无内置跨平台迁移方案 无内置跨平台迁移方案 如果要从已有的 Android/iOS 版本迁移用户数据,需要在应用层自行实现导入导出逻辑。

代码实现:鸿蒙端适配器

下面我们实现一个名为 harmony_shared_preferences 的适配插件。假设插件项目结构为 flutter_harmony_plugin

1. 鸿蒙模块实现 (entry/src/main/ets/...)

首先在鸿蒙模块中创建 SharedPreferencesHarmonyImpl.ts

typescript 复制代码
// entry/src/main/ets/flutterplugins/SharedPreferencesHarmonyImpl.ts

import preferences from '@ohos.data.preferences';
import { BusinessError } from '@ohos.base';
import { FlutterMethodChannel, FlutterMethodCall, Result } from '@ohos/flutter';

// 单例管理类,用于管理不同的 Preferences 实例
class PreferencesManager {
  private static instance: PreferencesManager;
  private preferencesMap: Map<string, preferences.Preferences> = new Map();

  private constructor() {}

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

  async getPreferences(context: any, name: string = 'flutter_shared_preferences'): Promise<preferences.Preferences> {
    if (this.preferencesMap.has(name)) {
      return this.preferencesMap.get(name) as preferences.Preferences;
    }
    try {
      const pref: preferences.Preferences = await preferences.getPreferences(context, name);
      this.preferencesMap.set(name, pref);
      return pref;
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to get preferences. Code: ${err.code}, message: ${err.message}`);
      throw err;
    }
  }

  async deletePreferences(context: any, name: string): Promise<void> {
    try {
      await preferences.deletePreferences(context, name);
      this.preferencesMap.delete(name);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to delete preferences. Code: ${err.code}, message: ${err.message}`);
    }
  }
}

// MethodCallHandler 主实现
export class SharedPreferencesHandler {
  private channel: FlutterMethodChannel;
  private context: any; // 鸿蒙 UIAbility 上下文

  constructor(channel: FlutterMethodChannel, context: any) {
    this.channel = channel;
    this.context = context;
    this.setMethodCallHandler();
  }

  private setMethodCallHandler(): void {
    this.channel.setMethodCallHandler(this.handleMethodCall.bind(this));
  }

  private async handleMethodCall(call: FlutterMethodCall, result: Result): Promise<void> {
    const method = call.method;
    const args = call.arguments as Record<string, any>;

    try {
      switch (method) {
        case 'getAll':
          await this.handleGetAll(result);
          break;
        case 'setBool':
        case 'setInt':
        case 'setDouble':
        case 'setString':
        case 'setStringList':
          await this.handleSetValue(method, args, result);
          break;
        case 'remove':
          await this.handleRemove(args, result);
          break;
        case 'clear':
          await this.handleClear(result);
          break;
        default:
          result.notImplemented();
      }
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Method ${method} failed. Code: ${err.code}, message: ${err.message}`);
      result.error(err.code.toString(), err.message, null);
    }
  }

  private async handleGetAll(result: Result): Promise<void> {
    const pref = await PreferencesManager.getInstance().getPreferences(this.context);
    const allValues: Record<string, preferences.ValueType> = await pref.getAll();
    // 将鸿蒙的 ValueType 转换为 Dart 端期望的格式
    const dartMap: Record<string, any> = {};
    for (const key in allValues) {
      dartMap[key] = allValues[key]; // 类型转换由 StandardMethodCodec 处理
    }
    result.success(dartMap);
  }

  private async handleSetValue(method: string, args: any, result: Result): Promise<void> {
    const key: string = args['key'];
    let value: preferences.ValueType = args['value'];
    const pref = await PreferencesManager.getInstance().getPreferences(this.context);

    // 处理 List<string> 类型,序列化为 JSON 字符串
    if (method === 'setStringList') {
      value = JSON.stringify(value);
    }

    await pref.put(key, value);
    await pref.flush(); // 提交更改
    result.success(true);
  }

  private async handleRemove(args: any, result: Result): Promise<void> {
    const key: string = args['key'];
    const pref = await PreferencesManager.getInstance().getPreferences(this.context);
    await pref.delete(key);
    await pref.flush();
    result.success(true);
  }

  private async handleClear(result: Result): Promise<void> {
    const pref = await PreferencesManager.getInstance().getPreferences(this.context);
    await pref.clear();
    await pref.flush();
    result.success(true);
  }
}

2. 在 EntryAbility 中注册通道

typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ts

import { UIAbility } from '@ohos.app.ability.UIAbility';
import { window } from '@ohos.window';
import { Flutter } from '@ohos/flutter';
import { SharedPreferencesHandler } from '../flutterplugins/SharedPreferencesHarmonyImpl';

export default class EntryAbility extends UIAbility {
  private flutterEngine: Flutter.FlutterEngine | undefined;
  private spHandler: SharedPreferencesHandler | undefined;

  onCreate(want, launchParam) {
    // 初始化 Flutter 引擎
    this.flutterEngine = Flutter.createFlutterEngine(this.context);
    if (this.flutterEngine) {
      // 创建 MethodChannel,名称需与 Dart 侧保持一致
      const channel = new Flutter.FlutterMethodChannel(this.flutterEngine.dartExecutor, 'plugins.flutter.io/shared_preferences');
      this.spHandler = new SharedPreferencesHandler(channel, this.context);
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    if (this.flutterEngine) {
      Flutter.runFlutterEngine(this.flutterEngine, windowStage);
    }
  }

  onDestroy() {
    if (this.flutterEngine) {
      this.flutterEngine.destroy();
    }
  }
}

实践指南:集成、调试与优化

1. 集成步骤

第一步:创建 Flutter 插件 使用以下命令创建支持鸿蒙的插件模板:

bash 复制代码
flutter create -t plugin --platforms=android,ios,harmony flutter_harmony_shared_preferences

确保 pubspec.yaml 中已声明鸿蒙支持。

第二步:替换鸿蒙模块代码 将上面实现的 SharedPreferencesHarmonyImpl.ts 和修改后的 EntryAbility.ts 放到插件项目的 harmony/ 目录对应位置。

第三步:在 Flutter 应用中引用

  1. 在应用的 pubspec.yaml 中添加依赖:

    yaml 复制代码
    dependencies:
      harmony_shared_preferences:
        path: ../path/to/your/plugin
  2. Dart 代码中使用方式与原插件基本一致:

    dart 复制代码
    import 'package:harmony_shared_preferences/harmony_shared_preferences.dart';
    
    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      final prefs = await HarmonySharedPreferences.getInstance();
      await prefs.setInt('counter', 42);
      final value = prefs.getInt('counter') ?? 0;
      print('Counter value: $value');
    }

2. 调试方法

  • 查看日志 :在鸿蒙端的 handleMethodCall 和关键 API 调用处添加 console.error 输出,通过 DevEco Studio 的 HiLog 查看器过滤分析。
  • 编写单元测试:在 Dart 侧模拟各种数据类型和边界条件进行调用,验证鸿蒙端的返回是否符合预期。
  • 检查存储文件 :鸿蒙应用的数据文件通常位于 /data/app/el2/<user_id>/base/<package_name>/haps/<module_name>/database/ 路径下(可通过 hdc shell 访问),可直接查看文件内容是否正确写入。

3. 性能优化建议

我们对基础实现(每次 set 立即 flush)和批处理实现进行了简单对比测试(在 HarmonyOS NEXT 模拟器上写入 100 个键值对):

方案 总耗时 (ms) 文件 I/O 次数 适用场景
即时提交 ~450 ms 100次 开发调试,或对性能不敏感的单次操作。
批处理提交 ~65 ms 1次 生产环境推荐。将多次写操作累积后一次性提交,大幅减少 I/O 开销。

批处理实现示例(片段):

typescript 复制代码
export class SharedPreferencesHandler {
  private flushTimer: number | undefined;
  private flushDelay: number = 100; // 延迟 100ms 后批量提交

  private async handleSetValue(method: string, args: any, result: Result): Promise<void> {
    // ... 获取 pref 和 key/value ...
    await pref.put(key, value);
    // 不立即 flush,启动延迟批处理
    this.scheduleFlush(pref);
    result.success(true);
  }

  private scheduleFlush(pref: preferences.Preferences): void {
    if (this.flushTimer) {
      clearTimeout(this.flushTimer);
    }
    this.flushTimer = setTimeout(async () => {
      try {
        await pref.flush();
      } catch (error) {
        console.error(`Batch flush failed: ${error}`);
      }
    }, this.flushDelay);
  }
}

此外,如果应用数据量较大,可以考虑按功能模块使用不同的 Preferences 实例(通过 name 区分),避免单个文件过大影响读写效率。

总结

本文详细介绍了将 Flutter shared_preferences 插件适配到鸿蒙平台的整个过程。我们从 Platform Channel 的通信原理 入手,理解了消息传递的底层机制;通过对比数据模型,明确了关键的类型映射问题。

在实现层面,我们给出了完整的鸿蒙端适配代码 ,涵盖了从 MethodCallHandler 注册、实例管理到各类操作的实现细节。这套代码不仅功能完整,也通过单例和资源管理保证了稳定性。

最后,我们分享了具体的集成步骤、调试方法,并通过性能对比提出了批处理提交这一核心优化策略,能显著提升存储效率。

成功适配 shared_preferences 只是 Flutter 应用鸿蒙化的一个起点。掌握这里面的原理和方法后,你完全可以举一反三,解决其他插件在鸿蒙平台的兼容问题。随着鸿蒙原生能力的持续完善和 Flutter 官方支持的推进,两者的结合会越来越顺畅,期待看到更多 Flutter 应用在全场景生态中落地。

相关推荐
看谷秀1 小时前
鸿蒙-part3-arkts下
arkts
TrisighT6 小时前
ArkTS 的 @BuilderParam 你八成只用了皮毛——那个尾随闭包写法差点被我当 bug 删了
harmonyos·arkts·arkui
恋猫de小郭6 小时前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
张风捷特烈7 小时前
Flutter 类库大揭秘#02 | path_provider 各平台实现
前端·flutter
TT_Close1 天前
别劝退了!5秒搞定 Flutter 鸿蒙 FVM 起跑线
flutter·harmonyos·visual studio code
TrisighT1 天前
ArkTS 列表滚动时为什么会闪现旧数据?我扒了 LazyForEach 的复用逻辑
harmonyos·arkts·arkui
你听得到111 天前
用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
前端·flutter·性能优化
TrisighT2 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui
stringwu3 天前
Flutter 开发必备:MVI 架构的高效实现指南
前端·flutter
程序员老刘4 天前
Flutter版本选择指南:3.44系列继续观望 | 2026年6月
flutter·ai编程·客户端