Display KML from a URL, portal item, or local KML file.

Use case
Keyhole Markup Language (KML) is a data format used by Google Earth. KML is popular as a transmission format for consumer use and for sharing geographic data between apps. You can use the Maps SDKs to display KML files, with full support for a variety of features, including network links, 3D models, screen overlays, and tours.
How to use the sample
Use the drop-down menu to select a source. A KML file from that source will be loaded and displayed in the map.
How it works
- To create a KML layer from a URL, create a
KmlDatasetusing the URL to the KML file. Then pass the dataset to theKmlLayerconstructor. - To create a KML layer from a portal item, construct a
PortalItemwith aPortaland the KML portal item ID. Pass the portal item to theKmlLayerconstructor. - To create a KML layer from a local file, create a
KmlDatasetusing the absolute file path to the local KML file. Then pass the dataset to theKmlLayerconstructor. - Add the layer as an operational layer to the map with
arcGISMap.operationalLayers.add(kmlLayer).
Relevant API
- KmlDataset
- KmlLayer
Offline data
This sample downloads the US State Capitals, as placemark points in the .kml format from ArcGIS Online.
About the data
This sample displays three different KML files:
- From URL - This is a map of the convective outlook produced by NOAA/NWS Storm Prediction Center. It uses KML network links to always show the latest data.
- From local file - this is a map of U.S. state capitals. It doesn’t define an icon, so the default pushpin is used for the points.
- From portal item - this is a map of U.S. states.
Tags
keyhole, KML, KMZ, OGC
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.addkmllayer
import android.content.Intentimport android.os.Bundleimport com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
class DownloadActivity : DownloaderActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadAndStartSample( Intent(this, MainActivity::class.java), getString(R.string.add_kml_layer_app_name), listOf( "https://www.arcgis.com/home/item.html?id=324e4742820e46cfbe5029ff2c32cb1f" ) ) }}/* 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.addkmllayer
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.addkmllayer.screens.AddKMLLayerScreen
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 { AddKMLLayerApp() } } }
@Composable private fun AddKMLLayerApp() { Surface(color = MaterialTheme.colorScheme.background) { AddKMLLayerScreen( sampleName = getString(R.string.add_kml_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.addkmllayer.components
import android.app.Applicationimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.kml.KmlDatasetimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.layers.KmlLayerimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.esri.arcgismaps.sample.addkmllayer.Rimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.launchimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport java.io.File
/** * ViewModel for the Add KML Layer sample. */class AddKmlLayerViewModel(private val app: Application) : AndroidViewModel(app) {
// Lazy provision path for local sample resource. private val provisionPath: String by lazy { app.getExternalFilesDir(null)?.path.toString() + File.separator + app.getString(R.string.add_kml_layer_app_name) }
// The ArcGIS map used by the composable MapView. val arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight).apply { initialViewpoint = Viewpoint(39.8, -98.6, 10e7) }
// Build the list of KML options. val kmlOptions: List<KmlOption> = listOf( KmlOption.LocalFile(File(provisionPath, "US_State_Capitals.kml")), KmlOption.FromUrl(url = "https://www.spc.noaa.gov/products/outlook/SPC_outlooks.kml"), KmlOption.PortalItemOption( PortalItem( portal = Portal(url = "https://www.arcgis.com"), itemId = "9fe0b1bfdcd64c83bd77ea0452c76253" ) ) )
// Selected label shown in the drop-down TextField. private val _selectedKmlOption = MutableStateFlow(kmlOptions[0]) val selectedKmlOption = _selectedKmlOption.asStateFlow()
// Map proxy to allow the ViewModel to set viewpoint. val mapViewProxy: MapViewProxy = MapViewProxy()
// Loading state to show/hide the loading dialog. private val _isLoading = MutableStateFlow(false) val isLoading = _isLoading.asStateFlow()
// Message dialog ViewModel. val messageDialogVM = MessageDialogViewModel()
init { // Validate local file then load the map. val localKmlFile = File(provisionPath, "US_State_Capitals.kml") if (!localKmlFile.exists()) { messageDialogVM.showMessageDialog( title = "Local KML not found", description = "Expected file at: ${localKmlFile.canonicalPath}" ) }
// Load the default KML layer. setKmlLayer(0) }
/** * Sets the active KML layer on the map based on the provided index from the KML options list. */ fun setKmlLayer(index: Int) { _selectedKmlOption.value = kmlOptions[index]
viewModelScope.launch { _isLoading.value = true val kmlLayer = _selectedKmlOption.value.createLayer() // Replace the map's operational layers with the KML layer. arcGISMap.operationalLayers.apply { clear() add(kmlLayer) } // Attempt to load the KML layer. kmlLayer.load().onSuccess { arcGISMap.load().onFailure { messageDialogVM.showMessageDialog(it) } // If the layer has a full extent after loading, use MapViewProxy to set the viewpoint. kmlLayer.fullExtent?.let { extent -> mapViewProxy.setViewpointGeometry(boundingGeometry = extent, paddingInDips = 25.0) } }.onFailure { error -> messageDialogVM.showMessageDialog( title = "Failed to load KML layer", description = error.message.toString() ) } _isLoading.value = false } }}
/** * Encapsulates the three KML sources used by this sample. */sealed class KmlOption(val label: String) { class FromUrl(val url: String) : KmlOption("From URL") { override fun createLayer(): KmlLayer = KmlLayer(KmlDataset(url)) }
class LocalFile(val file: File) : KmlOption("Local File") { override fun createLayer(): KmlLayer = KmlLayer(KmlDataset(file.canonicalPath)) }
class PortalItemOption(val portalItem: PortalItem) : KmlOption("Portal Item") { override fun createLayer(): KmlLayer = KmlLayer(item = portalItem) }
abstract fun createLayer(): KmlLayer}/* 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.addkmllayer.screens
import androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.addkmllayer.components.AddKmlLayerViewModelimport com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBoximport 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 Add KML Layer sample. */@Composablefun AddKMLLayerScreen(sampleName: String) { val mapViewModel: AddKmlLayerViewModel = viewModel() // Observe the current selected KML option for the DropDown text field. val selectedKmlOption by mapViewModel.selectedKmlOption.collectAsStateWithLifecycle() // Observe loading state from the viewmodel to show loading dialog. val isLoading by mapViewModel.isLoading.collectAsStateWithLifecycle(false)
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(paddingValues), horizontalAlignment = Alignment.CenterHorizontally ) { MapView( modifier = Modifier .weight(1f) .fillMaxWidth(), arcGISMap = mapViewModel.arcGISMap, mapViewProxy = mapViewModel.mapViewProxy ) // Drop down menu to switch between KML sources. Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { DropDownMenuBox( textFieldValue = selectedKmlOption.label, textFieldLabel = "KML Source", dropDownItemList = mapViewModel.kmlOptions.map { it.label }, onIndexSelected = mapViewModel::setKmlLayer ) } }
// Display dialog while loading a KML layer. if (isLoading) { LoadingDialog(loadingMessage = "Loading KmlLayer...") }
// Display errors in a dialog. mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}