Files
mapbox__mapbox-agent-skills/examples/conversations/android-compose-setup.md
Matthew Podwysocki 3c3702c3d7 Add Prettier formatter for consistent code formatting
- Add prettier as dev dependency
- Create .prettierrc config with markdown-specific settings
- Add .prettierignore to exclude node_modules and generated files
- Add npm scripts:
  - format: Format all files with prettier
  - format:check: Check formatting without changes
- Update npm check script to include format:check
- Add format check step to GitHub Actions CI
- Format all existing files with prettier

Prettier config:
- Preserve line wrapping in markdown (proseWrap: preserve)
- 120 char print width for markdown
- Consistent formatting for JSON and JS files

This ensures all markdown, JSON, and JS files maintain consistent
formatting across the repository.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 14:17:21 -05:00

382 lines
11 KiB
Markdown

# Conversation Example: Android Jetpack Compose Setup
This conversation demonstrates how the **mapbox-android-patterns** skill helps integrate Mapbox Maps SDK into a Jetpack Compose app with proper lifecycle handling.
## Skills Used
- 📱 **mapbox-android-patterns** - Provides Jetpack Compose AndroidView pattern and lifecycle management
- 🔐 **mapbox-token-security** - Guides secure token storage in Android apps
---
## Conversation
### User
> How do I add a Mapbox map to my Jetpack Compose screen with proper lifecycle handling?
### AI Assistant
I'll help you integrate Mapbox Maps SDK into your Jetpack Compose app using the correct patterns from the **mapbox-android-patterns** skill.
### Step 1: Add Mapbox SDK Dependency
**Add to your module-level `build.gradle.kts`:**
```kotlin
dependencies {
implementation("com.mapbox.maps:android:11.1.0")
// Compose
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.activity:activity-compose:1.8.2")
}
```
**Add to `settings.gradle.kts`:**
```kotlin
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
credentials {
username = "mapbox"
password = providers.gradleProperty("MAPBOX_DOWNLOADS_TOKEN").get()
}
}
}
}
```
### Step 2: Configure Access Token (Secure Pattern)
**Following mapbox-token-security skill**, store your token securely:
**Create `local.properties` (add to `.gitignore`):**
```properties
MAPBOX_ACCESS_TOKEN=pk.your_actual_token_here
MAPBOX_DOWNLOADS_TOKEN=sk.your_secret_token_here
```
**Update `build.gradle.kts` to read token:**
```kotlin
android {
defaultConfig {
// Read 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 resources
resValue(
"string",
"mapbox_access_token",
properties.getProperty("MAPBOX_ACCESS_TOKEN", "")
)
}
buildFeatures {
buildConfig = true
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
}
```
### Step 3: Create Lifecycle-Aware Composable
**Following the AndroidView pattern from mapbox-android-patterns:**
```kotlin
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
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 = Point.fromLngLat(-122.4194, 37.7749),
zoom: Double = 12.0,
onMapReady: (MapView) -> Unit = {}
) {
// Create and remember MapView with lifecycle awareness
val mapView = rememberMapViewWithLifecycle()
// AndroidView bridges traditional Android Views into Compose
AndroidView(
modifier = modifier,
factory = { mapView },
update = { view ->
// Update map when state changes
view.mapboxMap.setCamera(
CameraOptions.Builder()
.center(center)
.zoom(zoom)
.build()
)
}
)
// Load style and notify when ready
LaunchedEffect(mapView) {
mapView.mapboxMap.loadStyle(Style.MAPBOX_STREETS) {
onMapReady(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 management to prevent memory leaks
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)
onDispose {
lifecycle.removeObserver(observer)
mapView.onDestroy()
}
}
return mapView
}
```
### Step 4: Use in Your Compose Screen
```kotlin
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import com.mapbox.geojson.Point
@Composable
fun MapScreen() {
var center by remember {
mutableStateOf(Point.fromLngLat(-122.4194, 37.7749))
}
var zoom by remember { mutableDoubleStateOf(12.0) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("San Francisco") }
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
MapboxMap(
modifier = Modifier.fillMaxSize(),
center = center,
zoom = zoom,
onMapReady = { mapView ->
// Map is ready, add markers, etc.
}
)
// Optional: Floating action button
FloatingActionButton(
onClick = {
// Update center - map will automatically update
center = Point.fromLngLat(-122.4, 37.77)
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
) {
Icon(Icons.Default.Place, "Center Map")
}
}
}
}
```
### Step 5: Update AndroidManifest.xml
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application>
<!-- ... -->
</application>
</manifest>
```
### Key Points from mapbox-android-patterns
✅ **Proper Lifecycle Management:**
- `rememberMapViewWithLifecycle()` handles ON_START, ON_STOP, ON_DESTROY
- `DisposableEffect` ensures cleanup when composable leaves composition
- Prevents memory leaks and crashes
✅ **Token Security:**
- Token stored in `local.properties` (not in source code)
- `local.properties` added to `.gitignore`
- Token accessed via `BuildConfig` at runtime
✅ **Compose Integration:**
- `AndroidView` bridges traditional Views into Compose
- `remember` prevents MapView recreation on recomposition
- State changes trigger `update` block, not recreation
✅ **Memory Safety:**
- Explicit cleanup in `DisposableEffect.onDispose`
- Lifecycle observer properly removed
- No memory leaks
### Optional: Adding Markers
```kotlin
@Composable
fun MapboxMap(
// ... other parameters
markers: List<MarkerData> = emptyList()
) {
val mapView = rememberMapViewWithLifecycle()
AndroidView(
modifier = modifier,
factory = { mapView },
update = { view ->
// Update camera
view.mapboxMap.setCamera(
CameraOptions.Builder()
.center(center)
.zoom(zoom)
.build()
)
}
)
// Add markers when map is ready
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)
}
}
return mapView
}
```
### Testing Lifecycle
To verify proper lifecycle handling:
1. **Rotate device** - Map should survive configuration changes
2. **Navigate away** - Check LogCat for proper onStop/onDestroy calls
3. **Use Android Studio Profiler** - Verify no memory leaks
4. **Background app** - Map should pause properly
### Common Pitfalls Avoided
❌ **Not calling onDestroy** - Memory leaks and crashes
❌ **Recreating MapView on recomposition** - Performance issues
❌ **Not handling lifecycle events** - App crashes when backgrounded
❌ **Hardcoding token** - Security vulnerability
### Next Steps
Based on your needs:
- **Add clustering** - Use mapbox-web-performance-patterns guidance (applies to Android too)
- **Offline maps** - See offline patterns in mapbox-android-patterns skill
- **Navigation** - Navigation SDK integration patterns
- **Custom styles** - Use mapbox-cartography and mapbox-style-patterns skills
Does this cover what you need?
---
## Skills Application Analysis
### How the skill helped:
1. **Provided correct Compose pattern** - AndroidView with rememberMapViewWithLifecycle
2. **Lifecycle management** - Complete lifecycle event handling (START, STOP, DESTROY)
3. **Security guidance** - Secure token storage using local.properties and BuildConfig
4. **Memory safety** - Proper cleanup in DisposableEffect to prevent leaks
5. **Compose idioms** - Correct use of remember, DisposableEffect, LaunchedEffect
### Without the skill:
An AI assistant without the skill might:
- Forget lifecycle callbacks (causes crashes)
- Not use DisposableEffect for cleanup (memory leaks)
- Hardcode token in source (security risk)
- Recreate MapView on every recomposition (performance issues)
- Not handle configuration changes properly
### Common mistakes avoided:
❌ Missing `mapView.onDestroy()` in cleanup
❌ Not using `remember` - MapView recreated on every recomposition
❌ Forgetting `LifecycleEventObserver` - crashes when app goes to background
❌ Hardcoding token: `ResourceOptionsManager.getDefault(context).update { ... }`
### Result:
The skill-enhanced AI provided a complete, secure, lifecycle-aware Compose implementation following Android best practices and Mapbox's recommended patterns.