Skip to content

Commit 9a2d3d2

Browse files
add button states to task viewModels and add onclick listener handlers
1 parent 8b40d6e commit 9a2d3d2

File tree

15 files changed

+388
-14
lines changed

15 files changed

+388
-14
lines changed

app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,13 @@ internal constructor(
205205

206206
viewModel?.let { created ->
207207
val taskData = if (shouldLoadFromDraft) getValueFromDraft(state.job, task) else null
208-
created.initialize(state.job, task, taskData)
208+
created.initialize(
209+
job = state.job,
210+
task = task,
211+
taskData = taskData,
212+
isFirstPosition = { isFirstPosition(task.id) },
213+
isLastPosition = { isLastPositionWithValue(task, it) },
214+
)
209215
updateDataAndInvalidateTasks(task, taskData)
210216
taskViewModels.value[task.id] = created
211217
}

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractMapTaskViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import org.groundplatform.android.model.map.CameraPosition
2222
import org.groundplatform.android.ui.common.BaseMapViewModel
2323

2424
/** Defines the state of an inflated Map [Task] and controls its UI. */
25-
open class AbstractMapTaskViewModel internal constructor() : AbstractTaskViewModel() {
25+
abstract class AbstractMapTaskViewModel internal constructor() : AbstractTaskViewModel() {
2626

2727
/** Allows control for triggering the location lock programmatically. */
2828
private val _enableLocationLockFlow = MutableStateFlow(LocationLockEnabledState.UNKNOWN)

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskFragment.kt

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,14 +240,15 @@ abstract class AbstractTaskFragment<T : AbstractTaskViewModel> : AbstractFragmen
240240
Column(modifier = Modifier.fillMaxWidth()) {
241241
HeaderCard()
242242
Spacer(Modifier.height(12.dp))
243-
ActionButtonsRow()
243+
Footer()
244244
}
245245
} else {
246-
ActionButtonsRow()
246+
Footer()
247247
}
248248
}
249249
}
250250

251+
@Suppress("UnusedPrivateMember") // andreia: revert this later
251252
@Composable
252253
private fun ActionButtonsRow() {
253254
Row(
@@ -258,6 +259,33 @@ abstract class AbstractTaskFragment<T : AbstractTaskViewModel> : AbstractFragmen
258259
}
259260
}
260261

262+
@Composable
263+
private fun Footer() {
264+
val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle()
265+
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
266+
taskActionButtonsStates.forEach { state ->
267+
if (state.isVisible) { // andreia:review
268+
org.groundplatform.android.ui.datacollection.components.refactor.TaskButton(
269+
state = state,
270+
onClick = { handleButtonClick(state.action) },
271+
)
272+
}
273+
}
274+
}
275+
}
276+
277+
private fun handleButtonClick(action: ButtonAction) {
278+
when (action) {
279+
// Navigation actions
280+
ButtonAction.PREVIOUS -> moveToPrevious()
281+
ButtonAction.NEXT,
282+
ButtonAction.DONE -> handleNext()
283+
ButtonAction.SKIP -> onSkip()
284+
// Task-specific actions - delegate to ViewModel
285+
else -> viewModel.onButtonClick(action)
286+
}
287+
}
288+
261289
// This function can allow any task to show a Header card on top of the Button row.
262290
open fun shouldShowHeader() = false
263291

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,36 @@ import org.groundplatform.android.R
2323
import org.groundplatform.android.model.job.Job
2424
import org.groundplatform.android.model.submission.SkippedTaskData
2525
import org.groundplatform.android.model.submission.TaskData
26+
import org.groundplatform.android.model.submission.isNotNullOrEmpty
2627
import org.groundplatform.android.model.submission.isNullOrEmpty
2728
import org.groundplatform.android.model.task.Task
2829
import org.groundplatform.android.ui.common.AbstractViewModel
30+
import org.groundplatform.android.ui.datacollection.components.ButtonAction
31+
import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState
2932

