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:
parent
cce2687312
commit
0c59000065
|
|
@ -59,7 +59,6 @@ import app.closer.ui.components.LoadingState
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
import app.closer.ui.theme.closerCardColor
|
import app.closer.ui.theme.closerCardColor
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
|
||||||
import com.google.firebase.functions.FirebaseFunctions
|
import com.google.firebase.functions.FirebaseFunctions
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -67,6 +66,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import app.closer.ui.components.CloserGlyphs
|
import app.closer.ui.components.CloserGlyphs
|
||||||
|
|
@ -91,14 +91,14 @@ class PartnerHomeViewModel @Inject constructor(
|
||||||
private val coupleRepository: CoupleRepository,
|
private val coupleRepository: CoupleRepository,
|
||||||
private val userRepository: UserRepository,
|
private val userRepository: UserRepository,
|
||||||
private val answerDataSource: FirestoreAnswerDataSource,
|
private val answerDataSource: FirestoreAnswerDataSource,
|
||||||
private val db: FirebaseFirestore,
|
|
||||||
private val functions: FirebaseFunctions,
|
private val functions: FirebaseFunctions,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(PartnerHomeUiState())
|
private val _uiState = MutableStateFlow(PartnerHomeUiState())
|
||||||
val uiState: StateFlow<PartnerHomeUiState> = _uiState.asStateFlow()
|
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 {
|
init {
|
||||||
load()
|
load()
|
||||||
|
|
@ -148,21 +148,16 @@ class PartnerHomeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observePartnerAnswer(coupleId: String, userIds: List<String>, questionId: String?) {
|
private fun observePartnerAnswer(coupleId: String, userIds: List<String>, questionId: String?) {
|
||||||
partnerAnswerListener?.remove()
|
partnerObserverJob?.cancel()
|
||||||
val uid = authRepository.currentUserId ?: return
|
val uid = authRepository.currentUserId ?: return
|
||||||
val partnerId = userIds.firstOrNull { it != uid } ?: return
|
val partnerId = userIds.firstOrNull { it != uid } ?: return
|
||||||
val qId = questionId ?: return
|
if (questionId == null) return
|
||||||
val today = FirestoreAnswerDataSource.todayLocalDateString()
|
val today = FirestoreAnswerDataSource.todayLocalDateString()
|
||||||
partnerAnswerListener = db.collection("couples").document(coupleId)
|
partnerObserverJob = viewModelScope.launch {
|
||||||
.collection("daily_question").document(today)
|
answerDataSource.observeAnswerForUser(coupleId, partnerId, today).collect { partnerAnswer ->
|
||||||
.collection("answers").document(partnerId)
|
_uiState.update { it.copy(hasPartnerAnsweredToday = partnerAnswer != null) }
|
||||||
.addSnapshotListener { snapshot, error ->
|
|
||||||
if (error != null) {
|
|
||||||
Log.w(TAG, "Partner answer listener error", error)
|
|
||||||
return@addSnapshotListener
|
|
||||||
}
|
|
||||||
_uiState.update { it.copy(hasPartnerAnsweredToday = snapshot?.exists() == true) }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendReminder() {
|
fun sendReminder() {
|
||||||
|
|
@ -185,7 +180,7 @@ class PartnerHomeViewModel @Inject constructor(
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
partnerAnswerListener?.remove()
|
partnerObserverJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,13 @@ import app.closer.domain.repository.AuthRepository
|
||||||
import app.closer.domain.repository.CoupleRepository
|
import app.closer.domain.repository.CoupleRepository
|
||||||
import app.closer.domain.repository.LocalAnswerRepository
|
import app.closer.domain.repository.LocalAnswerRepository
|
||||||
import app.closer.domain.usecase.DailyQuestionResolver
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
data class LocalQuestionUiState(
|
data class LocalQuestionUiState(
|
||||||
|
|
@ -47,19 +46,18 @@ class DailyQuestionViewModel @Inject constructor(
|
||||||
private val coupleRepository: CoupleRepository,
|
private val coupleRepository: CoupleRepository,
|
||||||
private val crashReporter: CrashReporter,
|
private val crashReporter: CrashReporter,
|
||||||
private val dailyQuestionResolver: DailyQuestionResolver,
|
private val dailyQuestionResolver: DailyQuestionResolver,
|
||||||
private val retentionAnalytics: RetentionAnalytics,
|
private val retentionAnalytics: RetentionAnalytics
|
||||||
private val db: FirebaseFirestore
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(LocalQuestionUiState())
|
private val _uiState = MutableStateFlow(LocalQuestionUiState())
|
||||||
val uiState: StateFlow<LocalQuestionUiState> = _uiState.asStateFlow()
|
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() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
partnerAnswerListener?.remove()
|
partnerObserverJob?.cancel()
|
||||||
partnerAnswerListener = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -124,21 +122,14 @@ class DailyQuestionViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPartnerAnswerObserver(coupleId: String, date: String) {
|
private fun startPartnerAnswerObserver(coupleId: String, date: String) {
|
||||||
viewModelScope.launch {
|
partnerObserverJob?.cancel()
|
||||||
|
partnerObserverJob = viewModelScope.launch {
|
||||||
val userId = authRepository.currentUserId ?: return@launch
|
val userId = authRepository.currentUserId ?: return@launch
|
||||||
val couple = runCatching { coupleRepository.getCoupleForUser(userId) }.getOrNull() ?: return@launch
|
val couple = runCatching { coupleRepository.getCoupleForUser(userId) }.getOrNull() ?: return@launch
|
||||||
val partnerId = couple.userIds.firstOrNull { it != userId } ?: return@launch
|
val partnerId = couple.userIds.firstOrNull { it != userId } ?: return@launch
|
||||||
partnerAnswerListener?.remove()
|
firestoreAnswerDataSource.observeAnswerForUser(coupleId, partnerId, date).collect { partnerAnswer ->
|
||||||
partnerAnswerListener = db.collection("couples")
|
_uiState.update { it.copy(partnerHasAnswered = partnerAnswer != null) }
|
||||||
.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) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue