P1: All Kotlin VMs align with DataManager single-source-of-truth
Four broken VMs refactored to derive read-state from IDataManager, three gaps closed: 1. TaskViewModel: tasksState / tasksByResidenceState / taskCompletionsState now derived via .map + .stateIn / combine. isLoading / loadError separated. 2. ResidenceViewModel: residencesState / myResidencesState / summaryState / residenceTasksState / residenceContractorsState all derived. 8 mutation states retained as independent (legit one-shot feedback). 3. ContractorViewModel: contractorsState / contractorDetailState derived. 4 mutation states retained. 4. DocumentViewModel: documentsState / documentDetailState derived. 6 mutation states retained. 5. AuthViewModel: currentUserState now derived from dataManager.currentUser. 10 other states stay independent (one-shot mutation feedback by design). 6. LookupsViewModel: accepts IDataManager ctor param for test injection consistency. Direct-exposure pattern preserved. Legacy ApiResult-wrapped states now derived from DataManager instead of manual _xxxState.value =. 7. NotificationPreferencesViewModel: preferencesState derived from new IDataManager.notificationPreferences. APILayer writes through on both getNotificationPreferences and updateNotificationPreferences. IDataManager also grew notificationPreferences: StateFlow<NotificationPreference?>. DataManager, InMemoryDataManager updated. No screen edits needed — screens consume viewModel.xxxState the same way; the source just switched. Architecture enforcement test comes in P3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,25 +2,79 @@ package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.data.IDataManager
|
||||
import com.tt.honeyDue.models.ContractorSummary
|
||||
import com.tt.honeyDue.models.MyResidencesResponse
|
||||
import com.tt.honeyDue.models.Residence
|
||||
import com.tt.honeyDue.models.ResidenceCreateRequest
|
||||
import com.tt.honeyDue.models.TotalSummary
|
||||
import com.tt.honeyDue.models.MyResidencesResponse
|
||||
import com.tt.honeyDue.models.TaskColumnsResponse
|
||||
import com.tt.honeyDue.models.ContractorSummary
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.models.TotalSummary
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ResidenceViewModel : ViewModel() {
|
||||
/**
|
||||
* ResidenceViewModel — read-state derived from [IDataManager].
|
||||
*
|
||||
* All list/detail reads (`residencesState`, `myResidencesState`,
|
||||
* `summaryState`, `residenceTasksState`, `residenceContractorsState`)
|
||||
* are reactive projections of [IDataManager] StateFlows. Mutation
|
||||
* feedback (create/update/delete/join/cancel/uncancel/updateTask/
|
||||
* generateReport) remains owned by the VM as one-shot [ApiResult]
|
||||
* fields — they track API operation outcomes, not cached data.
|
||||
*/
|
||||
class ResidenceViewModel(
|
||||
private val dataManager: IDataManager = DataManager,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _residencesState = MutableStateFlow<ApiResult<List<Residence>>>(ApiResult.Idle)
|
||||
val residencesState: StateFlow<ApiResult<List<Residence>>> = _residencesState
|
||||
// ---------- Read state (derived from DataManager) ----------
|
||||
|
||||
private val _summaryState = MutableStateFlow<ApiResult<TotalSummary>>(ApiResult.Idle)
|
||||
val summaryState: StateFlow<ApiResult<TotalSummary>> = _summaryState
|
||||
val residencesState: StateFlow<ApiResult<List<Residence>>> =
|
||||
dataManager.residences
|
||||
.map { list -> if (list.isNotEmpty()) ApiResult.Success(list) else ApiResult.Idle }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
|
||||
|
||||
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> =
|
||||
dataManager.myResidences
|
||||
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
|
||||
|
||||
val summaryState: StateFlow<ApiResult<TotalSummary>> =
|
||||
dataManager.totalSummary
|
||||
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
|
||||
|
||||
/** Drives the residence-scoped projections. */
|
||||
private val _selectedResidenceId = MutableStateFlow<Int?>(null)
|
||||
|
||||
val residenceTasksState: StateFlow<ApiResult<TaskColumnsResponse>> =
|
||||
combine(_selectedResidenceId, dataManager.tasksByResidence) { id, map ->
|
||||
if (id == null) ApiResult.Idle
|
||||
else map[id]?.let { ApiResult.Success(it) } ?: ApiResult.Idle
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
|
||||
|
||||
val residenceContractorsState: StateFlow<ApiResult<List<ContractorSummary>>> =
|
||||
combine(_selectedResidenceId, dataManager.contractorsByResidence) { id, map ->
|
||||
if (id == null) ApiResult.Idle
|
||||
else map[id]?.let { ApiResult.Success(it) } ?: ApiResult.Idle
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
|
||||
|
||||
// ---------- Loading / error feedback ----------
|
||||
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading: StateFlow<Boolean> = _isLoading
|
||||
|
||||
private val _loadError = MutableStateFlow<String?>(null)
|
||||
val loadError: StateFlow<String?> = _loadError
|
||||
|
||||
// ---------- Mutation-feedback (one-shot, owned by VM) ----------
|
||||
|
||||
private val _createResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
|
||||
val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState
|
||||
@@ -28,11 +82,11 @@ class ResidenceViewModel : ViewModel() {
|
||||
private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
|
||||
val updateResidenceState: StateFlow<ApiResult<Residence>> = _updateResidenceState
|
||||
|
||||
private val _residenceTasksState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
||||
val residenceTasksState: StateFlow<ApiResult<TaskColumnsResponse>> = _residenceTasksState
|
||||
private val _deleteResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val deleteResidenceState: StateFlow<ApiResult<Unit>> = _deleteResidenceState
|
||||
|
||||
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Idle)
|
||||
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
|
||||
private val _joinResidenceState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>>(ApiResult.Idle)
|
||||
val joinResidenceState: StateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>> = _joinResidenceState
|
||||
|
||||
private val _cancelTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
|
||||
val cancelTaskState: StateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>> = _cancelTaskState
|
||||
@@ -46,37 +100,77 @@ class ResidenceViewModel : ViewModel() {
|
||||
private val _generateReportState = MutableStateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>>(ApiResult.Idle)
|
||||
val generateReportState: StateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>> = _generateReportState
|
||||
|
||||
private val _deleteResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val deleteResidenceState: StateFlow<ApiResult<Unit>> = _deleteResidenceState
|
||||
// ---------- Projection selectors ----------
|
||||
|
||||
private val _residenceContractorsState = MutableStateFlow<ApiResult<List<ContractorSummary>>>(ApiResult.Idle)
|
||||
val residenceContractorsState: StateFlow<ApiResult<List<ContractorSummary>>> = _residenceContractorsState
|
||||
fun selectResidence(residenceId: Int?) {
|
||||
_selectedResidenceId.value = residenceId
|
||||
}
|
||||
|
||||
// ---------- Load methods (write-through to DataManager) ----------
|
||||
|
||||
/**
|
||||
* Load residences from cache. If cache is empty or force refresh is requested,
|
||||
* fetch from API and update cache.
|
||||
*/
|
||||
fun loadResidences(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_residencesState.value = ApiResult.Loading
|
||||
_residencesState.value = APILayer.getResidences(forceRefresh = forceRefresh)
|
||||
_isLoading.value = true
|
||||
_loadError.value = null
|
||||
_loadError.value = (APILayer.getResidences(forceRefresh = forceRefresh) as? ApiResult.Error)?.message
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSummary(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_summaryState.value = ApiResult.Loading
|
||||
_summaryState.value = APILayer.getSummary(forceRefresh = forceRefresh)
|
||||
_isLoading.value = true
|
||||
_loadError.value = null
|
||||
_loadError.value = (APILayer.getSummary(forceRefresh = forceRefresh) as? ApiResult.Error)?.message
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun getResidence(id: Int, onResult: (ApiResult<Residence>) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
val result = APILayer.getResidence(id)
|
||||
onResult(result)
|
||||
onResult(APILayer.getResidence(id))
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMyResidences(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
_loadError.value = null
|
||||
_loadError.value = (APILayer.getMyResidences(forceRefresh = forceRefresh) as? ApiResult.Error)?.message
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun loadResidenceTasks(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
_selectedResidenceId.value = residenceId
|
||||
_isLoading.value = true
|
||||
_loadError.value = null
|
||||
_loadError.value = (APILayer.getTasksByResidence(residenceId) as? ApiResult.Error)?.message
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun loadResidenceContractors(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
_selectedResidenceId.value = residenceId
|
||||
_isLoading.value = true
|
||||
_loadError.value = null
|
||||
_loadError.value = (APILayer.getContractorsByResidence(residenceId) as? ApiResult.Error)?.message
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun resetResidenceTasksState() {
|
||||
_selectedResidenceId.value = null
|
||||
}
|
||||
|
||||
fun resetResidenceContractorsState() {
|
||||
_selectedResidenceId.value = null
|
||||
}
|
||||
|
||||
// ---------- Mutations ----------
|
||||
|
||||
fun createResidence(request: ResidenceCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_createResidenceState.value = ApiResult.Loading
|
||||
@@ -84,17 +178,6 @@ class ResidenceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetResidenceTasksState() {
|
||||
_residenceTasksState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun loadResidenceTasks(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
_residenceTasksState.value = ApiResult.Loading
|
||||
_residenceTasksState.value = APILayer.getTasksByResidence(residenceId)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateResidence(residenceId: Int, request: ResidenceCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_updateResidenceState.value = ApiResult.Loading
|
||||
@@ -102,20 +185,8 @@ class ResidenceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCreateState() {
|
||||
_createResidenceState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUpdateState() {
|
||||
_updateResidenceState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun loadMyResidences(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_myResidencesState.value = ApiResult.Loading
|
||||
_myResidencesState.value = APILayer.getMyResidences(forceRefresh = forceRefresh)
|
||||
}
|
||||
}
|
||||
fun resetCreateState() { _createResidenceState.value = ApiResult.Idle }
|
||||
fun resetUpdateState() { _updateResidenceState.value = ApiResult.Idle }
|
||||
|
||||
fun cancelTask(taskId: Int) {
|
||||
viewModelScope.launch {
|
||||
@@ -138,17 +209,9 @@ class ResidenceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCancelTaskState() {
|
||||
_cancelTaskState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUncancelTaskState() {
|
||||
_uncancelTaskState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUpdateTaskState() {
|
||||
_updateTaskState.value = ApiResult.Idle
|
||||
}
|
||||
fun resetCancelTaskState() { _cancelTaskState.value = ApiResult.Idle }
|
||||
fun resetUncancelTaskState() { _uncancelTaskState.value = ApiResult.Idle }
|
||||
fun resetUpdateTaskState() { _updateTaskState.value = ApiResult.Idle }
|
||||
|
||||
fun generateTasksReport(residenceId: Int, email: String? = null) {
|
||||
viewModelScope.launch {
|
||||
@@ -157,9 +220,7 @@ class ResidenceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetGenerateReportState() {
|
||||
_generateReportState.value = ApiResult.Idle
|
||||
}
|
||||
fun resetGenerateReportState() { _generateReportState.value = ApiResult.Idle }
|
||||
|
||||
fun deleteResidence(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
@@ -168,12 +229,7 @@ class ResidenceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetDeleteResidenceState() {
|
||||
_deleteResidenceState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
private val _joinResidenceState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>>(ApiResult.Idle)
|
||||
val joinResidenceState: StateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>> = _joinResidenceState
|
||||
fun resetDeleteResidenceState() { _deleteResidenceState.value = ApiResult.Idle }
|
||||
|
||||
fun joinWithCode(code: String) {
|
||||
viewModelScope.launch {
|
||||
@@ -182,18 +238,5 @@ class ResidenceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetJoinResidenceState() {
|
||||
_joinResidenceState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun loadResidenceContractors(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
_residenceContractorsState.value = ApiResult.Loading
|
||||
_residenceContractorsState.value = APILayer.getContractorsByResidence(residenceId)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetResidenceContractorsState() {
|
||||
_residenceContractorsState.value = ApiResult.Idle
|
||||
}
|
||||
fun resetJoinResidenceState() { _joinResidenceState.value = ApiResult.Idle }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user