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

@@ -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)

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

View File

@@ -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 -> {