Skip to content

Commit bbf2cba

Browse files
authored
Forms: Iterable collection of states (#263)
1 parent 82848cf commit bbf2cba

File tree

5 files changed

+229
-152
lines changed

5 files changed

+229
-152
lines changed

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

+129-144
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.height
1414
import androidx.compose.foundation.layout.padding
1515
import androidx.compose.foundation.layout.width
1616
import androidx.compose.foundation.lazy.LazyColumn
17-
import androidx.compose.foundation.lazy.items
1817
import androidx.compose.foundation.lazy.rememberLazyListState
1918
import androidx.compose.material3.CircularProgressIndicator
2019
import androidx.compose.material3.Divider
@@ -39,15 +38,15 @@ import com.arcgismaps.mapping.featureforms.ComboBoxFormInput
3938
import com.arcgismaps.mapping.featureforms.DateTimePickerFormInput
4039
import com.arcgismaps.mapping.featureforms.FeatureForm
4140
import com.arcgismaps.mapping.featureforms.FieldFormElement
42-
import com.arcgismaps.mapping.featureforms.FormElement
4341
import com.arcgismaps.mapping.featureforms.GroupFormElement
4442
import com.arcgismaps.mapping.featureforms.RadioButtonsFormInput
4543
import com.arcgismaps.mapping.featureforms.SwitchFormInput
4644
import com.arcgismaps.mapping.featureforms.TextAreaFormInput
4745
import com.arcgismaps.mapping.featureforms.TextBoxFormInput
4846
import com.arcgismaps.toolkit.featureforms.components.base.BaseFieldState
49-
import com.arcgismaps.toolkit.featureforms.components.base.BaseGroupState
50-
import com.arcgismaps.toolkit.featureforms.components.base.FormElementState
47+
import com.arcgismaps.toolkit.featureforms.components.base.MutableStateCollection
48+
import com.arcgismaps.toolkit.featureforms.components.base.FormStateCollection
49+
import com.arcgismaps.toolkit.featureforms.components.base.getState
5150
import com.arcgismaps.toolkit.featureforms.components.base.rememberBaseGroupState
5251
import com.arcgismaps.toolkit.featureforms.components.codedvalue.rememberCodedValueFieldState
5352
import com.arcgismaps.toolkit.featureforms.components.codedvalue.rememberRadioButtonFieldState
@@ -59,7 +58,6 @@ import com.arcgismaps.toolkit.featureforms.components.text.rememberFormTextField
5958
import com.arcgismaps.toolkit.featureforms.utils.FeatureFormDialog
6059
import kotlinx.coroutines.CoroutineScope
6160
import kotlinx.coroutines.delay
62-
import java.util.Objects
6361

6462
/**
6563
* A composable Form toolkit component that enables users to edit field values of features in a
@@ -135,20 +133,10 @@ internal fun FeatureFormContent(
135133
) {
136134
val context = LocalContext.current
137135
val scope = rememberCoroutineScope()
138-
val fieldStateMap = rememberFieldStates(
139-
form = form,
140-
elements = form.elements,
141-
context = context,
142-
scope = scope
143-
)
144-
val groupStateMap = rememberGroupStates(
145-
form = form,
146-
context = context,
147-
scope = scope
148-
)
136+
val states = rememberStates(form = form, context = context, scope = scope)
149137
FeatureFormBody(
150138
form = form,
151-
states = fieldStateMap + groupStateMap,
139+
states = states,
152140
modifier = modifier
153141
)
154142
FeatureFormDialog()
@@ -157,7 +145,7 @@ internal fun FeatureFormContent(
157145
@Composable
158146
private fun FeatureFormBody(
159147
form: FeatureForm,
160-
states: Map<Int, FormElementState>,
148+
states: FormStateCollection,
161149
modifier: Modifier = Modifier
162150
) {
163151
val lazyListState = rememberLazyListState()
@@ -178,28 +166,19 @@ private fun FeatureFormBody(
178166
modifier = Modifier.fillMaxSize(),
179167
state = lazyListState
180168
) {
181-
items(form.elements) { formElement ->
182-
when (formElement) {
183-
is FieldFormElement -> {
184-
val state = states[formElement.id] as? BaseFieldState<*>
185-
if (state != null) {
186-
FieldElement(state = state)
169+
states.forEach { entry ->
170+
item {
171+
when (entry.formElement) {
172+
is FieldFormElement -> {
173+
FieldElement(state = entry.getState<BaseFieldState<*>>())
187174
}
188-
}
189175

190-
is GroupFormElement -> {
191-
val state = states[formElement.id] as? BaseGroupState
192-
if (state != null) {
176+
is GroupFormElement -> {
193177
GroupElement(
194-
state,
178+
state = entry.getState(),
195179
modifier = Modifier
196180
.fillMaxWidth()
197-
.padding(
198-
start = 15.dp,
199-
end = 15.dp,
200-
top = 10.dp,
201-
bottom = 10.dp
202-
)
181+
.padding(horizontal = 15.dp, vertical = 10.dp)
203182
)
204183
}
205184
}
@@ -209,136 +188,142 @@ private fun FeatureFormBody(
209188
}
210189
}
211190

191+
/**
192+
* Creates and remembers state objects for all the supported element types that are part of the
193+
* provided FeatureForm. These state objects are returned as part of a [FormStateCollection].
194+
*
195+
* @param form the [FeatureForm] to create the states for.
196+
* @param context a [Context].
197+
* @param scope a [CoroutineScope] to run collectors and calculations on.
198+
*
199+
* @return returns the [FormStateCollection] created.
200+
*/
212201
@Composable
213-
internal fun rememberGroupStates(
202+
internal fun rememberStates(
214203
form: FeatureForm,
215204
context: Context,
216-
scope: CoroutineScope,
217-
): Map<Int, BaseGroupState> {
218-
return form.elements.filterIsInstance<GroupFormElement>().associateBy(
219-
{ groupElement ->
220-
groupElement.id
221-
},
222-
{ groupElement ->
223-
val fieldStates = rememberFieldStates(
224-
form = form,
225-
elements = groupElement.formElements,
226-
context = context,
227-
scope = scope
228-
)
229-
rememberBaseGroupState(groupElement = groupElement, fieldStates = fieldStates)
205+
scope: CoroutineScope
206+
): FormStateCollection {
207+
val states = MutableStateCollection()
208+
form.elements.forEach { element ->
209+
when (element) {
210+
is FieldFormElement -> {
211+
val state = rememberFieldState(element, form, context, scope)
212+
if (state != null) {
213+
states.add(element, state)
214+
}
215+
}
216+
217+
is GroupFormElement -> {
218+
val fieldStateCollection = MutableStateCollection()
219+
element.formElements.forEach {
220+
if (it is FieldFormElement) {
221+
val state = rememberFieldState(
222+
element = it,
223+
form = form,
224+
context = context,
225+
scope = scope
226+
)
227+
if (state != null) {
228+
fieldStateCollection.add(it, state)
229+
}
230+
}
231+
}
232+
val groupState = rememberBaseGroupState(
233+
groupElement = element,
234+
fieldStates = fieldStateCollection
235+
)
236+
states.add(element, groupState)
237+
}
230238
}
231-
)
239+
}
240+
return states
232241
}
233242

243+
/**
244+
* Creates and remembers a [BaseFieldState] for the provided [element].
245+
*
246+
* @param element the [FieldFormElement] to create the state for.
247+
* @param form the [FeatureForm] the [element] is part of.
248+
* @param context a [Context].
249+
* @param scope a [CoroutineScope] to run collectors and calculations on.
250+
*
251+
* @return returns the [BaseFieldState] created.
252+
*/
234253
@Composable
235-
internal fun rememberFieldStates(
254+
internal fun rememberFieldState(
255+
element: FieldFormElement,
236256
form: FeatureForm,
237-
elements: List<FormElement>,
238257
context: Context,
239258
scope: CoroutineScope
240-
): Map<Int, BaseFieldState<*>> {
241-
val stateMap = mutableMapOf<Int, BaseFieldState<*>>()
242-
elements.forEach { element ->
243-
if (element is FieldFormElement) {
244-
val state = when (element.input) {
245-
is TextBoxFormInput, is TextAreaFormInput -> {
246-
val minLength = if (element.input is TextBoxFormInput) {
247-
(element.input as TextBoxFormInput).minLength.toInt()
248-
} else {
249-
(element.input as TextAreaFormInput).minLength.toInt()
250-
}
251-
val maxLength = if (element.input is TextBoxFormInput) {
252-
(element.input as TextBoxFormInput).maxLength.toInt()
253-
} else {
254-
(element.input as TextAreaFormInput).maxLength.toInt()
255-
}
256-
rememberFormTextFieldState(
257-
field = element,
258-
minLength = minLength,
259-
maxLength = maxLength,
260-
form = form,
261-
context = context,
262-
scope = scope
263-
)
264-
}
259+
): BaseFieldState<out Any?>? {
260+
return when (element.input) {
261+
is TextBoxFormInput, is TextAreaFormInput -> {
262+
val minLength = if (element.input is TextBoxFormInput) {
263+
(element.input as TextBoxFormInput).minLength.toInt()
264+
} else {
265+
(element.input as TextAreaFormInput).minLength.toInt()
266+
}
267+
val maxLength = if (element.input is TextBoxFormInput) {
268+
(element.input as TextBoxFormInput).maxLength.toInt()
269+
} else {
270+
(element.input as TextAreaFormInput).maxLength.toInt()
271+
}
272+
rememberFormTextFieldState(
273+
field = element,
274+
minLength = minLength,
275+
maxLength = maxLength,
276+
form = form,
277+
context = context,
278+
scope = scope
279+
)
280+
}
265281

266-
is DateTimePickerFormInput -> {
267-
val input = element.input as DateTimePickerFormInput
268-
rememberDateTimeFieldState(
269-
field = element,
270-
minEpochMillis = input.min,
271-
maxEpochMillis = input.max,
272-
shouldShowTime = input.includeTime,
273-
form = form,
274-
scope = scope
275-
)
276-
}
282+
is DateTimePickerFormInput -> {
283+
val input = element.input as DateTimePickerFormInput
284+
rememberDateTimeFieldState(
285+
field = element,
286+
minEpochMillis = input.min,
287+
maxEpochMillis = input.max,
288+
shouldShowTime = input.includeTime,
289+
form = form,
290+
scope = scope
291+
)
292+
}
277293

278-
is ComboBoxFormInput -> {
279-
rememberCodedValueFieldState(
280-
field = element,
281-
form = form,
282-
scope = scope
283-
)
284-
}
294+
is ComboBoxFormInput -> {
295+
rememberCodedValueFieldState(
296+
field = element,
297+
form = form,
298+
scope = scope
299+
)
300+
}
285301

286-
is SwitchFormInput -> {
287-
val input = element.input as SwitchFormInput
288-
val initialValue = element.formattedValue
289-
val fallback = initialValue.isEmpty()
290-
|| (element.value.value != input.onValue.code && element.value.value != input.offValue.code)
291-
rememberSwitchFieldState(
292-
field = element,
293-
form = form,
294-
fallback = fallback,
295-
scope = scope,
296-
noValueString = context.getString(R.string.no_value)
297-
)
298-
}
302+
is SwitchFormInput -> {
303+
rememberSwitchFieldState(
304+
field = element,
305+
form = form,
306+
scope = scope,
307+
noValueString = context.getString(R.string.no_value)
308+
)
309+
}
299310

300-
is RadioButtonsFormInput -> {
301-
rememberRadioButtonFieldState(
302-
field = element,
303-
form = form,
304-
scope = scope
305-
)
306-
}
311+
is RadioButtonsFormInput -> {
312+
rememberRadioButtonFieldState(
313+
field = element,
314+
form = form,
315+
scope = scope
316+
)
317+
}
307318

308-
else -> {
309-
null
310-
}
311-
}
312-
if (state != null) {
313-
stateMap[element.id] = state
314-
}
319+
else -> {
320+
null
315321
}
316322
}
317-
return stateMap
318323
}
319324

320325
@Preview(showBackground = true, backgroundColor = 0xFFFFFF)
321326
@Composable
322327
private fun InitializingExpressionsPreview() {
323328
InitializingExpressions { false }
324329
}
325-
326-
/**
327-
* Unique id for each form element.
328-
*/
329-
internal val FieldFormElement.id: Int
330-
get() {
331-
return Objects.hash(fieldName, label, description, hint)
332-
}
333-
334-
/**
335-
* Unique id for each form element.
336-
*/
337-
internal val GroupFormElement.id: Int
338-
get() {
339-
return Objects.hash(
340-
formElements.forEach { if (it is FieldFormElement) it.id },
341-
label,
342-
description
343-
)
344-
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal class BaseGroupState(
3030
description: String,
3131
isVisible: StateFlow<Boolean>,
3232
expanded: Boolean,
33-
val fieldStates: Map<Int, BaseFieldState<*>>
33+
val fieldStates: FormStateCollection
3434
) : FormElementState(
3535
label = label,
3636
description = description,
@@ -46,7 +46,7 @@ internal class BaseGroupState(
4646
companion object {
4747
fun Saver(
4848
groupElement: GroupFormElement,
49-
fieldStates: Map<Int, BaseFieldState<*>>
49+
fieldStates: FormStateCollection
5050
): Saver<BaseGroupState, Boolean> = Saver(
5151
save = {
5252
it.expanded.value
@@ -67,7 +67,7 @@ internal class BaseGroupState(
6767
@Composable
6868
internal fun rememberBaseGroupState(
6969
groupElement: GroupFormElement,
70-
fieldStates: Map<Int, BaseFieldState<*>>
70+
fieldStates: FormStateCollection
7171
): BaseGroupState = rememberSaveable(
7272
saver = BaseGroupState.Saver(groupElement, fieldStates)
7373
) {

0 commit comments

Comments
 (0)