Persist lookup data to disk and refresh on app foreground
- DataManager now persists lookup data (residence types, task categories, priorities, statuses, specialties, templates) to disk - Loads cached lookups on app startup for faster launch - iOS: Refresh lookups when app becomes active, refresh widget on background - Android: Initialize DataManager in onCreate, already had onResume refresh - Only send ETag if lookup data is actually in memory to avoid 304 with no data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,8 @@ import com.example.casera.fcm.FCMManager
|
||||
import com.example.casera.platform.BillingManager
|
||||
import com.example.casera.network.APILayer
|
||||
import com.example.casera.sharing.ContractorSharingManager
|
||||
import com.example.casera.data.DataManager
|
||||
import com.example.casera.data.PersistenceManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
|
||||
@@ -55,6 +57,14 @@ class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
|
||||
ThemeStorage.initialize(ThemeStorageManager.getInstance(applicationContext))
|
||||
ThemeManager.initialize()
|
||||
|
||||
// Initialize DataManager with platform-specific managers
|
||||
// This loads cached lookup data from disk for faster startup
|
||||
DataManager.initialize(
|
||||
tokenMgr = TokenManager.getInstance(applicationContext),
|
||||
themeMgr = ThemeStorageManager.getInstance(applicationContext),
|
||||
persistenceMgr = PersistenceManager.getInstance(applicationContext)
|
||||
)
|
||||
|
||||
// Initialize BillingManager for subscription management
|
||||
billingManager = BillingManager.getInstance(applicationContext)
|
||||
|
||||
|
||||
@@ -592,6 +592,7 @@ object DataManager {
|
||||
/**
|
||||
* Set all lookups from unified seeded data response.
|
||||
* Also stores the ETag for future conditional requests.
|
||||
* Persists lookup data to disk for faster app startup.
|
||||
*/
|
||||
fun setAllLookupsFromSeededData(seededData: SeededDataResponse, etag: String?) {
|
||||
setResidenceTypes(seededData.residenceTypes)
|
||||
@@ -603,6 +604,8 @@ object DataManager {
|
||||
setTaskTemplatesGrouped(seededData.taskTemplates)
|
||||
setSeededDataETag(etag)
|
||||
_lookupsInitialized.value = true
|
||||
// Persist lookups to disk for faster startup
|
||||
persistLookupsToDisk()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -723,32 +726,63 @@ object DataManager {
|
||||
|
||||
/**
|
||||
* Persist current state to disk.
|
||||
* Only persists user data - all other data is fetched fresh from API.
|
||||
* No offline mode support - network required for app functionality.
|
||||
* Persists user data and lookup data for faster app startup.
|
||||
*/
|
||||
private fun persistToDisk() {
|
||||
val manager = persistenceManager ?: return
|
||||
|
||||
try {
|
||||
// Only persist user data - everything else is fetched fresh from API
|
||||
_currentUser.value?.let {
|
||||
manager.save(KEY_CURRENT_USER, json.encodeToString(it))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("DataManager: Error persisting to disk: ${e.message}")
|
||||
println("DataManager: Error persisting user to disk: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist lookup data to disk separately (called less frequently).
|
||||
*/
|
||||
private fun persistLookupsToDisk() {
|
||||
val manager = persistenceManager ?: return
|
||||
|
||||
try {
|
||||
if (_residenceTypes.value.isNotEmpty()) {
|
||||
manager.save(KEY_RESIDENCE_TYPES, json.encodeToString(_residenceTypes.value))
|
||||
}
|
||||
if (_taskFrequencies.value.isNotEmpty()) {
|
||||
manager.save(KEY_TASK_FREQUENCIES, json.encodeToString(_taskFrequencies.value))
|
||||
}
|
||||
if (_taskPriorities.value.isNotEmpty()) {
|
||||
manager.save(KEY_TASK_PRIORITIES, json.encodeToString(_taskPriorities.value))
|
||||
}
|
||||
if (_taskStatuses.value.isNotEmpty()) {
|
||||
manager.save(KEY_TASK_STATUSES, json.encodeToString(_taskStatuses.value))
|
||||
}
|
||||
if (_taskCategories.value.isNotEmpty()) {
|
||||
manager.save(KEY_TASK_CATEGORIES, json.encodeToString(_taskCategories.value))
|
||||
}
|
||||
if (_contractorSpecialties.value.isNotEmpty()) {
|
||||
manager.save(KEY_CONTRACTOR_SPECIALTIES, json.encodeToString(_contractorSpecialties.value))
|
||||
}
|
||||
_taskTemplatesGrouped.value?.let {
|
||||
manager.save(KEY_TASK_TEMPLATES_GROUPED, json.encodeToString(it))
|
||||
}
|
||||
println("DataManager: Lookup data persisted to disk")
|
||||
} catch (e: Exception) {
|
||||
println("DataManager: Error persisting lookups to disk: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cached state from disk.
|
||||
* Only loads user data - all other data is fetched fresh from API.
|
||||
* No offline mode support - network required for app functionality.
|
||||
* Loads user data and lookup data for faster app startup.
|
||||
*/
|
||||
private fun loadFromDisk() {
|
||||
val manager = persistenceManager ?: return
|
||||
|
||||
try {
|
||||
// Only load user data - everything else is fetched fresh from API
|
||||
// Load user data
|
||||
manager.load(KEY_CURRENT_USER)?.let { data ->
|
||||
_currentUser.value = json.decodeFromString<User>(data)
|
||||
}
|
||||
@@ -762,15 +796,86 @@ object DataManager {
|
||||
manager.load(KEY_SEEDED_DATA_ETAG)?.let { data ->
|
||||
_seededDataETag.value = data
|
||||
}
|
||||
|
||||
// Load lookup data
|
||||
loadLookupsFromDisk()
|
||||
} catch (e: Exception) {
|
||||
println("DataManager: Error loading from disk: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load lookup data from disk.
|
||||
* If lookups are successfully loaded, mark as initialized.
|
||||
*/
|
||||
private fun loadLookupsFromDisk() {
|
||||
val manager = persistenceManager ?: return
|
||||
var lookupsLoaded = false
|
||||
|
||||
try {
|
||||
manager.load(KEY_RESIDENCE_TYPES)?.let { data ->
|
||||
val types = json.decodeFromString<List<ResidenceType>>(data)
|
||||
_residenceTypes.value = types
|
||||
_residenceTypesMap.value = types.associateBy { it.id }
|
||||
lookupsLoaded = true
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_FREQUENCIES)?.let { data ->
|
||||
val frequencies = json.decodeFromString<List<TaskFrequency>>(data)
|
||||
_taskFrequencies.value = frequencies
|
||||
_taskFrequenciesMap.value = frequencies.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_PRIORITIES)?.let { data ->
|
||||
val priorities = json.decodeFromString<List<TaskPriority>>(data)
|
||||
_taskPriorities.value = priorities
|
||||
_taskPrioritiesMap.value = priorities.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_STATUSES)?.let { data ->
|
||||
val statuses = json.decodeFromString<List<TaskStatus>>(data)
|
||||
_taskStatuses.value = statuses
|
||||
_taskStatusesMap.value = statuses.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_CATEGORIES)?.let { data ->
|
||||
val categories = json.decodeFromString<List<TaskCategory>>(data)
|
||||
_taskCategories.value = categories
|
||||
_taskCategoriesMap.value = categories.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_CONTRACTOR_SPECIALTIES)?.let { data ->
|
||||
val specialties = json.decodeFromString<List<ContractorSpecialty>>(data)
|
||||
_contractorSpecialties.value = specialties
|
||||
_contractorSpecialtiesMap.value = specialties.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_TEMPLATES_GROUPED)?.let { data ->
|
||||
val grouped = json.decodeFromString<TaskTemplatesGroupedResponse>(data)
|
||||
_taskTemplatesGrouped.value = grouped
|
||||
_taskTemplates.value = grouped.categories.flatMap { it.templates }
|
||||
}
|
||||
|
||||
// Mark lookups as initialized if we loaded any data
|
||||
if (lookupsLoaded) {
|
||||
_lookupsInitialized.value = true
|
||||
println("DataManager: Lookup data loaded from disk")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("DataManager: Error loading lookups from disk: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PERSISTENCE KEYS ====================
|
||||
// Only user data is persisted - all other data fetched fresh from API
|
||||
|
||||
private const val KEY_CURRENT_USER = "dm_current_user"
|
||||
private const val KEY_HAS_COMPLETED_ONBOARDING = "dm_has_completed_onboarding"
|
||||
private const val KEY_SEEDED_DATA_ETAG = "dm_seeded_data_etag"
|
||||
private const val KEY_RESIDENCE_TYPES = "dm_residence_types"
|
||||
private const val KEY_TASK_FREQUENCIES = "dm_task_frequencies"
|
||||
private const val KEY_TASK_PRIORITIES = "dm_task_priorities"
|
||||
private const val KEY_TASK_STATUSES = "dm_task_statuses"
|
||||
private const val KEY_TASK_CATEGORIES = "dm_task_categories"
|
||||
private const val KEY_CONTRACTOR_SPECIALTIES = "dm_contractor_specialties"
|
||||
private const val KEY_TASK_TEMPLATES_GROUPED = "dm_task_templates_grouped"
|
||||
}
|
||||
|
||||
@@ -80,8 +80,12 @@ object APILayer {
|
||||
|
||||
try {
|
||||
// Use seeded data endpoint with ETag support (PUBLIC - no auth required)
|
||||
println("🔄 Fetching seeded data (all lookups + templates)...")
|
||||
val seededDataResult = lookupsApi.getSeededData(currentETag, token)
|
||||
// Only send ETag if lookups are already in memory - otherwise we need full data
|
||||
// (ETag may be persisted from previous session but lookup data wasn't loaded)
|
||||
val hasLookupsInMemory = DataManager.residenceTypes.value.isNotEmpty()
|
||||
val etagToSend = if (hasLookupsInMemory) currentETag else null
|
||||
println("🔄 Fetching seeded data (all lookups + templates)... ETag: $etagToSend (has data in memory: $hasLookupsInMemory)")
|
||||
val seededDataResult = lookupsApi.getSeededData(etagToSend, token)
|
||||
println("📦 Seeded data result: $seededDataResult")
|
||||
|
||||
when (seededDataResult) {
|
||||
@@ -146,9 +150,14 @@ object APILayer {
|
||||
val token = getToken()
|
||||
val currentETag = DataManager.seededDataETag.value
|
||||
|
||||
println("🔄 [APILayer] Checking if lookups have changed (ETag: $currentETag)...")
|
||||
// Only send ETag if we actually have lookup data in memory
|
||||
// Otherwise we need a full fetch to populate the cache
|
||||
val hasLookupsInMemory = DataManager.residenceTypes.value.isNotEmpty()
|
||||
val etagToSend = if (hasLookupsInMemory) currentETag else null
|
||||
|
||||
val seededDataResult = lookupsApi.getSeededData(currentETag, token)
|
||||
println("🔄 [APILayer] Checking if lookups have changed (ETag: $etagToSend, has data: $hasLookupsInMemory)...")
|
||||
|
||||
val seededDataResult = lookupsApi.getSeededData(etagToSend, token)
|
||||
|
||||
when (seededDataResult) {
|
||||
is ConditionalResult.Success -> {
|
||||
|
||||
Reference in New Issue
Block a user