Skip to content
This site is a preview of the main branch. The published documentation is at maplibre.org.

Download maps for offline use

An offline pack stores the style, tiles, fonts, and icons for one area, so the map can show that area without a network connection. Use packs when people need the map in places with no signal, such as on a hike or a flight.

Each map runtime has one OfflineStorage. Get the one for the default runtime with the offlineStorage extension. It also works outside composables, such as in a view model:

App.kt
val offlineStorage = DefaultMapRuntime.instance.offlineStorage

OfflineStorage.create saves a new pack, and OfflineStorage.resume starts its download:

App.kt
val offlineState by offlineStorage.state.collectAsState()
val pixelRatio = LocalDensity.current.density
Button(
enabled = offlineState is OfflineStorageState.Ready,
onClick = {
scope.launch {
val pack =
offlineStorage.create(
definition =
OfflinePackDefinition.TilePyramid(
styleUrl = "https://tiles.openfreemap.org/styles/liberty",
bounds = BoundingBox(west = -122.46, south = 47.48, east = -122.22, north = 47.74),
pixelRatio = pixelRatio,
minZoom = 10.0,
maxZoom = 14.0,
),
metadata = "Seattle".encodeToByteArray(),
)
offlineStorage.resume(pack)
}
},
) {
Text("Download Seattle")
}

The pack is defined by:

  • styleUrl: the style that the map uses. Use the same URL in the map, because MapLibre finds downloaded resources by their URL.
  • bounds: the area to download. To download the area of a GeoJSON shape, such as a hiking route, use OfflinePackDefinition.Shape instead.
  • minZoom and maxZoom: the zoom levels to download. Each extra zoom level needs about four times as many tiles as the one before, so keep maxZoom as low as your use case allows.
  • pixelRatio: the screen density, so the pack has tiles for this screen.

metadata holds any bytes you want to keep with the pack. The example stores a name to show in a list.

The button stays disabled while the storage opens its database.

OfflineStorageState.Ready lists every pack in the database, including packs from earlier app launches. The maplibre-compose-material3 module has OfflinePackListItem, which shows a pack’s progress and has buttons to pause, resume, update, and delete it:

App.kt
val packs = (offlineState as? OfflineStorageState.Ready)?.packs.orEmpty()
Column {
for (pack in packs) {
key(pack) {
val metadata by pack.metadata.collectAsState()
OfflinePackListItem(pack = pack, offlineStorage = offlineStorage) {
Text(metadata?.decodeToString() ?: "Unnamed region")
}
}
}
}

A download stops when the app closes. On the next launch, an incomplete pack is paused until you call resume again. OfflinePackListItem shows a resume button for it.

To build your own UI, collect OfflinePack.downloadProgress. It updates as resources download:

PackProgress.kt
@Composable
fun PackProgress(pack: OfflinePack) {
val progress by pack.downloadProgress.collectAsState()
when (val current = progress) {
is DownloadProgress.Healthy -> {
val fraction =
if (current.requiredResourceCount > 0) {
current.completedResourceCount.toFloat() / current.requiredResourceCount
} else {
0f
}
Column {
LinearProgressIndicator(progress = { fraction })
when (current.status) {
DownloadStatus.Complete -> Text("Ready to use offline")
DownloadStatus.Paused -> Text("Paused")
else -> Text("Downloading")
}
}
}
is DownloadProgress.Error -> Text("Download failed: ${current.message}")
is DownloadProgress.TileLimitExceeded -> Text("Too many tiles; the limit is ${current.limit}")
else -> Text("Waiting for status")
}
}

DownloadProgress.Healthy counts downloaded and required resources and tells you whether the pack is downloading, paused, or complete. DownloadProgress.Error and DownloadProgress.TileLimitExceeded report a problem.

OfflineStorage.delete removes a pack:

DeletePackButton.kt
@Composable
fun DeletePackButton(offlineStorage: OfflineStorage, pack: OfflinePack) {
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { offlineStorage.delete(pack) } }) { Text("Delete") }
}

You do not need to configure the map. A map from the same runtime uses the pack’s resources, so the downloaded area shows when the device has no network.