Display a layer from a WFS service, requesting only features for the current extent.

Use case
WFS is an open standard with functionality similar to ArcGIS feature services. ArcGIS Maps SDK support for WFS allows you to interoperate with open systems, which are often used in inter-agency efforts, like those for disaster relief.
How to use the sample
Pan and zoom the map to see features within the current map extent. The WFS layer will be populated with features for the visible area whenever you stop navigating the map.
How it works
- Create a
WfsFeatureTablewith a service URL and table name. - Set the feature request mode to
ManualCacheand axis order toNoSwap. - Create a
FeatureLayerfrom the WFS feature table and add it to the map’s operational layers. - Listen for the
onVisibleAreaChangedevent to detect the visible map area, and for theonNavigationChangedevent to detect when the user has stopped navigating. - When navigation ends, call
populateFromService(...)on the WFS feature table, passing a query for the current visible extent. - Display a loading indicator while the WFS table is being populated.
Relevant API
- FeatureLayer
- MapView.onNavigationChanged
- MapView.onVisibleAreaChanged
- WfsFeatureTable
- WfsFeatureTable.populateFromService
About the data
This sample uses a WFS service showing building footprints for downtown Seattle. For more information, see the ArcGIS Online item.
Tags
browse, catalog, feature, interaction cache, layers, OGC, service, web, WFS
Sample Code
/* 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.addwfslayer
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.addwfslayer.screens.AddWfsLayerScreenimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
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)
enableEdgeToEdge() setContent { SampleAppTheme { AddWfsLayerApp() } } }
@Composable private fun AddWfsLayerApp() { Surface(color = MaterialTheme.colorScheme.background) { AddWfsLayerScreen( sampleName = getString(R.string.add_wfs_layer_app_name) ) } }}/* 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.addwfslayer.components
import android.app.Applicationimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.Colorimport com.arcgismaps.data.FeatureRequestModeimport com.arcgismaps.data.QueryParametersimport com.arcgismaps.data.SpatialRelationship.Intersectsimport com.arcgismaps.data.WfsFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.Polygonimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.layers.OgcAxisOrderimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleRendererimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launch
class AddWfsLayerViewModel(app: Application) : AndroidViewModel(app) {
// Message dialog for error reporting val messageDialogVM = MessageDialogViewModel()
// WFS service URL and table name private val wfsUrl = "https://dservices2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/services/Seattle_Downtown_Features/WFSServer?service=wfs&request=getcapabilities" private val wfsTableName = "Seattle_Downtown_Features:Buildings"
// Hold a reference to the WFS feature table for population private val wfsFeatureTable = WfsFeatureTable(url = wfsUrl, tableName = wfsTableName).apply { // Set manual cache mode so features are only requested when we call populateFromService featureRequestMode = FeatureRequestMode.ManualCache // Set axis order to NoSwap as required by the service axisOrder = OgcAxisOrder.NoSwap } // The ArcGISMap with the Seattle downtown initial viewpoint val arcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic).apply { // Envelope for Seattle downtown area (used for initial viewpoint) val seattleEnvelope = Envelope( xMin = -122.341581, yMin = 47.613758, xMax = -122.332662, yMax = 47.617207, spatialReference = SpatialReference.wgs84() )
// FeatureLayer for displaying the WFS features val wfsFeatureLayer = FeatureLayer.createWithFeatureTable(wfsFeatureTable).apply { // Apply a simple red line renderer to building features renderer = SimpleRenderer( SimpleLineSymbol( style = SimpleLineSymbolStyle.Solid, color = Color.red, width = 3f ) ) } initialViewpoint = Viewpoint(boundingGeometry = seattleEnvelope) operationalLayers.add(wfsFeatureLayer) }
// Track if the WFS table is currently populating private val _isPopulating = MutableStateFlow(false) val isPopulating: StateFlow<Boolean> = _isPopulating.asStateFlow()
// Used to track the latest visible area (Polygon) private var visibleArea: Polygon? = null
init { // The FeatureLayer is the only operational layer val featureLayer = arcGISMap.operationalLayers.first() as FeatureLayer
// Load the map and layer viewModelScope.launch { arcGISMap.load().onFailure { messageDialogVM.showMessageDialog(it) } featureLayer.load().onFailure { messageDialogVM.showMessageDialog(it) } } }
/** * Called when the visible area changes. Used to update the visible area and trigger population if needed. */ fun onVisibleAreaChanged(polygon: Polygon) { // Only populate on first visible area event if (visibleArea == null) { visibleArea = polygon populateWfsLayer(polygon.extent) } else { visibleArea = polygon } }
/** * Called when navigation ends. Populates the WFS table for the current visible extent. */ fun onNavigatingChanged(isNavigating: Boolean) { if (!isNavigating) { visibleArea?.extent?.let { populateWfsLayer(it) } } }
/** * Populate the WFS feature table for the given extent. */ private fun populateWfsLayer(extent: Envelope) { viewModelScope.launch { _isPopulating.value = true val query = QueryParameters().apply { geometry = extent spatialRelationship = Intersects } wfsFeatureTable.populateFromService( parameters = query, clearCache = false, outFields = emptyList() ).onFailure { messageDialogVM.showMessageDialog(it) } _isPopulating.value = false } }
}/* 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.addwfslayer.screens
import androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.ui.Modifierimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.geometry.Polygonimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.addwfslayer.components.AddWfsLayerViewModelimport com.esri.arcgismaps.sample.sampleslib.components.LoadingDialogimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen layout for the AddWfsLayer sample app. */@Composablefun AddWfsLayerScreen(sampleName: String) { val mapViewModel: AddWfsLayerViewModel = viewModel() val isPopulating by mapViewModel.isPopulating.collectAsStateWithLifecycle() Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { padding -> Box(modifier = Modifier .fillMaxSize() .padding(padding)) { Column(modifier = Modifier.fillMaxSize()) { MapView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISMap = mapViewModel.arcGISMap, onVisibleAreaChanged = { polygon: Polygon -> mapViewModel.onVisibleAreaChanged(polygon) }, onNavigationChanged = { isNavigating -> mapViewModel.onNavigatingChanged(isNavigating) } ) } if (isPopulating) { LoadingDialog(loadingMessage = "Populating features...") } mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } } )}