Take a map offline using a preplanned map area.

Use case
Generating offline maps on demand for a specific area can be time consuming for users and a processing load on the server. If areas of interest are known ahead of time, a web map author can pre-create packages for these areas. This way, the generation only needs to happen once, making the workflow more efficient for users and servers.
An archeology team could define preplanned map areas for dig sites which can be taken offline for field use. To see the difference, compare this sample to the “Generate offline map” sample.
How to use the sample
Select a map area from the Preplanned map areas list. The download progress will be shown and when a download is complete it will be displayed in the map view.
How it works
- Open the online
ArcGISMapfrom aPortalItemand display it. - Create an
OfflineMapTaskusing the portal item. - Get the
PreplannedMapAreas from the task. - To download a selected map area, create default
DownloadPreplannedOfflineMapParametersfrom theOfflineMapTaskusing the selected preplanned map area. - Set the update mode of the preplanned map area.
- Use the parameters and a local path to create a
DownloadPreplannedOfflineMapJobfrom the task. - Start the
DownloadPreplannedOfflineMapJob. Once it has completed, get theDownloadPreplannedOfflineMapResult. - Get the
ArcGISMapfrom the result and display it in theMapView.
Relevant API
- DownloadPreplannedOfflineMapJob
- DownloadPreplannedOfflineMapParameters
- DownloadPreplannedOfflineMapResult
- OfflineMapTask
- PreplannedMapArea
About the data
The Naperville stormwater network map is based on ArcGIS Solutions for Stormwater and provides a realistic depiction of a theoretical stormwater network.
Additional information
PreplannedUpdateMode can be used to set the way the preplanned map area receives updates in several ways:
NoUpdates- No feature updates will be performed.DownloadScheduledUpdates- Scheduled, read-only updates will be downloaded from the online map area and applied to the local mobile geodatabases.DownloadScheduledUpdatesAndUploadNewFeatures- Scheduled, read-only updates are downloaded from the online map area and applied to the local mobile geodatabases. Newly added features can also be uploaded to the feature service.SyncWithFeatureServices- Changes, including local edits, will be synced directly with the underlying feature services.
For more information about offline workflows, see Offline maps, scenes, and data in the ArcGIS Developers guide.
Tags
map area, offline, pre-planned, preplanned
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.downloadpreplannedmaparea
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.downloadpreplannedmaparea.screens.DownloadPreplannedMapAreaScreenimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport java.io.File
class MainActivity : ComponentActivity() {
// The directory where the offline map will be saved private val offlineMapPath by lazy { application.getExternalFilesDir(null)?.path.toString() + File.separator + application.getString( R.string.download_preplanned_map_area_app_name ) }
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)
// Delete any existing offline maps, to reset sample state File(offlineMapPath).deleteRecursively()
enableEdgeToEdge() setContent { SampleAppTheme { DownloadPreplannedMapAreaApp() } } }
@Composable private fun DownloadPreplannedMapAreaApp() { Surface(color = MaterialTheme.colorScheme.background) { DownloadPreplannedMapAreaScreen( sampleName = getString(R.string.download_preplanned_map_area_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.downloadpreplannedmaparea.components
import android.app.Applicationimport androidx.compose.material3.SnackbarHostStateimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateListOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.portal.Portalimport com.arcgismaps.tasks.offlinemaptask.DownloadPreplannedOfflineMapJobimport com.arcgismaps.tasks.offlinemaptask.DownloadPreplannedOfflineMapParametersimport com.arcgismaps.tasks.offlinemaptask.GenerateOfflineMapJobimport com.arcgismaps.tasks.offlinemaptask.OfflineMapTaskimport com.arcgismaps.tasks.offlinemaptask.PreplannedMapAreaimport com.arcgismaps.tasks.offlinemaptask.PreplannedUpdateModeimport com.esri.arcgismaps.sample.downloadpreplannedmaparea.Rimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport java.io.File
class DownloadPreplannedMapAreaViewModel(application: Application) : AndroidViewModel(application) {
// The directory where the offline map will be saved private val offlineMapPath by lazy { application.getExternalFilesDir(null)?.path.toString() + File.separator + application.getString( R.string.download_preplanned_map_area_app_name ) }
// Create a portal to ArcGIS Online private val portal = Portal("https://www.arcgis.com")
// create a portal item using the portal and the item id of a map service private val portalItem = PortalItem(portal, "acc027394bc84c2fb04d1ed317aac674")
private val offlineMapTask = OfflineMapTask(portalItem)
// A list of preplanned map areas populated by the offline map task private var preplannedMapAreas = mutableListOf<PreplannedMapArea>()
// Keep a hash map of downloaded maps private var downloadedMapAreas: HashMap<String, ArcGISMap> = hashMapOf()
// An online map created from the portal item private val onlineMap = ArcGISMap(portalItem)
// The current map shown in the map view var currentMap by mutableStateOf(onlineMap)
val preplannedMapAreaInfoList = mutableStateListOf<PreplannedMapAreaInfo>()
// Defined to send messages related to offlineMapJob val snackbarHostState = SnackbarHostState()
// Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()
init { with(viewModelScope) { launch(Dispatchers.IO) { offlineMapTask.getPreplannedMapAreas().onSuccess { // Keep a list of all preplanned map areas preplannedMapAreas.addAll(it)
// Add all of the preplanned map areas name and download status to a list it.forEachIndexed { index, preplannedMapArea -> preplannedMapAreaInfoList.add( PreplannedMapAreaInfo( index = index, name = preplannedMapArea.portalItem.title, progress = 0f, isDownloaded = false ) ) } } }
launch(Dispatchers.Main) { onlineMap.load().onFailure { messageDialogVM.showMessageDialog(it) } } } }
/** * Show the original map from the portal item. */ fun showOnlineMap() { currentMap = onlineMap }
/** * Download or show the already downloaded preplanned map area. */ fun downloadOrShowOfflineMap(preplannedMapAreaInfo: PreplannedMapAreaInfo) { if (preplannedMapAreaInfo.isDownloaded) { showOfflineMap(preplannedMapAreaInfo) } else { downloadOfflineMap(preplannedMapAreaInfo) } }
/** * Show the offline map of the given preplanned map area name. */ private fun showOfflineMap(preplannedMapAreaInfo: PreplannedMapAreaInfo) { downloadedMapAreas[preplannedMapAreaInfo.name]?.let { selectedArcGISMap -> currentMap = selectedArcGISMap } }
/** * Use the [OfflineMapTask] to create [DownloadPreplannedOfflineMapParameters] for the given [PreplannedMapArea]. * Then use the task to create a [DownloadPreplannedOfflineMapJob] to download the preplanned offline map. */ private fun downloadOfflineMap(preplannedMapAreaInfo: PreplannedMapAreaInfo) { viewModelScope.launch(Dispatchers.IO) { // Get the area of interest for the preplanned map area preplannedMapAreas.find { it.portalItem.title == preplannedMapAreaInfo.name }?.let { preplannedMapArea -> // Create default download parameters from the offline map task offlineMapTask.createDefaultDownloadPreplannedOfflineMapParameters(preplannedMapArea).onSuccess { // Set the update mode to receive no updates it.updateMode = PreplannedUpdateMode.NoUpdates // Define the path where the map will be saved val downloadDirectoryPath = offlineMapPath + File.separator + preplannedMapAreaInfo.name File(downloadDirectoryPath).mkdirs() // Create a job to download the preplanned offline map val downloadPreplannedOfflineMapJob = offlineMapTask.createDownloadPreplannedOfflineMapJob( parameters = it, downloadDirectoryPath = downloadDirectoryPath ) runOfflineMapJob(downloadPreplannedOfflineMapJob, preplannedMapAreaInfo) } } } }
/** * Starts the [GenerateOfflineMapJob], shows the progress dialog and displays the result offline map to the MapView. */ private fun runOfflineMapJob( downloadPreplannedOfflineMapJob: DownloadPreplannedOfflineMapJob, preplannedMapAreaInfo: PreplannedMapAreaInfo ) { with(viewModelScope) { // Collect the job's progress flow val progressCoroutine = launch(Dispatchers.IO) { downloadPreplannedOfflineMapJob.progress.collect { progress -> // Update the UI to this preplanned map area's downloads progress preplannedMapAreaInfoList[preplannedMapAreaInfo.index] = preplannedMapAreaInfo.copy(progress = progress.toFloat() / 100) } } // Start the job and handle the result launch(Dispatchers.IO) { // Start the job and wait for Job result downloadPreplannedOfflineMapJob.start() downloadPreplannedOfflineMapJob.result().onSuccess { downloadedMap -> // Set the offline map result as the displayed currentMap = downloadedMap.offlineMap // Update the UI to show the map as downloaded preplannedMapAreaInfoList[preplannedMapAreaInfo.index] = preplannedMapAreaInfo.copy(isDownloaded = true) // Add the downloaded map to the list of downloaded maps downloadedMapAreas[preplannedMapAreaInfo.name] = downloadedMap.offlineMap // Show user where map was locally saved snackbarHostState.showSnackbar(message = "Map saved at: " + downloadPreplannedOfflineMapJob.downloadDirectoryPath) }.onFailure { messageDialogVM.showMessageDialog(it) } // Cancel the coroutine handling progress reporting progressCoroutine.cancel() } } }}
data class PreplannedMapAreaInfo(val index: Int, val name: String, val progress: Float, val isDownloaded: Boolean)/* 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.downloadpreplannedmaparea.screens
import androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport 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.foundation.layout.requiredSizeimport androidx.compose.foundation.layout.wrapContentSizeimport androidx.compose.foundation.rememberScrollStateimport androidx.compose.foundation.verticalScrollimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Settingsimport androidx.compose.material3.Cardimport androidx.compose.material3.CircularProgressIndicatorimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.FloatingActionButtonimport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.Iconimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.ModalBottomSheetimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.SnackbarHostimport androidx.compose.material3.Textimport androidx.compose.material3.rememberModalBottomSheetStateimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.runtime.snapshots.SnapshotStateListimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Alignment.Companion.CenterVerticallyimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.painterResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.downloadpreplannedmaparea.Rimport com.esri.arcgismaps.sample.downloadpreplannedmaparea.components.DownloadPreplannedMapAreaViewModelimport com.esri.arcgismaps.sample.downloadpreplannedmaparea.components.PreplannedMapAreaInfoimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen layout for the sample app */@OptIn(ExperimentalMaterial3Api::class)@Composablefun DownloadPreplannedMapAreaScreen(sampleName: String) { val mapViewModel: DownloadPreplannedMapAreaViewModel = viewModel()
// Set up the bottom sheet controls var showBottomSheet by remember { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
Scaffold(snackbarHost = { SnackbarHost(hostState = mapViewModel.snackbarHostState) }, topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( modifier = Modifier .fillMaxSize() .padding(it), ) { MapView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISMap = mapViewModel.currentMap, ) // Show bottom sheet with override parameter options if (showBottomSheet) { ModalBottomSheet( modifier = Modifier.wrapContentSize(), sheetState = sheetState, onDismissRequest = { showBottomSheet = false } ) { DownloadPreplannedMap( mapViewModel::showOnlineMap, mapViewModel::downloadOrShowOfflineMap, mapViewModel.preplannedMapAreaInfoList ) } } } mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } }, floatingActionButton = { if (!showBottomSheet) { FloatingActionButton(modifier = Modifier.padding(bottom = 36.dp, end = 12.dp), onClick = { showBottomSheet = true }) { Icon( Icons.Filled.Settings, contentDescription = "Select map" ) } } })}
@Composablefun DownloadPreplannedMap( showOnlineMap: () -> Unit, downloadOrShowMap: (PreplannedMapAreaInfo) -> Unit, preplannedMapAreaInfoList: SnapshotStateList<PreplannedMapAreaInfo>) { Column( modifier = Modifier .wrapContentSize() .background(MaterialTheme.colorScheme.background) .padding(12.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.Start, ) { Text( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(16.dp), text = "Select map", style = MaterialTheme.typography.titleLarge ) Card(modifier = Modifier.wrapContentSize()) { Row( modifier = Modifier .fillMaxWidth() .padding(8.dp) .clickable { showOnlineMap() }, horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = CenterVertically, ) { Text("Web map (online)") } } Text(text = "Preplanned map areas", modifier = Modifier.padding(top = 16.dp, start = 8.dp, bottom = 4.dp)) Card(modifier = Modifier.wrapContentSize()) { preplannedMapAreaInfoList.forEach { Row( modifier = Modifier .fillMaxWidth() .clickable { downloadOrShowMap(it) } .padding(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = CenterVertically, ) { if (!it.isDownloaded) { if (it.progress <= 0) { Icon( painter = painterResource(R.drawable.download_to_24), contentDescription = "Download" ) } else { CircularProgressIndicator( modifier = Modifier.requiredSize(18.dp), progress = { it.progress }) } } Text( text = it.name, color = if (it.isDownloaded) { MaterialTheme.colorScheme.onSurface } else { MaterialTheme.colorScheme.outline } ) } HorizontalDivider() } } }}