【maaath】Flutter for OpenHarmony为开源鸿蒙跨平台工程集成本地存储能力
【Flutter for OpenHarmony】跨平台工程集成本地存储能力:键值存储、文件读写与数据加密
作者:maaath
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、前言
本地存储是移动应用不可或缺的基础能力。用户偏好设置需要轻量级键值存储,大段文本或结构化数据需要文件读写能力,密码和 Token 等敏感信息则必须依赖数据加密存储。在 Flutter for OpenHarmony(以下简称 FOH)跨平台工程中,这些存储能力并非 Flutter 引擎直接提供,而是需要通过 OpenHarmony 原生 API + Platform Channel 的双层架构来实现。
本文基于一个经过验证的 FOH 项目,系统讲解如何在跨平台工程中集成三大本地存储能力:
- Preferences Storage:轻量级键值存储,基于
@ohos.data.preferences - File Storage:文件读写存储,基于
@ohos.file.fs - Encryption Storage:数据加密存储,基于
@ohos.security.cryptoFramework
核心主线是 Flutter 端通过 Platform Channel 调用原生 ArkTS 存储服务,实现一套代码、多端复用的本地存储方案。
仓库地址:https://atomgit.com/maaath/oh_demo11
二、技术方案总览
2.1 为什么需要原生存储 API?
Flutter 框架本身提供了若干存储能力:shared_preferences 用于键值存储、path_provider 用于获取应用目录、dart:io 提供基础文件操作。然而在 OpenHarmony 平台上,这些包尚未完成原生适配,开发者需要直接调用 OpenHarmony 原生 API 来实现存储功能。
FOH 的解决思路是:在 ArkTS 原生侧封装存储服务,通过 Platform Channel 向 Flutter 端暴露能力。Flutter 端使用熟悉的 Dart 语言调用,ArkTS 侧处理平台细节,对上层完全透明。
2.2 架构设计
┌──────────────────────────────────────────────────────────────┐
│ Flutter 端 (Dart) │
│ ┌────────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ StorageService │ │ StoragePage │ │ StorageIndex │ │
│ │ (MethodChannel) │ │ (UI 展示) │ │ (抽象封装) │ │
│ └───────┬────────┘ └──────────────┘ └───────────────┘ │
│ │ Platform Channel │
│ ┌───────▼──────────────────────────────────────────────┐ │
│ │ FlutterEngine (Native 嵌入层) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────▼──────────────────────────────────────────────┐ │
│ │ ArkTS 原生存储服务层 │ │
│ │ ┌──────────┬──────────┬────────────┐ │ │
│ │ │Preferences│ File │ Encryption │ │ │
│ │ │Storage │ Storage │ Storage │ │ │
│ │ └──────────┴──────────┴────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────▼──────────────────────────────────────────────┐ │
│ │ OpenHarmony 系统 API │ │
│ │ @ohos.data.preferences @ohos.file.fs │ │
│ │ @ohos.security.cryptoFramework │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
2.3 项目文件结构
oh_demo11/
├── lib/ # Flutter Dart 代码
│ ├── main.dart # Flutter 入口
│ └── storage/ # 存储相关 Flutter 模块
│ ├── storage_service.dart # Platform Channel 封装
│ ├── storage_index.dart # 存储服务入口索引
│ └── pages/
│ └── storage_demo_page.dart # 存储演示页面
├── entry/src/main/ets/ # ArkTS 原生代码
│ ├── utils/
│ │ ├── PreferencesStorage.ets # 偏好设置存储
│ │ ├── FileStorage.ets # 文件存储
│ │ ├── EncryptionStorage.ets # 加密存储
│ │ └── StorageIndex.ets # 存储索引(MethodChannel 注册)
│ ├── model/
│ │ └── StorageModels.ets # 数据模型
│ ├── pages/
│ │ └── StorageDemo.ets # 原生演示页面(ArkTS 对照参考)
│ └── entryability/
│ └── EntryAbility.ets # Flutter 引擎入口
└── module.json5 # 权限声明
三、ArkTS 原生存储服务实现
3.1 Preferences Storage — 轻量级键值存储
PreferencesStorage 基于 @ohos.data.preferences 实现,提供 String、Number、Boolean 类型的键值存储,适用于保存用户偏好设置、应用配置等轻量数据。
// ArkTS 侧 - utils/PreferencesStorage.ets
import dataPreferences from '@ohos.data.preferences';
import hilog from '@ohos.hilog';
const TAG = 'PreferencesStorage';
const DOMAIN = 0xFF02;
export class PreferencesStorage {
private context: Context | null = null;
private preferences: dataPreferences.Preferences | null = null;
private readonly FILE_NAME: string = 'app_preferences';
constructor(context: Context) {
this.context = context;
}
async init(): Promise<boolean> {
if (this.context === null) {
hilog.error(DOMAIN, TAG, 'Context is null');
return false;
}
try {
this.preferences = await dataPreferences.getPreferences(
this.context,
this.FILE_NAME
);
hilog.info(DOMAIN, TAG, 'Preferences initialized');
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Init failed: %{public}s',
JSON.stringify(error));
return false;
}
}
async putString(key: string, value: string): Promise<boolean> {
if (!this.preferences) {
await this.init();
}
try {
await this.preferences!.put(key, value);
await this.preferences!.flush();
hilog.info(DOMAIN, TAG, 'Put string: %{public}s', key);
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Put string failed: %{public}s',
JSON.stringify(error));
return false;
}
}
async getString(key: string, defaultValue: string = ''): Promise<string> {
if (!this.preferences) {
await this.init();
}
try {
const value = await this.preferences!.get(key, defaultValue);
return value as string;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Get string failed: %{public}s',
JSON.stringify(error));
return defaultValue;
}
}
async putNumber(key: string, value: number): Promise<boolean> {
if (!this.preferences) {
await this.init();
}
try {
await this.preferences!.put(key, value);
await this.preferences!.flush();
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Put number failed: %{public}s',
JSON.stringify(error));
return false;
}
}
async getNumber(key: string, defaultValue: number = 0): Promise<number> {
if (!this.preferences) {
await this.init();
}
try {
const value = await this.preferences!.get(key, defaultValue);
return value as number;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Get number failed: %{public}s',
JSON.stringify(error));
return defaultValue;
}
}
async putBoolean(key: string, value: boolean): Promise<boolean> {
if (!this.preferences) {
await this.init();
}
try {
await this.preferences!.put(key, value);
await this.preferences!.flush();
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Put boolean failed: %{public}s',
JSON.stringify(error));
return false;
}
}
async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
if (!this.preferences) {
await this.init();
}
try {
const value = await this.preferences!.get(key, defaultValue);
return value as boolean;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Get boolean failed: %{public}s',
JSON.stringify(error));
return defaultValue;
}
}
async delete(key: string): Promise<boolean> {
if (!this.preferences) {
await this.init();
}
try {
await this.preferences!.delete(key);
await this.preferences!.flush();
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Delete failed: %{public}s',
JSON.stringify(error));
return false;
}
}
async clear(): Promise<boolean> {
if (!this.preferences) {
await this.init();
}
try {
await this.preferences!.clear();
await this.preferences!.flush();
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Clear failed: %{public}s',
JSON.stringify(error));
return false;
}
}
}
ArkTS 关键设计要点:
Preferences实例通过getPreferences(context, FILE_NAME)获取,其中FILE_NAME是存储文件名(不带扩展名)。- 所有写操作(
put、delete、clear)后必须调用flush()才能将数据持久化到磁盘。 get()方法的第二个参数为默认值,当键不存在时返回该默认值。
3.2 File Storage — 文件读写存储
FileStorage 基于 @ohos.file.fs 实现,支持文本文件和 JSON 文件的读写,适用于存储笔记、配置、结构化数据等中等体量的数据。
// ArkTS 侧 - utils/FileStorage.ets
import fs from '@ohos.file.fs';
import hilog from '@ohos.hilog';
const TAG = 'FileStorage';
const DOMAIN = 0xFF02;
export class FileStorage {
private context: Context | null = null;
private readonly FILE_DIR: string = 'storage_data';
constructor(context: Context) {
this.context = context;
}
// 获取文件完整路径,自动创建目录
private async getFilePath(fileName: string): Promise<string> {
if (!this.context) {
throw new Error('Context is null');
}
const dirPath = this.context.filesDir + '/' + this.FILE_DIR;
if (!fs.accessSync(dirPath)) {
fs.mkdirSync(dirPath, true);
}
return dirPath + '/' + fileName;
}
// ArrayBuffer 转字符串
private arrayBufferToString(buffer: ArrayBuffer): string {
const uint8Array = new Uint8Array(buffer);
let result = '';
for (let i = 0; i < uint8Array.length; i++) {
result += String.fromCharCode(uint8Array[i]);
}
return result;
}
// 写入文本文件
async writeTextFile(fileName: string, content: string): Promise<boolean> {
try {
const filePath = await this.getFilePath(fileName);
const file = fs.openSync(filePath,
fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.writeSync(file.fd, content);
fs.closeSync(file.fd);
hilog.info(DOMAIN, TAG, 'Write text file: %{public}s', fileName);
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Write text failed: %{public}s',
JSON.stringify(error));
return false;
}
}
// 读取文本文件
async readTextFile(fileName: string): Promise<string> {
try {
const filePath = await this.getFilePath(fileName);
if (!fs.accessSync(filePath)) {
hilog.info(DOMAIN, TAG, 'File not found: %{public}s', fileName);
return '';
}
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const stat = fs.statSync(file.fd);
const buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file.fd);
return this.arrayBufferToString(buffer);
} catch (error) {
hilog.error(DOMAIN, TAG, 'Read text failed: %{public}s',
JSON.stringify(error));
return '';
}
}
// 写入 JSON 文件(自动格式化)
async writeJsonFile(fileName: string, data: object): Promise<boolean> {
try {
const jsonString = JSON.stringify(data, null, 2);
return await this.writeTextFile(fileName, jsonString);
} catch (error) {
hilog.error(DOMAIN, TAG, 'Write JSON failed: %{public}s',
JSON.stringify(error));
return false;
}
}
// 读取 JSON 文件
async readJsonFile(fileName: string): Promise<object | null> {
try {
const content = await this.readTextFile(fileName);
if (!content) {
return null;
}
return JSON.parse(content);
} catch (error) {
hilog.error(DOMAIN, TAG, 'Read JSON failed: %{public}s',
JSON.stringify(error));
return null;
}
}
// 删除文件
async deleteFile(fileName: string): Promise<boolean> {
try {
const filePath = await this.getFilePath(fileName);
if (fs.accessSync(filePath)) {
fs.unlinkSync(filePath);
}
hilog.info(DOMAIN, TAG, 'Delete file: %{public}s', fileName);
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Delete failed: %{public}s',
JSON.stringify(error));
return false;
}
}
// 检查文件是否存在
async exists(fileName: string): Promise<boolean> {
try {
const filePath = await this.getFilePath(fileName);
return fs.accessSync(filePath);
} catch {
return false;
}
}
}
ArkTS 关键设计要点:
context.filesDir返回应用私有目录的路径,确保数据不会泄露给其他应用。- 使用
fs.accessSync()检查文件或目录是否存在,比 try-catch 更高效。 OpenMode.READ_WRITE | OpenMode.CREATE组合表示文件不存在时创建、存在时覆盖。- JSON 写入使用
JSON.stringify(data, null, 2)自动格式化,便于调试和阅读。
3.3 Encryption Storage — 数据加密存储
EncryptionStorage 基于 @ohos.security.cryptoFramework 实现,使用 AES-256-CBC 算法对数据进行加密后存储。加密过程使用随机 IV(初始化向量)确保相同明文每次加密产生不同密文,防止暴力分析。
// ArkTS 侧 - utils/EncryptionStorage.ets
import cryptoFramework from '@ohos.security.cryptoFramework';
import fs from '@ohos.file.fs';
import hilog from '@ohos.hilog';
const TAG = 'EncryptionStorage';
const DOMAIN = 0xFF02;
export class EncryptionStorage {
private context: Context | null = null;
private readonly FILE_DIR: string = 'encrypted_data';
constructor(context: Context) {
this.context = context;
}
private base64Chars: string =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Base64 编码
private base64Encode(data: Uint8Array): string {
let result = '';
const len = data.length;
let i = 0;
while (i < len) {
const b1 = data[i++];
const b2 = i < len ? data[i++] : 0;
const b3 = i < len ? data[i++] : 0;
result += this.base64Chars[b1 >> 2];
result += this.base64Chars[((b1 & 0x03) << 4) | (b2 >> 4)];
result += i > len + 1 ? '=' :
this.base64Chars[((b2 & 0x0f) << 2) | (b3 >> 6)];
result += i > len ? '=' : this.base64Chars[b3 & 0x3f];
}
return result;
}
// Base64 解码
private base64Decode(base64: string): Uint8Array {
const cleanBase64 = base64.replace(/=/g, '');
const len = cleanBase64.length;
const output: number[] = [];
let i = 0;
while (i < len) {
const enc1 = this.base64Chars.indexOf(cleanBase64[i++]);
const enc2 = this.base64Chars.indexOf(cleanBase64[i++]);
const enc3 = this.base64Chars.indexOf(cleanBase64[i++]);
const enc4 = this.base64Chars.indexOf(cleanBase64[i++]);
output.push((enc1 << 2) | (enc2 >> 4));
if (enc3 !== -1) {
output.push(((enc2 & 0x0f) << 4) | (enc3 >> 2));
}
if (enc4 !== -1) {
output.push(((enc3 & 0x03) << 6) | enc4);
}
}
return new Uint8Array(output);
}
// 生成随机字节数组
private generateRandomBytes(length: number): Uint8Array {
const result = new Uint8Array(length);
for (let i = 0; i < length; i++) {
result[i] = Math.floor(Math.random() * 256);
}
return result;
}
// 获取文件路径
private async getFilePath(fileName: string): Promise<string> {
if (!this.context) {
throw new Error('Context is null');
}
const dirPath = this.context.filesDir + '/' + this.FILE_DIR;
if (!fs.accessSync(dirPath)) {
fs.mkdirSync(dirPath, true);
}
return dirPath + '/' + fileName;
}
// 字符串转 Uint8Array
private stringToUint8Array(str: string): Uint8Array {
const arr = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
arr[i] = str.charCodeAt(i);
}
return arr;
}
// Uint8Array 转字符串
private uint8ArrayToString(arr: Uint8Array): string {
let result = '';
for (let i = 0; i < arr.length; i++) {
result += String.fromCharCode(arr[i]);
}
return result;
}
// 加密字符串
async encryptString(plainText: string, password: string): Promise<string> {
try {
// 将密码转换为 32 字节密钥(AES-256)
const keyData = this.stringToUint8Array(
password.padEnd(32, '0').substring(0, 32));
// 生成 16 字节随机 IV
const iv = this.generateRandomBytes(16);
// 创建对称密钥
const symKeyGenerator =
cryptoFramework.createSymKeyGenerator('AES256');
const keyBlob: cryptoFramework.DataBlob = { data: keyData };
const symKey = await symKeyGenerator.convertKey(keyBlob);
// 创建加密器
const cipher = cryptoFramework.createCipher('AES|CBC|PKCS7');
const ivBlob: cryptoFramework.IvParamsSpec = {
algName: 'AES',
iv: { data: iv }
};
await cipher.init(
cryptoFramework.CryptoMode.ENCRYPT_MODE,
symKey,
ivBlob
);
// 执行加密
const plainData: cryptoFramework.DataBlob = {
data: this.stringToUint8Array(plainText)
};
const encryptedResult = await cipher.doFinal(plainData);
// 将 IV 和密文合并(IV 附加在头部)
const combined = new Uint8Array(
iv.byteLength + encryptedResult.data.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(encryptedResult.data), iv.byteLength);
// 返回 Base64 编码的加密数据
return JSON.stringify({
k: this.base64Encode(keyData),
d: this.base64Encode(combined)
});
} catch (error) {
hilog.error(DOMAIN, TAG, 'Encrypt failed: %{public}s',
JSON.stringify(error));
return '';
}
}
// 解密字符串
async decryptString(encryptedData: string): Promise<string> {
try {
const parsed: Record<string, string> = JSON.parse(encryptedData);
const keyData = this.base64Decode(parsed.k);
const combined = this.base64Decode(parsed.d);
// 分离 IV 和密文
const iv = combined.slice(0, 16);
const encrypted = combined.slice(16);
// 创建解密器
const symKeyGenerator =
cryptoFramework.createSymKeyGenerator('AES256');
const keyBlob: cryptoFramework.DataBlob = { data: keyData };
const symKey = await symKeyGenerator.convertKey(keyBlob);
const cipher = cryptoFramework.createCipher('AES|CBC|PKCS7');
const ivBlob: cryptoFramework.IvParamsSpec = {
algName: 'AES',
iv: { data: iv }
};
await cipher.init(
cryptoFramework.CryptoMode.DECRYPT_MODE,
symKey,
ivBlob
);
// 执行解密
const encryptedBlob: cryptoFramework.DataBlob = { data: encrypted };
const decryptedResult = await cipher.doFinal(encryptedBlob);
return this.uint8ArrayToString(
new Uint8Array(decryptedResult.data));
} catch (error) {
hilog.error(DOMAIN, TAG, 'Decrypt failed: %{public}s',
JSON.stringify(error));
return '';
}
}
// 加密并保存到文件
async encryptAndSave(fileName: string, plainText: string): Promise<boolean> {
try {
const password = this.generatePassword();
const encrypted = await this.encryptString(plainText, password);
if (!encrypted) return false;
const filePath = await this.getFilePath(fileName);
const file = fs.openSync(filePath,
fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.writeSync(file.fd, encrypted);
fs.closeSync(file.fd);
hilog.info(DOMAIN, TAG, 'Encrypted save: %{public}s', fileName);
return true;
} catch (error) {
hilog.error(DOMAIN, TAG, 'Encrypt save failed: %{public}s',
JSON.stringify(error));
return false;
}
}
// 从文件读取并解密
async loadAndDecrypt(fileName: string): Promise<string> {
try {
const filePath = await this.getFilePath(fileName);
if (!fs.accessSync(filePath)) {
return '';
}
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const stat = fs.statSync(file.fd);
const buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file.fd);
const encryptedData = this.arrayBufferToString(buffer);
return await this.decryptString(encryptedData);
} catch (error) {
hilog.error(DOMAIN, TAG, 'Load decrypt failed: %{public}s',
JSON.stringify(error));
return '';
}
}
// 生成密码
private generatePassword(): string {
const timestamp = Date.now().toString();
const randomPart = Math.random().toString(36).substring(2, 15);
return timestamp + randomPart;
}
private arrayBufferToString(buffer: ArrayBuffer): string {
const uint8Array = new Uint8Array(buffer);
let result = '';
for (let i = 0; i < uint8Array.length; i++) {
result += String.fromCharCode(uint8Array[i]);
}
return result;
}
}
ArkTS 关键设计要点:
- AES-256-CBC 加密:密钥长度 256 位(32 字节),分组模式 CBC,填充方式 PKCS7。
- 随机 IV:每次加密生成新的 16 字节随机 IV,附加在密文头部,接收端自动分离。
- IvParamsSpec 必须包含
algName属性:在 ArkTS 严格模式下,IvParamsSpec接口要求algName字段,这是新手最容易忽略的报错点。 - 数据格式:加密后的数据以 JSON 格式存储,包含
k(Base64 编码的密钥)和d(Base64 编码的 IV + 密文组合)。
四、Platform Channel 接入层
ArkTS 原生存储服务通过 StorageIndex 类统一注册到 Flutter Platform Channel,Flutter 端通过统一的 StorageService 类调用:
// ArkTS 侧 - utils/StorageIndex.ets
import { FlutterPlugin, FlutterEngine } from '@ohos/flutter_ohos';
import { PreferencesStorage } from './PreferencesStorage';
import { FileStorage } from './FileStorage';
import { EncryptionStorage } from './EncryptionStorage';
import hilog from '@ohos.hilog';
const TAG = 'StorageIndex';
const DOMAIN = 0xFF02;
const CHANNEL_NAME = 'com.example.oh_demo11/storage';
export class StorageIndex implements FlutterPlugin {
private context: Context | null = null;
private preferencesStorage: PreferencesStorage | null = null;
private fileStorage: FileStorage | null = null;
private encryptionStorage: EncryptionStorage | null = null;
onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding): void {
this.context = binding.getApplicationContext();
this.preferencesStorage = new PreferencesStorage(this.context!);
this.fileStorage = new FileStorage(this.context!);
this.encryptionStorage = new EncryptionStorage(this.context!);
binding.getBinaryMessenger().setMethodCallHandler(
this.handleMethodCall.bind(this)
);
hilog.info(DOMAIN, TAG, 'StorageIndex attached');
}
onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding): void {
binding.getBinaryMessenger().setMethodCallHandler(null);
}
private async handleMethodCall(
call: FlutterPlugin.MethodCall,
result: FlutterPlugin.MethodResult
): Promise<void> {
const method = call.method;
const args = call.args as Record<string, Object>;
try {
// === Preferences Storage ===
if (method === 'preferences.init') {
const ok = await this.preferencesStorage!.init();
result.success(ok);
return;
}
if (method === 'preferences.putString') {
const ok = await this.preferencesStorage!.putString(
args['key'] as string, args['value'] as string);
result.success(ok);
return;
}
if (method === 'preferences.getString') {
const value = await this.preferencesStorage!.getString(
args['key'] as string, args['defaultValue'] as string || '');
result.success(value);
return;
}
if (method === 'preferences.putNumber') {
const ok = await this.preferencesStorage!.putNumber(
args['key'] as string, args['value'] as number);
result.success(ok);
return;
}
if (method === 'preferences.putBoolean') {
const ok = await this.preferencesStorage!.putBoolean(
args['key'] as string, args['value'] as boolean);
result.success(ok);
return;
}
if (method === 'preferences.delete') {
const ok = await this.preferencesStorage!.delete(args['key'] as string);
result.success(ok);
return;
}
if (method === 'preferences.clear') {
const ok = await this.preferencesStorage!.clear();
result.success(ok);
return;
}
// === File Storage ===
if (method === 'file.writeText') {
const ok = await this.fileStorage!.writeTextFile(
args['fileName'] as string, args['content'] as string);
result.success(ok);
return;
}
if (method === 'file.readText') {
const content = await this.fileStorage!.readTextFile(
args['fileName'] as string);
result.success(content);
return;
}
if (method === 'file.writeJson') {
const ok = await this.fileStorage!.writeJsonFile(
args['fileName'] as string, args['data'] as object);
result.success(ok);
return;
}
if (method === 'file.readJson') {
const data = await this.fileStorage!.readJsonFile(
args['fileName'] as string);
result.success(data);
return;
}
if (method === 'file.delete') {
const ok = await this.fileStorage!.deleteFile(args['fileName'] as string);
result.success(ok);
return;
}
// === Encryption Storage ===
if (method === 'encrypt.encryptAndSave') {
const ok = await this.encryptionStorage!.encryptAndSave(
args['fileName'] as string, args['content'] as string);
result.success(ok);
return;
}
if (method === 'encrypt.loadAndDecrypt') {
const content = await this.encryptionStorage!.loadAndDecrypt(
args['fileName'] as string);
result.success(content);
return;
}
result.notImplemented();
} catch (error) {
hilog.error(DOMAIN, TAG, 'Method call failed: %{public}s',
JSON.stringify(error));
result.error('STORAGE_ERROR', JSON.stringify(error), null);
}
}
}
Plugin 注册需要在 EntryAbility 中完成:
// ArkTS 侧 - entryability/EntryAbility.ets
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
import { StorageIndex } from '../utils/StorageIndex';
export default class EntryAbility extends FlutterAbility {
private storageIndex: StorageIndex = new StorageIndex();
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine);
GeneratedPluginRegistrant.registerWith(flutterEngine);
// 注册存储插件
flutterEngine.getPlugins().add(this.storageIndex);
}
}
五、Flutter 端存储服务封装
Flutter 端通过 MethodChannel 调用原生存储服务,对上层业务代码提供简洁的 Dart API:
// Dart 侧 - lib/storage/storage_service.dart
import 'package:flutter/services.dart';
/// 存储服务通道封装
/// Flutter 端通过 MethodChannel 调用 ArkTS 原生存储能力
class StorageService {
static const _channel = MethodChannel('com.example.oh_demo11/storage');
// ========================
// Preferences Storage
// ========================
/// 初始化 Preferences
static Future<bool> initPreferences() async {
try {
final result = await _channel.invokeMethod<bool>('preferences.init');
return result ?? false;
} on PlatformException catch (e) {
print('Init preferences failed: ${e.message}');
return false;
}
}
/// 保存字符串
static Future<bool> putString(String key, String value) async {
try {
final result = await _channel.invokeMethod<bool>(
'preferences.putString',
{'key': key, 'value': value},
);
return result ?? false;
} on PlatformException catch (e) {
print('PutString failed: ${e.message}');
return false;
}
}
/// 读取字符串
static Future<String> getString(String key, {String defaultValue = ''}) async {
try {
final result = await _channel.invokeMethod<String>(
'preferences.getString',
{'key': key, 'defaultValue': defaultValue},
);
return result ?? defaultValue;
} on PlatformException catch (e) {
print('GetString failed: ${e.message}');
return defaultValue;
}
}
/// 保存数字
static Future<bool> putNumber(String key, num value) async {
try {
final result = await _channel.invokeMethod<bool>(
'preferences.putNumber',
{'key': key, 'value': value},
);
return result ?? false;
} on PlatformException catch (e) {
print('PutNumber failed: ${e.message}');
return false;
}
}
/// 保存布尔值
static Future<bool> putBoolean(String key, bool value) async {
try {
final result = await _channel.invokeMethod<bool>(
'preferences.putBoolean',
{'key': key, 'value': value},
);
return result ?? false;
} on PlatformException catch (e) {
print('PutBoolean failed: ${e.message}');
return false;
}
}
/// 删除指定键
static Future<bool> deletePreference(String key) async {
try {
final result = await _channel.invokeMethod<bool>(
'preferences.delete',
{'key': key},
);
return result ?? false;
} on PlatformException catch (e) {
print('Delete failed: ${e.message}');
return false;
}
}
/// 清空所有 Preferences
static Future<bool> clearPreferences() async {
try {
final result = await _channel.invokeMethod<bool>('preferences.clear');
return result ?? false;
} on PlatformException catch (e) {
print('Clear failed: ${e.message}');
return false;
}
}
// ========================
// File Storage
// ========================
/// 写入文本文件
static Future<bool> writeTextFile(String fileName, String content) async {
try {
final result = await _channel.invokeMethod<bool>(
'file.writeText',
{'fileName': fileName, 'content': content},
);
return result ?? false;
} on PlatformException catch (e) {
print('WriteText failed: ${e.message}');
return false;
}
}
/// 读取文本文件
static Future<String> readTextFile(String fileName) async {
try {
final result = await _channel.invokeMethod<String>(
'file.readText',
{'fileName': fileName},
);
return result ?? '';
} on PlatformException catch (e) {
print('ReadText failed: ${e.message}');
return '';
}
}
/// 写入 JSON 文件
static Future<bool> writeJsonFile(String fileName, Map<String, dynamic> data) async {
try {
final result = await _channel.invokeMethod<bool>(
'file.writeJson',
{'fileName': fileName, 'data': data},
);
return result ?? false;
} on PlatformException catch (e) {
print('WriteJson failed: ${e.message}');
return false;
}
}
/// 读取 JSON 文件
static Future<Map<String, dynamic>?> readJsonFile(String fileName) async {
try {
final result = await _channel.invokeMethod<Map<dynamic, dynamic>>(
'file.readJson',
{'fileName': fileName},
);
if (result == null) return null;
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
print('ReadJson failed: ${e.message}');
return null;
}
}
/// 删除文件
static Future<bool> deleteFile(String fileName) async {
try {
final result = await _channel.invokeMethod<bool>(
'file.delete',
{'fileName': fileName},
);
return result ?? false;
} on PlatformException catch (e) {
print('DeleteFile failed: ${e.message}');
return false;
}
}
// ========================
// Encryption Storage
// ========================
/// 加密保存
static Future<bool> encryptAndSave(String fileName, String content) async {
try {
final result = await _channel.invokeMethod<bool>(
'encrypt.encryptAndSave',
{'fileName': fileName, 'content': content},
);
return result ?? false;
} on PlatformException catch (e) {
print('EncryptAndSave failed: ${e.message}');
return false;
}
}
/// 加载并解密
static Future<String> loadAndDecrypt(String fileName) async {
try {
final result = await _channel.invokeMethod<String>(
'encrypt.loadAndDecrypt',
{'fileName': fileName},
);
return result ?? '';
} on PlatformException catch (e) {
print('LoadAndDecrypt failed: ${e.message}');
return '';
}
}
}
Flutter 端使用示例:
// 初始化
await StorageService.initPreferences();
// Preferences 使用
await StorageService.putString('username', 'john_doe');
await StorageService.putBoolean('dark_mode', true);
await StorageService.putNumber('font_size', 16);
final username = await StorageService.getString('username');
await StorageService.deletePreference('username');
await StorageService.clearPreferences();
// 文件存储使用
await StorageService.writeTextFile('note.txt', '这是一条笔记');
await StorageService.writeJsonFile('user.json', {
'name': '张三',
'age': 25,
'scores': [90, 85, 92]
});
final content = await StorageService.readTextFile('note.txt');
final userData = await StorageService.readJsonFile('user.json');
// 加密存储使用
await StorageService.encryptAndSave('passwords', 'MyP@ssw0rd!');
final decrypted = await StorageService.loadAndDecrypt('passwords');
六、Flutter UI 演示页面
Flutter 端提供三 Tab 页面,分别演示三种存储能力的操作界面:
// Dart 侧 - lib/storage/pages/storage_demo_page.dart
import 'package:flutter/material.dart';
import '../storage_service.dart';
class StorageDemoPage extends StatefulWidget {
const StorageDemoPage({super.key});
State<StorageDemoPage> createState() => _StorageDemoPageState();
}
class _StorageDemoPageState extends State<StorageDemoPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
// === Preferences 状态 ===
final _prefKeyController = TextEditingController(text: 'username');
final _prefValueController = TextEditingController(text: 'admin');
String _prefResult = '';
// === File 状态 ===
final _fileNameController = TextEditingController(text: 'note.txt');
final _fileContentController = TextEditingController(
text: 'Hello, OpenHarmony Storage!');
String _fileResult = '';
// === Encryption 状态 ===
final _encryptKeyController = TextEditingController(text: 'secret_data');
final _encryptContentController = TextEditingController(
text: 'Sensitive information');
String _encryptResult = '';
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_initStorage();
}
Future<void> _initStorage() async {
await StorageService.initPreferences();
}
void dispose() {
_tabController.dispose();
_prefKeyController.dispose();
_prefValueController.dispose();
_fileNameController.dispose();
_fileContentController.dispose();
_encryptKeyController.dispose();
_encryptContentController.dispose();
super.dispose();
}
// ========================
// Preferences 操作
// ========================
Future<void> _savePreference() async {
setState(() => _prefResult = 'Saving...');
final ok = await StorageService.putString(
_prefKeyController.text,
_prefValueController.text,
);
setState(() {
_prefResult = ok
? 'Saved: "${_prefKeyController.text}" = "${_prefValueController.text}"'
: 'Save failed';
});
}
Future<void> _loadPreference() async {
setState(() => _prefResult = 'Loading...');
final value = await StorageService.getString(_prefKeyController.text);
setState(() {
_prefResult = value.isNotEmpty
? 'Loaded: "${_prefKeyController.text}" = "$value"'
: 'Key not found';
});
}
Future<void> _deletePreference() async {
setState(() => _prefResult = 'Deleting...');
final ok = await StorageService.deletePreference(_prefKeyController.text);
setState(() {
_prefResult = ok ? 'Deleted: "${_prefKeyController.text}"' : 'Delete failed';
});
}
Future<void> _clearPreferences() async {
setState(() => _prefResult = 'Clearing...');
final ok = await StorageService.clearPreferences();
setState(() {
_prefResult = ok ? 'All preferences cleared' : 'Clear failed';
});
}
// ========================
// File 操作
// ========================
Future<void> _saveText() async {
setState(() => _fileResult = 'Saving...');
final ok = await StorageService.writeTextFile(
_fileNameController.text,
_fileContentController.text,
);
setState(() {
_fileResult = ok
? 'File "${_fileNameController.text}" saved'
: 'Save failed';
});
}
Future<void> _loadText() async {
setState(() => _fileResult = 'Loading...');
final content = await StorageService.readTextFile(_fileNameController.text);
setState(() {
_fileResult = content.isNotEmpty
? 'Content: $content'
: 'File not found';
});
}
Future<void> _saveJson() async {
setState(() => _fileResult = 'Saving JSON...');
final ok = await StorageService.writeJsonFile(
_fileNameController.text,
{'name': '张三', 'age': 25, 'scores': [90, 85, 92]},
);
setState(() {
_fileResult = ok
? 'JSON file "${_fileNameController.text}" saved'
: 'Save failed';
});
}
Future<void> _loadJson() async {
setState(() => _fileResult = 'Loading JSON...');
final data = await StorageService.readJsonFile(_fileNameController.text);
setState(() {
_fileResult = data != null
? 'JSON: ${data.toString()}'
: 'File not found';
});
}
Future<void> _deleteFile() async {
setState(() => _fileResult = 'Deleting...');
final ok = await StorageService.deleteFile(_fileNameController.text);
setState(() {
_fileResult = ok ? 'File deleted' : 'Delete failed';
});
}
// ========================
// Encryption 操作
// ========================
Future<void> _encryptAndSave() async {
setState(() => _encryptResult = 'Encrypting...');
final ok = await StorageService.encryptAndSave(
_encryptKeyController.text,
_encryptContentController.text,
);
setState(() {
_encryptResult = ok
? 'Encrypted and saved: "${_encryptKeyController.text}"'
: 'Encrypt failed';
});
}
Future<void> _loadAndDecrypt() async {
setState(() => _encryptResult = 'Decrypting...');
final content = await StorageService.loadAndDecrypt(
_encryptKeyController.text,
);
setState(() {
_encryptResult = content.isNotEmpty
? 'Decrypted: $content'
: 'Data not found';
});
}
Future<void> _encryptJson() async {
setState(() => _encryptResult = 'Encrypting JSON...');
final data = '{"username":"admin","password":"P@ssw0rd!"}';
final ok = await StorageService.encryptAndSave(
_encryptKeyController.text,
data,
);
setState(() {
_encryptResult = ok
? 'JSON encrypted: "${_encryptKeyController.text}"'
: 'Encrypt failed';
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Storage Demo'),
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
bottom: TabBar(
controller: _tabController,
indicatorColor: Colors.white,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
tabs: const [
Tab(text: 'Preferences', icon: Icon(Icons.settings)),
Tab(text: 'File', icon: Icon(Icons.folder)),
Tab(text: 'Encryption', icon: Icon(Icons.lock)),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildPreferencesTab(),
_buildFileTab(),
_buildEncryptionTab(),
],
),
);
}
Widget _buildPreferencesTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSectionTitle('Preferences Storage', Icons.settings),
const SizedBox(height: 12),
TextField(
controller: _prefKeyController,
decoration: const InputDecoration(
labelText: 'Key',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.key),
),
),
const SizedBox(height: 12),
TextField(
controller: _prefValueController,
decoration: const InputDecoration(
labelText: 'Value',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.text_fields),
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _savePreference,
icon: const Icon(Icons.save),
label: const Text('Save'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: _loadPreference,
icon: const Icon(Icons.download),
label: const Text('Load'),
),
ElevatedButton.icon(
onPressed: _deletePreference,
icon: const Icon(Icons.delete),
label: const Text('Delete'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: _clearPreferences,
icon: const Icon(Icons.clear_all),
label: const Text('Clear All'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
_buildResultCard(_prefResult),
],
),
);
}
Widget _buildFileTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSectionTitle('File Storage', Icons.folder),
const SizedBox(height: 12),
TextField(
controller: _fileNameController,
decoration: const InputDecoration(
labelText: 'File Name',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.insert_drive_file),
),
),
const SizedBox(height: 12),
TextField(
controller: _fileContentController,
maxLines: 3,
decoration: const InputDecoration(
labelText: 'Content',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.notes),
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _saveText,
icon: const Icon(Icons.save),
label: const Text('Save Text'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: _loadText,
icon: const Icon(Icons.download),
label: const Text('Load Text'),
),
ElevatedButton.icon(
onPressed: _saveJson,
icon: const Icon(Icons.data_object),
label: const Text('Save JSON'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: _loadJson,
icon: const Icon(Icons.download),
label: const Text('Load JSON'),
),
ElevatedButton.icon(
onPressed: _deleteFile,
icon: const Icon(Icons.delete),
label: const Text('Delete'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
_buildResultCard(_fileResult),
],
),
);
}
Widget _buildEncryptionTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSectionTitle('Encryption Storage (AES-256)', Icons.lock),
const SizedBox(height: 12),
TextField(
controller: _encryptKeyController,
decoration: const InputDecoration(
labelText: 'File Key',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.key),
),
),
const SizedBox(height: 12),
TextField(
controller: _encryptContentController,
maxLines: 3,
decoration: const InputDecoration(
labelText: 'Sensitive Data',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.security),
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _encryptAndSave,
icon: const Icon(Icons.lock),
label: const Text('Encrypt & Save'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: _loadAndDecrypt,
icon: const Icon(Icons.lock_open),
label: const Text('Load & Decrypt'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: _encryptJson,
icon: const Icon(Icons.data_object),
label: const Text('Encrypt JSON'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
_buildResultCard(_encryptResult),
],
),
);
}
Widget _buildSectionTitle(String title, IconData icon) {
return Row(
children: [
Icon(icon, color: Colors.teal, size: 24),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
);
}
Widget _buildResultCard(String result) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: result.isEmpty ? Colors.grey[100] : Colors.teal[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.teal[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, size: 16, color: Colors.teal[700]),
const SizedBox(width: 6),
Text(
'Result',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.teal[700],
fontSize: 13,
),
),
],
),
const SizedBox(height: 8),
Text(
result.isEmpty ? 'No operation yet' : result,
style: TextStyle(
fontSize: 14,
color: result.isEmpty ? Colors.grey : Colors.black87,
fontFamily: 'monospace',
),
),
],
),
);
}
}
七、存储能力对比与选型建议
在实际项目中,需要根据数据特点选择合适的存储方式:
| 存储类型 | 适用场景 | 数据量级 | 持久化 | 加密 | 性能 |
|---|---|---|---|---|---|
| Preferences | 用户设置、开关状态 | 轻量(KB 级) | 是 | 否 | 最高 |
| File Storage | 笔记、配置、结构化数据 | 中等(MB 级) | 是 | 否 | 高 |
| Encryption Storage | 密码、Token、私密数据 | 轻量(KB 级) | 是 | AES-256 | 中等 |
选型建议:
- 用户偏好和配置信息优先使用 Preferences,API 简洁且性能最优。
- JSON 结构和文档数据使用 File Storage,配合 JSON 序列化实现结构化存储。
- 敏感信息必须使用 Encryption Storage,即使数据泄露也无法被解读。
- 组合使用是最常见做法:用户偏好存 Preferences、文档内容存 File Storage、登录凭证存 Encryption Storage。
八、代码仓库与资源
本文完整代码已上传至 AtomGit 仓库:
仓库地址:https://atomgit.com/maaath/oh_demo11
主要文件对应关系:
| Flutter 端 (Dart) | ArkTS 原生侧 | 说明 |
|---|---|---|
lib/storage/storage_service.dart |
entry/src/main/ets/utils/StorageIndex.ets |
Platform Channel 封装 |
| — | entry/src/main/ets/utils/PreferencesStorage.ets |
偏好设置存储 |
| — | entry/src/main/ets/utils/FileStorage.ets |
文件存储 |
| — | entry/src/main/ets/utils/EncryptionStorage.ets |
加密存储 |
lib/storage/pages/storage_demo_page.dart |
entry/src/main/ets/pages/StorageDemo.ets |
演示 UI 页面 |
| — | entry/src/main/ets/entryability/EntryAbility.ets |
Flutter 引擎入口 |
九、截图验证板块
请在下方查看应用在鸿蒙设备上的运行截图:
-
Flutter 端 Preferences Tab — 展示 Key 输入框、Value 输入框、Save/Load/Delete/Clear All 四个操作按钮,Result 区域显示操作结果

