Skip to content

Commit e2b3604

Browse files
sorenoidSoren Rothkaushikrwgunt0001
authored
Forms: merge latest FeatureForms to v.next (#380)
* provide public webmap which has forms * `Forms`: Updated radio button tests (#366) * updated radio button tests * added test doc link * FeatureForm composable KDoc edit (#368) * slight edits to FeatureForm composable function KDoc. * Update toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt Co-authored-by: Gunther Heppner <[email protected]> --------- Co-authored-by: Soren Roth <[email protected]> Co-authored-by: Gunther Heppner <[email protected]> * add groups to final error validation reporting in the app (#367) Co-authored-by: Soren Roth <[email protected]> * `Forms` : Optimize FeatureForm (#369) * agree with exception name change (#377) * update exception name for API change * update exception name for API change * pull in the build of the SDK with the name change of IncorrectValueTypeException --------- Co-authored-by: Soren Roth <[email protected]> * use geo-compose in FeatureFormsApp (#378) * use geo-compose in FeatureFormsApp. * remove DI providing MapViewProxy. add AndroidViewModel usage --------- Co-authored-by: Soren Roth <[email protected]> * `Forms`: do not pass a selected date into date picker if it is out of range. (#376) * do not pass a selected date into date picker if it is out of range. * just expand the year range to incloude out of range field value's date * add an initialError property to the DateTimePickerState, weave it through to the PickerHeader. * use includeTime and adjust spacing. --------- Co-authored-by: Soren Roth <[email protected]> * remove gif. add screenshots (#381) Co-authored-by: Soren Roth <[email protected]> * `Forms`: fix title padding (#382) * swap out public webmap for a managed public webmap (#384) Co-authored-by: Soren Roth <[email protected]> * `Forms` : Update validation behavior for text fields (#385) * fix for breaking API change in dependency (#386) Co-authored-by: Soren Roth <[email protected]> --------- Co-authored-by: Soren Roth <[email protected]> Co-authored-by: Kaushik Meesala <[email protected]> Co-authored-by: Gunther Heppner <[email protected]>
1 parent 370fafb commit e2b3604

File tree

22 files changed

+384
-119
lines changed

22 files changed

+384
-119
lines changed

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ ignoreBuildNumber=false
5454
# these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency
5555
# and are generally not overridden at the command line unless a special build is requested.
5656
sdkVersionNumber=200.4.0
57-
sdkBuildNumber=4180
57+
sdkBuildNumber=4196

microapps/FeatureFormsApp/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This micro-app demonstrates the use of the [FeatureForm](../../toolkit/featureforms/README.md) toolkit component which provides a rich, dynamic, and responsive form
44
for editing Feature attributes.
55

6-
![Screenshot](screenshot.gif)
6+
![Screenshot](screenshot2.png) ![Screenshot](screenshot3.png)
77

88
## Usage
99

microapps/FeatureFormsApp/app/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all {
8181
dependencies {
8282
implementation(project(":authentication"))
8383
implementation(project(":featureforms"))
84-
implementation(project(":composable-map"))
84+
implementation(project(":geoview-compose"))
8585
// sdk
8686
implementation(arcgis.mapsSdk)
8787
// hilt

microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,5 @@ class PortalItemRepository(
166166
*/
167167
fun getListOfMaps(): List<String> =
168168
listOf(
169-
"https://www.arcgis.com/home/item.html?id=fe8b712a5bf7480e9781a4ad3dd5e0ff"
169+
"https://www.arcgis.com/home/item.html?id=f72207ac170a40d8992b7a3507b44fad"
170170
)

microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapScreen.kt

+18-14
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
7171
import androidx.window.core.layout.WindowSizeClass
7272
import androidx.window.layout.WindowMetricsCalculator
7373
import com.arcgismaps.exceptions.FeatureFormValidationException
74-
import com.arcgismaps.toolkit.composablemap.ComposableMap
7574
import com.arcgismaps.toolkit.featureforms.FeatureForm
7675
import com.arcgismaps.toolkit.featureforms.ValidationErrorVisibility
7776
import com.arcgismaps.toolkit.featureformsapp.R
@@ -81,6 +80,7 @@ import com.arcgismaps.toolkit.featureformsapp.screens.bottomsheet.SheetLayout
8180
import com.arcgismaps.toolkit.featureformsapp.screens.bottomsheet.SheetValue
8281
import com.arcgismaps.toolkit.featureformsapp.screens.bottomsheet.StandardBottomSheet
8382
import com.arcgismaps.toolkit.featureformsapp.screens.bottomsheet.rememberStandardBottomSheetState
83+
import com.arcgismaps.toolkit.geoviewcompose.MapView
8484
import kotlinx.coroutines.launch
8585

8686
@OptIn(ExperimentalMaterial3Api::class)
@@ -102,7 +102,7 @@ fun MapScreen(mapViewModel: MapViewModel = hiltViewModel(), onBackPressed: () ->
102102
ValidationErrorVisibility.Automatic
103103
)
104104
}
105-
105+
106106
is UIState.Switching -> {
107107
val state = uiState as UIState.Switching
108108
Pair(
@@ -130,7 +130,6 @@ fun MapScreen(mapViewModel: MapViewModel = hiltViewModel(), onBackPressed: () ->
130130
showDiscardEditsDialog = true
131131
},
132132
onSave = {
133-
//SubmitForm(mapViewModel = mapViewModel, featureForm = (uiState as UIState.Editing).featureForm)
134133
scope.launch {
135134
mapViewModel.commitEdits().onFailure {
136135
Log.w("Forms", "Applying edits failed : ${it.message}")
@@ -147,18 +146,25 @@ fun MapScreen(mapViewModel: MapViewModel = hiltViewModel(), onBackPressed: () ->
147146
}
148147
) { padding ->
149148
// show the composable map using the mapViewModel
150-
ComposableMap(
149+
MapView(
150+
arcGISMap = mapViewModel.map,
151+
mapViewProxy = mapViewModel.proxy,
151152
modifier = Modifier
152153
.padding(padding)
153154
.fillMaxSize(),
154-
mapInterface = mapViewModel
155+
onSingleTapConfirmed = { mapViewModel.onSingleTapConfirmed(it) }
155156
)
156157
AnimatedVisibility(
157158
visible = featureForm != null,
158159
enter = slideInVertically { h -> h },
159160
exit = slideOutVertically { h -> h },
160161
label = "feature form"
161162
) {
163+
val isSwitching = uiState is UIState.Switching
164+
// remember the form and update it when a new form is opened
165+
val rememberedForm = remember(this, isSwitching) {
166+
featureForm!!
167+
}
162168
val bottomSheetState = rememberStandardBottomSheetState(
163169
initialValue = SheetValue.PartiallyExpanded,
164170
confirmValueChange = { it != SheetValue.Hidden },
@@ -180,13 +186,11 @@ fun MapScreen(mapViewModel: MapViewModel = hiltViewModel(), onBackPressed: () ->
180186
sheetWidth = with(LocalDensity.current) { layoutWidth.toDp() }
181187
) {
182188
// set bottom sheet content to the FeatureForm
183-
if (featureForm != null) {
184-
FeatureForm(
185-
featureForm = featureForm,
186-
modifier = Modifier.fillMaxSize(),
187-
validationErrorVisibility = errorVisibility
188-
)
189-
}
189+
FeatureForm(
190+
featureForm = rememberedForm,
191+
modifier = Modifier.fillMaxSize(),
192+
validationErrorVisibility = errorVisibility
193+
)
190194
}
191195
}
192196
}
@@ -282,7 +286,7 @@ fun TopFormBar(
282286

283287
@OptIn(ExperimentalMaterial3Api::class)
284288
@Composable
285-
private fun SubmitForm(errors : List<ErrorInfo>, onDismissRequest: () -> Unit) {
289+
private fun SubmitForm(errors: List<ErrorInfo>, onDismissRequest: () -> Unit) {
286290
if (errors.isEmpty()) {
287291
// show a progress dialog if no errors are present
288292
AlertDialog(
@@ -358,7 +362,7 @@ private fun SubmitForm(errors : List<ErrorInfo>, onDismissRequest: () -> Unit) {
358362
@Composable
359363
fun FeatureFormValidationException.getString(): String {
360364
return when (this) {
361-
is FeatureFormValidationException.IncorrectValueTypeError -> {
365+
is FeatureFormValidationException.IncorrectValueTypeException -> {
362366
stringResource(id = R.string.value_must_be_of_correct_type)
363367
}
364368

microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt

+87-57
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,34 @@
1818

1919
package com.arcgismaps.toolkit.featureformsapp.screens.map
2020

21+
import android.app.Application
2122
import android.widget.Toast
2223
import androidx.compose.runtime.MutableState
2324
import androidx.compose.runtime.State
2425
import androidx.compose.runtime.mutableStateOf
26+
import androidx.compose.ui.unit.dp
27+
import androidx.lifecycle.AndroidViewModel
2528
import androidx.lifecycle.SavedStateHandle
26-
import androidx.lifecycle.ViewModel
27-
import androidx.lifecycle.viewModelScope
2829
import com.arcgismaps.data.ArcGISFeature
2930
import com.arcgismaps.data.ServiceFeatureTable
3031
import com.arcgismaps.exceptions.FeatureFormValidationException
3132
import com.arcgismaps.mapping.ArcGISMap
3233
import com.arcgismaps.mapping.PortalItem
3334
import com.arcgismaps.mapping.featureforms.FeatureForm
3435
import com.arcgismaps.mapping.featureforms.FieldFormElement
36+
import com.arcgismaps.mapping.featureforms.FormElement
37+
import com.arcgismaps.mapping.featureforms.GroupFormElement
3538
import com.arcgismaps.mapping.layers.FeatureLayer
36-
import com.arcgismaps.mapping.view.MapView
3739
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
38-
import com.arcgismaps.toolkit.composablemap.MapInterface
39-
import com.arcgismaps.toolkit.composablemap.MapInterfaceImpl
4040
import com.arcgismaps.toolkit.featureforms.ValidationErrorVisibility
4141
import com.arcgismaps.toolkit.featureformsapp.data.PortalItemRepository
42+
import com.arcgismaps.toolkit.featureformsapp.di.ApplicationScope
43+
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
4244
import dagger.hilt.android.lifecycle.HiltViewModel
4345
import kotlinx.coroutines.CoroutineScope
46+
import kotlinx.coroutines.Dispatchers
4447
import kotlinx.coroutines.launch
48+
import kotlinx.coroutines.withContext
4549
import javax.inject.Inject
4650

4751
/**
@@ -52,7 +56,7 @@ sealed class UIState {
5256
* Currently not editing.
5357
*/
5458
object NotEditing : UIState()
55-
59+
5660
/**
5761
* Currently selecting a new Feature
5862
*/
@@ -85,30 +89,36 @@ sealed class UIState {
8589
*/
8690
data class ErrorInfo(val fieldName: String, val error: FeatureFormValidationException)
8791

92+
/**
93+
* Base class for context aware AndroidViewModel. This class must have only a single application
94+
* parameter.
95+
*/
96+
open class BaseMapViewModel(application: Application): AndroidViewModel(application)
97+
8898
/**
8999
* A view model for the FeatureForms MapView UI
90100
* @constructor to be invoked by injection
91101
*/
92102
@HiltViewModel
93103
class MapViewModel @Inject constructor(
94104
savedStateHandle: SavedStateHandle,
95-
private val portalItemRepository: PortalItemRepository
96-
) : ViewModel(),
97-
MapInterface by MapInterfaceImpl(ArcGISMap()) {
105+
portalItemRepository: PortalItemRepository,
106+
application: Application,
107+
@ApplicationScope private val scope: CoroutineScope
108+
) : BaseMapViewModel(application) {
98109
private val itemId: String = savedStateHandle["uri"]!!
99-
lateinit var portalItem: PortalItem
110+
111+
val proxy: MapViewProxy = MapViewProxy()
112+
113+
var portalItem: PortalItem = portalItemRepository(itemId)
114+
?: throw IllegalStateException("portal item not found with id $itemId")
115+
116+
val map: ArcGISMap = ArcGISMap(portalItem)
100117

101118
private val _uiState: MutableState<UIState> = mutableStateOf(UIState.NotEditing)
102119
val uiState: State<UIState>
103120
get() = _uiState
104121

105-
init {
106-
viewModelScope.launch {
107-
portalItem = portalItemRepository(itemId) ?: return@launch
108-
setMap(ArcGISMap(portalItem))
109-
}
110-
}
111-
112122
/**
113123
* Apply attribute edits to the Geodatabase backing
114124
* the ServiceFeatureTable and refresh the local feature.
@@ -125,7 +135,7 @@ class MapViewModel @Inject constructor(
125135
val featureForm = state.featureForm
126136
featureForm.validationErrors.value.forEach { entry ->
127137
entry.value.forEach { error ->
128-
featureForm.getFormElement(entry.key)?.let { formElement ->
138+
featureForm.elements.getFormElement(entry.key)?.let { formElement ->
129139
if (formElement.isEditable.value) {
130140
errors.add(
131141
ErrorInfo(
@@ -182,17 +192,22 @@ class MapViewModel @Inject constructor(
182192
)
183193
return Result.success(Unit)
184194
}
185-
195+
186196
fun selectNewFeature() =
187197
(_uiState.value as? UIState.Switching)?.let { prevState ->
188198
prevState.oldState.featureForm.discardEdits()
189199
val layer = prevState.oldState.featureForm.feature.featureTable?.layer as FeatureLayer
190200
layer.clearSelection()
191201
layer.selectFeature(prevState.newFeature)
192202
_uiState.value =
193-
UIState.Editing(featureForm = FeatureForm(prevState.newFeature, layer.featureFormDefinition!!))
203+
UIState.Editing(
204+
featureForm = FeatureForm(
205+
prevState.newFeature,
206+
layer.featureFormDefinition!!
207+
)
208+
)
194209
}
195-
210+
196211
fun continueEditing() =
197212
(_uiState.value as? UIState.Switching)?.let { prevState ->
198213
_uiState.value = prevState.oldState
@@ -208,58 +223,73 @@ class MapViewModel @Inject constructor(
208223
} ?: return Result.failure(IllegalStateException("Not in editing state"))
209224
}
210225

211-
context(MapView, CoroutineScope) override fun onSingleTapConfirmed(singleTapEvent: SingleTapConfirmedEvent) {
212-
launch {
213-
this@MapView.identifyLayers(
214-
screenCoordinate = singleTapEvent.screenCoordinate,
215-
tolerance = 22.0,
216-
returnPopupsOnly = false
217-
).onSuccess { results ->
218-
try {
219-
results.forEach { result ->
220-
result.geoElements.firstOrNull {
221-
it is ArcGISFeature && (it.featureTable?.layer as? FeatureLayer)?.featureFormDefinition != null
222-
}?.let {
223-
if (_uiState.value is UIState.Editing) {
224-
val currentState = _uiState.value as UIState.Editing
225-
val newFeature = it as ArcGISFeature
226-
_uiState.value = UIState.Switching(
227-
oldState = currentState,
228-
newFeature = newFeature
229-
)
230-
} else if (_uiState.value is UIState.NotEditing) {
231-
val feature = it as ArcGISFeature
232-
val layer = feature.featureTable!!.layer as FeatureLayer
233-
val featureForm =
234-
FeatureForm(feature, layer.featureFormDefinition!!)
235-
// select the feature
236-
layer.selectFeature(feature)
237-
// set the UI to an editing state with the FeatureForm
238-
_uiState.value = UIState.Editing(featureForm)
239-
}
226+
fun onSingleTapConfirmed(singleTapEvent: SingleTapConfirmedEvent) {
227+
scope.launch {
228+
proxy.identifyLayers(
229+
screenCoordinate = singleTapEvent.screenCoordinate,
230+
tolerance = 22.dp,
231+
returnPopupsOnly = false
232+
).onSuccess { results ->
233+
try {
234+
results.forEach { result ->
235+
result.geoElements.firstOrNull {
236+
it is ArcGISFeature && (it.featureTable?.layer as? FeatureLayer)?.featureFormDefinition != null
237+
}?.let {
238+
if (_uiState.value is UIState.Editing) {
239+
val currentState = _uiState.value as UIState.Editing
240+
val newFeature = it as ArcGISFeature
241+
_uiState.value = UIState.Switching(
242+
oldState = currentState,
243+
newFeature = newFeature
244+
)
245+
} else if (_uiState.value is UIState.NotEditing) {
246+
val feature = it as ArcGISFeature
247+
val layer = feature.featureTable!!.layer as FeatureLayer
248+
val featureForm =
249+
FeatureForm(feature, layer.featureFormDefinition!!)
250+
// select the feature
251+
layer.selectFeature(feature)
252+
// set the UI to an editing state with the FeatureForm
253+
_uiState.value = UIState.Editing(featureForm)
240254
}
241255
}
242-
} catch (e: Exception) {
243-
e.printStackTrace()
256+
}
257+
} catch (e: Exception) {
258+
e.printStackTrace()
259+
withContext(Dispatchers.Main) {
244260
Toast.makeText(
245-
context,
261+
getApplication<Application>().applicationContext,
246262
"failed to create a FeatureForm for the feature",
247263
Toast.LENGTH_LONG
248264
).show()
249265
}
250266
}
251267
}
252268
}
269+
}
270+
253271
}
254272

255273
/**
256274
* Returns the [FieldFormElement] with the given [fieldName] in the [FeatureForm]. If none exists
257275
* null is returned.
258276
*/
259-
fun FeatureForm.getFormElement(fieldName: String): FieldFormElement? {
260-
return elements.firstNotNullOfOrNull {
261-
if (it is FieldFormElement && it.fieldName == fieldName) {
262-
it
277+
fun List<FormElement>.getFormElement(fieldName: String): FieldFormElement? {
278+
val fieldElements = filterIsInstance<FieldFormElement>()
279+
val element = if (fieldElements.isNotEmpty()) {
280+
fieldElements.firstNotNullOfOrNull {
281+
if (it.fieldName == fieldName) it else null
282+
}
283+
} else {
284+
null
285+
}
286+
287+
return element ?: run {
288+
val groupElements = filterIsInstance<GroupFormElement>()
289+
if (groupElements.isNotEmpty()) {
290+
groupElements.firstNotNullOfOrNull {
291+
it.elements.getFormElement(fieldName)
292+
}
263293
} else {
264294
null
265295
}
-1.69 MB
Binary file not shown.
38.3 KB
Loading
35.2 KB
Loading

0 commit comments

Comments
 (0)