在这里插入图片描述

前言

数据持久化是移动应用开发中的核心技术之一,它确保用户数据在应用重启后仍然可用。本文将基于我们的三国杀攻略App项目,详细介绍如何实现完整的数据持久化方案,包括用户设置、战绩记录、收藏数据等多种存储需求。

数据存储需求分析

应用数据分类

在我们的三国杀攻略App中,需要持久化的数据主要包括:

  • 用户设置:主题模式、语言设置、通知开关
  • 战绩记录:游戏胜负记录、积分统计
  • 收藏数据:收藏的武将、攻略文章
  • 缓存数据:武将图片、网络请求缓存
  • 用户状态:登录状态、个人信息

SharedPreferences 轻量存储

基础配置存储

import 'package:shared_preferences/shared_preferences.dart';

class PreferencesManager {
  static SharedPreferences? _prefs;
  
  static Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }
  
  // 主题设置
  static bool get isDarkMode => _prefs?.getBool('dark_mode') ?? false;
  static Future<void> setDarkMode(bool value) async {
    await _prefs?.setBool('dark_mode', value);
  }
  
  // 语言设置
  static String get language => _prefs?.getString('language') ?? 'zh_CN';
  static Future<void> setLanguage(String value) async {
    await _prefs?.setString('language', value);
  }
}

SharedPreferences 是存储简单键值对数据的最佳选择。这里我们创建了一个管理类来统一处理应用设置。init() 方法在应用启动时调用,确保 SharedPreferences 实例可用。使用 静态方法 让全局访问更加便捷。

用户状态管理

class UserPreferences {
  static const String _keyUserToken = 'user_token';
  static const String _keyUserInfo = 'user_info';
  static const String _keyLoginTime = 'login_time';
  
  // 保存用户登录信息
  static Future<void> saveUserLogin(String token, Map<String, dynamic> userInfo) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyUserToken, token);
    await prefs.setString(_keyUserInfo, json.encode(userInfo));
    await prefs.setInt(_keyLoginTime, DateTime.now().millisecondsSinceEpoch);
  }
  
  // 获取用户令牌
  static Future<String?> getUserToken() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_keyUserToken);
  }
  
  // 检查登录状态
  static Future<bool> isLoggedIn() async {
    final token = await getUserToken();
    if (token == null) return false;
    
    // 检查令牌是否过期(7天)
    final prefs = await SharedPreferences.getInstance();
    final loginTime = prefs.getInt(_keyLoginTime) ?? 0;
    final now = DateTime.now().millisecondsSinceEpoch;
    final daysPassed = (now - loginTime) / (1000 * 60 * 60 * 24);
    
    return daysPassed < 7;
  }
}

用户状态管理需要考虑 安全性和时效性。这里我们不仅存储了用户令牌和信息,还记录了登录时间用于检查令牌是否过期。使用 json.encode 将复杂对象转换为字符串存储。

SQLite 数据库存储

