From 9647e4f066572be4aad3cd4fbbfbee39f2e15240 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 30 Oct 2023 19:30:44 -0400 Subject: [PATCH] Refactor SurveyListItem --- .../SurveyItem.kt => model/SurveyListItem.kt} | 12 ++++---- .../persistence/remote/RemoteDataStore.kt | 4 +-- .../remote/firebase/FirestoreDataStore.kt | 11 +++++-- .../ground/repository/SurveyRepository.kt | 15 ++++++---- .../ui/surveyselector/SurveyListAdapter.kt | 13 +++++---- .../surveyselector/SurveySelectorViewModel.kt | 29 ++++--------------- .../src/main/res/layout/survey_card_item.xml | 14 ++++----- 7 files changed, 45 insertions(+), 53 deletions(-) rename ground/src/main/java/com/google/android/ground/{ui/surveyselector/SurveyItem.kt => model/SurveyListItem.kt} (75%) diff --git a/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveyItem.kt b/ground/src/main/java/com/google/android/ground/model/SurveyListItem.kt similarity index 75% rename from ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveyItem.kt rename to ground/src/main/java/com/google/android/ground/model/SurveyListItem.kt index d5b2f4379b..ea6b0275e3 100644 --- a/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveyItem.kt +++ b/ground/src/main/java/com/google/android/ground/model/SurveyListItem.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.google.android.ground.ui.surveyselector +package com.google.android.ground.model -data class SurveyItem( - val surveyId: String, - val surveyTitle: String, - val surveyDescription: String, - val isAvailableOffline: Boolean +data class SurveyListItem( + val id: String, + val title: String, + val description: String, + val availableOffline: Boolean ) diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/RemoteDataStore.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/RemoteDataStore.kt index 565163b68a..79399e671e 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/RemoteDataStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/RemoteDataStore.kt @@ -16,6 +16,7 @@ package com.google.android.ground.persistence.remote import com.google.android.ground.model.Survey +import com.google.android.ground.model.SurveyListItem import com.google.android.ground.model.TermsOfService import com.google.android.ground.model.User import com.google.android.ground.model.locationofinterest.LocationOfInterest @@ -28,8 +29,7 @@ import kotlinx.coroutines.flow.Flow * subscriptions are run in a background thread (i.e., not the Android main thread). */ interface RemoteDataStore { - // TODO: Refactor SurveyItem into model class SurveyListItem and return from here. - fun getSurveyList(user: User): Flow> + fun getSurveyList(user: User): Flow> /** * Loads the survey with the specified id from the remote data store. Returns `null` if the survey diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/FirestoreDataStore.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/FirestoreDataStore.kt index 2ef4808168..bed9199253 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/FirestoreDataStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/FirestoreDataStore.kt @@ -17,6 +17,7 @@ package com.google.android.ground.persistence.remote.firebase import com.google.android.ground.coroutines.IoDispatcher import com.google.android.ground.model.Survey +import com.google.android.ground.model.SurveyListItem import com.google.android.ground.model.TermsOfService import com.google.android.ground.model.User import com.google.android.ground.model.locationofinterest.LocationOfInterest @@ -35,6 +36,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import timber.log.Timber @@ -67,8 +69,13 @@ internal constructor( override suspend fun loadTermsOfService(): TermsOfService? = withContext(ioDispatcher) { db().termsOfService().terms().get() } - override fun getSurveyList(user: User): Flow> = flow { - emitAll(db().surveys().getReadable(user)) + override fun getSurveyList(user: User): Flow> = flow { + emitAll( + db().surveys().getReadable(user).map { list -> + // TODO(#2031): Return SurveyListItem from getReadable(), only fetch required fields. + list.map { SurveyListItem(it.id, it.title, it.description, false) } + } + ) } override suspend fun loadLocationsOfInterest(survey: Survey) = diff --git a/ground/src/main/java/com/google/android/ground/repository/SurveyRepository.kt b/ground/src/main/java/com/google/android/ground/repository/SurveyRepository.kt index 2a024bdaba..11823b82c2 100644 --- a/ground/src/main/java/com/google/android/ground/repository/SurveyRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/SurveyRepository.kt @@ -17,6 +17,7 @@ package com.google.android.ground.repository import com.google.android.ground.coroutines.ApplicationScope import com.google.android.ground.model.Survey +import com.google.android.ground.model.SurveyListItem import com.google.android.ground.model.User import com.google.android.ground.persistence.local.LocalValueStore import com.google.android.ground.persistence.local.stores.LocalSurveyStore @@ -118,25 +119,27 @@ constructor( activeSurvey = null } - fun getSurveyList(user: User): Flow>> = + fun getSurveyList(user: User): Flow> = @OptIn(ExperimentalCoroutinesApi::class) networkManager.networkStatusFlow.flatMapLatest { networkStatus -> if (networkStatus == NetworkStatus.AVAILABLE) { getRemoteSurveyList(user) } else { - getLocalSurveyList(user) + getLocalSurveyList() } } - private fun getRemoteSurveyList(user: User): Flow>> = + private fun getRemoteSurveyList(user: User): Flow> = remoteDataStore.getSurveyList(user).combine(localSurveysFlow) { remoteSurveys, localSurveys -> remoteSurveys.map { remoteSurvey -> - Pair(remoteSurvey, localSurveys.any { it.id == remoteSurvey.id }) + remoteSurvey.copy(availableOffline = localSurveys.any { it.id == remoteSurvey.id }) } } - private fun getLocalSurveyList(user: User): Flow>> = - localSurveysFlow.map { localSurveys -> localSurveys.map { survey -> Pair(survey, true) } } + private fun getLocalSurveyList(): Flow> = + localSurveysFlow.map { localSurveys -> + localSurveys.map { SurveyListItem(it.id, it.title, it.description, true) } + } /** Attempts to remove the locally synced survey. Doesn't throw an error if it doesn't exist. */ suspend fun removeOfflineSurvey(surveyId: String) { diff --git a/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveyListAdapter.kt b/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveyListAdapter.kt index d3513895de..5508da90b4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveyListAdapter.kt +++ b/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveyListAdapter.kt @@ -20,10 +20,11 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.google.android.ground.databinding.SurveyCardItemBinding +import com.google.android.ground.model.SurveyListItem import com.google.android.ground.ui.surveyselector.SurveyListAdapter.ViewHolder /** - * An implementation of [RecyclerView.Adapter] that associates [SurveyItem] data with the + * An implementation of [RecyclerView.Adapter] that associates [SurveyListItem] data with the * [ViewHolder] views. */ class SurveyListAdapter( @@ -31,7 +32,7 @@ class SurveyListAdapter( private val fragment: SurveySelectorFragment ) : RecyclerView.Adapter() { - private val surveys: MutableList = mutableListOf() + private val surveys: MutableList = mutableListOf() /** Creates a new [ViewHolder] item without any data. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -39,9 +40,9 @@ class SurveyListAdapter( return ViewHolder(binding) } - /** Binds [SurveyItem] data to [ViewHolder]. */ + /** Binds [SurveyListItem] data to [ViewHolder]. */ override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item: SurveyItem = surveys[position] + val item: SurveyListItem = surveys[position] holder.binding.item = item holder.binding.viewModel = viewModel holder.binding.fragment = fragment @@ -51,13 +52,13 @@ class SurveyListAdapter( override fun getItemCount() = surveys.size /** Overwrites existing cards. */ - fun updateData(newItemsList: List) { + fun updateData(newItemsList: List) { surveys.clear() surveys.addAll(newItemsList) notifyDataSetChanged() } - /** View item representing the [SurveyItem] data in the list. */ + /** View item representing the [SurveyListItem] data in the list. */ class ViewHolder(internal val binding: SurveyCardItemBinding) : RecyclerView.ViewHolder(binding.root) } diff --git a/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveySelectorViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveySelectorViewModel.kt index cb3b2ca864..4a26f457ae 100644 --- a/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveySelectorViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/surveyselector/SurveySelectorViewModel.kt @@ -19,7 +19,7 @@ import androidx.lifecycle.viewModelScope import com.google.android.ground.coroutines.ApplicationScope import com.google.android.ground.coroutines.IoDispatcher import com.google.android.ground.domain.usecases.survey.ActivateSurveyUseCase -import com.google.android.ground.model.Survey +import com.google.android.ground.model.SurveyListItem import com.google.android.ground.repository.SurveyRepository import com.google.android.ground.repository.UserRepository import com.google.android.ground.system.auth.AuthenticationManager @@ -29,7 +29,6 @@ import com.google.android.ground.ui.home.HomeScreenFragmentDirections import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map @@ -38,7 +37,6 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch /** Represents view state and behaviors of the survey selector dialog. */ -@OptIn(FlowPreview::class) class SurveySelectorViewModel @Inject internal constructor( @@ -52,7 +50,7 @@ internal constructor( ) : AbstractViewModel() { val surveyListState: MutableStateFlow = MutableStateFlow(null) - val surveySummaries: Flow> + val surveySummaries: Flow> init { surveySummaries = @@ -65,29 +63,12 @@ internal constructor( } } - /** Returns a flow of locally stored surveys. */ - private fun offlineSurveys(): Flow> = - surveyRepository.localSurveysFlow.onEach { setLoading() } - - /** Returns a flow of [SurveyItem] to be displayed to the user. */ - private fun getSurveyList(): Flow> = + /** Returns a flow of [SurveyListItem] to be displayed to the user. */ + private fun getSurveyList(): Flow> = surveyRepository .getSurveyList(authManager.currentUser) .onStart { setLoading() } - .map { surveys: List> -> - surveys - .map { createSurveyItem(it.first, it.second) } - .sortedBy { it.surveyTitle } - .sortedByDescending { it.isAvailableOffline } - } - - private fun createSurveyItem(survey: Survey, isAvailableOffline: Boolean): SurveyItem = - SurveyItem( - surveyId = survey.id, - surveyTitle = survey.title, - surveyDescription = survey.description, - isAvailableOffline = isAvailableOffline - ) + .map { surveys -> surveys.sortedBy { it.title }.sortedByDescending { it.availableOffline } } /** Triggers the specified survey to be loaded and activated. */ fun activateSurvey(surveyId: String) = diff --git a/ground/src/main/res/layout/survey_card_item.xml b/ground/src/main/res/layout/survey_card_item.xml index 55ed54ed13..2d52b0af65 100644 --- a/ground/src/main/res/layout/survey_card_item.xml +++ b/ground/src/main/res/layout/survey_card_item.xml @@ -23,7 +23,7 @@ + type="com.google.android.ground.model.SurveyListItem" /> @@ -42,7 +42,7 @@ style="@style/Widget.App.CardView.SurfaceContainerLowest" android:clickable="true" android:focusable="true" - android:onClick="@{() -> viewModel.activateSurvey(item.surveyId)}" + android:onClick="@{() -> viewModel.activateSurvey(item.id)}" android:orientation="vertical"> + app:visible="@{item.availableOffline}" />