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:
cogwheel0
2025-10-01 16:55:44 +05:30
parent 7d17c97d7e
commit 80129c5711
14 changed files with 723 additions and 741 deletions

View 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;
}
}

View 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;
}

View 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';
}

View 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);
}
}
}

View 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.');