3033
/** Defines the state of an inflated [Task] and controls its UI. */
31-
open class AbstractTaskViewModel internal constructor() : AbstractViewModel() {
34+
abstract class AbstractTaskViewModel internal constructor() : AbstractViewModel() {
3235

3336
/** Current value. */
3437
private val _taskDataFlow: MutableStateFlow<TaskData?> = MutableStateFlow(null)
3538
val taskTaskData: StateFlow<TaskData?> = _taskDataFlow.asStateFlow()
3639

40+
abstract val taskActionButtonStates: StateFlow<List<ButtonActionState>>
41+
3742
lateinit var task: Task
43+
private lateinit var isFirstPosition: () -> Boolean
44+
private lateinit var isLastPositionWithValue: (TaskData?) -> Boolean
3845

39-
open fun initialize(job: Job, task: Task, taskData: TaskData?) {
46+
open fun initialize(
47+
job: Job,
48+
task: Task,
49+
taskData: TaskData?,
50+
isFirstPosition: () -> Boolean,
51+
isLastPosition: (TaskData?) -> Boolean,
52+
) {
4053
this.task = task
54+
this.isFirstPosition = isFirstPosition
55+
this.isLastPositionWithValue = isLastPosition
4156
setValue(taskData)
4257
}
4358

@@ -74,4 +89,50 @@ open class AbstractTaskViewModel internal constructor() : AbstractViewModel() {
7489
fun isTaskOptional(): Boolean = !task.isRequired
7590

7691
fun hasNoData(): Boolean = taskTaskData.value.isNullOrEmpty()
92+
93+
fun getPreviousButtonState(): ButtonActionState =
94+
ButtonActionState(
95+
action = ButtonAction.PREVIOUS,
96+
isEnabled = !isFirstPosition(),
97+
isVisible = true,
98+
)
99+
100+
fun getNextButtonState(taskData: TaskData?, hideIfEmpty: Boolean = false): ButtonActionState {
101+
val isVisible = if (hideIfEmpty) taskData.isNotNullOrEmpty() else true
102+
return if (isLastPositionWithValue(taskData)) {
103+
ButtonActionState(
104+
action = ButtonAction.DONE,
105+
isEnabled = taskData.isNotNullOrEmpty(),
106+
isVisible = isVisible,
107+
)
108+
} else {
109+
ButtonActionState(
110+
action = ButtonAction.NEXT,
111+
isEnabled = taskData.isNotNullOrEmpty(),
112+
isVisible = isVisible,
113+
)
114+
}
115+
}
116+
117+
fun getSkipButtonState(taskData: TaskData?): ButtonActionState =
118+
ButtonActionState(
119+
action = ButtonAction.SKIP,
120+
isEnabled = isTaskOptional(),
121+
isVisible = isTaskOptional() && taskData.isNullOrEmpty(),
122+
)
123+
124+
fun getUndoButtonState(taskData: TaskData?): ButtonActionState =
125+
ButtonActionState(
126+
action = ButtonAction.UNDO,
127+
isEnabled = taskData.isNotNullOrEmpty(),
128+
isVisible = taskData.isNotNullOrEmpty(),
129+
)
130+
131+
open fun onButtonClick(action: ButtonAction) {
132+
if (action == ButtonAction.UNDO) {
133+
clearResponse()
134+
} else {
135+
// Subclasses handle other actions
136+
}
137+
}
77138
}

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskViewModel.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,23 @@
1515
*/
1616
package org.groundplatform.android.ui.datacollection.tasks.date
1717

18+
import androidx.lifecycle.viewModelScope
1819
import java.util.Date
1920
import javax.inject.Inject
21+
import kotlinx.coroutines.flow.SharingStarted
22+
import kotlinx.coroutines.flow.StateFlow
23+
import kotlinx.coroutines.flow.map
24+
import kotlinx.coroutines.flow.stateIn
2025
import org.groundplatform.android.model.submission.DateTimeTaskData.Companion.fromMillis
26+
import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState
2127
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel
2228

2329
class DateTaskViewModel @Inject constructor() : AbstractTaskViewModel() {
30+
override val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
31+
taskTaskData
32+
.map { listOf(getPreviousButtonState(), getSkipButtonState(it), getNextButtonState(it)) }
33+
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
34+
}
2435

2536
fun updateResponse(date: Date) {
2637
setValue(fromMillis(date.time))

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskViewModel.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,20 @@
1515
*/
1616
package org.groundplatform.android.ui.datacollection.tasks.instruction
1717

18+
import androidx.lifecycle.viewModelScope
1819
import javax.inject.Inject
20+
import kotlinx.coroutines.flow.SharingStarted
21+
import kotlinx.coroutines.flow.StateFlow
22+
import kotlinx.coroutines.flow.map
23+
import kotlinx.coroutines.flow.stateIn
24+
import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState
1925
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel
2026

2127
@Suppress("EmptyClassBlock")
22-
class InstructionTaskViewModel @Inject constructor() : AbstractTaskViewModel() {}
28+
class InstructionTaskViewModel @Inject constructor() : AbstractTaskViewModel() {
29+
override val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
30+
taskTaskData
31+
.map { listOf(getPreviousButtonState(), getNextButtonState(it)) }
32+
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
33+
}
34+
}

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,26 @@
1616
package org.groundplatform.android.ui.datacollection.tasks.location
1717

1818
import android.location.Location
19+
import androidx.lifecycle.viewModelScope
1920
import javax.inject.Inject
21+
import kotlin.lazy
2022
import kotlinx.coroutines.flow.Flow
2123
import kotlinx.coroutines.flow.MutableStateFlow
24+
import kotlinx.coroutines.flow.SharingStarted
25+
import kotlinx.coroutines.flow.StateFlow
2226
import kotlinx.coroutines.flow.asStateFlow
27+
import kotlinx.coroutines.flow.combine
28+
import kotlinx.coroutines.flow.distinctUntilChanged
2329
import kotlinx.coroutines.flow.map
30+
import kotlinx.coroutines.flow.stateIn
2431
import kotlinx.coroutines.flow.update
2532
import org.groundplatform.android.common.Constants.ACCURACY_THRESHOLD_IN_M
2633
import org.groundplatform.android.model.geometry.Point
2734
import org.groundplatform.android.model.submission.CaptureLocationTaskData
35+
import org.groundplatform.android.model.submission.TaskData
36+
import org.groundplatform.android.model.submission.isNullOrEmpty
37+
import org.groundplatform.android.ui.datacollection.components.ButtonAction
38+
import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState
2839
import org.groundplatform.android.ui.datacollection.tasks.AbstractMapTaskViewModel
2940
import org.groundplatform.android.ui.datacollection.tasks.LocationLockEnabledState
3041
import org.groundplatform.android.ui.map.gms.getAccuracyOrNull
@@ -42,6 +53,20 @@ class CaptureLocationTaskViewModel @Inject constructor() : AbstractMapTaskViewMo
4253
location != null && accuracy <= ACCURACY_THRESHOLD_IN_M
4354
}
4455

56+
override val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
57+
combine(isCaptureEnabled, taskTaskData) { captureEnabled, taskData ->
58+
listOf(
59+
getPreviousButtonState(),
60+
getSkipButtonState(taskData),
61+
getUndoButtonState(taskData),
62+
getCaptureLocationButtonState(captureEnabled, taskData),
63+
getNextButtonState(taskData, hideIfEmpty = true),
64+
)
65+
}
66+
.distinctUntilChanged()
67+
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
68+
}
69+
4570
fun updateLocation(location: Location) {
4671
_lastLocation.update { location }
4772
}
@@ -64,4 +89,22 @@ class CaptureLocationTaskViewModel @Inject constructor() : AbstractMapTaskViewMo
6489
)
6590
}
6691
}
92+
93+
private fun getCaptureLocationButtonState(
94+
captureEnabled: Boolean,
95+
taskData: TaskData?,
96+
): ButtonActionState =
97+
ButtonActionState(
98+
action = ButtonAction.CAPTURE_LOCATION,
99+
isEnabled = captureEnabled,
100+
isVisible = taskData.isNullOrEmpty(),
101+
)
102+
103+
override fun onButtonClick(action: ButtonAction) {
104+
if (action == ButtonAction.CAPTURE_LOCATION) {
105+
updateResponse()
106+
} else {
107+
super.onButtonClick(action)
108+
}
109+
}
67110
}

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskViewModel.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
*/
1616
package org.groundplatform.android.ui.datacollection.tasks.multiplechoice
1717

