An Android application demonstrating proper Mapbox Maps SDK integration in Jetpack Compose following the mapbox-android-patterns skill.
✅ AndroidView pattern - Proper Compose integration ✅ Lifecycle awareness - Handles START, STOP, DESTROY ✅ Token security - Using local.properties and BuildConfig ✅ Memory safety - DisposableEffect cleanup ✅ State management - Reactive camera updates
This example demonstrates the fundamental pattern for integrating Mapbox Maps SDK in Jetpack Compose:
AndroidViewfor bridging traditional Views → ComposerememberMapViewWithLifecycle()for lifecycle-aware mapsDisposableEffectfor proper cleanup- Token management with
local.propertiesandBuildConfig
- Android Studio Hedgehog (2023.1.1) or later
- Minimum SDK: 21 (Android 5.0)
- Target SDK: 34 (Android 14)
- A Mapbox access token (get one free)
# Open in Android Studio
File → Open → select ComposeMapExample directoryFollowing mapbox-token-security skill:
Create local.properties (add to .gitignore):
MAPBOX_ACCESS_TOKEN=pk.your_actual_token_here
MAPBOX_DOWNLOADS_TOKEN=sk.your_secret_downloads_token_hereThe build system will automatically inject these tokens into BuildConfig.
- Click "Sync Now" when prompted
- Select your device or emulator
- Click Run
▶️
ComposeMapExample/
├── app/
│ ├── src/main/
│ │ ├── java/com/example/composemapexample/
│ │ │ ├── MainActivity.kt # Entry point
│ │ │ ├── MapScreen.kt # Main screen with map
│ │ │ ├── MapboxMap.kt # Reusable map component
│ │ │ └── MapViewLifecycle.kt # Lifecycle helper
│ │ ├── AndroidManifest.xml
│ │ └── res/
│ └── build.gradle.kts # Token configuration
├── gradle/
├── local.properties # Tokens (gitignored)
└── README.md
This is the core pattern from mapbox-android-patterns:
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.Style
@Composable
fun MapboxMap(
modifier: Modifier = Modifier,
center: Point,
zoom: Double,
onMapReady: (MapView) -> Unit = {}
) {
// Create lifecycle-aware MapView
val mapView = rememberMapViewWithLifecycle()
// Bridge traditional View into Compose
AndroidView(
modifier = modifier,
factory = { mapView },
update = { view ->
// Update camera when state changes
view.mapboxMap.setCamera(
CameraOptions.Builder()
.center(center)
.zoom(zoom)
.build()
)
}
)
// Load style when map is ready
LaunchedEffect(mapView) {
mapView.mapboxMap.loadStyle(Style.MAPBOX_STREETS) {
onMapReady(mapView)
}
}
}CRITICAL for preventing memory leaks and crashes:
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.mapbox.maps.MapView
@Composable
fun rememberMapViewWithLifecycle(): MapView {
val context = LocalContext.current
val lifecycle = LocalLifecycleOwner.current.lifecycle
// Remember MapView across recompositions
val mapView = remember {
MapView(context).apply {
id = View.generateViewId()
}
}
// CRITICAL: Lifecycle observer prevents crashes
DisposableEffect(lifecycle, mapView) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> mapView.onStart()
Lifecycle.Event.ON_STOP -> mapView.onStop()
Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
else -> {}
}
}
lifecycle.addObserver(observer)
// Cleanup when leaving composition
onDispose {
lifecycle.removeObserver(observer)
mapView.onDestroy()
}
}
return mapView
}android {
defaultConfig {
// Read tokens from local.properties
val properties = Properties()
properties.load(project.rootProject.file("local.properties").inputStream())
// Make available in BuildConfig
buildConfigField(
"String",
"MAPBOX_ACCESS_TOKEN",
"\\"${properties.getProperty("MAPBOX_ACCESS_TOKEN", "")}\\" "
)
// Make available in AndroidManifest.xml
manifestPlaceholders["MAPBOX_ACCESS_TOKEN"] =
properties.getProperty("MAPBOX_ACCESS_TOKEN", "")
}
buildFeatures {
buildConfig = true
compose = true
}
}✅ Lifecycle Management:
rememberMapViewWithLifecycle()handles ON_START, ON_STOP, ON_DESTROYDisposableEffectensures cleanup when composable leaves composition- Prevents memory leaks and crashes
✅ Token Security:
- Token stored in
local.properties(not in code) local.propertiesin.gitignore- Accessed via
BuildConfigat runtime
✅ Compose Integration:
AndroidViewbridges traditional Views into Composerememberprevents MapView recreation on recomposition- State changes trigger
updateblock, not recreation
✅ Memory Safety:
- Explicit cleanup in
DisposableEffect.onDispose - Lifecycle observer properly removed
- No memory leaks
LaunchedEffect(mapView, markers) {
mapView.mapboxMap.loadStyle(Style.MAPBOX_STREETS) {
val annotationApi = mapView.annotations
val pointAnnotationManager = annotationApi.createPointAnnotationManager()
val pointAnnotations = markers.map { marker ->
PointAnnotationOptions()
.withPoint(marker.point)
.withIconImage(marker.iconId)
.withTextField(marker.title)
}
pointAnnotationManager.create(pointAnnotations)
}
}LaunchedEffect(mapView) {
mapView.mapboxMap.loadStyle(Style.MAPBOX_STREETS) {
mapView.mapboxMap.addOnMapClickListener { point ->
// Handle map click
onMapClick(point)
true
}
}
}mapView.mapboxMap.loadStyle(Style.DARK) {
// Style loaded
}To verify proper lifecycle handling:
- Rotate device - Map should survive configuration changes
- Navigate away - Check Logcat for onStop/onDestroy calls
- LeakCanary - Add LeakCanary to detect memory leaks
- Android Profiler - Verify memory usage is stable
This example follows patterns from:
- mapbox-android-patterns - Compose integration patterns
- mapbox-token-security - Secure token storage
Once you have this basic pattern working:
- Add offline maps - Download regions for offline use
- Add navigation - Integrate Navigation SDK
- Custom styling - Use mapbox-cartography patterns
- Performance - Battery optimization patterns
See mapbox-android-patterns skill for implementation details.
This example avoids common mistakes:
❌ Not calling onDestroy - Memory leaks ❌ Recreating MapView on recomposition - Performance issues ❌ Not handling lifecycle events - Crashes when backgrounded ❌ Hardcoding token - Security vulnerability ❌ Missing DisposableEffect - Resource leaks
Map not showing?
- Verify
MAPBOX_ACCESS_TOKENis set inlocal.properties - Check
BuildConfig.MAPBOX_ACCESS_TOKENis not empty - Verify token has required scopes
- Check Logcat for errors
Token not found?
- Ensure
local.propertiesexists in project root - Run "Sync Project with Gradle Files"
- Clean and rebuild project
Crashes on background?
- Verify
rememberMapViewWithLifecycle()is used - Check lifecycle callbacks in Logcat
- Ensure
DisposableEffectcleanup is present
Memory leaks?
- Add LeakCanary:
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") - Verify
mapView.onDestroy()is called - Check for missing lifecycle observer removal
Build errors?
- Update to Android Studio Hedgehog or later
- Verify Gradle plugin version is 8.2.0+
- Invalidate caches and restart: File → Invalidate Caches → Invalidate and Restart