Skip to content

Commit

Permalink
Refactor SurveyListItem
Browse files Browse the repository at this point in the history
  • Loading branch information
gino-m committed Oct 30, 2023
1 parent 01513bb commit 9647e4f
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<List<Survey>>
fun getSurveyList(user: User): Flow<List<SurveyListItem>>

/**
* Loads the survey with the specified id from the remote data store. Returns `null` if the survey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -67,8 +69,13 @@ internal constructor(
override suspend fun loadTermsOfService(): TermsOfService? =
withContext(ioDispatcher) { db().termsOfService().terms().get() }

override fun getSurveyList(user: User): Flow<List<Survey>> = flow {
emitAll(db().surveys().getReadable(user))
override fun getSurveyList(user: User): Flow<List<SurveyListItem>> = 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) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -118,25 +119,27 @@ constructor(
activeSurvey = null
}

fun getSurveyList(user: User): Flow<List<Pair<Survey, Boolean>>> =
fun getSurveyList(user: User): Flow<List<SurveyListItem>> =
@OptIn(ExperimentalCoroutinesApi::class)
networkManager.networkStatusFlow.flatMapLatest { networkStatus ->
if (networkStatus == NetworkStatus.AVAILABLE) {
getRemoteSurveyList(user)
} else {
getLocalSurveyList(user)
getLocalSurveyList()
}
}

private fun getRemoteSurveyList(user: User): Flow<List<Pair<Survey, Boolean>>> =
private fun getRemoteSurveyList(user: User): Flow<List<SurveyListItem>> =
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<List<Pair<Survey, Boolean>>> =
localSurveysFlow.map { localSurveys -> localSurveys.map { survey -> Pair(survey, true) } }
private fun getLocalSurveyList(): Flow<List<SurveyListItem>> =
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,29 @@ 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(
private val viewModel: SurveySelectorViewModel,
private val fragment: SurveySelectorFragment
) : RecyclerView.Adapter<ViewHolder>() {

private val surveys: MutableList<SurveyItem> = mutableListOf()
private val surveys: MutableList<SurveyListItem> = mutableListOf()

/** Creates a new [ViewHolder] item without any data. */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = SurveyCardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
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
Expand All @@ -51,13 +52,13 @@ class SurveyListAdapter(
override fun getItemCount() = surveys.size

/** Overwrites existing cards. */
fun updateData(newItemsList: List<SurveyItem>) {
fun updateData(newItemsList: List<SurveyListItem>) {
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -52,7 +50,7 @@ internal constructor(
) : AbstractViewModel() {

val surveyListState: MutableStateFlow<State?> = MutableStateFlow(null)
val surveySummaries: Flow<List<SurveyItem>>
val surveySummaries: Flow<List<SurveyListItem>>

init {
surveySummaries =
Expand All @@ -65,29 +63,12 @@ internal constructor(
}
}

/** Returns a flow of locally stored surveys. */
private fun offlineSurveys(): Flow<List<Survey>> =
surveyRepository.localSurveysFlow.onEach { setLoading() }

/** Returns a flow of [SurveyItem] to be displayed to the user. */
private fun getSurveyList(): Flow<List<SurveyItem>> =
/** Returns a flow of [SurveyListItem] to be displayed to the user. */
private fun getSurveyList(): Flow<List<SurveyListItem>> =
surveyRepository
.getSurveyList(authManager.currentUser)
.onStart { setLoading() }
.map { surveys: List<Pair<Survey, Boolean>> ->
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) =
Expand Down
14 changes: 7 additions & 7 deletions ground/src/main/res/layout/survey_card_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<data>
<variable
name="item"
type="com.google.android.ground.ui.surveyselector.SurveyItem" />
type="com.google.android.ground.model.SurveyListItem" />
<variable
name="fragment"
type="com.google.android.ground.ui.surveyselector.SurveySelectorFragment" />
Expand All @@ -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">

<androidx.constraintlayout.widget.ConstraintLayout
Expand All @@ -58,7 +58,7 @@
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="@{item.surveyTitle}"
android:text="@{item.title}"
app:layout_constraintEnd_toStartOf="@id/survey_card_item_widgets"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
Expand All @@ -74,7 +74,7 @@
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:text="@{item.surveyDescription}"
android:text="@{item.description}"
app:layout_constraintLeft_toLeftOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
Expand All @@ -101,7 +101,7 @@
app:layout_constraintEnd_toStartOf="@+id/overflowMenu"
app:layout_constraintTop_toTopOf="@id/overflowMenu"
app:srcCompat="@drawable/ic_offline_pin"
app:visible="@{item.isAvailableOffline}" />
app:visible="@{item.availableOffline}" />

<!-- Overflow menu "..." affordance. -->
<Button
Expand All @@ -110,8 +110,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:translationX="4dp"
android:onClick="@{(view) -> fragment.showPopupMenu(view, item.surveyId)}"
app:visible="@{item.isAvailableOffline}"
android:onClick="@{(view) -> fragment.showPopupMenu(view, item.id)}"
app:visible="@{item.availableOffline}"
app:icon="@drawable/ic_more_vert" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Expand Down

0 comments on commit 9647e4f

Please sign in to comment.