18+
import androidx.lifecycle.viewModelScope
1819
import javax.inject.Inject
1920
import kotlinx.coroutines.flow.MutableStateFlow
21+
import kotlinx.coroutines.flow.SharingStarted
2022
import kotlinx.coroutines.flow.StateFlow
23+
import kotlinx.coroutines.flow.map
24+
import kotlinx.coroutines.flow.stateIn
2125
import kotlinx.coroutines.flow.update
2226
import org.groundplatform.android.R
2327
import org.groundplatform.android.common.Constants
@@ -28,18 +32,31 @@ import org.groundplatform.android.model.submission.TaskData
2832
import org.groundplatform.android.model.task.MultipleChoice.Cardinality.SELECT_MULTIPLE
2933
import org.groundplatform.android.model.task.Option
3034
import org.groundplatform.android.model.task.Task
35+
import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState
3136
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel
3237

3338
class MultipleChoiceTaskViewModel @Inject constructor() : AbstractTaskViewModel() {
3439

40+
override val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
41+
taskTaskData
42+
.map { listOf(getPreviousButtonState(), getSkipButtonState(it), getNextButtonState(it)) }
43+
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
44+
}
45+
3546
private val _items: MutableStateFlow<List<MultipleChoiceItem>> = MutableStateFlow(emptyList())
3647
val items: StateFlow<List<MultipleChoiceItem>> = _items
3748

