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:
Trey t
2025-12-06 11:29:14 -06:00
parent a91efd5de2
commit 04c3389e4d
4 changed files with 145 additions and 12 deletions

View File

@@ -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"
}