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. + +![Screenshot of manage features](manage-features.png) + +## 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 +