Skip to content

Commit 2e4827c

Browse files
sorenoidSoren Roth
and
Soren Roth
authored
use datetime raw value (#184)
* expect a formatted value to be UTC * make BaseFieldState generic * make DateTimeFieldState vary with Instant? make DateTimeField work with Instant, not Long. * only support date time text field value changes that clear the text. * PR review suggestions * fuse usage of formattedValue and value into one type safe generic function --------- Co-authored-by: Soren Roth <[email protected]>
1 parent 1d67e28 commit 2e4827c

File tree

16 files changed

+111
-144
lines changed

16 files changed

+111
-144
lines changed

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,10 @@ internal fun FeatureFormContent(
199199
@Composable
200200
private fun FeatureFormBody(
201201
form: FeatureForm,
202-
fieldStateMap: Map<Int, BaseFieldState>,
202+
fieldStateMap: Map<Int, BaseFieldState<*>>,
203203
groupStateMap: Map<Int, BaseGroupState>,
204204
modifier: Modifier = Modifier,
205-
onFieldDialogRequest: ((BaseFieldState, Int) -> Unit)? = null
205+
onFieldDialogRequest: ((BaseFieldState<*>, Int) -> Unit)? = null
206206
) {
207207
val lazyListState = rememberLazyListState()
208208
Column(
@@ -290,8 +290,8 @@ internal fun rememberFieldStates(
290290
elements: List<FormElement>,
291291
context: Context,
292292
scope: CoroutineScope
293-
): Map<Int, BaseFieldState> {
294-
val stateMap = mutableMapOf<Int, BaseFieldState>()
293+
): Map<Int, BaseFieldState<*>> {
294+
val stateMap = mutableMapOf<Int, BaseFieldState<*>>()
295295
elements.forEach { element ->
296296
if (element is FieldFormElement) {
297297
val state = when (element.input) {
@@ -320,8 +320,8 @@ internal fun rememberFieldStates(
320320
val input = element.input as DateTimePickerFormInput
321321
rememberDateTimeFieldState(
322322
field = element,
323-
minEpochMillis = input.min?.toEpochMilli(),
324-
maxEpochMillis = input.max?.toEpochMilli(),
323+
minEpochMillis = input.min,
324+
maxEpochMillis = input.max,
325325
shouldShowTime = input.includeTime,
326326
form = form,
327327
scope = scope

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/base/BaseFieldState.kt

+7-7
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ import kotlinx.coroutines.flow.flattenMerge
2727
import kotlinx.coroutines.flow.flowOf
2828
import kotlinx.coroutines.flow.stateIn
2929

30-
internal open class FieldProperties(
30+
internal open class FieldProperties<T>(
3131
val label: String,
3232
val placeholder: String,
3333
val description: String,
34-
val value: StateFlow<String>,
34+
val value: StateFlow<T>,
3535
val required: StateFlow<Boolean>,
3636
val editable: StateFlow<Boolean>,
3737
val visible: StateFlow<Boolean>
@@ -48,9 +48,9 @@ internal open class FieldProperties(
4848
* @param onEditValue a callback to invoke when the user edits result in a change of value. This
4949
* is called on [BaseFieldState.onValueChanged].
5050
*/
51-
internal open class BaseFieldState(
52-
properties: FieldProperties,
53-
initialValue: String = properties.value.value,
51+
internal open class BaseFieldState<T>(
52+
properties: FieldProperties<T>,
53+
initialValue: T = properties.value.value,
5454
scope: CoroutineScope,
5555
protected val onEditValue: (Any?) -> Unit,
5656
) {
@@ -76,7 +76,7 @@ internal open class BaseFieldState(
7676
* Current value state for the field.
7777
*/
7878
@OptIn(ExperimentalCoroutinesApi::class)
79-
val value: StateFlow<String> = flowOf(_value, properties.value.drop(1))
79+
val value: StateFlow<T> = flowOf(_value, properties.value.drop(1))
8080
.flattenMerge()
8181
.stateIn(scope, SharingStarted.Eagerly, initialValue)
8282

@@ -98,7 +98,7 @@ internal open class BaseFieldState(
9898
/**
9999
* Callback to update the current value of the FormTextFieldState to the given [input].
100100
*/
101-
open fun onValueChanged(input: String) {
101+
open fun onValueChanged(input: T) {
102102
onEditValue(input)
103103
_value.value = input
104104
}

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/base/BaseGroupState.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal class GroupProperties(
3333

3434
internal class BaseGroupState(
3535
properties: GroupProperties,
36-
val fieldStates: Map<Int, BaseFieldState?>
36+
val fieldStates: Map<Int, BaseFieldState<*>?>
3737
) {
3838
val label = properties.label
3939

@@ -47,7 +47,7 @@ internal class BaseGroupState(
4747
}
4848

4949
companion object {
50-
fun Saver(fieldStates: Map<Int, BaseFieldState?>): Saver<BaseGroupState, Any> = listSaver(
50+
fun Saver(fieldStates: Map<Int, BaseFieldState<*>?>): Saver<BaseGroupState, Any> = listSaver(
5151
save = {
5252
listOf(it.label, it.description, it.expanded.value)
5353
},
@@ -69,7 +69,7 @@ internal class BaseGroupState(
6969
@Composable
7070
internal fun rememberBaseGroupState(
7171
groupElement: GroupFormElement,
72-
fieldStates: Map<Int, BaseFieldState?>
72+
fieldStates: Map<Int, BaseFieldState<*>?>
7373
): BaseGroupState = rememberSaveable(
7474
saver = BaseGroupState.Saver(fieldStates)
7575
) {

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/codedvalue/CodedValueFieldState.kt

+5-5
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import com.arcgismaps.toolkit.featureforms.components.base.FieldProperties
3232
import com.arcgismaps.toolkit.featureforms.components.text.TextFieldProperties
3333
import com.arcgismaps.toolkit.featureforms.utils.editValue
3434
import com.arcgismaps.toolkit.featureforms.utils.fieldType
35-
import com.arcgismaps.toolkit.featureforms.utils.formattedValueFlow
35+
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
3636
import kotlinx.coroutines.CoroutineScope
3737
import kotlinx.coroutines.flow.StateFlow
3838
import kotlinx.coroutines.launch
@@ -49,7 +49,7 @@ internal open class CodedValueFieldProperties(
4949
val codedValues: List<CodedValue>,
5050
val showNoValueOption: FormInputNoValueOption,
5151
val noValueLabel: String
52-
) : FieldProperties(label, placeholder, description, value, required, editable, visible)
52+
) : FieldProperties<String>(label, placeholder, description, value, required, editable, visible)
5353

5454
/**
5555
* A class to handle the state of a [ComboBoxField]. Essential properties are inherited
@@ -68,7 +68,7 @@ internal open class CodedValueFieldState(
6868
initialValue: String = properties.value.value,
6969
scope: CoroutineScope,
7070
onEditValue: ((Any?) -> Unit)
71-
) : BaseFieldState(
71+
) : BaseFieldState<String>(
7272
properties = properties,
7373
scope = scope,
7474
initialValue = initialValue,
@@ -134,7 +134,7 @@ internal open class CodedValueFieldState(
134134
label = formElement.label,
135135
placeholder = formElement.hint,
136136
description = formElement.description,
137-
value = formElement.formattedValueFlow(scope),
137+
value = formElement.valueFlow(scope),
138138
editable = formElement.isEditable,
139139
required = formElement.isRequired,
140140
visible = formElement.isVisible,
@@ -169,7 +169,7 @@ internal fun rememberCodedValueFieldState(
169169
label = field.label,
170170
placeholder = field.hint,
171171
description = field.description,
172-
value = field.formattedValueFlow(scope),
172+
value = field.valueFlow(scope),
173173
editable = field.isEditable,
174174
required = field.isRequired,
175175
visible = field.isVisible,

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/codedvalue/RadioButtonFieldState.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import com.arcgismaps.mapping.featureforms.FieldFormElement
2525
import com.arcgismaps.mapping.featureforms.RadioButtonsFormInput
2626
import com.arcgismaps.toolkit.featureforms.utils.editValue
2727
import com.arcgismaps.toolkit.featureforms.utils.fieldType
28-
import com.arcgismaps.toolkit.featureforms.utils.formattedValueFlow
28+
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
2929
import kotlinx.coroutines.CoroutineScope
3030
import kotlinx.coroutines.launch
3131

@@ -79,7 +79,7 @@ internal class RadioButtonFieldState(
7979
label = formElement.label,
8080
placeholder = formElement.hint,
8181
description = formElement.description,
82-
value = formElement.formattedValueFlow(scope),
82+
value = formElement.valueFlow(scope),
8383
editable = formElement.isEditable,
8484
required = formElement.isRequired,
8585
visible = formElement.isVisible,
@@ -114,7 +114,7 @@ internal fun rememberRadioButtonFieldState(
114114
label = field.label,
115115
placeholder = field.hint,
116116
description = field.description,
117-
value = field.formattedValueFlow(scope),
117+
value = field.valueFlow(scope),
118118
editable = field.isEditable,
119119
required = field.isRequired,
120120
visible = field.isVisible,

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/codedvalue/SwitchFieldState.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import com.arcgismaps.toolkit.featureforms.components.base.BaseFieldState
3131
import com.arcgismaps.toolkit.featureforms.utils.editValue
3232
import com.arcgismaps.toolkit.featureforms.utils.fieldIsNullable
3333
import com.arcgismaps.toolkit.featureforms.utils.fieldType
34-
import com.arcgismaps.toolkit.featureforms.utils.formattedValueFlow
34+
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
3535
import kotlinx.coroutines.CoroutineScope
3636
import kotlinx.coroutines.flow.StateFlow
3737
import kotlinx.coroutines.launch
@@ -121,7 +121,7 @@ internal class SwitchFieldState(
121121
label = formElement.label,
122122
placeholder = formElement.hint,
123123
description = formElement.description,
124-
value = formElement.formattedValueFlow(scope),
124+
value = formElement.valueFlow(scope),
125125
editable = formElement.isEditable,
126126
required = formElement.isRequired,
127127
visible = formElement.isVisible,
@@ -166,7 +166,7 @@ internal fun rememberSwitchFieldState(
166166
label = field.label,
167167
placeholder = field.hint,
168168
description = field.description,
169-
value = field.formattedValueFlow(scope),
169+
value = field.valueFlow(scope),
170170
editable = field.isEditable,
171171
required = field.isRequired,
172172
visible = field.isVisible,

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/datetime/DateTimeField.kt

+8-5
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ internal fun DateTimeField(
5050
) {
5151
val isEditable by state.isEditable.collectAsState()
5252
val isRequired by state.isRequired.collectAsState()
53-
val epochMillis by state.epochMillis.collectAsState()
53+
val instant by state.value.collectAsState()
5454
val interactionSource = remember { MutableInteractionSource() }
5555
// to check if the field was ever focused by the user
5656
var wasFocused by rememberSaveable { mutableStateOf(false) }
@@ -61,9 +61,12 @@ internal fun DateTimeField(
6161
}
6262

6363
BaseTextField(
64-
text = epochMillis?.formattedDateTime(state.shouldShowTime) ?: "",
64+
text = instant?.formattedDateTime(state.shouldShowTime) ?: "",
6565
onValueChange = {
66-
state.onValueChanged(it)
66+
// the only allowable change is to clear the text
67+
if (it.isEmpty()) {
68+
state.onValueChanged(null)
69+
}
6770
},
6871
modifier = modifier,
6972
readOnly = true,
@@ -75,7 +78,7 @@ internal fun DateTimeField(
7578
trailingIcon = Icons.Rounded.EditCalendar,
7679
supportingText = {
7780
// if the field was focused and is required, validate the current value
78-
if (wasFocused && isRequired && epochMillis == null) {
81+
if (wasFocused && isRequired && instant == null) {
7982
Text(
8083
text = stringResource(id = R.string.required),
8184
color = MaterialTheme.colorScheme.error
@@ -111,7 +114,7 @@ private fun DateTimeFieldPreview() {
111114
label = "Launch Date and Time",
112115
placeholder = "",
113116
description = "Enter the date for apollo 11 launch",
114-
value = MutableStateFlow(""),
117+
value = MutableStateFlow(null),
115118
editable = MutableStateFlow(true),
116119
required = MutableStateFlow(false),
117120
visible = MutableStateFlow(true),

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/datetime/DateTimeFieldState.kt

+17-62
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
package com.arcgismaps.toolkit.featureforms.components.datetime
2020

21-
import android.util.Log
2221
import androidx.compose.runtime.Composable
2322
import androidx.compose.runtime.saveable.Saver
2423
import androidx.compose.runtime.saveable.listSaver
@@ -31,31 +30,24 @@ import com.arcgismaps.toolkit.featureforms.components.base.FieldProperties
3130
import com.arcgismaps.toolkit.featureforms.components.text.FormTextFieldState
3231
import com.arcgismaps.toolkit.featureforms.components.text.TextFieldProperties
3332
import com.arcgismaps.toolkit.featureforms.utils.editValue
34-
import com.arcgismaps.toolkit.featureforms.utils.formattedValueFlow
33+
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
3534
import kotlinx.coroutines.CoroutineScope
36-
import kotlinx.coroutines.ExperimentalCoroutinesApi
37-
import kotlinx.coroutines.flow.SharingStarted
3835
import kotlinx.coroutines.flow.StateFlow
39-
import kotlinx.coroutines.flow.mapLatest
40-
import kotlinx.coroutines.flow.stateIn
4136
import kotlinx.coroutines.launch
42-
import java.time.LocalDateTime
43-
import java.time.format.DateTimeFormatter
44-
import java.time.format.DateTimeParseException
45-
import java.util.TimeZone
37+
import java.time.Instant
4638

4739
internal class DateTimeFieldProperties(
4840
label: String,
4941
placeholder: String,
5042
description: String,
51-
value: StateFlow<String>,
43+
value: StateFlow<Instant?>,
5244
required: StateFlow<Boolean>,
5345
editable: StateFlow<Boolean>,
5446
visible: StateFlow<Boolean>,
55-
val minEpochMillis: Long?,
56-
val maxEpochMillis: Long?,
47+
val minEpochMillis: Instant?,
48+
val maxEpochMillis: Instant?,
5749
val shouldShowTime: Boolean
58-
) : FieldProperties(label, placeholder, description, value, required, editable, visible)
50+
) : FieldProperties<Instant?>(label, placeholder, description, value, required, editable, visible)
5951

6052
/**
6153
* A class to handle the state of a [DateTimeField]. Essential properties are inherited from the
@@ -70,34 +62,21 @@ internal class DateTimeFieldProperties(
7062
*/
7163
internal class DateTimeFieldState(
7264
properties: DateTimeFieldProperties,
73-
initialValue: String = properties.value.value,
65+
initialValue: Instant? = properties.value.value,
7466
scope: CoroutineScope,
7567
onEditValue: (Any?) -> Unit
76-
) : BaseFieldState(
68+
) : BaseFieldState<Instant?>(
7769
properties = properties,
7870
initialValue = initialValue,
7971
scope = scope,
8072
onEditValue = onEditValue
8173
) {
82-
val minEpochMillis: Long? = properties.minEpochMillis
74+
val minEpochMillis: Instant? = properties.minEpochMillis
8375

84-
val maxEpochMillis: Long? = properties.maxEpochMillis
76+
val maxEpochMillis: Instant? = properties.maxEpochMillis
8577

8678
val shouldShowTime: Boolean = properties.shouldShowTime
87-
88-
@OptIn(ExperimentalCoroutinesApi::class)
89-
val epochMillis: StateFlow<Long?> = value.mapLatest {
90-
if (it.toLongOrNull() != null) {
91-
it.toLong()
92-
} else {
93-
dateTimeFromString(it)
94-
}
95-
}.stateIn(
96-
scope,
97-
started = SharingStarted.Eagerly,
98-
initialValue = dateTimeFromString(value.value)
99-
)
100-
79+
10180
companion object {
10281
fun Saver(
10382
field: FieldFormElement,
@@ -114,12 +93,12 @@ internal class DateTimeFieldState(
11493
label = field.label,
11594
placeholder = field.hint,
11695
description = field.description,
117-
value = field.formattedValueFlow(scope),
96+
value = field.valueFlow(scope),
11897
editable = field.isEditable,
11998
required = field.isRequired,
12099
visible = field.isVisible,
121-
minEpochMillis = input.min?.toEpochMilli(),
122-
maxEpochMillis = input.max?.toEpochMilli(),
100+
minEpochMillis = input.min,
101+
maxEpochMillis = input.max,
123102
shouldShowTime = input.includeTime
124103
),
125104
initialValue = list[0],
@@ -137,8 +116,8 @@ internal class DateTimeFieldState(
137116
@Composable
138117
internal fun rememberDateTimeFieldState(
139118
field: FieldFormElement,
140-
minEpochMillis: Long?,
141-
maxEpochMillis: Long?,
119+
minEpochMillis: Instant?,
120+
maxEpochMillis: Instant?,
142121
shouldShowTime: Boolean,
143122
form: FeatureForm,
144123
scope: CoroutineScope
@@ -154,7 +133,7 @@ internal fun rememberDateTimeFieldState(
154133
label = field.label,
155134
placeholder = field.hint,
156135
description = field.description,
157-
value = field.formattedValueFlow(scope),
136+
value = field.valueFlow(scope),
158137
editable = field.isEditable,
159138
required = field.isRequired,
160139
visible = field.isVisible,
@@ -170,27 +149,3 @@ internal fun rememberDateTimeFieldState(
170149
)
171150
}
172151

173-
/**
174-
* Maps the [FieldFormElement.value] from a String to Long?
175-
* Empty strings are made to be null Longs.
176-
*
177-
* @since 200.3.0
178-
*/
179-
internal fun dateTimeFromString(formattedDateTime: String): Long? {
180-
return if (formattedDateTime.isNotEmpty()) {
181-
try {
182-
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
183-
LocalDateTime.parse(formattedDateTime, formatter)
184-
.atZone(TimeZone.getDefault().toZoneId())
185-
.toInstant()
186-
.toEpochMilli()
187-
} catch (ex: DateTimeParseException) {
188-
Log.e(
189-
"DateTimeFieldState",
190-
"dateTimeFromString: Error parsing $formattedDateTime into a valid date time",
191-
ex
192-
)
193-
null
194-
}
195-
} else null
196-
}

0 commit comments

Comments
 (0)