Edit features using feature forms

View on GitHubSample viewer app

Display and edit feature attributes using feature forms.

Image of Edit features using feature forms

Use case

Feature forms help enhance the accuracy, efficiency, and user experience of attribute editing in your application. Forms can be authored as part of the web map using Field Maps Designer or using Map Viewer. This allows a simplified user experience to edit feature attribute data on the web map.

How to use the sample

Tap a feature on the map to open a sheet displaying the feature form. Select form elements in the list and perform edits to update the field values. Tap the submit icon to commit the changes on the web map.

How it works

  1. Add a Map to the MapView using Portal URL and item ID.
  2. When the map is tapped, perform an identify operation to check if the tapped location is an ArcGISFeature.
  3. Create a FeatureForm object using the identified ArcGISFeature.
  • Note: If the feature's FeatureLayer, ArcGISFeatureTable, or the SubtypeSublayer has an authored FeatureFormDefinition, then this definition will be used to create the FeatureForm. If such a definition is not found, a default definition is generated.
  1. Use the FeatureForm toolkit component to display the feature form configuration by providing the created featureForm object.
  2. Optionally, you can add a validationErrorVisibility option to the FeatureForm toolkit component that determines the visibility of validation errors.
  3. Once edits are added to the form fields, check if the validation errors list are empty using featureForm.validationErrors to verify that there are no errors.
  4. To commit edits on the service geodatabase:
    1. Call featureForm.finishEditing() to save edits to the database.
    2. Retrieve the backing service feature table's geodatabase using serviceFeatureTable?.serviceGeodatabase.
    3. Verify the service geodatabase can commit changes back to the service using serviceGeodatabase.serviceInfo?.canUseServiceGeodatabaseApplyEdits
    4. If apply edits are allowed, call serviceGeodatabase.applyEdits() to apply local edits to the online service.
    5. If edits are not allowed on the ServiceGeodatabase, then apply edits to the ServiceFeatureTable using ServiceFeatureTable.applyEdits()

Relevant API

  • ArcGISFeature
  • FeatureForm
  • FeatureLayer
  • FieldFormElement
  • GroupFormElement
  • ServiceFeatureTable
  • ServiceGeodatabase

About the data

This sample uses a feature forms enabled Feature Form Places web map, which contains fictional places in San Diego of various hotels, restaurants, and shopping centers, with relevant reviews and ratings.

Additional information

Follow the tutorial to create your own form using the Map Viewer. This sample uses the FeatureForm and GeoView-Compose Toolkit modules to be able to implement a composable MapView which displays a composable FeatureForm UI.

This sample uses the feature forms toolkit component. For information about setting up the toolkit, as well as code for the underlying component, visit the toolkit repository.

Tags

compose, edits, feature, featureforms, form, geoview-compose, jetpack, toolkit

Sample Code

MapViewModel.ktMapViewModel.ktMainActivity.ktMainScreen.kt
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/* Copyright 2024 Esri
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.esri.arcgismaps.sample.editfeaturesusingfeatureforms.components

import android.app.Application
import androidx.compose.ui.unit.dp
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.data.ServiceFeatureTable
import com.arcgismaps.exceptions.FeatureFormValidationException
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.PortalItem
import com.arcgismaps.mapping.featureforms.FeatureForm
import com.arcgismaps.mapping.featureforms.FeatureFormDefinition
import com.arcgismaps.mapping.featureforms.FieldFormElement
import com.arcgismaps.mapping.featureforms.FormElement
import com.arcgismaps.mapping.featureforms.GroupFormElement
import com.arcgismaps.mapping.layers.FeatureLayer
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import com.esri.arcgismaps.sample.editfeaturesusingfeatureforms.R
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

/**
 * A view model for the MainScreen UI
 */
class MapViewModel(application: Application) : AndroidViewModel(application) {

    val mapViewProxy = MapViewProxy()

    // feature forms enabled web-map showcasing places of interest with form fields
    private var portalItem = PortalItem(application.getString(R.string.feature_form_web_map))

    val map = ArcGISMap(portalItem)

    // keep track of the selected feature form
    private val _featureForm = MutableStateFlow<FeatureForm?>(null)
    val featureForm: StateFlow<FeatureForm?> = _featureForm.asStateFlow()

    // keep track of the list of validation errors
    private val _errors = MutableStateFlow<List<ErrorInfo>>(listOf())
    val errors: StateFlow<List<ErrorInfo>> = _errors.asStateFlow()

    // create a ViewModel to handle dialog interactions
    val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel()

    init {
        viewModelScope.launch {
            // load a map that has a FeatureFormDefinition on any of its layers
            map.load()
        }
    }