3849
private val selectedIds: MutableSet<String> = mutableSetOf()
3950
private var otherText: String = ""
4051

41-
override fun initialize(job: Job, task: Task, taskData: TaskData?) {
42-
super.initialize(job, task, taskData)
52+
override fun initialize(
53+
job: Job,
54+
task: Task,
55+
taskData: TaskData?,
56+
isFirstPosition: () -> Boolean,
57+
isLastPosition: (TaskData?) -> Boolean,
58+
) {
59+
super.initialize(job, task, taskData, isFirstPosition, isLastPosition)
4360
loadPendingSelections()
4461
updateMultipleChoiceItems()
4562
}

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskViewModel.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,26 @@ package org.groundplatform.android.ui.datacollection.tasks.number
1717

1818
import androidx.lifecycle.LiveData
1919
import androidx.lifecycle.asLiveData
20+
import androidx.lifecycle.viewModelScope
2021
import javax.inject.Inject
22+
import kotlinx.coroutines.flow.SharingStarted
23+
import kotlinx.coroutines.flow.StateFlow
2124
import kotlinx.coroutines.flow.filterIsInstance
2225
import kotlinx.coroutines.flow.map
26+
import kotlinx.coroutines.flow.stateIn
2327
import org.groundplatform.android.model.submission.NumberTaskData
2428
import org.groundplatform.android.model.submission.TaskData
29+
import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState
2530
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel
2631

2732
class NumberTaskViewModel @Inject constructor() : AbstractTaskViewModel() {
2833

34+
override val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
35+
taskTaskData
36+
.map { listOf(getPreviousButtonState(), getSkipButtonState(it), getNextButtonState(it)) }
37+
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
38+
}
39+
2940
/** Transcoded text to be displayed for the current [TaskData]. */
3041
val responseText: LiveData<String> =
3142
taskTaskData.filterIsInstance<NumberTaskData?>().map { it?.number ?: "" }.asLiveData()

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,37 @@ import androidx.lifecycle.viewModelScope
2222
import java.io.IOException
2323
import javax.inject.Inject
2424
import kotlinx.coroutines.Dispatchers
25+
import kotlinx.coroutines.flow.SharingStarted
26+
import kotlinx.coroutines.flow.StateFlow
2527
import kotlinx.coroutines.flow.filterIsInstance
2628
import kotlinx.coroutines.flow.map
29+
import kotlinx.coroutines.flow.stateIn
2730
import kotlinx.coroutines.launch
2831
import kotlinx.coroutines.withContext
2932
import org.groundplatform.android.data.remote.firebase.FirebaseStorageManager
3033
import org.groundplatform.android.model.submission.PhotoTaskData
3134
import org.groundplatform.android.model.submission.isNotNullOrEmpty
3235
import org.groundplatform.android.repository.UserMediaRepository
36+
import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState
3337
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel
3438
import timber.log.Timber
3539

3640
class PhotoTaskViewModel @Inject constructor(private val userMediaRepository: UserMediaRepository) :
3741
AbstractTaskViewModel() {
3842

43+
override val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
44+
taskTaskData
45+
.map {
46+
listOf(
47+
getPreviousButtonState(),
48+
getUndoButtonState(it),
49+
getSkipButtonState(it),
50+
getNextButtonState(it),
51+
)
52+
}
53+
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
54+
}
55+
3956
/**
4057
* Task id waiting for a photo result. As only one photo result is returned at a time, we can
4158
* directly map it 1:1 with the task waiting for a photo result.

0 commit comments

Comments
 (0)