diff --git a/.gitignore b/.gitignore
index 7528ac37f..1a8e1e708 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ build/
*.classpath
.settings/
.vscode
+*.salive
# OS generated files #
.DS_Store
diff --git a/.kotlin/sessions/kotlin-compiler-17391608372419177291.salive b/.kotlin/sessions/kotlin-compiler-17391608372419177291.salive
deleted file mode 100644
index e69de29bb..000000000
diff --git a/samples/manage-features/README.md b/samples/manage-features/README.md
new file mode 100644
index 000000000..d820d1e61
--- /dev/null
+++ b/samples/manage-features/README.md
@@ -0,0 +1,42 @@
+# Manage features
+
+Create, update, and delete features to manage a feature layer.
+
+
+
+## Use case
+
+An end-user performing a survey may want to manage features on the map in various ways during the course of their work.
+
+## How to use the sample
+
+Pick an operation, then tap on the map to perform the operation at that location. Available feature management operations include: "Create feature", "Delete feature", "Update attribute", and "Update geometry".
+
+## How it works
+
+1. Create a `ServiceGeodatabase` from a URL.
+2. Get a `ServiceFeatureTable` from the `ServiceGeodatabase`.
+3. Create a `FeatureLayer` derived from the `ServiceFeatureTable` instance.
+4. Apply the feature management operation upon tapping the map.
+ - Create features: create a `Feature` with attributes and a location using the `ServiceFeatureTable`.
+ - Delete features: delete the selected `Feature` from the `FeatureTable`.
+ - Update attribute: update the attribute of the selected `Feature`.
+ - Update geometry: update the geometry of the selected `Feature`.
+5. Update the `FeatureTable` locally.
+6. Update the `ServiceGeodatabase` of the `ServiceFeatureTable` by calling `applyEdits()`. This pushes the changes to the server.
+
+## Relevant API
+
+* Feature
+* FeatureEditResult
+* FeatureLayer
+* ServiceFeatureTable
+* ServiceGeodatabase
+
+## Additional information
+
+When editing feature tables that are subject to database behavior (operations on one table affecting another table), it's recommended to call these methods (apply or undo edits) on the `ServiceGeodatabase` object rather than on the `ServiceFeatureTable` object. Using the `ServiceGeodatabase` object to call these operations will prevent possible data inconsistencies and ensure transactional integrity so that all changes can be committed or rolled back.
+
+## Tags
+
+amend, attribute, create, delete, deletion, details, edit, editing, feature, feature layer, feature table, geodatabase, information, moving, online service, service, update, updating, value
diff --git a/samples/manage-features/README.metadata.json b/samples/manage-features/README.metadata.json
new file mode 100644
index 000000000..c54b956aa
--- /dev/null
+++ b/samples/manage-features/README.metadata.json
@@ -0,0 +1,50 @@
+{
+ "category": "Edit and Manage Data",
+ "description": "Create, update, and delete features to manage a feature layer.",
+ "formal_name": "ManageFeatures",
+ "ignore": false,
+ "images": [
+ "manage-features.png"
+ ],
+ "keywords": [
+ "amend",
+ "attribute",
+ "create",
+ "delete",
+ "deletion",
+ "details",
+ "edit",
+ "editing",
+ "feature",
+ "feature layer",
+ "feature table",
+ "geodatabase",
+ "information",
+ "moving",
+ "online service",
+ "service",
+ "update",
+ "updating",
+ "value",
+ "Feature",
+ "FeatureEditResult",
+ "FeatureLayer",
+ "ServiceFeatureTable",
+ "ServiceGeodatabase"
+ ],
+ "language": "kotlin",
+ "redirect_from": "",
+ "relevant_apis": [
+ "Feature",
+ "FeatureEditResult",
+ "FeatureLayer",
+ "ServiceFeatureTable",
+ "ServiceGeodatabase"
+ ],
+ "snippets": [
+ "src/main/java/com/esri/arcgismaps/sample/managefeatures/components/ManageFeaturesViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/managefeatures/MainActivity.kt",
+ "src/main/java/com/esri/arcgismaps/sample/managefeatures/screens/ManageFeaturesScreen.kt"
+ ],
+ "title": "Manage features"
+}
diff --git a/samples/manage-features/build.gradle.kts b/samples/manage-features/build.gradle.kts
new file mode 100644
index 000000000..7c48d294f
--- /dev/null
+++ b/samples/manage-features/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ alias(libs.plugins.arcgismaps.android.library)
+ alias(libs.plugins.arcgismaps.android.library.compose)
+ alias(libs.plugins.arcgismaps.kotlin.sample)
+ alias(libs.plugins.gradle.secrets)
+}
+
+secrets {
+ // this file doesn't contain secrets, it just provides defaults which can be committed into git.
+ defaultPropertiesFileName = "secrets.defaults.properties"
+}
+
+android {
+ namespace = "com.esri.arcgismaps.sample.managefeatures"
+ buildFeatures {
+ buildConfig = true
+ }
+}
+
+dependencies {
+ // Only module specific dependencies needed here
+}
diff --git a/samples/manage-features/manage-features.png b/samples/manage-features/manage-features.png
new file mode 100644
index 000000000..07e595c46
Binary files /dev/null and b/samples/manage-features/manage-features.png differ
diff --git a/samples/manage-features/src/main/AndroidManifest.xml b/samples/manage-features/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..4035bbdae
--- /dev/null
+++ b/samples/manage-features/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/MainActivity.kt b/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/MainActivity.kt
new file mode 100644
index 000000000..e63896c5c
--- /dev/null
+++ b/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/MainActivity.kt
@@ -0,0 +1,53 @@
+/* Copyright 2025 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.managefeatures
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import com.arcgismaps.ApiKey
+import com.arcgismaps.ArcGISEnvironment
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.managefeatures.screens.ManageFeaturesScreen
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // authentication with an API key or named user is
+ // required to access basemaps and other location services
+ ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
+
+ setContent {
+ SampleAppTheme {
+ ManageFeaturesApp()
+ }
+ }
+ }
+
+ @Composable
+ private fun ManageFeaturesApp() {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ ManageFeaturesScreen(
+ sampleName = getString(R.string.manage_features_app_name)
+ )
+ }
+ }
+}
diff --git a/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/components/ManageFeaturesViewModel.kt b/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/components/ManageFeaturesViewModel.kt
new file mode 100644
index 000000000..c8e44e6e5
--- /dev/null
+++ b/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/components/ManageFeaturesViewModel.kt
@@ -0,0 +1,332 @@
+/* Copyright 2025 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.managefeatures.components
+
+import android.app.Application
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.arcgismaps.LoadStatus
+import com.arcgismaps.data.ArcGISFeature
+import com.arcgismaps.data.CodedValueDomain
+import com.arcgismaps.data.ServiceFeatureTable
+import com.arcgismaps.data.ServiceGeodatabase
+import com.arcgismaps.geometry.GeometryEngine
+import com.arcgismaps.geometry.Point
+import com.arcgismaps.geometry.SpatialReference
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.Viewpoint
+import com.arcgismaps.mapping.layers.FeatureLayer
+import com.arcgismaps.mapping.view.ScreenCoordinate
+import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
+import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
+import kotlinx.coroutines.launch
+
+
+class ManageFeaturesViewModel(application: Application) : AndroidViewModel(application) {
+
+ val mapViewProxy = MapViewProxy()
+
+ // Hold a reference to the feature table.
+ private var damageFeatureTable: ServiceFeatureTable? = null
+
+ // Hold a reference to the feature layer.
+ private var damageLayer: FeatureLayer? = null
+
+ // Hold a reference to the selected feature.
+ var selectedFeature: ArcGISFeature? by mutableStateOf(null)
+
+ // The current feature operation to perform.
+ var currentFeatureOperation by mutableStateOf(FeatureOperationType.CREATE)
+
+ // The list of damage types to update the feature attribute.
+ var damageTypeList: List = mutableListOf()
+
+ var currentDamageType by mutableStateOf("")
+
+ // Create the map with streets basemap.
+ val arcGISMap = ArcGISMap(BasemapStyle.ArcGISStreets).apply {
+ // Zoom to the United States.
+ initialViewpoint = Viewpoint(
+ Point(x = -10800000.0, y = 4500000.0, spatialReference = SpatialReference.webMercator()), scale = 3e7
+ )
+ }
+
+ // Create a snackbar message to display the result of feature operations.
+ var snackBarMessage: String by mutableStateOf("")
+
+ // Create a message dialog view model for handling error messages
+ val messageDialogVM = MessageDialogViewModel()
+
+ init {
+ viewModelScope.launch {
+ // Create a service geodatabase from the feature service.
+ val serviceGeodatabase =
+ ServiceGeodatabase("https://sampleserver6.arcgisonline.com/arcgis/rest/services/DamageAssessment/FeatureServer/0")
+ serviceGeodatabase.load().onSuccess {
+ // Get the feature table from the service geodatabase referencing the Damage Assessment feature service.
+ serviceGeodatabase.getTable(0)?.let { serviceFeatureTable ->
+ // Load the feature table to get the coded value domain for the attribute field.
+ serviceFeatureTable.load().onSuccess {
+ // Hold a reference to the feature table.
+ damageFeatureTable = serviceFeatureTable
+ // Get the field from the feature table that will be updated.
+ val typeDamageField = serviceFeatureTable.fields.first { it.name == "typdamage" }
+ // Get the coded value domain for the field.
+ val attributeDomain = typeDamageField.domain as CodedValueDomain
+ // Add the damage types to the list.
+ attributeDomain.codedValues.forEach {
+ damageTypeList += it.name
+ }
+ // Create a feature layer to visualize the features in the table.
+ FeatureLayer.createWithFeatureTable(serviceFeatureTable).let { featureLayer ->
+ // Hold a reference to the feature layer.
+ damageLayer = featureLayer
+ // Add it to the map.
+ arcGISMap.operationalLayers.add(featureLayer)
+ // Load the map.
+ arcGISMap.load().onFailure { error ->
+ messageDialogVM.showMessageDialog(
+ "Failed to load map", error.message.toString()
+ )
+ }
+ }
+ }.onFailure { error ->
+ messageDialogVM.showMessageDialog(
+ "Failed to load feature table", error.message.toString()
+ )
+ }
+ }
+ }.onFailure { error ->
+ // Show the message dialog and pass the error message to be displayed in the dialog.
+ messageDialogVM.showMessageDialog(
+ "Failed to load service geodatabase", error.message.toString()
+ )
+ }
+ }
+ }
+
+ /**
+ * Directs the behaviour of tap's on the map view.
+ */
+ fun onTap(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
+ if (damageLayer?.loadStatus?.value != LoadStatus.Loaded) {
+ snackBarMessage = "Layer not loaded!"
+ return
+ }
+ when (currentFeatureOperation) {
+ FeatureOperationType.CREATE -> createFeatureAt(singleTapConfirmedEvent.screenCoordinate)
+ FeatureOperationType.DELETE -> deleteFeatureAt(singleTapConfirmedEvent.screenCoordinate)
+ FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate)
+ FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(singleTapConfirmedEvent.screenCoordinate)
+ }
+ }
+
+ /**
+ * Set the current feature operation to perform based on the selected index from the dropdown. Also, reset feature
+ * selection.
+ */
+ fun onFeatureOperationSelected(index: Int) {
+ currentFeatureOperation = FeatureOperationType.entries[index]
+ // Reset the selected feature when the operation changes.
+ damageLayer?.clearSelection()
+ selectedFeature = null
+ }
+
+ /**
+ * Create a new feature at the tapped location with some default attributes
+ */
+ private fun createFeatureAt(screenCoordinate: ScreenCoordinate) {
+ // Create the feature.
+ val feature = damageFeatureTable?.createFeature()?.apply {
+ // Get the normalized geometry for the tapped location and use it as the feature's geometry.
+ mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
+ geometry = GeometryEngine.normalizeCentralMeridian(mapPoint)
+ // Set feature attributes.
+ attributes["typdamage"] = "Minor"
+ attributes["primcause"] = "Earthquake"
+ }
+ }
+ feature?.let {
+ viewModelScope.launch {
+ // Add the feature to the table.
+ damageFeatureTable?.addFeature(it)
+ // Apply the edits to the service on the service geodatabase.
+ damageFeatureTable?.serviceGeodatabase?.applyEdits()
+ // Update the feature to get the updated objectid - a temporary ID is used before the feature is added.
+ it.refresh()
+ // Confirm feature addition.
+ snackBarMessage = "Created feature ${it.attributes["objectid"]}"
+ }
+ }
+ }
+
+ /**
+ * Selects a feature at the tapped location in preparation for deletion.
+ */
+ private fun deleteFeatureAt(screenCoordinate: ScreenCoordinate) {
+ damageLayer?.let { damageLayer ->
+ // Clear any existing selection.
+ damageLayer.clearSelection()
+ selectedFeature = null
+ viewModelScope.launch {
+ // Determine if a user tapped on a feature.
+ mapViewProxy.identify(damageLayer, screenCoordinate, 10.dp).onSuccess { identifyResult ->
+ selectedFeature = (identifyResult.geoElements.firstOrNull() as? ArcGISFeature)?.also {
+ damageLayer.selectFeature(it)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Delete the selected feature from the feature table and service geodatabase.
+ */
+ fun deleteSelectedFeature() {
+ selectedFeature?.let {
+ // Get the feature's object id.
+ val featureId = it.attributes["objectid"] as Long
+ viewModelScope.launch {
+ // Delete the feature from the feature table.
+ damageFeatureTable?.deleteFeature(it)?.onSuccess {
+ snackBarMessage = "Deleted feature $featureId"
+ // Apply the edits to the service geodatabase.
+ damageFeatureTable?.serviceGeodatabase?.applyEdits()
+ selectedFeature = null
+ }?.onFailure {
+ snackBarMessage = "Failed to delete feature $featureId"
+ }
+ }
+ }
+ }
+
+ /**
+ * Selects a feature at the tapped location in preparation for attribute editing.
+ */
+ private fun selectFeatureForAttributeEditAt(screenCoordinate: ScreenCoordinate) {
+ damageLayer?.let { damageLayer ->
+ // Clear any existing selection.
+ damageLayer.clearSelection()
+ selectedFeature = null
+ viewModelScope.launch {
+ // Determine if a user tapped on a feature.
+ mapViewProxy.identify(damageLayer, screenCoordinate, 10.dp).onSuccess { identifyResult ->
+ // Get the identified feature.
+ val identifiedFeature = identifyResult.geoElements.firstOrNull() as? ArcGISFeature
+ identifiedFeature?.let {
+ val currentAttributeValue = it.attributes["typdamage"] as String
+ currentDamageType = currentAttributeValue
+ selectedFeature = it.also {
+ damageLayer.selectFeature(it)
+ }
+ } ?: run {
+ // Reset damage type if no feature identified.
+ currentDamageType = ""
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the attribute value of the selected feature to the new value from the new damage type.
+ */
+ fun onDamageTypeSelected(index: Int) {
+ // Get the new value.
+ currentDamageType = damageTypeList[index]
+ selectedFeature?.let { selectedFeature ->
+ viewModelScope.launch {
+ // Load the feature.
+ selectedFeature.load().onSuccess {
+ // Update the attribute value.
+ selectedFeature.attributes["typdamage"] = currentDamageType
+ // Update the table.
+ damageFeatureTable?.updateFeature(selectedFeature)
+ // Update the service on the service geodatabase.
+ damageFeatureTable?.serviceGeodatabase?.applyEdits()?.onSuccess {
+ snackBarMessage =
+ "Updated feature ${selectedFeature.attributes["objectid"]} to $currentDamageType"
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Select a feature, if none is selected. If a feature is selected, update its geometry to the tapped location.
+ */
+ private fun updateFeatureGeometryAt(screenCoordinate: ScreenCoordinate) {
+
+ damageLayer?.let { damageLayer ->
+ when (selectedFeature) {
+ // When no feature is selected.
+ null -> {
+ viewModelScope.launch {
+ // Determine if a user tapped on a feature.
+ mapViewProxy.identify(damageLayer, screenCoordinate, 10.dp).onSuccess { identifyResult ->
+ // Get the identified feature.
+ val identifiedFeature = identifyResult.geoElements.firstOrNull() as? ArcGISFeature
+ identifiedFeature?.let {
+ selectedFeature = it.also {
+ damageLayer.selectFeature(it)
+ }
+ }
+ }
+ }
+ }
+ // When a feature is selected, update its geometry to the tapped location.
+ else -> {
+ mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
+ // Normalize the point - needed when the tapped location is over the international date line.
+ val destinationPoint = GeometryEngine.normalizeCentralMeridian(mapPoint)
+ viewModelScope.launch {
+ selectedFeature?.let { selectedFeature ->
+ // Load the feature.
+ selectedFeature.load().onSuccess {
+ // Update the geometry of the selected feature.
+ selectedFeature.geometry = destinationPoint
+ // Apply the edit to the feature table.
+ damageFeatureTable?.updateFeature(selectedFeature)
+ // Push the update to the service with the service geodatabase.
+ damageFeatureTable?.serviceGeodatabase?.applyEdits()?.onSuccess {
+ snackBarMessage = "Moved feature ${selectedFeature.attributes["objectid"]}"
+ }?.onFailure {
+ snackBarMessage =
+ "Failed to move feature ${selectedFeature.attributes["objectid"]}"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+enum class FeatureOperationType(val operationName: String, val instruction: String) {
+ CREATE("Create feature", "Tap on the map to create a new feature."),
+ DELETE("Delete feature", "Select an existing feature to delete it."),
+ UPDATE_ATTRIBUTE("Update attribute", "Select an existing feature to edit its attribute."),
+ UPDATE_GEOMETRY("Update geometry", "Select an existing feature and tap the map to move it to a new position.")
+}
diff --git a/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/screens/ManageFeaturesScreen.kt b/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/screens/ManageFeaturesScreen.kt
new file mode 100644
index 000000000..bfb20cba8
--- /dev/null
+++ b/samples/manage-features/src/main/java/com/esri/arcgismaps/sample/managefeatures/screens/ManageFeaturesScreen.kt
@@ -0,0 +1,162 @@
+/* Copyright 2025 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.managefeatures.screens
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.arcgismaps.toolkit.geoviewcompose.MapView
+import com.esri.arcgismaps.sample.managefeatures.components.FeatureOperationType
+import com.esri.arcgismaps.sample.managefeatures.components.ManageFeaturesViewModel
+import com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBox
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+import kotlinx.coroutines.launch
+
+/**
+ * Main screen layout for the sample app
+ */
+@Composable
+fun ManageFeaturesScreen(sampleName: String) {
+ val mapViewModel: ManageFeaturesViewModel = viewModel()
+
+ var featureManagementDropdownIndex by remember { mutableIntStateOf(0) }
+
+ val snackbarHostState = remember { SnackbarHostState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ Scaffold(
+ topBar = { SampleTopAppBar(title = sampleName) },
+ snackbarHost = {
+ SnackbarHost(
+ modifier = Modifier.padding(bottom = 128.dp),
+ hostState = snackbarHostState
+ )
+ },
+ content = {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ MapView(
+ modifier = Modifier
+ .weight(1f),
+ mapViewProxy = mapViewModel.mapViewProxy,
+ arcGISMap = mapViewModel.arcGISMap,
+ onSingleTapConfirmed = mapViewModel::onTap,
+ ) {
+ mapViewModel.selectedFeature?.let { selectedFeature ->
+ // Only show the delete button when on the delete feature operation and a feature is selected.
+ if (mapViewModel.currentFeatureOperation == FeatureOperationType.DELETE) {
+ Callout(geoElement = selectedFeature) {
+ Button(onClick = mapViewModel::deleteSelectedFeature) {
+ Text(text = "Delete")
+ }
+ }
+ }
+ // Only show the dropdown for damage type when on the update feature operation.
+ if (mapViewModel.currentFeatureOperation == FeatureOperationType.UPDATE_ATTRIBUTE) {
+ Callout(geoElement = selectedFeature) {
+ DropDownMenuBox(
+ modifier = Modifier
+ .padding(8.dp),
+ textFieldLabel = "Select damage type",
+ textFieldValue = mapViewModel.currentDamageType,
+ dropDownItemList = mapViewModel.damageTypeList,
+ onIndexSelected = { index ->
+ if (mapViewModel.selectedFeature != null) {
+ mapViewModel.onDamageTypeSelected(index)
+ } else {
+ coroutineScope.launch {
+ snackbarHostState.showSnackbar("Please select a feature to update")
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ // Start of drop down and instruction UI.
+ Row(
+ modifier = Modifier
+ .padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ // Show the dropdown for feature management operations.
+ DropDownMenuBox(
+ modifier = Modifier.padding(end = 8.dp),
+ textFieldLabel = "Feature management operation",
+ textFieldValue = mapViewModel.currentFeatureOperation.operationName,
+ dropDownItemList = FeatureOperationType.entries.map { entry -> entry.operationName },
+ onIndexSelected = { index ->
+ mapViewModel.onFeatureOperationSelected(index)
+ featureManagementDropdownIndex = index
+ })
+ }
+ // Show instructions for the current feature operation.
+ Text(
+ text = FeatureOperationType.entries[featureManagementDropdownIndex].instruction,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(8.dp)
+ .animateContentSize()
+ )
+ }
+ // Show snack bar messages with information about feature operations.
+ if (mapViewModel.snackBarMessage != "") {
+ LaunchedEffect(mapViewModel.snackBarMessage) {
+ snackbarHostState.showSnackbar(mapViewModel.snackBarMessage)
+ }
+ }
+ // Show any errors in a message dialog.
+ mapViewModel.messageDialogVM.apply {
+ if (dialogStatus) {
+ MessageDialog(
+ title = messageTitle,
+ description = messageDescription,
+ onDismissRequest = ::dismissDialog
+ )
+ }
+ }
+ }
+ )
+}
diff --git a/samples/manage-features/src/main/res/values/strings.xml b/samples/manage-features/src/main/res/values/strings.xml
new file mode 100644
index 000000000..b284e379a
--- /dev/null
+++ b/samples/manage-features/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Manage features
+