← Back to list

Offline Navigation with Mapsforge in Jetpack Compose

Introduction

Yurii Lukiianchuk · 2025-01-24 19:00 · 4 claps · 3.4 min read
#android-app-development #offline-navigation #mapsforge #android-navigation #jetpack-compose-tutorial
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Offline Navigation with Mapsforge in Jetpack Compose

Introduction

Offline navigation is a critical feature for many applications, especially when users venture into areas with limited connectivity. Leveraging Mapsforge, an open-source library, we can implement offline map rendering and advanced features such as dynamic marker management. This article explores integrating Mapsforge with Jetpack Compose to create an offline navigation experience.

Why Mapsforge?

Mapsforge offers an efficient way to render offline .map files. It supports:

  • Vector rendering for optimal performance.
  • Dynamic addition and removal of overlays.
  • Customizable render themes.

With no comprehensive resources available for using Mapsforge in Jetpack Compose, this guide aims to fill that gap.

Setting Up Mapsforge in Your Project

Step 1: Adding Dependencies

Add the Mapsforge dependencies to your build.gradle file:

dependencies {
    implementation 'org.mapsforge:mapsforge-map-android:<version>'
    implementation 'org.mapsforge:mapsforge-map:<version>'
    implementation 'org.mapsforge:mapsforge-core:<version>'
}

Step 2: Preparing Offline Map Files

Ensure your .map files are placed in the assets directory or copied to a cache directory. You can use utility functions like:

fun copyAssetToCache(context: Context, fileName: String): File? {
    val file = File(context.cacheDir, fileName)
    if (!file.exists()) {
        context.assets.open(fileName).use { input ->
            file.outputStream().use { output ->
                input.copyTo(output)
            }
        }
    }
    return file
}

Implementing Mapsforge in Jetpack Compose

Step 1: Creating a Mapsforge Composable

The MapsforgeMapView composable integrates Mapsforge into Compose:

@Composable
fun MapsforgeMapView(
    androidGraphicFactory: AndroidGraphicFactory,
    context: Context,
    mapLocation: String,
    markers: State<Map<String, Marker>>,
    onMapTap: (LatLong) -> Unit
) {
    AndroidView(factory = { ctx ->
        val mapView = MapView(ctx).apply {
            isClickable = true
            val tileCache = AndroidUtil.createTileCache(
                ctx, "cache", model.displayModel.tileSize, 1f, model.frameBufferModel.overdrawFactor
            )
            val mapFile = MapFile(File(mapLocation))
            val tileLayer = TileRendererLayer(
                tileCache, mapFile, model.mapViewPosition, androidGraphicFactory
            ).apply {
                setXmlRenderTheme(InternalRenderTheme.OSMARENDER)
            }
            layerManager.layers.add(tileLayer)
        }
        mapView
    }, update = { mapView ->
        markers.value.forEach { (_, marker) ->
            if (!mapView.layerManager.layers.contains(marker)) {
                mapView.layerManager.layers.add(marker)
            }
        }
    })
}

Step 2: Adding Dynamic Markers

Markers can be dynamically added or removed using MutableState:

val markers = remember { mutableStateOf(mapOf<String, Marker>()) }
fun addMarker(id: String, position: LatLong, color: Int) {
    val marker = Marker(
        position,
        AndroidGraphicFactory.convertToBitmap(context.getDrawable(R.drawable.marker)?.apply {
            setTint(color)
        }),
        0,
        0
    )
    markers.value = markers.value + (id to marker)
}
fun removeMarker(id: String) {
    markers.value = markers.value - id
}

Step 3: Handling User Interactions

Detect taps on the map using GestureDetector:

val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
    override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
        val projection = MapViewProjection(mapView)
        val tappedPosition = projection.fromPixels(event.x.toDouble(), event.y.toDouble())
        onMapTap(tappedPosition)
        return true
    }
})
mapView.setOnTouchListener { _, event ->
    gestureDetector.onTouchEvent(event)
}

