Implement centralized data caching system
This commit adds a comprehensive caching system that loads all data on app launch and keeps it in memory, eliminating redundant API calls when navigating between screens. Core Implementation: - DataCache: Singleton holding all app data in StateFlow - DataPrefetchManager: Loads all data in parallel on app launch - Automatic cache updates on create/update/delete operations Features: - ✅ Instant screen loads from cached data - ✅ Reduced API calls (no redundant requests) - ✅ Better UX (no loading spinners on navigation) - ✅ Offline support (data remains available) - ✅ Consistent state across all screens Cache Contents: - Residences (all + my residences + summaries) - Tasks (all tasks + tasks by residence) - Documents (all + by residence) - Contractors (all) - Lookup data (categories, priorities, frequencies, statuses) ViewModels Updated: - ResidenceViewModel: Uses cache with forceRefresh option - TaskViewModel: Uses cache with forceRefresh option - Updates cache on successful create/update/delete iOS Integration: - Data prefetch on successful login - Cache cleared on logout - Background prefetch doesn't block authentication Usage: // Load from cache (instant) viewModel.loadResidences() // Force refresh from API viewModel.loadResidences(forceRefresh: true) Next Steps: - Update DocumentViewModel and ContractorViewModel (same pattern) - Add Android MainActivity integration - Add pull-to-refresh support See composeApp/src/commonMain/kotlin/com/example/mycrib/cache/README_CACHING.md for complete documentation and implementation guide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
237
composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt
vendored
Normal file
237
composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
package com.mycrib.cache
|
||||
|
||||
import com.mycrib.shared.network.*
|
||||
import com.mycrib.storage.TokenStorage
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Manager responsible for prefetching and caching data when the app launches.
|
||||
* This ensures all screens have immediate access to data without making API calls.
|
||||
*/
|
||||
class DataPrefetchManager {
|
||||
|
||||
private val residenceApi = ResidenceApi()
|
||||
private val taskApi = TaskApi()
|
||||
private val documentApi = DocumentApi()
|
||||
private val contractorApi = ContractorApi()
|
||||
private val lookupsApi = LookupsApi()
|
||||
|
||||
/**
|
||||
* Prefetch all essential data on app launch.
|
||||
* This runs asynchronously and populates the DataCache.
|
||||
*/
|
||||
suspend fun prefetchAllData(): Result<Unit> = withContext(Dispatchers.Default) {
|
||||
try {
|
||||
val token = TokenStorage.getToken()
|
||||
if (token == null) {
|
||||
return@withContext Result.failure(Exception("Not authenticated"))
|
||||
}
|
||||
|
||||
println("DataPrefetchManager: Starting data prefetch...")
|
||||
|
||||
// Launch all prefetch operations in parallel
|
||||
val jobs = listOf(
|
||||
async { prefetchResidences(token) },
|
||||
async { prefetchMyResidences(token) },
|
||||
async { prefetchTasks(token) },
|
||||
async { prefetchDocuments(token) },
|
||||
async { prefetchContractors(token) },
|
||||
async { prefetchLookups(token) }
|
||||
)
|
||||
|
||||
// Wait for all jobs to complete
|
||||
jobs.awaitAll()
|
||||
|
||||
// Mark cache as initialized
|
||||
DataCache.setCacheInitialized(true)
|
||||
|
||||
println("DataPrefetchManager: Data prefetch completed successfully")
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
println("DataPrefetchManager: Error during prefetch: ${e.message}")
|
||||
e.printStackTrace()
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh specific data types.
|
||||
* Useful for pull-to-refresh functionality.
|
||||
*/
|
||||
suspend fun refreshResidences(): Result<Unit> = withContext(Dispatchers.Default) {
|
||||
try {
|
||||
val token = TokenStorage.getToken() ?: return@withContext Result.failure(Exception("Not authenticated"))
|
||||
prefetchResidences(token)
|
||||
prefetchMyResidences(token)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshTasks(): Result<Unit> = withContext(Dispatchers.Default) {
|
||||
try {
|
||||
val token = TokenStorage.getToken() ?: return@withContext Result.failure(Exception("Not authenticated"))
|
||||
prefetchTasks(token)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshDocuments(): Result<Unit> = withContext(Dispatchers.Default) {
|
||||
try {
|
||||
val token = TokenStorage.getToken() ?: return@withContext Result.failure(Exception("Not authenticated"))
|
||||
prefetchDocuments(token)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshContractors(): Result<Unit> = withContext(Dispatchers.Default) {
|
||||
try {
|
||||
val token = TokenStorage.getToken() ?: return@withContext Result.failure(Exception("Not authenticated"))
|
||||
prefetchContractors(token)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Private prefetch methods
|
||||
private suspend fun prefetchResidences(token: String) {
|
||||
try {
|
||||
println("DataPrefetchManager: Fetching residences...")
|
||||
val result = residenceApi.getResidences(token)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updateResidences(result.data)
|
||||
println("DataPrefetchManager: Cached ${result.data.size} residences")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("DataPrefetchManager: Error fetching residences: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prefetchMyResidences(token: String) {
|
||||
try {
|
||||
println("DataPrefetchManager: Fetching my residences...")
|
||||
val result = residenceApi.getMyResidences(token)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updateMyResidences(result.data)
|
||||
println("DataPrefetchManager: Cached my residences")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("DataPrefetchManager: Error fetching my residences: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prefetchTasks(token: String) {
|
||||
try {
|
||||
println("DataPrefetchManager: Fetching tasks...")
|
||||
val result = taskApi.getTasks(token)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updateAllTasks(result.data)
|
||||
println("DataPrefetchManager: Cached tasks")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("DataPrefetchManager: Error fetching tasks: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prefetchDocuments(token: String) {
|
||||
try {
|
||||
println("DataPrefetchManager: Fetching documents...")
|
||||
val result = documentApi.getDocuments(
|
||||
token = token,
|
||||
residenceId = null,
|
||||
documentType = null,
|
||||
category = null,
|
||||
contractorId = null,
|
||||
isActive = null,
|
||||
expiringSoon = null,
|
||||
tags = null,
|
||||
search = null
|
||||
)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updateDocuments(result.data.documents)
|
||||
println("DataPrefetchManager: Cached ${result.data.documents.size} documents")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("DataPrefetchManager: Error fetching documents: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prefetchContractors(token: String) {
|
||||
try {
|
||||
println("DataPrefetchManager: Fetching contractors...")
|
||||
val result = contractorApi.getContractors(
|
||||
token = token,
|
||||
specialty = null,
|
||||
isFavorite = null,
|
||||
isActive = null,
|
||||
search = null
|
||||
)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updateContractors(result.data.contractors)
|
||||
println("DataPrefetchManager: Cached ${result.data.contractors.size} contractors")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("DataPrefetchManager: Error fetching contractors: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prefetchLookups(token: String) {
|
||||
try {
|
||||
println("DataPrefetchManager: Fetching lookups...")
|
||||
|
||||
// Fetch all lookup data in parallel
|
||||
coroutineScope {
|
||||
launch {
|
||||
val result = lookupsApi.getCategories(token)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updateCategories(result.data)
|
||||
println("DataPrefetchManager: Cached ${result.data.size} categories")
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
val result = lookupsApi.getPriorities(token)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updatePriorities(result.data)
|
||||
println("DataPrefetchManager: Cached ${result.data.size} priorities")
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
val result = lookupsApi.getFrequencies(token)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updateFrequencies(result.data)
|
||||
println("DataPrefetchManager: Cached ${result.data.size} frequencies")
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
val result = lookupsApi.getStatuses(token)
|
||||
if (result is ApiResult.Success) {
|
||||
DataCache.updateStatuses(result.data)
|
||||
println("DataPrefetchManager: Cached ${result.data.size} statuses")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("DataPrefetchManager: Error fetching lookups: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var instance: DataPrefetchManager? = null
|
||||
|
||||
fun getInstance(): DataPrefetchManager {
|
||||
if (instance == null) {
|
||||
instance = DataPrefetchManager()
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user