Flutter+HarmonyOS跨端实战—第06篇:本地存储的跨平台实现
·
实现一个支持三端的 SharedPreferences 插件
前言
本地存储是移动应用的基础功能,用于保存用户设置、登录状态、缓存数据等。CleanMark AI 使用本地存储保存:
- 用户登录 Token
- 引导页是否已显示
- 用户信息缓存
- 应用配置
这篇文章我会带你实现一个完整的本地存储插件,支持 Android、iOS、HarmonyOS 三端。
一、本地存储方案对比
1.1 三端原生方案
| 平台 | 原生方案 | 特点 |
|---|---|---|
| Android | SharedPreferences | XML 文件存储,键值对 |
| iOS | UserDefaults | plist 文件存储,键值对 |
| HarmonyOS | Preferences | 数据库存储,键值对 |
共同特点:
- 都是键值对存储
- 支持基本数据类型(String、int、bool、double)
- 异步读写
- 自动持久化
1.2 数据类型支持
| Dart 类型 | Android | iOS | HarmonyOS |
|---|---|---|---|
| String | ✅ | ✅ | ✅ |
| int | ✅ | ✅ | ✅ |
| double | ✅ | ✅ | ✅ |
| bool | ✅ | ✅ | ✅ |
| List<String> | ✅ | ✅ | ✅ |
二、插件架构设计
2.1 分层架构
shared_preferences/ # 应用层
└─ 依赖 shared_preferences_platform_interface
shared_preferences_platform_interface/ # 接口层
└─ 定义抽象接口
shared_preferences_android/ # Android 实现
shared_preferences_foundation/ # iOS/macOS 实现
shared_preferences_ohos/ # HarmonyOS 实现
2.2 接口层设计
// packages/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart
import 'dart:async';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
/// SharedPreferences 平台接口
abstract class SharedPreferencesStorePlatform extends PlatformInterface {
SharedPreferencesStorePlatform() : super(token: _token);
static final Object _token = Object();
static SharedPreferencesStorePlatform _instance = MethodChannelSharedPreferencesStore();
static SharedPreferencesStorePlatform get instance => _instance;
static set instance(SharedPreferencesStorePlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
/// 获取所有数据
Future<Map<String, Object>> getAll() {
throw UnimplementedError('getAll() has not been implemented.');
}
/// 移除指定 key
Future<bool> remove(String key) {
throw UnimplementedError('remove() has not been implemented.');
}
/// 清空所有数据
Future<bool> clear() {
throw UnimplementedError('clear() has not been implemented.');
}
/// 设置 String 值
Future<bool> setValue(String valueType, String key, Object value) {
throw UnimplementedError('setValue() has not been implemented.');
}
}
2.3 MethodChannel 实现
// packages/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart
import 'package:flutter/services.dart';
import 'shared_preferences_platform_interface.dart';
/// 基于 MethodChannel 的实现
class MethodChannelSharedPreferencesStore extends SharedPreferencesStorePlatform {
final MethodChannel channel = const MethodChannel('plugins.flutter.io/shared_preferences');
Future<Map<String, Object>> getAll() async {
final Map<String, Object>? preferences =
await channel.invokeMapMethod<String, Object>('getAll');
return preferences ?? <String, Object>{};
}
Future<bool> remove(String key) async {
return await channel.invokeMethod<bool>('remove', <String, dynamic>{
'key': key,
}) ?? false;
}
Future<bool> clear() async {
return await channel.invokeMethod<bool>('clear') ?? false;
}
Future<bool> setValue(String valueType, String key, Object value) async {
return await channel.invokeMethod<bool>('set$valueType', <String, dynamic>{
'key': key,
'value': value,
}) ?? false;
}
}
三、应用层封装
3.1 SharedPreferences 类
// packages/shared_preferences/lib/shared_preferences.dart
import 'dart:async';
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
/// SharedPreferences 本地存储
class SharedPreferences {
SharedPreferences._(this._preferenceCache);
static const String _prefix = 'flutter.';
static SharedPreferences? _instance;
static SharedPreferencesStorePlatform get _store =>
SharedPreferencesStorePlatform.instance;
final Map<String, Object> _preferenceCache;
/// 获取实例(单例模式)
static Future<SharedPreferences> getInstance() async {
if (_instance != null) {
return _instance!;
}
final Map<String, Object> preferencesMap = await _store.getAll();
_instance = SharedPreferences._(preferencesMap);
return _instance!;
}
/// 获取所有 key
Set<String> getKeys() => _preferenceCache.keys.toSet();
/// 获取 String 值
String? getString(String key) {
return _preferenceCache[_prefix + key] as String?;
}
/// 设置 String 值
Future<bool> setString(String key, String value) {
_preferenceCache[_prefix + key] = value;
return _store.setValue('String', _prefix + key, value);
}
/// 获取 int 值
int? getInt(String key) {
return _preferenceCache[_prefix + key] as int?;
}
/// 设置 int 值
Future<bool> setInt(String key, int value) {
_preferenceCache[_prefix + key] = value;
return _store.setValue('Int', _prefix + key, value);
}
/// 获取 double 值
double? getDouble(String key) {
return _preferenceCache[_prefix + key] as double?;
}
/// 设置 double 值
Future<bool> setDouble(String key, double value) {
_preferenceCache[_prefix + key] = value;
return _store.setValue('Double', _prefix + key, value);
}
/// 获取 bool 值
bool? getBool(String key) {
return _preferenceCache[_prefix + key] as bool?;
}
/// 设置 bool 值
Future<bool> setBool(String key, bool value) {
_preferenceCache[_prefix + key] = value;
return _store.setValue('Bool', _prefix + key, value);
}
/// 获取 List<String> 值
List<String>? getStringList(String key) {
final List<dynamic>? list = _preferenceCache[_prefix + key] as List<dynamic>?;
return list?.cast<String>();
}
/// 设置 List<String> 值
Future<bool> setStringList(String key, List<String> value) {
_preferenceCache[_prefix + key] = value;
return _store.setValue('StringList', _prefix + key, value);
}
/// 移除指定 key
Future<bool> remove(String key) {
_preferenceCache.remove(_prefix + key);
return _store.remove(_prefix + key);
}
/// 清空所有数据
Future<bool> clear() {
_preferenceCache.clear();
return _store.clear();
}
/// 检查 key 是否存在
bool containsKey(String key) {
return _preferenceCache.containsKey(_prefix + key);
}
/// 重新加载数据
Future<void> reload() async {
final Map<String, Object> preferences = await _store.getAll();
_preferenceCache.clear();
_preferenceCache.addAll(preferences);
}
}
设计要点:
- 单例模式:全局只有一个实例,避免重复加载
- 内存缓存:读取时直接从缓存获取,提升性能
- key 前缀:添加
flutter.前缀,避免与原生数据冲突 - 类型安全:每种类型都有独立的 get/set 方法
3.2 使用示例
// 保存数据
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', 'abc123');
await prefs.setInt('userId', 12345);
await prefs.setBool('isLoggedIn', true);
// 读取数据
final token = prefs.getString('token');
final userId = prefs.getInt('userId');
final isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
// 删除数据
await prefs.remove('token');
// 清空所有数据
await prefs.clear();
四、Android 平台实现
4.1 Kotlin 插件代码
// packages/shared_preferences_android/android/src/main/kotlin/SharedPreferencesPlugin.kt
package io.flutter.plugins.sharedpreferences
import android.content.Context
import android.content.SharedPreferences
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
class SharedPreferencesPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var preferences: SharedPreferences
companion object {
private const val CHANNEL = "plugins.flutter.io/shared_preferences"
private const val SHARED_PREFERENCES_NAME = "FlutterSharedPreferences"
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(binding.binaryMessenger, CHANNEL)
channel.setMethodCallHandler(this)
preferences = binding.applicationContext.getSharedPreferences(
SHARED_PREFERENCES_NAME,
Context.MODE_PRIVATE
)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"getAll" -> {
result.success(getAllPrefs())
}
"setBool" -> {
val key = call.argument<String>("key")!!
val value = call.argument<Boolean>("value")!!
result.success(preferences.edit().putBoolean(key, value).commit())
}
"setInt" -> {
val key = call.argument<String>("key")!!
val value = call.argument<Int>("value")!!
result.success(preferences.edit().putInt(key, value).commit())
}
"setDouble" -> {
val key = call.argument<String>("key")!!
val value = call.argument<Double>("value")!!
// Android 没有 putDouble,使用 Float
result.success(preferences.edit().putFloat(key, value.toFloat()).commit())
}
"setString" -> {
val key = call.argument<String>("key")!!
val value = call.argument<String>("value")!!
result.success(preferences.edit().putString(key, value).commit())
}
"setStringList" -> {
val key = call.argument<String>("key")!!
val value = call.argument<List<String>>("value")!!
result.success(preferences.edit().putStringSet(key, value.toSet()).commit())
}
"remove" -> {
val key = call.argument<String>("key")!!
result.success(preferences.edit().remove(key).commit())
}
"clear" -> {
result.success(preferences.edit().clear().commit())
}
else -> {
result.notImplemented()
}
}
}
private fun getAllPrefs(): Map<String, Any> {
val allPrefs = preferences.all
val result = mutableMapOf<String, Any>()
for ((key, value) in allPrefs) {
when (value) {
is Boolean -> result[key] = value
is Int -> result[key] = value
is Long -> result[key] = value.toInt()
is Float -> result[key] = value.toDouble()
is String -> result[key] = value
is Set<*> -> result[key] = value.toList()
}
}
return result
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
关键点:
- SharedPreferences API:Android 原生键值对存储
- commit() vs apply():commit() 同步写入,apply() 异步写入
- 类型转换:Android 没有 double 类型,使用 float
本篇小结(第一部分)
这部分我们完成了:
- ✅ 本地存储方案对比
- ✅ 插件架构设计
- ✅ 接口层设计
- ✅ 应用层封装(SharedPreferences 类)
- ✅ Android 平台实现
下一部分我会继续讲解 iOS 和 HarmonyOS 的实现。
五、iOS 平台实现
5.1 Swift 插件代码
// packages/shared_preferences_foundation/ios/Classes/SharedPreferencesPlugin.swift
import Flutter
import UIKit
public class SharedPreferencesPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "plugins.flutter.io/shared_preferences",
binaryMessenger: registrar.messenger()
)
let instance = SharedPreferencesPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let userDefaults = UserDefaults.standard
switch call.method {
case "getAll":
result(getAllPrefs())
case "setBool":
guard let args = call.arguments as? [String: Any],
let key = args["key"] as? String,
let value = args["value"] as? Bool else {
result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
return
}
userDefaults.set(value, forKey: key)
result(userDefaults.synchronize())
case "setInt":
guard let args = call.arguments as? [String: Any],
let key = args["key"] as? String,
let value = args["value"] as? Int else {
result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
return
}
userDefaults.set(value, forKey: key)
result(userDefaults.synchronize())
case "setDouble":
guard let args = call.arguments as? [String: Any],
let key = args["key"] as? String,
let value = args["value"] as? Double else {
result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
return
}
userDefaults.set(value, forKey: key)
result(userDefaults.synchronize())
case "setString":
guard let args = call.arguments as? [String: Any],
let key = args["key"] as? String,
let value = args["value"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
return
}
userDefaults.set(value, forKey: key)
result(userDefaults.synchronize())
case "setStringList":
guard let args = call.arguments as? [String: Any],
let key = args["key"] as? String,
let value = args["value"] as? [String] else {
result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
return
}
userDefaults.set(value, forKey: key)
result(userDefaults.synchronize())
case "remove":
guard let args = call.arguments as? [String: Any],
let key = args["key"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
return
}
userDefaults.removeObject(forKey: key)
result(userDefaults.synchronize())
case "clear":
let domain = Bundle.main.bundleIdentifier!
userDefaults.removePersistentDomain(forName: domain)
result(userDefaults.synchronize())
default:
result(FlutterMethodNotImplemented)
}
}
private func getAllPrefs() -> [String: Any] {
let userDefaults = UserDefaults.standard
var result: [String: Any] = [:]
// 获取所有键值对
if let allPrefs = userDefaults.dictionaryRepresentation() as? [String: Any] {
for (key, value) in allPrefs {
// 只返回 flutter. 前缀的数据
if key.hasPrefix("flutter.") {
result[key] = value
}
}
}
return result
}
}
关键点:
- UserDefaults API:iOS 原生键值对存储
- synchronize():强制同步到磁盘(iOS 12+ 已废弃,但为了兼容性保留)
- 类型支持:iOS 原生支持所有基本类型
六、HarmonyOS 平台实现
6.1 ArkTS 插件代码
// packages/shared_preferences_ohos/ohos/src/main/ets/SharedPreferencesPlugin.ets
import { FlutterPlugin, MethodCall, MethodChannel, MethodResult } from '@ohos/flutter_ohos';
import dataPreferences from '@ohos.data.preferences';
import { BusinessError } from '@ohos.base';
export class SharedPreferencesPlugin implements FlutterPlugin {
private channel: MethodChannel | null = null;
private preferences: dataPreferences.Preferences | null = null;
getUniqueClassName(): string {
return "SharedPreferencesPlugin";
}
async onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding): Promise<void> {
this.channel = new MethodChannel(
binding.getBinaryMessenger(),
'plugins.flutter.io/shared_preferences'
);
this.channel.setMethodCallHandler(this.onMethodCall.bind(this));
// 初始化 Preferences
try {
const context = binding.getApplicationContext();
this.preferences = await dataPreferences.getPreferences(context, 'FlutterSharedPreferences');
console.info('[SharedPreferences] Preferences initialized');
} catch (err) {
console.error(`[SharedPreferences] Failed to initialize: ${(err as Error).message}`);
}
}
async onMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
const method = call.method;
try {
if (this.preferences === null) {
result.error('ERROR', 'Preferences not initialized', null);
return;
}
switch (method) {
case 'getAll':
await this.getAll(result);
break;
case 'setBool':
await this.setBool(call, result);
break;
case 'setInt':
await this.setInt(call, result);
break;
case 'setDouble':
await this.setDouble(call, result);
break;
case 'setString':
await this.setString(call, result);
break;
case 'setStringList':
await this.setStringList(call, result);
break;
case 'remove':
await this.remove(call, result);
break;
case 'clear':
await this.clear(result);
break;
default:
result.notImplemented();
}
} catch (err) {
result.error('ERROR', (err as Error).message, null);
}
}
private async getAll(result: MethodResult): Promise<void> {
if (this.preferences === null) {
result.error('ERROR', 'Preferences not initialized', null);
return;
}
try {
const allData: Record<string, any> = {};
const keys = await this.preferences.getAll();
for (const key in keys) {
allData[key] = keys[key];
}
result.success(allData);
} catch (err) {
result.error('ERROR', (err as Error).message, null);
}
}
private async setBool(call: MethodCall, result: MethodResult): Promise<void> {
const args = call.args as Record<string, any>;
const key = args['key'] as string;
const value = args['value'] as boolean;
if (this.preferences !== null) {
await this.preferences.put(key, value);
await this.preferences.flush();
result.success(true);
} else {
result.error('ERROR', 'Preferences not initialized', null);
}
}
private async setInt(call: MethodCall, result: MethodResult): Promise<void> {
const args = call.args as Record<string, any>;
const key = args['key'] as string;
const value = args['value'] as number;
if (this.preferences !== null) {
await this.preferences.put(key, value);
await this.preferences.flush();
result.success(true);
} else {
result.error('ERROR', 'Preferences not initialized', null);
}
}
private async setDouble(call: MethodCall, result: MethodResult): Promise<void> {
const args = call.args as Record<string, any>;
const key = args['key'] as string;
const value = args['value'] as number;
if (this.preferences !== null) {
await this.preferences.put(key, value);
await this.preferences.flush();
result.success(true);
} else {
result.error('ERROR', 'Preferences not initialized', null);
}
}
private async setString(call: MethodCall, result: MethodResult): Promise<void> {
const args = call.args as Record<string, any>;
const key = args['key'] as string;
const value = args['value'] as string;
if (this.preferences !== null) {
await this.preferences.put(key, value);
await this.preferences.flush();
result.success(true);
} else {
result.error('ERROR', 'Preferences not initialized', null);
}
}
private async setStringList(call: MethodCall, result: MethodResult): Promise<void> {
const args = call.args as Record<string, any>;
const key = args['key'] as string;
const value = args['value'] as string[];
if (this.preferences !== null) {
// HarmonyOS Preferences 支持数组类型
await this.preferences.put(key, value);
await this.preferences.flush();
result.success(true);
} else {
result.error('ERROR', 'Preferences not initialized', null);
}
}
private async remove(call: MethodCall, result: MethodResult): Promise<void> {
const args = call.args as Record<string, any>;
const key = args['key'] as string;
if (this.preferences !== null) {
await this.preferences.delete(key);
await this.preferences.flush();
result.success(true);
} else {
result.error('ERROR', 'Preferences not initialized', null);
}
}
private async clear(result: MethodResult): Promise<void> {
if (this.preferences !== null) {
await this.preferences.clear();
await this.preferences.flush();
result.success(true);
} else {
result.error('ERROR', 'Preferences not initialized', null);
}
}
onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding): void {
if (this.channel !== null) {
this.channel.setMethodCallHandler(null);
}
}
}
关键点:
- Preferences API:HarmonyOS 使用
@ohos.data.preferences - 异步操作:所有操作都是异步的,需要 await
- flush() 调用:修改后必须调用 flush() 持久化
- 类型支持:HarmonyOS 原生支持数组类型
七、实战应用
7.1 封装工具类
在 CleanMark AI 项目中,我们封装了一个工具类来简化使用:
// lib/core/utils/app_prefs.dart
import 'package:shared_preferences/shared_preferences.dart';
import '../../features/auth/user_model.dart';
/// 应用本地存储工具类
class AppPrefs {
AppPrefs._();
static SharedPreferences? _prefs;
/// 初始化(在 main 函数中调用)
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
/// 获取实例
static SharedPreferences get instance {
if (_prefs == null) {
throw StateError('AppPrefs not initialized. Call AppPrefs.init() first.');
}
return _prefs!;
}
// ==================== 引导页 ====================
/// 引导页是否已完成
static Future<bool> isOnboardingDone() async {
return instance.getBool('onboarding_done') ?? false;
}
/// 标记引导页已完成
static Future<void> setOnboardingDone() async {
await instance.setBool('onboarding_done', true);
}
// ==================== 用户信息 ====================
/// 保存用户信息
static Future<void> saveUser(UserModel user) async {
await instance.setString('user_id', user.id);
await instance.setString('user_email', user.email);
await instance.setString('user_nickname', user.nickname ?? '');
await instance.setInt('user_credits', user.credits);
}
/// 加载用户信息
static Future<UserModel?> loadUser() async {
final userId = instance.getString('user_id');
if (userId == null) return null;
return UserModel(
id: userId,
email: instance.getString('user_email') ?? '',
nickname: instance.getString('user_nickname'),
credits: instance.getInt('user_credits') ?? 0,
);
}
/// 清除用户信息
static Future<void> clearUser() async {
await instance.remove('user_id');
await instance.remove('user_email');
await instance.remove('user_nickname');
await instance.remove('user_credits');
}
// ==================== Token ====================
/// 保存 Token
static Future<void> saveToken(String token) async {
await instance.setString('auth_token', token);
}
/// 获取 Token
static String? getToken() {
return instance.getString('auth_token');
}
/// 清除 Token
static Future<void> clearToken() async {
await instance.remove('auth_token');
}
// ==================== 应用配置 ====================
/// 保存语言设置
static Future<void> setLanguage(String languageCode) async {
await instance.setString('language', languageCode);
}
/// 获取语言设置
static String getLanguage() {
return instance.getString('language') ?? 'zh';
}
}
7.2 在项目中使用
// lib/main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化本地存储
await AppPrefs.init();
// 检查引导页状态
final skipOnboarding = await AppPrefs.isOnboardingDone();
// 检查登录状态
final token = AppPrefs.getToken();
final isLoggedIn = token != null && token.isNotEmpty;
runApp(MyApp(
skipOnboarding: skipOnboarding,
isLoggedIn: isLoggedIn,
));
}
八、性能优化
8.1 批量操作优化
// ❌ 不好的做法:多次写入
await prefs.setString('key1', 'value1');
await prefs.setString('key2', 'value2');
await prefs.setString('key3', 'value3');
// ✅ 好的做法:批量写入(减少 I/O 次数)
await Future.wait([
prefs.setString('key1', 'value1'),
prefs.setString('key2', 'value2'),
prefs.setString('key3', 'value3'),
]);
8.2 避免频繁读写
// ❌ 不好的做法:每次都读取
Widget build(BuildContext context) {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token');
return Text(token ?? '');
}
// ✅ 好的做法:缓存到内存
class MyWidget extends StatefulWidget {
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
String? _token;
void initState() {
super.initState();
_loadToken();
}
Future<void> _loadToken() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_token = prefs.getString('token');
});
}
Widget build(BuildContext context) {
return Text(_token ?? '');
}
}
本篇完整小结
这篇文章我们完成了:
- ✅ 本地存储方案对比
- ✅ 插件架构设计(联邦插件模式)
- ✅ 接口层设计
- ✅ 应用层封装(SharedPreferences 类)
- ✅ Android 平台实现(SharedPreferences)
- ✅ iOS 平台实现(UserDefaults)
- ✅ HarmonyOS 平台实现(Preferences)
- ✅ 实战应用(工具类封装)
- ✅ 性能优化建议
关键要点:
- 三端使用各自的原生存储 API
- 采用单例模式 + 内存缓存提升性能
- 添加 key 前缀避免冲突
- HarmonyOS 需要调用 flush() 持久化
- 批量操作减少 I/O 次数
思考题
- 为什么要使用单例模式?
- SharedPreferences 适合存储什么类型的数据?不适合存储什么?
- 如何实现数据加密存储?
下一篇预告:第07篇 - 视频播放器的 HarmonyOS 适配
更多推荐


所有评论(0)