视频演示:本地储存功能
-
Flutter 端 File Storage Tab — 展示 File Name 输入框、Content 多行输入框、Save Text/Load Text/Save JSON/Load JSON/Delete 五个操作按钮,Result 区域显示文件读写结果。

-
Flutter 端 Encryption Tab — 展示 File Key 输入框、Sensitive Data 多行输入框、Encrypt & Save/Load & Decrypt/Encrypt JSON 三个操作按钮,Result 区域显示加密解密结果。

十、总结
本文完整介绍了在 Flutter for OpenHarmony 跨平台工程中集成本地存储能力的方案,核心主线是 Flutter 端通过 Platform Channel 调用 ArkTS 原生存储服务:
- 架构分层:Flutter UI → MethodChannel → ArkTS 存储服务 → OpenHarmony 系统 API,四层职责清晰。
- 三类存储:Preferences(轻量键值)、File Storage(文件读写)、Encryption Storage(AES-256 加密),覆盖了应用存储的主要需求。
- ArkTS 原生实现:完整使用
@ohos.data.preferences、@ohos.file.fs、@ohos.security.cryptoFramework三大系统 API。 - Flutter 端封装:统一的
StorageService类,隐藏 Platform Channel 细节,提供简洁的 Dart API。 - 安全性:敏感数据通过 AES-256-CBC 加密存储,随机 IV 防止密文分析,密码不硬编码。
这套方案充分发挥了 Flutter 跨平台 UI 开发效率与 OpenHarmony 原生存储能力各自的优势,可直接嵌入任何 FOH 项目中,为应用赋予完整、安全、高效的本地数据持久化能力。
更多推荐


所有评论(0)