-
Notifications
You must be signed in to change notification settings - Fork 0
Dev Storage Hive Sync
Local-first persistence with optional Supabase-based cloud sync (TankSync).
lib/core/storage/hive_boxes.dart
Eight boxes total. Six are AES-encrypted with a key derived from FlutterSecureStorage; two non-PII boxes are unencrypted.
| Box | Encrypted | Contents | Access class |
|---|---|---|---|
settings |
✅ | App config, locale, units, setup state, secure-storage migration flags | SettingsHiveStore |
profiles |
✅ | User profiles (country, fuel preference, radius, landing, vehicle ref) | ProfilesHiveStore |
favorites |
✅ | Favorite IDs + cached station data + EV favorites | FavoritesHiveStore |
cache |
✅ | API response cache, itineraries | CacheHiveStore |
priceHistory |
✅ | 30-day price records per station | PriceHistoryHiveStore |
alerts |
✅ | Price alert rules | AlertsHiveStore |
obd2Baselines |
— | Per-vehicle consumption baselines (#769) | — |
obd2TripHistory |
— | OBD2 trip logs (#726) | — |
lib/core/storage/storage_keys.dart holds every string key as a constant: StorageKeys.favoriteStationIds, StorageKeys.activeProfileId, StorageKeys.supabaseAnonKey, etc. A pinning test (test/core/storage/storage_keys_uniqueness_test.dart) guarantees uniqueness and snake_case.
Hive round-trips nested maps as Map<dynamic, dynamic>, but freezed's generated fromJson expects Map<String, dynamic>. Solution: HiveBoxes.toStringDynamicMap() deep-converts at every read (hive_boxes.dart:152–177). Every *HiveStore applies this before calling fromJson.
Three migrations live in HiveBoxes._migrate*:
-
Encrypt-in-place — on first init, unencrypted boxes are copied to encrypted and the plaintext box is deleted (
hive_boxes.dart:57–84). -
Legacy landing screen enum — rewrites
'search'→'nearest'for profiles saved before v4.2.0 (profile_repository.dart:40–46). -
Supabase anon key — moves the legacy plain-Hive key to secure storage on first TankSync load (
settings_hive_store.dart:98–112).
Migrations are idempotent and log via debugPrint.
lib/core/storage/secure_storage.dart
Wraps FlutterSecureStorage:
- Android → Android Keystore (hardware-backed where available)
- iOS → Keychain (not yet shipped)
- Windows → DPAPI
Stored here: Germany API key, OpenChargeMap API key, Supabase URL, Supabase anon key. Never logged, never sent to remote, never written to Hive plain.
lib/features/profile/data/repositories/profile_repository.dart
CRUD: createProfile, updateProfile, deleteProfile, getActiveProfile, getAllProfiles, setActiveProfile. First profile is auto-activated; deleting the active profile reassigns to the first remaining one.
UserProfile (freezed, lib/features/profile/data/models/user_profile.dart:32–78):
-
Core:
id,name,preferredFuelType -
Search:
defaultSearchRadius,homeZipCode,countryCode,languageCode -
UI:
landingScreen(enum: favorites/map/cheapest/nearest),routeSegmentKm,avoidHighways -
Fuel/EV:
showFuel,showElectric,hybridFuelChoice,defaultVehicleId -
Rating:
ratingMode(local/private/shared) -
Filtering:
preferredAmenities -
Consumption:
showConsumptionTab
activeProfileProvider is keepAlive: true; changes cascade to every feature that watches it.
lib/core/sync/supabase_client.dart, lib/core/sync/sync_service.dart
Optional. Disabled by default. When enabled:
- Anonymous — UUID-only session, no email. Ensures
public.usersrow exists (FK compliance). - Email — optional, for multi-device linking.
TankSyncClient.init(url, anonKey):
- Sanitises URL (trim, strip trailing slash).
- Validates format.
- Idempotent (safe to call on every app start).
- Stores the anon key in secure storage.
| Object | Provider | Conflict strategy |
|---|---|---|
| Favorites | syncFavorites() |
Union: upload local-only, return server ∪ local |
| Ignored stations | syncIgnoredStations() |
Union |
| Ratings | syncRating(rating) |
Upsert; shared flag controls row visibility |
| Alerts | syncAlerts() |
Union |
| Profiles | via syncProfiles()
|
Last-write-wins per profile |
| Trajets (OBD2 + GPS) | itineraries_sync.dart |
Opt-in even after TankSync is enabled; off by default. Settings → TankSync → Sync trajets. New trips sync from the moment the toggle is on; a manual Backfill action pushes pre-existing trips. Forget all synced trajets scrubs just the trip rows server-side without disabling sync (#2055-era). |
Local-first always. The server never silently overwrites local data. Only an explicit user delete triggers a server delete (see SyncAfterChangeMixin).
- Initial sync after
TankSyncClient.init()succeeds (non-blocking). -
SyncAfterChangeMixintriggers a sync after any favorite/alert mutation. - Manual sync button in Settings → TankSync.
- No periodic sync — it's event-driven.
-
Community instance — pre-configured in
CommunityConfig; user taps Enable. - Self-hosted — user pastes URL + key from QR or text.
All tables enforce user_id = auth.uid() in RLS policies. The Supabase schema + RLS is in supabase/migrations/*.sql.
lib/core/background/
AndroidBackgroundPriceFetcher registers two WorkManager periodic tasks:
priceRefreshTask — frequency 1 h
constraints: NetworkType.connected, requiresBatteryNotLow: true
priceRefreshChargingTask — frequency 30 min
constraints: NetworkType.connected, requiresCharging: true
- Fetch live prices for all favorite + alert stations (batch API).
- Record into
PriceHistoryHiveStorewith 60-min dedup + 30-day retention trim. - Update cached station data.
- Evaluate alert thresholds →
LocalNotificationService.show()if matched.
Background tasks run in a separate Dart isolate:
- No Riverpod (provider tree lives in the main isolate only).
- Dio initialised inline.
-
HiveStorage.initInIsolate()re-opens the same encrypted boxes. -
HiveIsolateLockis a file-based lock that prevents concurrent Hive access with the main isolate. - Boxes closed at task end via
HiveBoxes.closeIsolateBoxes().
- Caching Strategy — the cache box is one of the Hive stores
- Fuzzy Logic Price Predictions — how PriceHistoryHiveStore feeds predictions
👤 User Guide
🛠️ Developer Guide
Architecture
Code patterns
Quality
Deep dives
Reference
Workflow