chore: integrate Hive for local storage management
- Added `hive_ce` and `hive_ce_flutter` dependencies for enhanced local storage capabilities. - Refactored the main application to initialize Hive and migrate existing data. - Updated storage service implementations to utilize Hive for managing application settings and task queues. - Removed the deprecated `StorageService` class to streamline the codebase and improve maintainability.
This commit is contained in:
46
lib/core/persistence/hive_bootstrap.dart
Normal file
46
lib/core/persistence/hive_bootstrap.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
|
||||
import 'hive_boxes.dart';
|
||||
|
||||
/// Sets up Hive and exposes lazily opened boxes used across the app.
|
||||
class HiveBootstrap {
|
||||
HiveBootstrap._();
|
||||
|
||||
static final HiveBootstrap instance = HiveBootstrap._();
|
||||
|
||||
HiveBoxes? _boxes;
|
||||
|
||||
/// Ensures Hive is initialized and all required boxes are open.
|
||||
Future<HiveBoxes> ensureInitialized() async {
|
||||
if (_boxes != null) {
|
||||
return _boxes!;
|
||||
}
|
||||
|
||||
await Hive.initFlutter('conduit_hive');
|
||||
|
||||
final preferences = await Hive.openBox<dynamic>(HiveBoxNames.preferences);
|
||||
final caches = await Hive.openBox<dynamic>(HiveBoxNames.caches);
|
||||
final attachmentQueue = await Hive.openBox<dynamic>(
|
||||
HiveBoxNames.attachmentQueue,
|
||||
);
|
||||
final metadata = await Hive.openBox<dynamic>(HiveBoxNames.metadata);
|
||||
|
||||
_boxes = HiveBoxes(
|
||||
preferences: preferences,
|
||||
caches: caches,
|
||||
attachmentQueue: attachmentQueue,
|
||||
metadata: metadata,
|
||||
);
|
||||
|
||||
return _boxes!;
|
||||
}
|
||||
|
||||
/// Access the cached boxes after [ensureInitialized] has completed.
|
||||
HiveBoxes get boxes {
|
||||
final cached = _boxes;
|
||||
if (cached == null) {
|
||||
throw StateError('HiveBootstrap.ensureInitialized must run first.');
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
35
lib/core/persistence/hive_boxes.dart
Normal file
35
lib/core/persistence/hive_boxes.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
/// Logical names for Hive boxes used by the app.
|
||||
final class HiveBoxNames {
|
||||
static const String preferences = 'preferences_v1';
|
||||
static const String caches = 'caches_v1';
|
||||
static const String attachmentQueue = 'attachment_queue_v1';
|
||||
static const String metadata = 'metadata_v1';
|
||||
}
|
||||
|
||||
/// Well-known keys stored inside Hive boxes.
|
||||
final class HiveStoreKeys {
|
||||
// Metadata
|
||||
static const String migrationVersion = 'migration_version';
|
||||
|
||||
// Cache entries
|
||||
static const String localConversations = 'local_conversations';
|
||||
static const String attachmentQueueEntries = 'attachment_queue_entries';
|
||||
static const String taskQueue = 'outbound_task_queue_v1';
|
||||
}
|
||||
|
||||
/// Grouped Hive boxes that remain open for the app lifecycle.
|
||||
class HiveBoxes {
|
||||
HiveBoxes({
|
||||
required this.preferences,
|
||||
required this.caches,
|
||||
required this.attachmentQueue,
|
||||
required this.metadata,
|
||||
});
|
||||
|
||||
final Box<dynamic> preferences;
|
||||
final Box<dynamic> caches;
|
||||
final Box<dynamic> attachmentQueue;
|
||||
final Box<dynamic> metadata;
|
||||
}
|
||||
29
lib/core/persistence/persistence_keys.dart
Normal file
29
lib/core/persistence/persistence_keys.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
/// Keys previously stored in SharedPreferences. Centralized so Hive-based
|
||||
/// storage and migration logic stay aligned.
|
||||
final class PreferenceKeys {
|
||||
static const String reduceMotion = 'reduce_motion';
|
||||
static const String animationSpeed = 'animation_speed';
|
||||
static const String hapticFeedback = 'haptic_feedback';
|
||||
static const String highContrast = 'high_contrast';
|
||||
static const String largeText = 'large_text';
|
||||
static const String darkMode = 'dark_mode';
|
||||
static const String defaultModel = 'default_model';
|
||||
static const String omitProviderInModelName = 'omit_provider_in_model_name';
|
||||
static const String voiceLocaleId = 'voice_locale_id';
|
||||
static const String voiceHoldToTalk = 'voice_hold_to_talk';
|
||||
static const String voiceAutoSendFinal = 'voice_auto_send_final';
|
||||
static const String socketTransportMode = 'socket_transport_mode';
|
||||
static const String quickPills = 'quick_pills';
|
||||
static const String sendOnEnterKey = 'send_on_enter';
|
||||
static const String rememberCredentials = 'remember_credentials';
|
||||
static const String activeServerId = 'active_server_id';
|
||||
static const String themeMode = 'theme_mode';
|
||||
static const String localeCode = 'locale_code_v1';
|
||||
static const String onboardingSeen = 'onboarding_seen_v1';
|
||||
static const String reviewerMode = 'reviewer_mode_v1';
|
||||
}
|
||||
|
||||
final class LegacyPreferenceKeys {
|
||||
static const String attachmentUploadQueue = 'attachment_upload_queue';
|
||||
static const String taskQueue = 'outbound_task_queue_v1';
|
||||
}
|
||||
210
lib/core/persistence/persistence_migrator.dart
Normal file
210
lib/core/persistence/persistence_migrator.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../utils/debug_logger.dart';
|
||||
import 'hive_boxes.dart';
|
||||
import 'persistence_keys.dart';
|
||||
|
||||
/// Handles one-time migration from SharedPreferences to Hive-backed storage.
|
||||
class PersistenceMigrator {
|
||||
PersistenceMigrator({required HiveBoxes hiveBoxes}) : _boxes = hiveBoxes;
|
||||
|
||||
static const int _targetVersion = 1;
|
||||
|
||||
final HiveBoxes _boxes;
|
||||
|
||||
Future<void> migrateIfNeeded() async {
|
||||
final currentVersion =
|
||||
_boxes.metadata.get(HiveStoreKeys.migrationVersion) as int?;
|
||||
if (currentVersion != null && currentVersion >= _targetVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
DebugLogger.log(
|
||||
'Starting SharedPreferences → Hive migration',
|
||||
scope: 'persistence/migration',
|
||||
);
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await _migratePreferences(prefs);
|
||||
await _migrateCaches(prefs);
|
||||
await _migrateAttachmentQueue(prefs);
|
||||
await _migrateTaskQueue(prefs);
|
||||
|
||||
await _boxes.metadata.put(HiveStoreKeys.migrationVersion, _targetVersion);
|
||||
|
||||
await _cleanupLegacyKeys(prefs);
|
||||
DebugLogger.log('Migration completed', scope: 'persistence/migration');
|
||||
} catch (error, stack) {
|
||||
DebugLogger.error(
|
||||
'Migration failed',
|
||||
scope: 'persistence/migration',
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _migratePreferences(SharedPreferences prefs) async {
|
||||
final updates = <String, Object?>{};
|
||||
|
||||
void copyBool(String key) {
|
||||
final value = prefs.getBool(key);
|
||||
if (value != null) updates[key] = value;
|
||||
}
|
||||
|
||||
void copyDouble(String key) {
|
||||
final value = prefs.getDouble(key);
|
||||
if (value != null) updates[key] = value;
|
||||
}
|
||||
|
||||
void copyString(String key) {
|
||||
final value = prefs.getString(key);
|
||||
if (value != null && value.isNotEmpty) updates[key] = value;
|
||||
}
|
||||
|
||||
void copyStringList(String key) {
|
||||
final value = prefs.getStringList(key);
|
||||
if (value != null && value.isNotEmpty) {
|
||||
updates[key] = List<String>.from(value);
|
||||
}
|
||||
}
|
||||
|
||||
copyBool(PreferenceKeys.reduceMotion);
|
||||
copyDouble(PreferenceKeys.animationSpeed);
|
||||
copyBool(PreferenceKeys.hapticFeedback);
|
||||
copyBool(PreferenceKeys.highContrast);
|
||||
copyBool(PreferenceKeys.largeText);
|
||||
copyBool(PreferenceKeys.darkMode);
|
||||
copyString(PreferenceKeys.defaultModel);
|
||||
copyBool(PreferenceKeys.omitProviderInModelName);
|
||||
copyString(PreferenceKeys.voiceLocaleId);
|
||||
copyBool(PreferenceKeys.voiceHoldToTalk);
|
||||
copyBool(PreferenceKeys.voiceAutoSendFinal);
|
||||
copyString(PreferenceKeys.socketTransportMode);
|
||||
copyStringList(PreferenceKeys.quickPills);
|
||||
copyBool(PreferenceKeys.sendOnEnterKey);
|
||||
copyBool(PreferenceKeys.rememberCredentials);
|
||||
copyString(PreferenceKeys.activeServerId);
|
||||
copyString(PreferenceKeys.themeMode);
|
||||
copyString(PreferenceKeys.localeCode);
|
||||
copyBool(PreferenceKeys.onboardingSeen);
|
||||
copyBool(PreferenceKeys.reviewerMode);
|
||||
|
||||
if (updates.isNotEmpty) {
|
||||
await _boxes.preferences.putAll(updates);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _migrateCaches(SharedPreferences prefs) async {
|
||||
final jsonString = prefs.getString(HiveStoreKeys.localConversations);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is List) {
|
||||
final list = decoded
|
||||
.map((entry) => Map<String, dynamic>.from(entry as Map))
|
||||
.toList(growable: false);
|
||||
await _boxes.caches.put(HiveStoreKeys.localConversations, list);
|
||||
}
|
||||
} catch (error, stack) {
|
||||
DebugLogger.error(
|
||||
'Failed to migrate local conversations',
|
||||
scope: 'persistence/migration',
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _migrateAttachmentQueue(SharedPreferences prefs) async {
|
||||
final jsonString = prefs.getString(
|
||||
LegacyPreferenceKeys.attachmentUploadQueue,
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is List) {
|
||||
final list = decoded
|
||||
.map((entry) => Map<String, dynamic>.from(entry as Map))
|
||||
.toList(growable: false);
|
||||
await _boxes.attachmentQueue.put(
|
||||
HiveStoreKeys.attachmentQueueEntries,
|
||||
list,
|
||||
);
|
||||
}
|
||||
} catch (error, stack) {
|
||||
DebugLogger.error(
|
||||
'Failed to migrate attachment queue',
|
||||
scope: 'persistence/migration',
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _migrateTaskQueue(SharedPreferences prefs) async {
|
||||
final jsonString = prefs.getString(LegacyPreferenceKeys.taskQueue);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is List) {
|
||||
final list = decoded
|
||||
.map((entry) => Map<String, dynamic>.from(entry as Map))
|
||||
.toList(growable: false);
|
||||
await _boxes.caches.put(HiveStoreKeys.taskQueue, list);
|
||||
}
|
||||
} catch (error, stack) {
|
||||
DebugLogger.error(
|
||||
'Failed to migrate outbound task queue',
|
||||
scope: 'persistence/migration',
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cleanupLegacyKeys(SharedPreferences prefs) async {
|
||||
final keysToRemove = <String>[
|
||||
PreferenceKeys.reduceMotion,
|
||||
PreferenceKeys.animationSpeed,
|
||||
PreferenceKeys.hapticFeedback,
|
||||
PreferenceKeys.highContrast,
|
||||
PreferenceKeys.largeText,
|
||||
PreferenceKeys.darkMode,
|
||||
PreferenceKeys.defaultModel,
|
||||
PreferenceKeys.omitProviderInModelName,
|
||||
PreferenceKeys.voiceLocaleId,
|
||||
PreferenceKeys.voiceHoldToTalk,
|
||||
PreferenceKeys.voiceAutoSendFinal,
|
||||
PreferenceKeys.socketTransportMode,
|
||||
PreferenceKeys.quickPills,
|
||||
PreferenceKeys.sendOnEnterKey,
|
||||
PreferenceKeys.rememberCredentials,
|
||||
PreferenceKeys.activeServerId,
|
||||
PreferenceKeys.themeMode,
|
||||
PreferenceKeys.localeCode,
|
||||
PreferenceKeys.onboardingSeen,
|
||||
PreferenceKeys.reviewerMode,
|
||||
HiveStoreKeys.localConversations,
|
||||
HiveStoreKeys.attachmentQueueEntries,
|
||||
LegacyPreferenceKeys.attachmentUploadQueue,
|
||||
LegacyPreferenceKeys.taskQueue,
|
||||
];
|
||||
|
||||
for (final key in keysToRemove) {
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
lib/core/persistence/persistence_providers.dart
Normal file
10
lib/core/persistence/persistence_providers.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'hive_boxes.dart';
|
||||
|
||||
part 'persistence_providers.g.dart';
|
||||
|
||||
/// Provides access to eagerly opened Hive boxes. Must be overridden in [main].
|
||||
@Riverpod(keepAlive: true)
|
||||
HiveBoxes hiveBoxes(Ref ref) =>
|
||||
throw UnimplementedError('Hive boxes must be provided during bootstrap.');
|
||||
Reference in New Issue
Block a user