Files
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

340 lines
9.1 KiB
Markdown

# Jetpack Compose Map Example
An Android application demonstrating proper Mapbox Maps SDK integration in Jetpack Compose following the **mapbox-android-patterns** skill.
## Patterns Demonstrated
✅ **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
## What This Example Shows
This example demonstrates the **fundamental pattern** for integrating Mapbox Maps SDK in Jetpack Compose:
- `AndroidView` for bridging traditional Views → Compose
- `rememberMapViewWithLifecycle()` for lifecycle-aware maps
- `DisposableEffect` for proper cleanup
- Token management with `local.properties` and `BuildConfig`
## Prerequisites
- 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](https://account.mapbox.com/access-tokens/))
## Setup
### 1. Clone and Open Project
```bash
# Open in Android Studio
File → Open → select ComposeMapExample directory
```
### 2. Configure Access Token (Secure Pattern)
Following **mapbox-token-security** skill:
**Create `local.properties` (add to `.gitignore`):**
```properties
MAPBOX_ACCESS_TOKEN=pk.your_actual_token_here
MAPBOX_DOWNLOADS_TOKEN=sk.your_secret_downloads_token_here
```
The build system will automatically inject these tokens into `BuildConfig`.
### 3. Sync and Run
1. Click "Sync Now" when prompted
2. Select your device or emulator
3. Click Run ▶️
## Project Structure
```
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
```
## Key Implementation Details
### MapboxMap.kt - AndroidView Pattern
This is the core pattern from **mapbox-android-patterns**:
```kotlin
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)
}
}
}
```
### MapViewLifecycle.kt - Lifecycle Management
**CRITICAL for preventing memory leaks and crashes:**
```kotlin
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
}
```
### build.gradle.kts - Token Configuration
```kotlin
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
}
}
```
## Key Points from mapbox-android-patterns
✅ **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 code)
- `local.properties` in `.gitignore`
- 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
## Common Modifications
### Adding Markers
```kotlin
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)
}
}
```
### Handling Click Events
```kotlin
LaunchedEffect(mapView) {
mapView.mapboxMap.loadStyle(Style.MAPBOX_STREETS) {
mapView.mapboxMap.addOnMapClickListener { point ->
// Handle map click
onMapClick(point)
true
}
}
}
```
### Custom Style
```kotlin
mapView.mapboxMap.loadStyle(Style.DARK) {
// Style loaded
}
```
## Testing Lifecycle
To verify proper lifecycle handling:
1. **Rotate device** - Map should survive configuration changes
2. **Navigate away** - Check Logcat for onStop/onDestroy calls
3. **LeakCanary** - Add LeakCanary to detect memory leaks
4. **Android Profiler** - Verify memory usage is stable
## Skills Reference
This example follows patterns from:
- **mapbox-android-patterns** - Compose integration patterns
- **mapbox-token-security** - Secure token storage
## Next Steps
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.
## Common Pitfalls Avoided
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
## Troubleshooting
**Map not showing?**
- Verify `MAPBOX_ACCESS_TOKEN` is set in `local.properties`
- Check `BuildConfig.MAPBOX_ACCESS_TOKEN` is not empty
- Verify token has required scopes
- Check Logcat for errors
**Token not found?**
- Ensure `local.properties` exists 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 `DisposableEffect` cleanup 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