Skip to content

Commit 809e729

Browse files
Refactor JobMapComponentState (#3653)
* refactor JobMapComponentState into a sealed class with the different mutually exclusive states * extract JobSelectionModal state * update tests * move logic deciding OnAddLoiButtonClicked to the VM * fix failing tests * fix code format * add more tests
1 parent ff74695 commit 809e729

File tree

8 files changed

+229
-130
lines changed

8 files changed

+229
-130
lines changed

app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ constructor(
107107
}
108108
.stateIn(viewModelScope, SharingStarted.Lazily, MapFloatingActionButtonType.LocationNotLocked)
109109

110-
private val _shouldShowMapActions: MutableStateFlow<Boolean> = MutableStateFlow(false)
110+
private val _shouldShowMapActions: MutableStateFlow<Boolean> = MutableStateFlow(true)
111111
val shouldShowMapActions: StateFlow<Boolean> = _shouldShowMapActions
112112

113113
val showMapTypeSelector: MutableStateFlow<Boolean> = MutableStateFlow(false)

app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,28 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() {
185185
action: JobMapComponentAction,
186186
) {
187187
when (action) {
188-
is JobMapComponentAction.OnAddDataClicked -> onCollectData(action.selectedLoi)
189-
is JobMapComponentAction.OnDeleteSiteClicked -> onDeleteSite(action.selectedLoi)
190-
JobMapComponentAction.OnJobCardDismissed ->
188+
is JobMapComponentAction.OnAddDataClicked -> {
189+
onCollectData(action.selectedLoi)
190+
}
191+
is JobMapComponentAction.OnDeleteSiteClicked -> {
192+
onDeleteSite(action.selectedLoi)
193+
}
194+
JobMapComponentAction.OnJobCardDismissed -> {
191195
mapContainerViewModel.selectLocationOfInterest(null)
192-
is JobMapComponentAction.OnJobSelected ->
193-
jobMapComponentState.adHocDataCollectionButtonData
194-
.firstOrNull { it.job == action.job }
195-
?.let { onCollectData(it) }
196-
is JobMapComponentAction.OnJobSelectionModalVisibilityChanged ->
197-
mapContainerViewModel.onJobSelectionModalVisibilityChanged(action.isShown)
196+
}
197+
is JobMapComponentAction.OnJobSelected -> {
198+
mapContainerViewModel.setJobSelectionModalVisibility(false)
199+
val jobs =
200+
(jobMapComponentState as? JobMapComponentState.AddLoiButton)?.jobs
201+
?: (jobMapComponentState as? JobMapComponentState.JobSelectionModal)?.jobs
202+
jobs?.firstOrNull { it.job == action.job }?.let { onCollectData(it) }
203+
}
204+
is JobMapComponentAction.OnAddLoiButtonClicked -> {
205+
mapContainerViewModel.resolveAddLoiAction(jobMapComponentState)?.let { onCollectData(it) }
206+
}
207+
JobMapComponentAction.OnJobSelectionModalDismissed -> {
208+
mapContainerViewModel.setJobSelectionModalVisibility(false)
209+
}
198210
}
199211
}
200212

app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerScreen.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,8 @@ private fun HomeScreenMapContainerScreenPreview() {
135135
HomeScreenMapContainerScreen(
136136
locationLockButtonType = MapFloatingActionButtonType.LocationNotLocked,
137137
jobComponentState =
138-
JobMapComponentState(
139-
selectedLoi = null,
140-
adHocDataCollectionButtonData =
138+
JobMapComponentState.AddLoiButton(
139+
jobs =
141140
listOf(
142141
AdHocDataCollectionButtonData(
143142
canCollectData = true,
@@ -149,7 +148,7 @@ private fun HomeScreenMapContainerScreenPreview() {
149148
tasks = emptyMap(),
150149
),
151150
)
152-
),
151+
)
153152
),
154153
shouldShowMapActions = true,
155154
shouldShowRecenter = true,

app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ import org.groundplatform.android.system.SettingsManager
4949
import org.groundplatform.android.ui.common.BaseMapViewModel
5050
import org.groundplatform.android.ui.common.SharedViewModel
5151
import org.groundplatform.android.ui.home.mapcontainer.jobs.AdHocDataCollectionButtonData
52-
import org.groundplatform.android.ui.home.mapcontainer.jobs.DataCollectionEntryPointData
5352
import org.groundplatform.android.ui.home.mapcontainer.jobs.JobMapComponentState
5453
import org.groundplatform.android.ui.home.mapcontainer.jobs.SelectedLoiSheetData
5554
import org.groundplatform.android.ui.map.Feature
@@ -125,6 +124,8 @@ internal constructor(
125124
*/
126125
private val adHocLoiJobs: Flow<List<Job>>
127126

127+
private val showJobSelectionModal = MutableStateFlow(false)
128+
128129
/** Emits whether the current zoom has crossed the zoomed-in threshold or not to cluster LOIs. */
129130
private val isZoomedInFlow: Flow<Boolean>
130131

@@ -169,12 +170,11 @@ internal constructor(
169170
}
170171

171172
jobMapComponentState =
172-
processDataCollectionEntryPoints()
173-
.map { (loiCard, jobCards) -> JobMapComponentState(loiCard, jobCards) }
173+
processJobMapComponentState()
174174
.stateIn(
175175
scope = viewModelScope,
176176
started = SharingStarted.Lazily,
177-
initialValue = JobMapComponentState(),
177+
initialValue = JobMapComponentState.Hidden,
178178
)
179179
}
180180

@@ -196,13 +196,16 @@ internal constructor(
196196
fun getDataSharingTerms(): Result<Survey.DataSharingTerms?> = getDataSharingTermsUseCase()
197197

198198
/**
199-
* Returns a flow of [DataCollectionEntryPointData] associated with the active survey's LOIs and
200-
* adhoc jobs for displaying the cards.
199+
* Returns a flow of [JobMapComponentState] associated with the active survey's LOIs and adhoc
200+
* jobs for displaying the cards.
201201
*/
202202
@VisibleForTesting
203-
fun processDataCollectionEntryPoints():
204-
Flow<Pair<SelectedLoiSheetData?, List<AdHocDataCollectionButtonData>>> =
205-
combine(loisInViewport, featureClicked, adHocLoiJobs) { loisInView, feature, jobs ->
203+
fun processJobMapComponentState(): Flow<JobMapComponentState> =
204+
combine(loisInViewport, featureClicked, adHocLoiJobs, showJobSelectionModal) {
205+
loisInView,
206+
feature,
207+
jobs,
208+
isModalShown ->
206209
val canUserSubmitData = userRepository.canUserSubmitData()
207210
val loiCard =
208211
loisInView
@@ -216,15 +219,45 @@ internal constructor(
216219
showDeleteLoiButton = canDelete,
217220
)
218221
}
222+
219223
if (loiCard == null && feature != null) {
220224
// The feature is not in view anymore.
221225
featureClicked.value = null
222226
}
223-
val jobCard = jobs.map {
227+
228+
val jobCards = jobs.map {
224229
AdHocDataCollectionButtonData(canCollectData = canUserSubmitData, job = it)
225230
}
226-
Pair(loiCard, jobCard)
231+
232+
when {
233+
loiCard != null -> JobMapComponentState.LoiSelected(loiCard)
234+
isModalShown && jobCards.isNotEmpty() -> JobMapComponentState.JobSelectionModal(jobCards)
235+
jobCards.isNotEmpty() -> JobMapComponentState.AddLoiButton(jobCards)
236+
else -> JobMapComponentState.Hidden
237+
}
238+
}
239+
240+
fun setJobSelectionModalVisibility(isVisible: Boolean) {
241+
showJobSelectionModal.value = isVisible
242+
onJobSelectionModalVisibilityChanged(isVisible)
243+
}
244+
245+
/**
246+
* Resolves the result of an "Add LOI" button click based on the current UI state.
247+
*
248+
* @return The single available [AdHocDataCollectionButtonData], or `null` if a selection modal
249+
* should be shown or the action is not applicable.
250+
*/
251+
fun resolveAddLoiAction(currentState: JobMapComponentState): AdHocDataCollectionButtonData? {
252+
val state = currentState as? JobMapComponentState.AddLoiButton ?: return null
253+
254+
return if (state.jobs.size > 1) {
255+
setJobSelectionModalVisibility(true)
256+
null
257+
} else {
258+
state.jobs.firstOrNull()
227259
}
260+
}
228261

229262
private fun updatedLoiSelectedStates(
230263
features: Set<Feature>,

app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt

Lines changed: 37 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ import androidx.compose.foundation.layout.windowInsetsPadding
2626
import androidx.compose.material.icons.Icons
2727
import androidx.compose.material.icons.filled.Add
2828
import androidx.compose.runtime.Composable
29-
import androidx.compose.runtime.LaunchedEffect
30-
import androidx.compose.runtime.getValue
31-
import androidx.compose.runtime.mutableStateOf
32-
import androidx.compose.runtime.saveable.rememberSaveable
33-
import androidx.compose.runtime.setValue
3429
import androidx.compose.ui.Alignment
3530
import androidx.compose.ui.Modifier
3631
import androidx.compose.ui.res.stringResource
@@ -47,45 +42,29 @@ import org.groundplatform.ui.theme.AppTheme
4742

4843
@Composable
4944
fun JobMapComponent(state: JobMapComponentState, onAction: (JobMapComponentAction) -> Unit) {
50-
var showJobSelectionModal by rememberSaveable { mutableStateOf(false) }
51-
LaunchedEffect(showJobSelectionModal) {
52-
onAction(JobMapComponentAction.OnJobSelectionModalVisibilityChanged(showJobSelectionModal))
53-
}
54-
55-
state.adHocDataCollectionButtonData
56-
.takeIf { it.isNotEmpty() }
57-
?.let { data ->
58-
if (showJobSelectionModal) {
59-
JobSelectionModal(
60-
jobs = data.map { it.job },
61-
onJobClicked = { job ->
62-
showJobSelectionModal = false
63-
onAction(OnJobSelected(job))
64-
},
65-
onDismiss = { showJobSelectionModal = false },
66-
)
67-
} else {
68-
AddLoiButton(
69-
onClick = {
70-
if (data.size > 1) {
71-
showJobSelectionModal = true
72-
} else {
73-
onAction(OnJobSelected(data.first().job))
74-
}
75-
}
76-
)
77-
}
45+
when (state) {
46+
is JobMapComponentState.LoiSelected -> {
47+
LoiJobSheet(
48+
loi = state.loi.loi,
49+
onCollectClicked = { onAction(OnAddDataClicked(state.loi)) },
50+
onDeleteClicked = { onAction(OnDeleteSiteClicked(state.loi)) },
51+
onDismiss = { onAction(JobMapComponentAction.OnJobCardDismissed) },
52+
canUserSubmitData = state.loi.canCollectData,
53+
submissionCount = state.loi.submissionCount,
54+
showDeleteLoiButton = state.loi.showDeleteLoiButton,
55+
)
7856
}
79-
state.selectedLoi?.let { loi ->
80-
LoiJobSheet(
81-
loi = loi.loi,
82-
canUserSubmitData = loi.canCollectData,
83-
submissionCount = loi.submissionCount,
84-
showDeleteLoiButton = loi.showDeleteLoiButton,
85-
onCollectClicked = { onAction(OnAddDataClicked(loi)) },
86-
onDeleteClicked = { onAction(OnDeleteSiteClicked(loi)) },
87-
onDismiss = { onAction(JobMapComponentAction.OnJobCardDismissed) },
88-
)
57+
is JobMapComponentState.AddLoiButton -> {
58+
AddLoiButton(onClick = { onAction(JobMapComponentAction.OnAddLoiButtonClicked) })
59+
}
60+
is JobMapComponentState.JobSelectionModal -> {
61+
JobSelectionModal(
62+
jobs = state.jobs.map { it.job },
63+
onJobClicked = { job -> onAction(OnJobSelected(job)) },
64+
onDismiss = { onAction(JobMapComponentAction.OnJobSelectionModalDismissed) },
65+
)
66+
}
67+
is JobMapComponentState.Hidden -> {}
8968
}
9069
}
9170

@@ -104,13 +83,20 @@ private fun AddLoiButton(onClick: () -> Unit) {
10483
}
10584
}
10685

107-
data class JobMapComponentState(
108-
val selectedLoi: SelectedLoiSheetData? = null,
109-
val adHocDataCollectionButtonData: List<AdHocDataCollectionButtonData> = emptyList(),
110-
)
86+
sealed interface JobMapComponentState {
87+
data class LoiSelected(val loi: SelectedLoiSheetData) : JobMapComponentState
88+
89+
data class AddLoiButton(val jobs: List<AdHocDataCollectionButtonData>) : JobMapComponentState
90+
91+
data class JobSelectionModal(val jobs: List<AdHocDataCollectionButtonData>) : JobMapComponentState
92+
93+
data object Hidden : JobMapComponentState
94+
}
11195

11296
sealed interface JobMapComponentAction {
113-
data class OnJobSelectionModalVisibilityChanged(val isShown: Boolean) : JobMapComponentAction
97+
data object OnAddLoiButtonClicked : JobMapComponentAction
98+
99+
data object OnJobSelectionModalDismissed : JobMapComponentAction
114100

115101
data class OnJobSelected(val job: Job) : JobMapComponentAction
116102

@@ -128,9 +114,8 @@ private fun JobMapComponentPreview() {
128114
AppTheme {
129115
JobMapComponent(
130116
state =
131-
JobMapComponentState(
132-
selectedLoi = null,
133-
adHocDataCollectionButtonData =
117+
JobMapComponentState.AddLoiButton(
118+
jobs =
134119
listOf(
135120
AdHocDataCollectionButtonData(
136121
canCollectData = true,
@@ -142,7 +127,7 @@ private fun JobMapComponentPreview() {
142127
tasks = emptyMap(),
143128
),
144129
)
145-
),
130+
)
146131
)
147132
) {}
148133
}

app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerScreenTest.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,24 +148,23 @@ class HomeScreenMapContainerScreenTest {
148148
val performedActions = mutableListOf<JobMapComponentAction>()
149149
setContent(
150150
jobComponentState =
151-
JobMapComponentState(
152-
adHocDataCollectionButtonData =
153-
listOf(AdHocDataCollectionButtonData(canCollectData = true, job = ADHOC_JOB))
151+
JobMapComponentState.AddLoiButton(
152+
jobs = listOf(AdHocDataCollectionButtonData(canCollectData = true, job = ADHOC_JOB))
154153
),
155154
onJobComponentAction = { performedActions += it },
156155
)
157156

158157
composeTestRule.onNodeWithContentDescription(getString(R.string.add_site)).performClick()
159158

160-
assertTrue(performedActions.last() is JobMapComponentAction.OnJobSelected)
159+
assertTrue(performedActions.last() is JobMapComponentAction.OnAddLoiButtonClicked)
161160
}
162161

163162
private fun setContent(
164163
locationLockButtonType: MapFloatingActionButtonType =
165164
MapFloatingActionButtonType.LocationNotLocked,
166165
shouldShowMapActions: Boolean = true,
167166
shouldShowRecenterButton: Boolean = true,
168-
jobComponentState: JobMapComponentState = JobMapComponentState(),
167+
jobComponentState: JobMapComponentState = JobMapComponentState.Hidden,
169168
onBaseMapAction: (BaseMapAction) -> Unit = {},
170169
onJobComponentAction: (JobMapComponentAction) -> Unit = {},
171170
) {

0 commit comments

Comments
 (0)