    /**
     * Apply attribute edits to the Geodatabase backing the ServiceFeatureTable
     * and refresh the local feature. Persisting changes to attributes is
     * not part of the FeatureForm API.
     *
     * @param onEditsCompleted Invoked when edits are applied successfully
     */
    fun applyEdits(onEditsCompleted: () -> Unit) {
        val featureForm = _featureForm.value
            ?: return messageDialogVM.showMessageDialog("Feature form state is not configured")

        // update the state flow with the list of validation errors found
        _errors.value = validateFormInputEdits(featureForm)
        // if there are no errors then update the feature
        if (_errors.value.isEmpty()) {
            val serviceFeatureTable = featureForm.feature.featureTable as? ServiceFeatureTable
                ?: return messageDialogVM.showMessageDialog("Cannot save feature edit without a ServiceFeatureTable")

            viewModelScope.launch {
                // commits changes of the edited feature to the database
                featureForm.finishEditing().onSuccess {
                    serviceFeatureTable.serviceGeodatabase?.let { database ->
                        if (database.serviceInfo?.canUseServiceGeodatabaseApplyEdits == true) {
                            // applies all local edits in the tables to the service
                            database.applyEdits().onFailure {
                                return@onFailure messageDialogVM.showMessageDialog(
                                    title = it.message.toString(),
                                    description = it.cause.toString()
                                )
                            }
                        } else {
                            // uploads any changes to the local table to the feature service
                            serviceFeatureTable.applyEdits().onFailure {
                                return@onFailure messageDialogVM.showMessageDialog(
                                    title = it.message.toString(),
                                    description = it.cause.toString()
                                )
                            }
                        }
                    }
                    // resets the attributes and geometry to the values in the data source
                    featureForm.feature.refresh()
                    // unselect the feature after the edits have been saved
                    (featureForm.feature.featureTable?.layer as FeatureLayer).clearSelection()
                    // dismiss dialog when edits are completed
                    onEditsCompleted()
                }.onFailure {
                    return@onFailure messageDialogVM.showMessageDialog(
                        title = it.message.toString(),
                        description = it.cause.toString()
                    )
                }
            }
        }
    }

    /**
     * Performs validation checks on the given [featureForm] with local edits.
     * Return a list of [ErrorInfo] if errors are found, if not, empty list is returned.
     */
    private fun validateFormInputEdits(featureForm: FeatureForm): List<ErrorInfo> {
        val errors = mutableListOf<ErrorInfo>()
        // If an element is editable or derives its value from an arcade expression,
        // its errors must be corrected before submitting the form
        featureForm.validationErrors.value.forEach { entry ->
            entry.value.forEach { error ->
                featureForm.elements.getFormElement(entry.key)?.let { formElement ->
                    // Ignore validation on non-editable and non-visible elements
                    if (formElement.isEditable.value || formElement.hasValueExpression) {
                        errors.add(
                            ErrorInfo(
                                fieldName = formElement.label,
                                error = error as FeatureFormValidationException
                            )
                        )
                    }
                }
            }
        }
        return errors
    }

    /**
     * Cancels the commit by resetting the validation errors.
     */
    fun cancelCommit() {
        // reset the validation errors
        _errors.value = listOf()
    }

    /**
     * Discard edits and unselects feature from the layer
     */
    fun rollbackEdits() {
        // discard local edits to the feature form
        _featureForm.value?.discardEdits()
        // unselect the feature
        (_featureForm.value?.feature?.featureTable?.layer as FeatureLayer).clearSelection()
        // reset the validation errors
        _errors.value = listOf()
    }

    /**
     * Perform an identify the tapped [ArcGISFeature] and retrieve the
     * layer's [FeatureFormDefinition] to create the respective [FeatureForm]
     */
    fun onSingleTapConfirmed(singleTapEvent: SingleTapConfirmedEvent) {
        viewModelScope.launch {
            mapViewProxy.identifyLayers(
                screenCoordinate = singleTapEvent.screenCoordinate,
                tolerance = 22.dp,
                returnPopupsOnly = false
            ).onSuccess { results ->
                try {
                    results.forEach { result ->
                        result.geoElements.firstOrNull {
                            it is ArcGISFeature && (it.featureTable?.layer as? FeatureLayer)?.featureFormDefinition != null
                        }?.let {
                            val feature = it as ArcGISFeature
                            val layer = feature.featureTable!!.layer as FeatureLayer
                            val featureForm = FeatureForm(feature)
                            // select the feature
                            layer.selectFeature(feature)
                            // set the UI to an editing state with the FeatureForm
                            _featureForm.value = featureForm
                        }
                    }
                } catch (e: Exception) {
                    messageDialogVM.showMessageDialog(
                        title = "Failed to create feature form for the feature",
                        description = e.message.toString()
                    )
                }
            }
        }
    }
}

/**
 * Returns the [FieldFormElement] with the given [fieldName] in the [FeatureForm]. If none exists
 * null is returned.
 */
fun List<FormElement>.getFormElement(fieldName: String): FieldFormElement? {
    val fieldElements = filterIsInstance<FieldFormElement>()
    val element = if (fieldElements.isNotEmpty()) {
        fieldElements.firstNotNullOfOrNull {
            if (it.fieldName == fieldName) it else null
        }
    } else {
        null
    }

    return element ?: run {
        val groupElements = filterIsInstance<GroupFormElement>()
        if (groupElements.isNotEmpty()) {
            groupElements.firstNotNullOfOrNull {
                it.elements.getFormElement(fieldName)
            }
        } else {
            null
        }
    }
}

/**
 * Class that provides a validation error [error] for the field with name [fieldName].
 */
data class ErrorInfo(val fieldName: String, val error: FeatureFormValidationException)

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.