1. 项目概述
在移动应用开发中,数据持久化是一个基础但至关重要的功能。对于游戏类应用而言,良好的数据持久化实现能够保存玩家的游戏进度、统计数据和个性化设置,极大提升用户体验。本文将基于Flutter框架,详细讲解如何在OpenHarmony平台上为数独游戏实现一套完整的本地数据持久化方案。
这个方案的核心在于:
- 使用SharedPreferences作为基础存储引擎
- 设计分层服务架构管理不同类型的数据
- 处理复杂数据结构的序列化与反序列化
- 实现数据完整性检查和自动保存机制
2. 存储服务设计与实现
2.1 SharedPreferences基础封装
SharedPreferences是Flutter中用于存储简单键值对的轻量级解决方案。我们先创建一个StorageService类来封装其基本操作:
dart复制import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
class StorageService {
static SharedPreferences? _prefs;
// 初始化存储服务
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// 安全的存储实例访问
static SharedPreferences get prefs {
if (_prefs == null) {
throw Exception('StorageService not initialized');
}
return _prefs!;
}
}
这里有几个关键设计点:
- 使用单例模式确保全局只有一个SharedPreferences实例
- 静态初始化方法init()需要在应用启动时调用
- 通过getter提供安全的实例访问,避免空指针异常
2.2 基础数据类型支持
接下来我们为各种基础数据类型添加支持:
dart复制// 布尔值操作
static Future<void> setBool(String key, bool value) async {
await prefs.setBool(key, value);
}
static bool getBool(String key, {bool defaultValue = false}) {
return prefs.getBool(key) ?? defaultValue;
}
// 整数操作
static Future<void> setInt(String key, int value) async {
await prefs.setInt(key, value);
}
static int getInt(String key, {int defaultValue = 0}) {
return prefs.getInt(key) ?? defaultValue;
}
// 字符串操作
static Future<void> setString(String key, String value) async {
await prefs.setString(key, value);
}
static String getString(String key, {String defaultValue = ''}) {
return prefs.getString(key) ?? defaultValue;
}
每个方法都提供了defaultValue参数,确保在没有对应数据时返回合理的默认值。这种防御性编程可以避免空指针异常。
2.3 JSON数据支持
对于复杂数据结构,我们需要通过JSON序列化来存储:
dart复制// JSON对象操作
static Future<void> setJson(String key, Map<String, dynamic> value) async {
await prefs.setString(key, jsonEncode(value));
}
static Map<String, dynamic>? getJson(String key) {
String? jsonStr = prefs.getString(key);
if (jsonStr == null) return null;
return jsonDecode(jsonStr) as Map<String, dynamic>;
}
// JSON数组操作
static Future<void> setJsonList(String key, List<Map<String, dynamic>> value) async {
await prefs.setString(key, jsonEncode(value));
}
static List<Map<String, dynamic>>? getJsonList(String key) {
String? jsonStr = prefs.getString(key);
if (jsonStr == null) return null;
List<dynamic> list = jsonDecode(jsonStr);
return list.map((e) => e as Map<String, dynamic>).toList();
}
JSON处理需要注意:
- 使用jsonEncode/jsonDecode进行序列化和反序列化
- 类型转换确保数据结构正确
- 处理可能的null值情况
3. 游戏数据管理
3.1 游戏存档服务设计
数独游戏需要保存以下状态信息:
- 当前棋盘状态
- 正确答案
- 固定数字位置
- 玩家笔记
- 游戏难度
- 已用时间
- 使用提示次数
- 当前选中单元格
我们创建GameSaveService专门处理这些数据的保存和加载:
dart复制class GameSaveService {
static const String _saveKey = 'currentGame';
static Future<void> saveGame(GameController controller) async {
Map<String, dynamic> data = {
'board': controller.board,
'solution': controller.solution,
'isFixed': controller.isFixed,
'notes': controller.notes.map((row) =>
row.map((set) => set.toList()).toList()
).toList(),
'difficulty': controller.difficulty,
'elapsedSeconds': controller.elapsedSeconds,
'hintsUsed': controller.hintsUsed,
'selectedRow': controller.selectedRow,
'selectedCol': controller.selectedCol,
'savedAt': DateTime.now().toIso8601String(),
};
await StorageService.setJson(_saveKey, data);
}
}
特别需要注意的是notes的处理,因为Set不能直接序列化为JSON,需要先转换为List。
3.2 游戏状态恢复
加载游戏状态时需要进行反向操作:
dart复制static Future<bool> loadGame(GameController controller) async {
Map<String, dynamic>? data = StorageService.getJson(_saveKey);
if (data == null) return false;
// 恢复棋盘状态
controller.board = (data['board'] as List)
.map((row) => (row as List).map((e) => e as int).toList())
.toList();
// 恢复答案和固定数字
controller.solution = (data['solution'] as List)
.map((row) => (row as List).map((e) => e as int).toList())
.toList();
controller.isFixed = (data['isFixed'] as List)
.map((row) => (row as List).map((e) => e as bool).toList())
.toList();
// 恢复笔记数据
controller.notes = (data['notes'] as List)
.map((row) => (row as List)
.map((set) => (set as List).map((e) => e as int).toSet())
.toList())
.toList();
// 恢复其他状态
controller.difficulty = data['difficulty'] as String;
controller.elapsedSeconds = data['elapsedSeconds'] as int;
controller.hintsUsed = data['hintsUsed'] as int;
controller.selectedRow = data['selectedRow'] as int;
controller.selectedCol = data['selectedCol'] as int;
controller.update();
return true;
}
类型转换在这里非常关键,因为JSON解析后所有类型都会变为基本类型,需要手动转换回我们需要的具体类型。
3.3 自动保存机制
为了防止数据丢失,我们实现自动保存功能:
dart复制class AutoSaveService {
static Timer? _autoSaveTimer;
static void startAutoSave(GameController controller) {
_autoSaveTimer?.cancel();
_autoSaveTimer = Timer.periodic(
const Duration(seconds: 30),
(_) => GameSaveService.saveGame(controller),
);
}
static void stopAutoSave() {
_autoSaveTimer?.cancel();
_autoSaveTimer = null;
}
}
这个实现有几个优点:
- 使用Timer.periodic创建周期性任务
- 在启动新定时器前取消旧的,避免重复保存
- 提供明确的停止方法释放资源
4. 统计数据和用户设置
4.1 游戏统计模型
数独游戏通常需要跟踪以下统计数据:
- 总游戏次数
- 胜利次数
- 当前连胜
- 最佳连胜
- 按难度分类的统计
- 最佳时间记录
我们定义GameStats类来表示这些数据:
dart复制class GameStats {
int totalGames;
int gamesWon;
int currentStreak;
int bestStreak;
Map<String, int> gamesByDifficulty;
Map<String, int> winsByDifficulty;
Map<String, int> bestTimeByDifficulty;
GameStats({
this.totalGames = 0,
this.gamesWon = 0,
this.currentStreak = 0,
this.bestStreak = 0,
Map<String, int>? gamesByDifficulty,
Map<String, int>? winsByDifficulty,
Map<String, int>? bestTimeByDifficulty,
}) : gamesByDifficulty = gamesByDifficulty ?? {},
winsByDifficulty = winsByDifficulty ?? {},
bestTimeByDifficulty = bestTimeByDifficulty ?? {};
// 序列化方法
Map<String, dynamic> toJson() => {
'totalGames': totalGames,
'gamesWon': gamesWon,
'currentStreak': currentStreak,
'bestStreak': bestStreak,
'gamesByDifficulty': gamesByDifficulty,
'winsByDifficulty': winsByDifficulty,
'bestTimeByDifficulty': bestTimeByDifficulty,
};
// 反序列化工厂方法
factory GameStats.fromJson(Map<String, dynamic> json) => GameStats(
totalGames: json['totalGames'] ?? 0,
gamesWon: json['gamesWon'] ?? 0,
currentStreak: json['currentStreak'] ?? 0,
bestStreak: json['bestStreak'] ?? 0,
gamesByDifficulty: Map<String, int>.from(json['gamesByDifficulty'] ?? {}),
winsByDifficulty: Map<String, int>.from(json['winsByDifficulty'] ?? {}),
bestTimeByDifficulty: Map<String, int>.from(json['bestTimeByDifficulty'] ?? {}),
);
}
4.2 用户设置模型
用户设置通常包括:
- 是否显示计时器
- 是否自动检查错误
- 音效开关
- 震动反馈开关
- 暗黑模式
- 主题选择
对应的模型类如下:
dart复制class UserSettings {
bool showTimer;
bool autoCheckErrors;
bool soundEnabled;
bool vibrationEnabled;
bool darkMode;
String theme;
UserSettings({
this.showTimer = true,
this.autoCheckErrors = true,
this.soundEnabled = true,
this.vibrationEnabled = true,
this.darkMode = false,
this.theme = 'classic',
});
Map<String, dynamic> toJson() => {
'showTimer': showTimer,
'autoCheckErrors': autoCheckErrors,
'soundEnabled': soundEnabled,
'vibrationEnabled': vibrationEnabled,
'darkMode': darkMode,
'theme': theme,
};
factory UserSettings.fromJson(Map<String, dynamic> json) => UserSettings(
showTimer: json['showTimer'] ?? true,
autoCheckErrors: json['autoCheckErrors'] ?? true,
soundEnabled: json['soundEnabled'] ?? true,
vibrationEnabled: json['vibrationEnabled'] ?? true,
darkMode: json['darkMode'] ?? false,
theme: json['theme'] ?? 'classic',
);
}
4.3 统计和设置服务
为了管理统计数据和用户设置,我们创建专门的服务类:
dart复制class StatsService {
static const String _statsKey = 'gameStats';
static Future<void> saveStats(GameStats stats) async {
await StorageService.setJson(_statsKey, stats.toJson());
}
static GameStats loadStats() {
Map<String, dynamic>? data = StorageService.getJson(_statsKey);
if (data == null) return GameStats();
return GameStats.fromJson(data);
}
}
class SettingsService {
static const String _settingsKey = 'userSettings';
static Future<void> saveSettings(UserSettings settings) async {
await StorageService.setJson(_settingsKey, settings.toJson());
}
static UserSettings loadSettings() {
Map<String, dynamic>? data = StorageService.getJson(_settingsKey);
if (data == null) return UserSettings();
return UserSettings.fromJson(data);
}
}
这种分层设计使得代码结构清晰,各类数据独立管理,便于维护和扩展。
5. 高级数据管理功能
5.1 数据迁移方案
当应用更新导致数据结构变化时,我们需要数据迁移机制:
dart复制class DataMigration {
static const int currentVersion = 2;
static Future<void> migrate() async {
int storedVersion = StorageService.getInt('dataVersion', defaultValue: 1);
if (storedVersion < 2) {
await _migrateToV2();
}
await StorageService.setInt('dataVersion', currentVersion);
}
static Future<void> _migrateToV2() async {
Map<String, dynamic>? oldStats = StorageService.getJson('stats');
if (oldStats != null) {
Map<String, dynamic> newStats = {
'totalGames': oldStats['games'] ?? 0,
'gamesWon': oldStats['wins'] ?? 0,
'currentStreak': 0,
'bestStreak': 0,
'gamesByDifficulty': {},
'winsByDifficulty': {},
'bestTimeByDifficulty': {},
};
await StorageService.setJson('gameStats', newStats);
await StorageService.prefs.remove('stats');
}
}
}
关键点:
- 使用版本号标识数据结构
- 按需执行迁移脚本
- 更新后设置新版本号
- 清理旧格式数据
5.2 数据备份与恢复
实现数据备份功能可以让用户在不同设备间迁移数据:
dart复制class BackupService {
static Future<String> createBackup() async {
Map<String, dynamic> backup = {
'version': DataMigration.currentVersion,
'timestamp': DateTime.now().toIso8601String(),
'stats': StorageService.getJson('gameStats'),
'settings': StorageService.getJson('userSettings'),
'currentGame': StorageService.getJson('currentGame'),
};
return jsonEncode(backup);
}
static Future<bool> restoreBackup(String backupJson) async {
try {
Map<String, dynamic> backup = jsonDecode(backupJson);
if (backup['stats'] != null) {
await StorageService.setJson('gameStats', backup['stats']);
}
if (backup['settings'] != null) {
await StorageService.setJson('userSettings', backup['settings']);
}
if (backup['currentGame'] != null) {
await StorageService.setJson('currentGame', backup['currentGame']);
}
return true;
} catch (e) {
return false;
}
}
}
5.3 数据完整性检查
为了防止损坏的数据导致应用崩溃,我们需要验证数据完整性:
dart复制class DataIntegrityService {
static bool validateGameSave(Map<String, dynamic>? data) {
if (data == null) return false;
// 检查必需字段
List<String> requiredFields = [
'board', 'solution', 'isFixed', 'notes',
'difficulty', 'elapsedSeconds'
];
for (String field in requiredFields) {
if (!data.containsKey(field)) return false;
}
// 检查棋盘结构
List<dynamic>? board = data['board'];
if (board == null || board.length != 9) return false;
for (var row in board) {
if (row is! List || row.length != 9) return false;
}
return true;
}
static Future<void> repairCorruptedData() async {
Map<String, dynamic>? gameSave = StorageService.getJson('currentGame');
if (!validateGameSave(gameSave)) {
await StorageService.prefs.remove('currentGame');
}
}
}
6. 性能优化技巧
6.1 并行数据加载
应用启动时并行加载各类数据可以显著减少启动时间:
dart复制class DataLoader {
static Future<void> preloadData() async {
await Future.wait([
_loadStats(),
_loadSettings(),
_loadGameProgress(),
]);
}
static Future<GameStats> _loadStats() async {
return StatsService.loadStats();
}
static Future<UserSettings> _loadSettings() async {
return SettingsService.loadSettings();
}
static Future<bool> _loadGameProgress() async {
GameController controller = Get.find<GameController>();
return GameSaveService.loadGame(controller);
}
}
6.2 数据变更通知
使用Stream实现数据变更通知,让UI能够自动响应数据变化:
dart复制class DataChangeNotifier {
static final StreamController<String> _controller =
StreamController<String>.broadcast();
static Stream<String> get onDataChanged => _controller.stream;
static void notifyChange(String dataType) {
_controller.add(dataType);
}
static void dispose() {
_controller.close();
}
}
// 使用示例
class StorageServiceWithNotification {
static Future<void> setJson(String key, Map<String, dynamic> value) async {
await StorageService.setJson(key, value);
DataChangeNotifier.notifyChange(key);
}
}
7. 实际应用中的经验分享
在实现数独游戏的数据持久化过程中,我总结了以下几点经验:
-
类型安全至关重要:JSON序列化会丢失类型信息,恢复时必须进行显式类型转换。我建议为每个数据模型编写严格的fromJson方法,确保类型安全。
-
版本迁移要考虑周全:数据格式变更时,迁移脚本需要处理各种边界情况。在实际项目中,我遇到过用户跳过多个版本直接升级的情况,因此迁移脚本需要能够处理所有历史版本的数据格式。
-
自动保存频率要合理:最初我设置为每10秒自动保存一次,但在性能较差的设备上会导致卡顿。经过测试,30秒是一个比较平衡的间隔,既能防止数据丢失过多,又不会影响游戏流畅度。
-
数据验证不可少:曾经有用户反映游戏存档损坏导致崩溃。加入数据完整性检查后,这类问题可以通过自动修复机制处理,大大提升了应用的健壮性。
-
内存管理要注意:使用StreamController时,务必记得在适当的时候调用close(),否则可能导致内存泄漏。我在应用的dispose方法中统一释放所有资源。
-
测试要充分:数据持久化相关的bug往往在特定条件下才会出现,比如:
- 应用在保存过程中被强制关闭
- 设备存储空间不足
- 不同版本间的数据迁移
- 特殊字符和边界值处理
建议针对这些场景编写专门的测试用例。