feat: add offline mode with Room DB for usage without registration - #49
feat: add offline mode with Room DB for usage without registration#49fkischewski99 wants to merge 9 commits into
Conversation
Users can now use the app without creating an account by clicking "App offline nutzen" on the login screen. Recipes, ingredients and materials are downloaded once from Firebase on first offline start, then stored locally in Room DB. - Add Room 2.8.4 with KSP for Android, Desktop and iOS - Annotate existing model classes as Room @entity (no duplication) - Add TypeConverters using existing kotlinx.serialization infrastructure - Implement RoomRepository with all 41 EventRepository methods - Add DelegatingRepository/DelegatingLoginAndRegister for runtime mode switching - Add AppModeHolder with StateFlow for reactive mode changes - Add platform-specific DatabaseBuilder (expect/actual) and AppModePreferences - Add SeedDataService for one-time recipe/ingredient download from Firebase - Add "App offline nutzen" button to login screen
- Add OfflineFirstRepository: reads from Firebase when online (caches in Room), falls back to Room when offline, queues writes for sync - Add NetworkMonitor (expect/actual) for Android, Desktop, iOS - Add PendingOperation table and SyncManager for queued write replay - Add ForeignKeys on Meal.eventId and ParticipantTime (CASCADE delete) - Remove @entity from ShoppingIngredient/RecipeSelection (stored as JSON) - Fix insert→update for existing entity updates in RoomRepository - Wire all dependencies through Koin (no manual instantiation) - Update login screen offline hint with internet requirement note
- Add AppDatabaseConstructor (expect object) required by Room KMP for non-Android platforms - Add Index on foreign key columns (eventId, participantRef) to prevent full table scans - Remove fallbackToDestructiveMigration to preserve data across restarts
- Add read caching to all OfflineFirstRepository methods so Room stays in sync with Firebase for offline fallback - Add InitialSyncService for full data sync on app start (events, participants, meals, recipes, ingredients, materials, shopping lists) - Extract writeToFirebaseOrQueue helper to reduce duplication - Use OFFLINE_FIRST for all authenticated users (ONLINE delegates to OfflineFirstRepository too) for automatic offline fallback - Switch mode back to ONLINE on logout so login uses Firebase auth
| @@ -0,0 +1,585 @@ | |||
| { | |||
There was a problem hiding this comment.
warum brauche ich mehrere schemata, ich habe ja noch keine migrationen. Bitte nutze version 1 mit dem aktuellen schema und lösche die anderen
| val prefs = remember { AppModePreferences() } | ||
| val appModeHolder = remember { AppModeHolder(prefs.getAppMode()) } | ||
|
|
||
| val appModeModule = remember { |
There was a problem hiding this comment.
bitte hier keine Modules anlegen, sondern nur unter dem richtigen ordner
| materials.filter { it.uid.isNotBlank() }.forEach { db.materialDao().insert(it) } | ||
| Logger.i("InitialSync: Synced ${materials.size} materials") | ||
|
|
||
| val events = firebaseRepository.getEventList(group).first() |
There was a problem hiding this comment.
wenn offline sollte hier vor returned werden oder macht das keinen unterschied?
| db.participantDao().update(participant) | ||
| } | ||
|
|
||
| override suspend fun deleteParticipant(participantId: String) { |
There was a problem hiding this comment.
can this be done threw a cascade delete?
| // --- Meals --- | ||
|
|
||
| override suspend fun getAllMealsOfEvent(eventId: String): List<Meal> { | ||
| val meals = db.mealDao().getByEventId(eventId) |
There was a problem hiding this comment.
is there also no sql way of doing it? with a join?
| ?: throw NoSuchElementException("Meal $mealId not found in event $eventId") | ||
|
|
||
| val recipeIds = meal.recipeSelections.map { it.recipeRef }.distinct() | ||
| if (recipeIds.isNotEmpty()) { |
There was a problem hiding this comment.
here there it should also be possible to join
| events.forEach { db.eventDao().insert(it) } | ||
| Logger.i("InitialSync: Synced ${events.size} events") | ||
|
|
||
| events.forEach { event -> |
There was a problem hiding this comment.
is there no better way doing this, using the repo directly??
There was a problem hiding this comment.
versuche die Room DB generell so zu gestalten das die relationalen Features genutzt werden
| import kotlinx.serialization.json.Json | ||
| import model.* | ||
|
|
||
| class OfflineFirstRepository( |
There was a problem hiding this comment.
verzögert dass nicht alle operationen extrem, weil wir immer noch auf den insert und dann auf den get warten müssen. Gibt es da keinen effizienteren weg? Vlt auch mehr reusable
| val eventId = op.parentId ?: return | ||
| firebaseRepository.deleteMeal(eventId, op.entityId) | ||
| } | ||
| "CREATE_RECIPE", "UPDATE_RECIPE" -> { |
There was a problem hiding this comment.
warum ist create und update immer gleich?
| package data | ||
|
|
||
| enum class AppMode { | ||
| ONLINE, |
There was a problem hiding this comment.
brauche ich den modus online überhaupt noch, wenn ich eh immer offline first mache?
| import data.FireBaseRepository | ||
| import data.local.AppDatabase | ||
|
|
||
| class SeedDataService( |
There was a problem hiding this comment.
Bitte das mit dem Sync Manager zusammenführen. Die Logik reusen und wenn der User online ist den ganzen Sync machen, sonst nur Rezepte etc
| try { | ||
| onOfflineSelected() | ||
| } catch (e: Exception) { | ||
| loginError = "Fehler beim Laden der Daten. Bitte überprüfe deine Internetverbindung." |
There was a problem hiding this comment.
Fehler beim Laden der Rezepte
| return Routes.Home | ||
| } | ||
| fun getStartDestination(loginService: LoginAndRegister, appMode: AppMode): Any { | ||
| if (appMode == AppMode.OFFLINE_ONLY) return Routes.Home |
There was a problem hiding this comment.
ist man im AppMode.OFFLINE_ONLY nicht immer auch automatisch authenticated, also ist das nicht irgendwie doppelt??
| import java.io.File | ||
|
|
||
| actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> { | ||
| val dbDir = File(System.getProperty("user.home"), ".futterbock") |
There was a problem hiding this comment.
ist es nicht besser hier den installationsort zu nehmen?
- Replace @insert(REPLACE) with @upsert in BaseDao to prevent CASCADE deletes when syncing events from Firebase - Remove AppMode.ONLINE, simplify to OFFLINE_ONLY and OFFLINE_FIRST - Merge SeedDataService into InitialSyncService with syncBaseData() - Move Koin module definitions from App.kt to DataModules.kt - Switch OfflineFirstRepository to pure Room reads (no background Firebase refresh) to prevent race conditions with pending deletes - Fix RoomRepository DI to use DelegatingLoginAndRegister instead of hardcoded OfflineLoginAndRegister (group mismatch bug) - Cache Firebase user group to avoid network calls on every operation - Add getByIds() to RecipeDao/IngredientDao, extract enrichMealsWithRecipes helper - Fix participant deletion passing wrong ID (uid vs participantRef) - Add Snackbar success message after creating/updating participants - Clean up schema versions to single v1, platform-specific DB paths - Rename iOS app from Futterbock_App to Futterbock
- SyncManager: skip failed pending ops instead of blocking entire queue - SyncManager: remove leaked CoroutineScope, use suspend startObserving() bound to LaunchedEffect lifecycle - Material: add eventId field, filter getMaterialListOfEvent by eventId instead of returning all materials - Move LoginAndRegister binding from ServiceModules to DataModules to eliminate fragile cross-module dependency - Fix syncBaseData() lazy evaluation bug where ingredient null-clearing was never executed (discarded filter+onEach chain)
Set participant field via .also {} instead of constructor parameter
since it is @ignore in Room.
Users can now use the app without creating an account by clicking "App offline nutzen" on the login screen. Recipes, ingredients and materials are downloaded once from Firebase on first offline start, then stored locally in Room DB.