By implementing this, you can handle single taps and retrieve the geographical coordinates of the tapped location. This is especially useful for adding markers dynamically based on user input.

Handling Overlapping Markers

When multiple markers are added near the same location, overlaps can occur, making it hard to interact with individual markers. To address this:

  • Offset Markers: Slightly offset markers to ensure visibility.
  • Clustering: Use clustering techniques to group markers when zoomed out, expanding them when zoomed in.
fun offsetMarker(position: LatLong, offset: Double): LatLong {
    return LatLong(position.latitude + offset, position.longitude + offset)
}

Theming in Mapsforge

Mapsforge supports theming through XML render themes, allowing developers to customize map appearance based on the app’s design language. Themes can define colors, line styles, and more.

Applying a Render Theme

To use a custom theme, place the XML file in the assets directory and load it as follows:

val customTheme = File(context.assets.open("custom_theme.xml").bufferedReader().use { it.readText() })
val tileLayer = TileRendererLayer(
    tileCache, mapFile, model.mapViewPosition, androidGraphicFactory
).apply {
    setXmlRenderTheme(ExternalRenderTheme(customTheme))
}

Adapting to Dark Mode

Mapsforge provides a Filter enum for applying effects like inversion. This can be used to adapt maps to dark mode dynamically:

mapView.model.displayModel.filter = if (isDarkMode) Filter.INVERT else Filter.NONE

This ensures a seamless experience for users switching between light and dark themes.

Understanding Layering in Mapsforge

Mapsforge uses a hierarchical layering system for managing map elements. Layers are rendered in the order they are added, making it crucial to manage their sequence properly.

Types of Layers

  1. Tile Layers: Base map tiles rendered from .map files.
  2. Overlay Layers: Elements like markers, polylines, and polygons.
  3. Custom Layers: User-defined elements for specialized use cases.

Managing Layers

Layers are added to the LayerManager:

mapView.layerManager.layers.add(tileLayer)
mapView.layerManager.layers.add(markerLayer)

To remove a layer:

mapView.layerManager.layers.remove(markerLayer)

You can also iterate through layers to apply bulk updates:

mapView.layerManager.layers.forEach { layer ->
    if (layer is Marker) {
        layer.isVisible = false
    }
}

Performance Considerations

For applications with numerous overlays:

  • Limit the number of visible layers based on zoom level.
  • Use tile caching to reduce rendering overhead.
  • Remove offscreen layers dynamically.

Example Application

Here’s how to integrate the MapsforgeMapView into your Compose UI:

@Composable
fun MapScreen() {
    val context = LocalContext.current
    val androidGraphicFactory = AndroidGraphicFactory.INSTANCE
    val mapLocation = remember { copyAssetToCache(context, "world_low.map")?.absolutePath.orEmpty() }
    val markers = remember { mutableStateOf(mapOf<String, Marker>()) }
    Column {
        MapsforgeMapView(
            androidGraphicFactory = androidGraphicFactory,
            context = context,
            mapLocation = mapLocation,
            markers = markers,
            onMapTap = { position ->
                addMarker("marker_${System.currentTimeMillis()}", position, Color.RED.toArgb())
            }
        )
    }
}

Conclusion

Integrating Mapsforge with Jetpack Compose opens up opportunities for offline navigation in modern Android applications. With dynamic marker management, customizable themes, and support for offline .map files, developers can deliver robust and visually appealing mapping solutions. Addressing corner cases like marker overlaps and leveraging Mapsforge’s layering system ensures a seamless user experience. Start building your offline navigation app today!


메타데이터
post_id
cfb48422471f
slug
offline-navigation-with-mapsforge-in-jetpack-compose-cfb48422471f
url
https://medium.com/@ulukiancuk/offline-navigation-with-mapsforge-in-jetpack-compose-cfb48422471f
canonical_url
https://medium.com/@ulukiancuk/offline-navigation-with-mapsforge-in-jetpack-compose-cfb48422471f
author_url
https://medium.com/@ulukiancuk
status
ok
fetched_at
2026-07-25 13:10:30