refactor(ui): remove raw Firestore listeners from Daily/Partner VMs

DailyQuestionViewModel and PartnerHomeViewModel each held a raw
db.collection(...).addSnapshotListener on the partner's answer doc (just reading
snapshot.exists()). Replace with the existing, proven
FirestoreAnswerDataSource.observeAnswerForUser(...) Flow collected in a
viewModelScope Job (auto-cancelled; previous run cancelled on restart) — dropping
the FirebaseFirestore injection from both. Same behavior (exists → non-null),
now through the data layer.

Verified live: daily question shows the correct "waiting for your partner" state
via the new Flow; compiles + unit suite green.

HomeViewModel's two listeners use MetadataChanges.INCLUDE + inline analytics and
drive the core reveal — left as a scoped follow-on (needs a metadata-aware
data-source Flow + two-device real-time verification). See docs/standardization-review.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-06 22:18:30 -05:00
parent cce2687312
commit 0c59000065
2 changed files with 20 additions and 34 deletions

View File

@ -59,7 +59,6 @@ import app.closer.ui.components.LoadingState
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerCardColor
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.functions.FirebaseFunctions
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@ -67,6 +66,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import app.closer.ui.components.CloserGlyphs
@ -91,14 +91,14 @@ class PartnerHomeViewModel @Inject constructor(
private val coupleRepository: CoupleRepository,
private val userRepository: UserRepository,
private val answerDataSource: FirestoreAnswerDataSource,
private val db: FirebaseFirestore,
private val functions: FirebaseFunctions,
) : ViewModel() {
private val _uiState = MutableStateFlow(PartnerHomeUiState())
val uiState: StateFlow<PartnerHomeUiState> = _uiState.asStateFlow()
private var partnerAnswerListener: com.google.firebase.firestore.ListenerRegistration? = null
// Cancelled/replaced each time the observer (re)starts; auto-cancelled with viewModelScope.
private var partnerObserverJob: Job? = null
init {
load()
@ -148,20 +148,15 @@ class PartnerHomeViewModel @Inject constructor(
}
private fun observePartnerAnswer(coupleId: String, userIds: List<String>, questionId: String?) {
partnerAnswerListener?.remove()
partnerObserverJob?.cancel()
val uid = authRepository.currentUserId ?: return
val partnerId = userIds.firstOrNull { it != uid } ?: return
val qId = questionId ?: return
if (questionId == null) return
val today = FirestoreAnswerDataSource.todayLocalDateString()
partnerAnswerListener = db.collection("couples").document(coupleId)
.collection("daily_question").document(today)
.collection("answers").document(partnerId)
.addSnapshotListener { snapshot, error ->
if (error != null) {
Log.w(TAG, "Partner answer listener error", error)
return@addSnapshotListener
partnerObserverJob = viewModelScope.launch {
answerDataSource.observeAnswerForUser(coupleId, partnerId, today).collect { partnerAnswer ->
_uiState.update { it.copy(hasPartnerAnsweredToday = partnerAnswer != null) }
}
_uiState.update { it.copy(hasPartnerAnsweredToday = snapshot?.exists() == true) }
}
}
@ -185,7 +180,7 @@ class PartnerHomeViewModel @Inject constructor(
override fun onCleared() {
super.onCleared()
partnerAnswerListener?.remove()
partnerObserverJob?.cancel()
}
private companion object {

View File

@ -14,14 +14,13 @@ import app.closer.domain.repository.AuthRepository
import app.closer.domain.repository.CoupleRepository
import app.closer.domain.repository.LocalAnswerRepository
import app.closer.domain.usecase.DailyQuestionResolver
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ListenerRegistration
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
data class LocalQuestionUiState(
@ -47,19 +46,18 @@ class DailyQuestionViewModel @Inject constructor(
private val coupleRepository: CoupleRepository,
private val crashReporter: CrashReporter,
private val dailyQuestionResolver: DailyQuestionResolver,
private val retentionAnalytics: RetentionAnalytics,
private val db: FirebaseFirestore
private val retentionAnalytics: RetentionAnalytics
) : ViewModel() {
private val _uiState = MutableStateFlow(LocalQuestionUiState())
val uiState: StateFlow<LocalQuestionUiState> = _uiState.asStateFlow()
private var partnerAnswerListener: ListenerRegistration? = null
// Cancelled/replaced each time the observer (re)starts; auto-cancelled with viewModelScope.
private var partnerObserverJob: Job? = null
override fun onCleared() {
super.onCleared()
partnerAnswerListener?.remove()
partnerAnswerListener = null
partnerObserverJob?.cancel()
}
init {
@ -124,20 +122,13 @@ class DailyQuestionViewModel @Inject constructor(
}
private fun startPartnerAnswerObserver(coupleId: String, date: String) {
viewModelScope.launch {
partnerObserverJob?.cancel()
partnerObserverJob = viewModelScope.launch {
val userId = authRepository.currentUserId ?: return@launch
val couple = runCatching { coupleRepository.getCoupleForUser(userId) }.getOrNull() ?: return@launch
val partnerId = couple.userIds.firstOrNull { it != userId } ?: return@launch
partnerAnswerListener?.remove()
partnerAnswerListener = db.collection("couples")
.document(coupleId)
.collection("daily_question")
.document(date)
.collection("answers")
.document(partnerId)
.addSnapshotListener { snapshot, error ->
if (error != null) return@addSnapshotListener
_uiState.update { it.copy(partnerHasAnswered = snapshot?.exists() == true) }
firestoreAnswerDataSource.observeAnswerForUser(coupleId, partnerId, date).collect { partnerAnswer ->
_uiState.update { it.copy(partnerHasAnswered = partnerAnswer != null) }
}
}
}