数据库初始化

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static Database? _database;
  static const String _databaseName = 'sanguosha_app.db';
  static const int _databaseVersion = 1;
  
  static Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }
  
  static Future<Database> _initDatabase() async {
    String path = join(await getDatabasesPath(), _databaseName);
    return await openDatabase(
      path,
      version: _databaseVersion,
      onCreate: _onCreate,
      onUpgrade: _onUpgrade,
    );
  }
  
  static Future<void> _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE game_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        date INTEGER NOT NULL,
        is_win INTEGER NOT NULL,
        identity TEXT NOT NULL,
        score INTEGER DEFAULT 0,
        duration INTEGER DEFAULT 0,
        created_at INTEGER NOT NULL
      )
    ''');
    
    await db.execute('''
      CREATE TABLE favorite_heroes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        hero_id TEXT NOT NULL UNIQUE,
        hero_name TEXT NOT NULL,
        country TEXT NOT NULL,
        added_at INTEGER NOT NULL
      )
    ''');
  }
}

SQLite 适合存储结构化的复杂数据。这里我们创建了游戏记录和收藏武将两个表。使用 INTEGER PRIMARY KEY AUTOINCREMENT 创建自增主键,UNIQUE 约束防止重复收藏。onCreate 方法在数据库首次创建时执行。

战绩记录管理

class GameRecordDao {
  static Future<int> insertRecord(GameRecord record) async {
    final db = await DatabaseHelper.database;
    return await db.insert('game_records', record.toMap());
  }
  
  static Future<List<GameRecord>> getAllRecords() async {
    final db = await DatabaseHelper.database;
    final List<Map<String, dynamic>> maps = await db.query(
      'game_records',
      orderBy: 'created_at DESC',
    );
    
    return List.generate(maps.length, (i) {
      return GameRecord.fromMap(maps[i]);
    });
  }
  
  static Future<Map<String, int>> getStatistics() async {
    final db = await DatabaseHelper.database;
    final List<Map<String, dynamic>> result = await db.rawQuery('''
      SELECT 
        COUNT(*) as total_games,
        SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as wins,
        AVG(score) as avg_score
      FROM game_records
    ''');
    
    final stats = result.first;
    return {
      'totalGames': stats['total_games'] ?? 0,
      'wins': stats['wins'] ?? 0,
      'avgScore': (stats['avg_score'] ?? 0.0).round(),
    };
  }
}

数据访问对象(DAO)模式将数据库操作封装成方法。insertRecord 插入新记录,getAllRecords 获取所有记录并按时间倒序排列。getStatistics 使用 SQL聚合函数 计算统计数据,这比在应用层计算更高效。

数据模型定义

class GameRecord {
  final int? id;
  final DateTime date;
  final bool isWin;
  final String identity;
  final int score;
  final int duration;
  final DateTime createdAt;
  
  GameRecord({
    this.id,
    required this.date,
    required this.isWin,
    required this.identity,
    this.score = 0,
    this.duration = 0,
    DateTime? createdAt,
  }) : createdAt = createdAt ?? DateTime.now();
  
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'date': date.millisecondsSinceEpoch,
      'is_win': isWin ? 1 : 0,
      'identity': identity,
      'score': score,
      'duration': duration,
      'created_at': createdAt.millisecondsSinceEpoch,
    };
  }
  
  factory GameRecord.fromMap(Map<String, dynamic> map) {
    return GameRecord(
      id: map['id'],
      date: DateTime.fromMillisecondsSinceEpoch(map['date']),
      isWin: map['is_win'] == 1,
      identity: map['identity'],
      score: map['score'] ?? 0,
      duration: map['duration'] ?? 0,
      createdAt: DateTime.fromMillisecondsSinceEpoch(map['created_at']),
    );
  }
}

数据模型类提供了 对象关系映射(ORM)功能。toMap() 方法将对象转换为数据库可存储的 Map,fromMap() 工厂构造函数从 Map 创建对象。注意 SQLite 不支持布尔类型,需要转换为整数存储。

文件存储系统

图片缓存管理

import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';

class ImageCacheManager {
  static String _getCacheKey(String url) {
    var bytes = utf8.encode(url);
    var digest = md5.convert(bytes);
    return digest.toString();
  }
  
  static Future<String> getCacheDir() async {
    final directory = await getApplicationDocumentsDirectory();
    final cacheDir = Directory('${directory.path}/image_cache');
    if (!await cacheDir.exists()) {
      await cacheDir.create(recursive: true);
    }
    return cacheDir.path;
  }
  
  static Future<File?> getCachedImage(String url) async {
    try {
      final cacheDir = await getCacheDir();
      final cacheKey = _getCacheKey(url);
      final file = File('$cacheDir/$cacheKey.jpg');
      
      if (await file.exists()) {
        // 检查文件是否过期(7天)
        final stat = await file.stat();
        final age = DateTime.now().difference(stat.modified).inDays;
        if (age < 7) {
          return file;
        } else {
          await file.delete();
        }
      }
      return null;
    } catch (e) {
      print('获取缓存图片失败: $e');
      return null;
    }
  }
  
  static Future<File> cacheImage(String url, List<int> bytes) async {
    final cacheDir = await getCacheDir();
    final cacheKey = _getCacheKey(url);
    final file = File('$cacheDir/$cacheKey.jpg');
    return await file.writeAsBytes(bytes);
  }
}

图片缓存使用 文件系统存储,通过 MD5 哈希生成唯一的缓存键。path_provider 包提供了获取应用文档目录的方法。缓存文件设置了 7天过期时间,避免占用过多存储空间。

数据导出功能

class DataExportManager {
  static Future<File> exportGameRecords() async {
    final records = await GameRecordDao.getAllRecords();
    final directory = await getApplicationDocumentsDirectory();
    final file = File('${directory.path}/game_records_export.json');
    
    final exportData = {
      'export_time': DateTime.now().toIso8601String(),
      'total_records': records.length,
      'records': records.map((r) => r.toMap()).toList(),
    };
    
    final jsonString = json.encode(exportData);
    return await file.writeAsString(jsonString);
  }
  
  static Future<void> importGameRecords(File file) async {
    try {
      final jsonString = await file.readAsString();
      final data = json.decode(jsonString);
      final recordsData = data['records'] as List;
      
      for (final recordData in recordsData) {
        final record = GameRecord.fromMap(recordData);
        await GameRecordDao.insertRecord(record);
      }
    } catch (e) {
      throw Exception('导入数据失败: $e');
    }
  }
}

数据导出功能将数据库数据转换为 JSON格式 保存到文件。导出文件包含时间戳和记录总数等元信息。导入功能支持从JSON文件恢复数据,实现了数据的备份和迁移。

状态管理集成

GetX 状态持久化

class SettingsController extends GetxController {
  final isDarkMode = false.obs;
  final language = 'zh_CN'.obs;
  final notificationEnabled = true.obs;
  
  
  void onInit() {
    super.onInit();
    loadSettings();
  }
  
  Future<void> loadSettings() async {
    isDarkMode.value = PreferencesManager.isDarkMode;
    language.value = PreferencesManager.language;
    notificationEnabled.value = PreferencesManager.notificationEnabled;
  }
  
  Future<void> toggleDarkMode() async {
    isDarkMode.value = !isDarkMode.value;
    await PreferencesManager.setDarkMode(isDarkMode.value);
    Get.changeTheme(isDarkMode.value ? ThemeData.dark() : ThemeData.light());
  }
  
  Future<void> setLanguage(String lang) async {
    language.value = lang;
    await PreferencesManager.setLanguage(lang);
    // 触发语言切换
    Get.updateLocale(Locale(lang.split('_')[0], lang.split('_')[1]));
  }
}

GetX 控制器 将响应式状态与持久化存储结合。状态变化时自动保存到本地,应用启动时从本地恢复状态。这种模式确保了状态的一致性和持久性。

战绩数据控制器

class GameRecordsController extends GetxController {
  final records = <GameRecord>[].obs;
  final statistics = <String, int>{}.obs;
  final isLoading = false.obs;
  
  
  void onInit() {
    super.onInit();
    loadRecords();
  }
  
  Future<void> loadRecords() async {
    isLoading.value = true;
    try {
      records.value = await GameRecordDao.getAllRecords();
      statistics.value = await GameRecordDao.getStatistics();
    } catch (e) {
      Get.snackbar('错误', '加载战绩失败: $e');
    } finally {
      isLoading.value = false;
    }
  }
  
  Future<void> addRecord(bool isWin, String identity) async {
    final record = GameRecord(
      date: DateTime.now(),
      isWin: isWin,
      identity: identity,
      score: isWin ? 100 : 50,
    );
    
    try {
      await GameRecordDao.insertRecord(record);
      await loadRecords(); // 重新加载数据
      Get.snackbar('成功', '战绩记录已保存');
    } catch (e) {
      Get.snackbar('错误', '保存战绩失败: $e');
    }
  }
}

战绩控制器管理游戏记录的增删改查操作。使用 响应式列表 让界面自动更新。错误处理通过 SnackBar 向用户反馈操作结果。

数据同步策略

云端同步实现

class DataSyncManager {
  static const String _lastSyncKey = 'last_sync_time';
  
  static Future<void> syncToCloud() async {
    try {
      final records = await GameRecordDao.getAllRecords();
      final settings = await _getLocalSettings();
      
      final syncData = {
        'records': records.map((r) => r.toMap()).toList(),
        'settings': settings,
        'sync_time': DateTime.now().toIso8601String(),
      };
      
      // 上传到云端(示例)
      final response = await http.post(
        Uri.parse('https://api.example.com/sync'),
        headers: {'Content-Type': 'application/json'},
        body: json.encode(syncData),
      );
      
      if (response.statusCode == 200) {
        await _updateLastSyncTime();
        Get.snackbar('成功', '数据已同步到云端');
      }
    } catch (e) {
      Get.snackbar('错误', '同步失败: $e');
    }
  }
  
  static Future<void> syncFromCloud() async {
    try {
      final response = await http.get(
        Uri.parse('https://api.example.com/sync'),
        headers: await _getAuthHeaders(),
      );
      
      if (response.statusCode == 200) {
        final data = json.decode(response.body);
        await _restoreFromSyncData(data);
        Get.snackbar('成功', '数据已从云端恢复');
      }
    } catch (e) {
      Get.snackbar('错误', '恢复数据失败: $e');
    }
  }
  
  static Future<bool> shouldAutoSync() async {
    final prefs = await SharedPreferences.getInstance();
    final lastSync = prefs.getInt(_lastSyncKey) ?? 0;
    final now = DateTime.now().millisecondsSinceEpoch;
    final hoursSinceSync = (now - lastSync) / (1000 * 60 * 60);
    
    return hoursSinceSync >= 24; // 24小时自动同步一次
  }
}

云端同步功能实现了数据的备份和多设备共享。使用 HTTP请求 与服务器通信,支持上传和下载数据。自动同步策略避免了频繁的网络请求。

数据迁移方案

版本升级处理

class DatabaseMigration {
  static Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
    if (oldVersion < 2) {
      // 版本1到版本2的升级
      await db.execute('ALTER TABLE game_records ADD COLUMN score INTEGER DEFAULT 0');
      await db.execute('ALTER TABLE game_records ADD COLUMN duration INTEGER DEFAULT 0');
    }
    
    if (oldVersion < 3) {
      // 版本2到版本3的升级
      await db.execute('''
        CREATE TABLE user_achievements (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          achievement_id TEXT NOT NULL UNIQUE,
          unlocked_at INTEGER NOT NULL
        )
      ''');
    }
  }
  
  static Future<void> migrateUserData() async {
    final prefs = await SharedPreferences.getInstance();
    final migrationVersion = prefs.getInt('migration_version') ?? 0;
    
    if (migrationVersion < 1) {
      // 迁移旧版本的设置数据
      await _migrateSettingsV1();
      await prefs.setInt('migration_version', 1);
    }
    
    if (migrationVersion < 2) {
      // 迁移战绩数据格式
      await _migrateRecordsV2();
      await prefs.setInt('migration_version', 2);
    }
  }
}

数据迁移确保应用升级时的数据兼容性。数据库版本控制 通过 onUpgrade 回调处理表结构变更。用户数据迁移处理设置和数据格式的变化。

性能优化策略

批量操作优化

class BatchOperations {
  static Future<void> batchInsertRecords(List<GameRecord> records) async {
    final db = await DatabaseHelper.database;
    final batch = db.batch();
    
    for (final record in records) {
      batch.insert('game_records', record.toMap());
    }
    
    await batch.commit(noResult: true);
  }
  
  static Future<void> cleanupOldData() async {
    final db = await DatabaseHelper.database;
    final thirtyDaysAgo = DateTime.now().subtract(Duration(days: 30));
    
    await db.delete(
      'game_records',
      where: 'created_at < ?',
      whereArgs: [thirtyDaysAgo.millisecondsSinceEpoch],
    );
  }
}

批量操作 提高了数据库操作的效率。使用 batch 对象可以将多个操作合并为一个事务。定期清理旧数据避免数据库过大影响性能。

缓存策略优化

class CacheManager {
  static final Map<String, dynamic> _memoryCache = {};
  static const int _maxCacheSize = 100;
  
  static T? get<T>(String key) {
    return _memoryCache[key] as T?;
  }
  
  static void set<T>(String key, T value) {
    if (_memoryCache.length >= _maxCacheSize) {
      // 移除最旧的缓存项
      final firstKey = _memoryCache.keys.first;
      _memoryCache.remove(firstKey);
    }
    _memoryCache[key] = value;
  }
  
  static void clear() {
    _memoryCache.clear();
  }
}

内存缓存 减少了重复的数据库查询。使用 LRU(最近最少使用)策略管理缓存大小,避免内存泄漏。

数据安全考虑

敏感数据加密

import 'package:crypto/crypto.dart';

class SecurityManager {
  static String _encryptionKey = 'your_secret_key_here';
  
  static String encryptData(String data) {
    var key = utf8.encode(_encryptionKey);
    var bytes = utf8.encode(data);
    var hmacSha256 = Hmac(sha256, key);
    var digest = hmacSha256.convert(bytes);
    return digest.toString();
  }
  
  static Future<void> saveSecureData(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    final encryptedValue = encryptData(value);
    await prefs.setString(key, encryptedValue);
  }
  
  static Future<bool> verifySecureData(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    final storedValue = prefs.getString(key);
    if (storedValue == null) return false;
    
    final encryptedValue = encryptData(value);
    return storedValue == encryptedValue;
  }
}

敏感数据需要 加密存储,这里使用 HMAC-SHA256 算法。虽然这不是完全的加密,但提供了数据完整性验证。对于更高安全要求,应使用专门的加密库。

总结

通过本文的详细实现,我们构建了一个完整的数据持久化系统。这个系统涵盖了轻量级的 SharedPreferences 存储、结构化的 SQLite 数据库、文件系统缓存、云端同步等多个层面。

核心技术要点

  • 使用 SharedPreferences 存储简单配置数据
  • 通过 SQLite 管理复杂的结构化数据
  • 实现文件系统缓存提升性能
  • 集成状态管理确保数据一致性
  • 提供数据迁移和同步机制
  • 考虑安全性和性能优化

这套数据持久化方案为应用提供了可靠的数据存储基础,确保用户数据的安全性和可用性。在下一篇文章中,我们将探讨性能优化与最佳实践,进一步提升应用的用户体验。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