mapbox-android-patterns

安装量: 143
排名: #5984

安装

npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-android-patterns

Mapbox Android Integration Patterns Official integration patterns for Mapbox Maps SDK on Android. Covers Kotlin, Jetpack Compose, View system, proper lifecycle management, token handling, offline maps, and mobile-specific optimizations. Use this skill when: Setting up Mapbox Maps SDK for Android in a new or existing project Integrating maps with Jetpack Compose or View system Implementing proper lifecycle management and cleanup Managing tokens securely in Android apps Working with offline maps and caching Integrating Navigation SDK Optimizing for battery life and memory usage Debugging crashes, memory leaks, or performance issues Core Integration Patterns Jetpack Compose Pattern (Modern) Modern approach using Jetpack Compose and Kotlin import androidx . compose . runtime . * import androidx . compose . ui . Modifier import androidx . compose . ui . viewinterop . AndroidView import com . mapbox . maps . MapView import com . mapbox . maps . Style import com . mapbox . maps . plugin . animation . camera import com . mapbox . geojson . Point @Composable fun MapboxMap ( modifier : Modifier = Modifier , center : Point , zoom : Double , onMapReady : ( MapView ) -> Unit = { } ) { val mapView = rememberMapViewWithLifecycle ( ) AndroidView ( modifier = modifier , factory = { mapView } , update = { view -> // Update camera when state changes view . getMapboxMap ( ) . apply { setCamera ( CameraOptions . Builder ( ) . center ( center ) . zoom ( zoom ) . build ( ) ) } } ) LaunchedEffect ( mapView ) { mapView . getMapboxMap ( ) . loadStyleUri ( Style . MAPBOX_STREETS ) { onMapReady ( mapView ) } } } @Composable fun rememberMapViewWithLifecycle ( ) : MapView { val context = LocalContext . current val mapView = remember { MapView ( context ) . apply { id = View . generateViewId ( ) } } // Lifecycle-aware cleanup DisposableEffect ( mapView ) { onDispose { mapView . onDestroy ( ) } } return mapView } // Usage in Composable @Composable fun MapScreen ( ) { var center by remember { mutableStateOf ( Point . fromLngLat ( - 122.4194 , 37.7749 ) ) } var zoom by remember { mutableStateOf ( 12.0 ) } MapboxMap ( modifier = Modifier . fillMaxSize ( ) , center = center , zoom = zoom , onMapReady = { mapView -> // Add sources and layers } ) } Key points: Use AndroidView to integrate MapView in Compose Use remember to preserve MapView across recompositions Use DisposableEffect for proper lifecycle cleanup Handle state updates in update block View System Pattern (Classic) Traditional Android View system with proper lifecycle import android . os . Bundle import androidx . appcompat . app . AppCompatActivity import com . mapbox . maps . MapView import com . mapbox . maps . Style import com . mapbox . maps . plugin . gestures . addOnMapClickListener import com . mapbox . geojson . Point class MapActivity : AppCompatActivity ( ) { private lateinit var mapView : MapView override fun onCreate ( savedInstanceState : Bundle ? ) { super . onCreate ( savedInstanceState ) setContentView ( R . layout . activity_map ) mapView = findViewById ( R . id . mapView ) mapView . getMapboxMap ( ) . loadStyleUri ( Style . MAPBOX_STREETS ) { style -> // Map loaded, add sources and layers setupMap ( style ) } // Add click listener mapView . getMapboxMap ( ) . addOnMapClickListener { point -> handleMapClick ( point ) true } } private fun setupMap ( style : Style ) { // Add your custom sources and layers } private fun handleMapClick ( point : Point ) { // Handle map clicks } // CRITICAL: Lifecycle methods for proper cleanup override fun onStart ( ) { super . onStart ( ) mapView . onStart ( ) } override fun onStop ( ) { super . onStop ( ) mapView . onStop ( ) } override fun onDestroy ( ) { super . onDestroy ( ) mapView . onDestroy ( ) } override fun onLowMemory ( ) { super . onLowMemory ( ) mapView . onLowMemory ( ) } } XML layout (activity_map.xml):

< androidx.constraintlayout.widget.ConstraintLayout xmlns: android = " http://schemas.android.com/apk/res/android " android: layout_width = " match_parent " android: layout_height = " match_parent "

com.mapbox.maps.MapView android: id = " @+id/mapView " android: layout_width = " match_parent " android: layout_height = " match_parent " / </ androidx.constraintlayout.widget.ConstraintLayout

Key points: Call mapView.onStart() , onStop() , onDestroy() , onLowMemory() in corresponding Activity methods Wait for style to load before adding layers Store MapView reference as lateinit var (will be initialized in onCreate) Fragment Pattern import android . os . Bundle import android . view . LayoutInflater import android . view . View import android . view . ViewGroup import androidx . fragment . app . Fragment import com . mapbox . maps . MapView import com . mapbox . maps . Style class MapFragment : Fragment ( ) { private var mapView : MapView ? = null override fun onCreateView ( inflater : LayoutInflater , container : ViewGroup ? , savedInstanceState : Bundle ? ) : View { val view = inflater . inflate ( R . layout . fragment_map , container , false ) mapView = view . findViewById ( R . id . mapView ) mapView ? . getMapboxMap ( ) ? . loadStyleUri ( Style . MAPBOX_STREETS ) { style -> setupMap ( style ) } return view } private fun setupMap ( style : Style ) { // Add sources and layers } override fun onStart ( ) { super . onStart ( ) mapView ? . onStart ( ) } override fun onStop ( ) { super . onStop ( ) mapView ? . onStop ( ) } override fun onDestroyView ( ) { super . onDestroyView ( ) mapView ? . onDestroy ( ) mapView = null } override fun onLowMemory ( ) { super . onLowMemory ( ) mapView ? . onLowMemory ( ) } } Key points: Set mapView to null in onDestroyView() to prevent leaks Use nullable mapView? for safety Call lifecycle methods appropriately Token Management ✅ Recommended: String Resources with BuildConfig 1. Add to local.properties (DO NOT commit):

local.properties (add to .gitignore)

MAPBOX_ACCESS_TOKEN

pk.your_token_here
2. Configure in
build.gradle.kts
(Module):
android
{
defaultConfig
{
// Read from local.properties
val
properties
=
Properties
(
)
properties
.
load
(
project
.
rootProject
.
file
(
"local.properties"
)
.
inputStream
(
)
)
buildConfigField
(
"String"
,
"MAPBOX_ACCESS_TOKEN"
,
"\"
${
properties
.
getProperty
(
"MAPBOX_ACCESS_TOKEN"
,
""
)
}
\""
)
// Also add to resources for SDK
resValue
(
"string"
,
"mapbox_access_token"
,
properties
.
getProperty
(
"MAPBOX_ACCESS_TOKEN"
,
""
)
)
}
buildFeatures
{
buildConfig
=
true
}
}
3. Add to
.gitignore
:
local.properties
4. Usage in code:
import
com
.
yourapp
.
BuildConfig
// Access token automatically picked up from resources
// No need to set manually if in string resources
// Or access programmatically:
val
token
=
BuildConfig
.
MAPBOX_ACCESS_TOKEN
Why this pattern:
Token not in source code or version control
Works in local development and CI/CD (via environment variables)
Automatically injected at build time
No hardcoded secrets
❌ Anti-Pattern: Hardcoded Tokens
// ❌ NEVER DO THIS - Token in source code
MapboxOptions
.
accessToken
=
"pk.YOUR_MAPBOX_TOKEN_HERE"
Memory Management and Lifecycle
✅ Proper Lifecycle Management
import
androidx
.
lifecycle
.
DefaultLifecycleObserver
import
androidx
.
lifecycle
.
LifecycleOwner
import
com
.
mapbox
.
maps
.
MapView
class
MapLifecycleObserver
(
private
val
mapView
:
MapView
)
:
DefaultLifecycleObserver
{
override
fun
onStart
(
owner
:
LifecycleOwner
)
{
mapView
.
onStart
(
)
}
override
fun
onStop
(
owner
:
LifecycleOwner
)
{
mapView
.
onStop
(
)
}
override
fun
onDestroy
(
owner
:
LifecycleOwner
)
{
mapView
.
onDestroy
(
)
}
fun
onLowMemory
(
)
{
mapView
.
onLowMemory
(
)
}
}
// Usage in Activity/Fragment
class
MapActivity
:
AppCompatActivity
(
)
{
private
lateinit
var
mapView
:
MapView
private
lateinit
var
lifecycleObserver
:
MapLifecycleObserver
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
setContentView
(
R
.
layout
.
activity_map
)
mapView
=
findViewById
(
R
.
id
.
mapView
)
lifecycleObserver
=
MapLifecycleObserver
(
mapView
)
// Automatically handle lifecycle
lifecycle
.
addObserver
(
lifecycleObserver
)
}
override
fun
onLowMemory
(
)
{
super
.
onLowMemory
(
)
lifecycleObserver
.
onLowMemory
(
)
}
}
✅ ViewModel Pattern
import
androidx
.
lifecycle
.
ViewModel
import
androidx
.
lifecycle
.
viewModelScope
import
com
.
mapbox
.
geojson
.
Point
import
kotlinx
.
coroutines
.
flow
.
MutableStateFlow
import
kotlinx
.
coroutines
.
flow
.
StateFlow
import
kotlinx
.
coroutines
.
launch
data
class
MapState
(
val
center
:
Point
=
Point
.
fromLngLat
(
-
122.4194
,
37.7749
)
,
val
zoom
:
Double
=
12.0
,
val
markers
:
List
<
Point
>
=
emptyList
(
)
)
class
MapViewModel
:
ViewModel
(
)
{
private
val
_mapState
=
MutableStateFlow
(
MapState
(
)
)
val
mapState
:
StateFlow
<
MapState
>
=
_mapState
fun
updateCenter
(
point
:
Point
)
{
_mapState
.
value
=
_mapState
.
value
.
copy
(
center
=
point
)
}
fun
addMarker
(
point
:
Point
)
{
val
currentMarkers
=
_mapState
.
value
.
markers
_mapState
.
value
=
_mapState
.
value
.
copy
(
markers
=
currentMarkers
+
point
)
}
fun
loadData
(
)
{
viewModelScope
.
launch
{
// Load data from repository
// Update state when ready
}
}
}
Benefits:
State survives configuration changes
Separates business logic from UI
Lifecycle-aware
Easy to test
Offline Maps
Download Region for Offline Use
import
com
.
mapbox
.
maps
.
TileStore
import
com
.
mapbox
.
maps
.
TileRegionLoadOptions
import
com
.
mapbox
.
common
.
TileRegion
import
com
.
mapbox
.
geojson
.
Point
import
com
.
mapbox
.
bindgen
.
Expected
class
OfflineManager
(
private
val
context
:
Context
)
{
private
val
tileStore
=
TileStore
.
create
(
)
fun
downloadRegion
(
regionId
:
String
,
bounds
:
CoordinateBounds
,
minZoom
:
Int
=
0
,
maxZoom
:
Int
=
16
,
onProgress
:
(
Float
)
->
Unit
,
onComplete
:
(
Result
<
Unit
>
)
->
Unit
)
{
val
tilesetDescriptor
=
tileStore
.
createDescriptor
(
TilesetDescriptorOptions
.
Builder
(
)
.
styleURI
(
Style
.
MAPBOX_STREETS
)
.
minZoom
(
minZoom
.
toByte
(
)
)
.
maxZoom
(
maxZoom
.
toByte
(
)
)
.
build
(
)
)
val
loadOptions
=
TileRegionLoadOptions
.
Builder
(
)
.
geometry
(
bounds
.
toGeometry
(
)
)
.
descriptors
(
listOf
(
tilesetDescriptor
)
)
.
acceptExpired
(
false
)
.
build
(
)
val
cancelable
=
tileStore
.
loadTileRegion
(
regionId
,
loadOptions
,
{
progress
->
val
percent
=
(
progress
.
completedResourceCount
.
toFloat
(
)
/
progress
.
requiredResourceCount
.
toFloat
(
)
)
*
100
onProgress
(
percent
)
}
)
{
expected
->
if
(
expected
.
isValue
)
{
onComplete
(
Result
.
success
(
Unit
)
)
}
else
{
onComplete
(
Result
.
failure
(
Exception
(
expected
.
error
?
.
message
)
)
)
}
}
}
fun
getTileRegions
(
callback
:
(
List
<
TileRegion
>
)
->
Unit
)
{
tileStore
.
getAllTileRegions
{
expected
->
if
(
expected
.
isValue
)
{
callback
(
expected
.
value
?:
emptyList
(
)
)
}
else
{
callback
(
emptyList
(
)
)
}
}
}
fun
removeTileRegion
(
regionId
:
String
,
callback
:
(
Boolean
)
->
Unit
)
{
tileStore
.
removeTileRegion
(
regionId
)
callback
(
true
)
}
fun
estimateStorageSize
(
bounds
:
CoordinateBounds
,
minZoom
:
Int
,
maxZoom
:
Int
)
:
Long
{
// Rough estimate: 50 KB per tile average
val
tileCount
=
estimateTileCount
(
bounds
,
minZoom
,
maxZoom
)
return
tileCount
*
50_000L
// bytes
}
private
fun
estimateTileCount
(
bounds
:
CoordinateBounds
,
minZoom
:
Int
,
maxZoom
:
Int
)
:
Long
{
// Simplified tile count estimation
var
count
=
0L
for
(
zoom
in
minZoom
..
maxZoom
)
{
val
tilesAtZoom
=
Math
.
pow
(
4.0
,
zoom
.
toDouble
(
)
)
.
toLong
(
)
count
+=
tilesAtZoom
}
return
count
}
}
Key considerations:
Battery impact:
Downloading uses significant battery
Storage limits:
Monitor available disk space
Zoom levels:
Higher zoom = more tiles = more storage
Network type:
WiFi vs cellular
Check Available Storage
import
android
.
os
.
StatFs
import
android
.
os
.
Environment
fun
getAvailableStorageBytes
(
)
:
Long
{
val
stat
=
StatFs
(
Environment
.
getDataDirectory
(
)
.
path
)
return
stat
.
availableBlocksLong
*
stat
.
blockSizeLong
}
fun
hasEnoughStorage
(
requiredBytes
:
Long
)
:
Boolean
{
val
available
=
getAvailableStorageBytes
(
)
return
available
>
requiredBytes
*
2
// 2x buffer
}
Navigation SDK Integration
Basic Navigation Setup
import
com
.
mapbox
.
navigation
.
core
.
MapboxNavigation
import
com
.
mapbox
.
navigation
.
core
.
MapboxNavigationProvider
import
com
.
mapbox
.
navigation
.
core
.
directions
.
session
.
RoutesObserver
import
com
.
mapbox
.
navigation
.
core
.
trip
.
session
.
RouteProgressObserver
import
com
.
mapbox
.
navigation
.
core
.
trip
.
session
.
TripSessionState
import
com
.
mapbox
.
api
.
directions
.
v5
.
models
.
DirectionsRoute
import
com
.
mapbox
.
geojson
.
Point
class
NavigationActivity
:
AppCompatActivity
(
)
{
private
lateinit
var
mapboxNavigation
:
MapboxNavigation
private
lateinit
var
mapView
:
MapView
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
setContentView
(
R
.
layout
.
activity_navigation
)
mapView
=
findViewById
(
R
.
id
.
mapView
)
// Initialize Navigation SDK
mapboxNavigation
=
MapboxNavigationProvider
.
create
(
NavigationOptions
.
Builder
(
this
)
.
accessToken
(
getString
(
R
.
string
.
mapbox_access_token
)
)
.
build
(
)
)
setupObservers
(
)
}
private
fun
setupObservers
(
)
{
// Observe route updates
mapboxNavigation
.
registerRoutesObserver
(
object
:
RoutesObserver
{
override
fun
onRoutesChanged
(
result
:
RoutesUpdatedResult
)
{
val
routes
=
result
.
navigationRoutes
if
(
routes
.
isNotEmpty
(
)
)
{
// Show route on map
showRouteOnMap
(
routes
.
first
(
)
)
}
}
}
)
// Observe navigation progress
mapboxNavigation
.
registerRouteProgressObserver
(
object
:
RouteProgressObserver
{
override
fun
onRouteProgressChanged
(
routeProgress
:
RouteProgress
)
{
// Update UI with progress
val
distanceRemaining
=
routeProgress
.
distanceRemaining
val
durationRemaining
=
routeProgress
.
durationRemaining
}
}
)
}
fun
startNavigation
(
destination
:
Point
)
{
// Request route
val
origin
=
mapboxNavigation
.
navigationOptions
.
locationEngine
.
getLastLocation
{
location
->
location
?
.
let
{
val
originPoint
=
Point
.
fromLngLat
(
it
.
longitude
,
it
.
latitude
)
requestRoute
(
originPoint
,
destination
)
}
}
}
private
fun
requestRoute
(
origin
:
Point
,
destination
:
Point
)
{
val
routeOptions
=
RouteOptions
.
builder
(
)
.
applyDefaultNavigationOptions
(
)
.
coordinates
(
listOf
(
origin
,
destination
)
)
.
build
(
)
mapboxNavigation
.
requestRoutes
(
routeOptions
,
object
:
NavigationRouterCallback
{
override
fun
onRoutesReady
(
routes
:
List
<
NavigationRoute
>
,
routerOrigin
:
RouterOrigin
)
{
mapboxNavigation
.
setNavigationRoutes
(
routes
)
mapboxNavigation
.
startTripSession
(
)
}
override
fun
onFailure
(
reasons
:
List
<
RouterFailure
>
,
routeOptions
:
RouteOptions
)
{
// Handle error
}
override
fun
onCanceled
(
routeOptions
:
RouteOptions
,
routerOrigin
:
RouterOrigin
)
{
// Handle cancellation
}
}
)
}
private
fun
showRouteOnMap
(
route
:
NavigationRoute
)
{
// Draw route on map
}
override
fun
onDestroy
(
)
{
super
.
onDestroy
(
)
mapboxNavigation
.
onDestroy
(
)
}
}
Navigation SDK features:
Turn-by-turn guidance
Voice instructions
Route progress tracking
Rerouting
Traffic-aware routing
Offline navigation (with offline regions)
Mobile Performance Optimization
Battery Optimization
import
android
.
content
.
Context
import
android
.
os
.
PowerManager
class
BatteryAwareMapActivity
:
AppCompatActivity
(
)
{
private
lateinit
var
mapView
:
MapView
private
lateinit
var
powerManager
:
PowerManager
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
setContentView
(
R
.
layout
.
activity_map
)
mapView
=
findViewById
(
R
.
id
.
mapView
)
powerManager
=
getSystemService
(
Context
.
POWER_SERVICE
)
as
PowerManager
observeBatteryState
(
)
}
private
fun
observeBatteryState
(
)
{
if
(
powerManager
.
isPowerSaveMode
)
{
enableLowPowerMode
(
)
}
// Register broadcast receiver for power save mode changes
registerReceiver
(
object
:
BroadcastReceiver
(
)
{
override
fun
onReceive
(
context
:
Context
?
,
intent
:
Intent
?
)
{
if
(
powerManager
.
isPowerSaveMode
)
{
enableLowPowerMode
(
)
}
else
{
enableNormalMode
(
)
}
}
}
,
IntentFilter
(
PowerManager
.
ACTION_POWER_SAVE_MODE_CHANGED
)
)
}
private
fun
enableLowPowerMode
(
)
{
// Reduce frame rate
mapView
.
getMapboxMap
(
)
.
setMaximumFps
(
30
)
// Disable 3D features
// Reduce tile quality
}
private
fun
enableNormalMode
(
)
{
mapView
.
getMapboxMap
(
)
.
setMaximumFps
(
60
)
}
}
Memory Optimization
override
fun
onLowMemory
(
)
{
super
.
onLowMemory
(
)
mapView
.
onLowMemory
(
)
// Clear map cache
mapView
.
getMapboxMap
(
)
.
clearData
{
result
->
if
(
result
.
isValue
)
{
Log
.
d
(
"Map"
,
"Cache cleared"
)
}
}
}
override
fun
onTrimMemory
(
level
:
Int
)
{
super
.
onTrimMemory
(
level
)
when
(
level
)
{
ComponentCallbacks2
.
TRIM_MEMORY_RUNNING_LOW
,
ComponentCallbacks2
.
TRIM_MEMORY_RUNNING_CRITICAL
->
{
// Clear non-essential data
mapView
.
getMapboxMap
(
)
.
clearData
{
}
}
}
}
Network Optimization
import
android
.
net
.
ConnectivityManager
import
android
.
net
.
NetworkCapabilities
class
NetworkAwareMapActivity
:
AppCompatActivity
(
)
{
private
lateinit
var
connectivityManager
:
ConnectivityManager
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
connectivityManager
=
getSystemService
(
Context
.
CONNECTIVITY_SERVICE
)
as
ConnectivityManager
observeNetworkState
(
)
}
private
fun
observeNetworkState
(
)
{
val
networkCallback
=
object
:
ConnectivityManager
.
NetworkCallback
(
)
{
override
fun
onCapabilitiesChanged
(
network
:
Network
,
capabilities
:
NetworkCapabilities
)
{
when
{
capabilities
.
hasTransport
(
NetworkCapabilities
.
TRANSPORT_WIFI
)
->
{
// WiFi - use full quality
enableHighQuality
(
)
}
capabilities
.
hasTransport
(
NetworkCapabilities
.
TRANSPORT_CELLULAR
)
->
{
// Cellular - reduce data usage
enableLowDataMode
(
)
}
}
}
}
connectivityManager
.
registerDefaultNetworkCallback
(
networkCallback
)
}
private
fun
enableHighQuality
(
)
{
// Use full resolution tiles
}
private
fun
enableLowDataMode
(
)
{
// Reduce tile resolution
// Limit prefetching
}
}
Common Mistakes and Solutions
❌ Mistake 1: Not Calling Lifecycle Methods
// ❌ BAD: MapView lifecycle not managed
class
MapActivity
:
AppCompatActivity
(
)
{
private
lateinit
var
mapView
:
MapView
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
mapView
=
findViewById
(
R
.
id
.
mapView
)
// No lifecycle methods called!
}
}
// ✅ GOOD: Proper lifecycle management
class
MapActivity
:
AppCompatActivity
(
)
{
private
lateinit
var
mapView
:
MapView
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
mapView
=
findViewById
(
R
.
id
.
mapView
)
}
override
fun
onStart
(
)
{
super
.
onStart
(
)
mapView
.
onStart
(
)
}
override
fun
onStop
(
)
{
super
.
onStop
(
)
mapView
.
onStop
(
)
}
override
fun
onDestroy
(
)
{
super
.
onDestroy
(
)
mapView
.
onDestroy
(
)
}
override
fun
onLowMemory
(
)
{
super
.
onLowMemory
(
)
mapView
.
onLowMemory
(
)
}
}
❌ Mistake 2: Memory Leaks in Fragments
// ❌ BAD: MapView not cleaned up in Fragment
class
MapFragment
:
Fragment
(
)
{
private
lateinit
var
mapView
:
MapView
override
fun
onCreateView
(
inflater
:
LayoutInflater
,
container
:
ViewGroup
?
,
savedInstanceState
:
Bundle
?
)
:
View
{
val
view
=
inflater
.
inflate
(
R
.
layout
.
fragment_map
,
container
,
false
)
mapView
=
view
.
findViewById
(
R
.
id
.
mapView
)
return
view
}
// No cleanup!
}
// ✅ GOOD: Proper cleanup
class
MapFragment
:
Fragment
(
)
{
private
var
mapView
:
MapView
?
=
null
override
fun
onCreateView
(
inflater
:
LayoutInflater
,
container
:
ViewGroup
?
,
savedInstanceState
:
Bundle
?
)
:
View
{
val
view
=
inflater
.
inflate
(
R
.
layout
.
fragment_map
,
container
,
false
)
mapView
=
view
.
findViewById
(
R
.
id
.
mapView
)
return
view
}
override
fun
onDestroyView
(
)
{
super
.
onDestroyView
(
)
mapView
?
.
onDestroy
(
)
mapView
=
null
// Prevent leaks
}
}
❌ Mistake 3: Ignoring Location Permissions
// ❌ BAD: Enabling location without checking permissions
mapView
.
location
.
enabled
=
true
// ✅ GOOD: Request and check permissions
import
androidx
.
activity
.
result
.
contract
.
ActivityResultContracts
class
MapActivity
:
AppCompatActivity
(
)
{
private
val
locationPermissionRequest
=
registerForActivityResult
(
ActivityResultContracts
.
RequestMultiplePermissions
(
)
)
{
permissions
->
when
{
permissions
[
Manifest
.
permission
.
ACCESS_FINE_LOCATION
]
==
true
->
{
enableLocationTracking
(
)
}
permissions
[
Manifest
.
permission
.
ACCESS_COARSE_LOCATION
]
==
true
->
{
enableLocationTracking
(
)
}
else
->
{
// Handle denied
}
}
}
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
requestLocationPermissions
(
)
}
private
fun
requestLocationPermissions
(
)
{
when
{
ContextCompat
.
checkSelfPermission
(
this
,
Manifest
.
permission
.
ACCESS_FINE_LOCATION
)
==
PackageManager
.
PERMISSION_GRANTED
->
{
enableLocationTracking
(
)
}
else
->
{
locationPermissionRequest
.
launch
(
arrayOf
(
Manifest
.
permission
.
ACCESS_FINE_LOCATION
,
Manifest
.
permission
.
ACCESS_COARSE_LOCATION
)
)
}
}
}
private
fun
enableLocationTracking
(
)
{
mapView
.
location
.
enabled
=
true
}
}
Add to AndroidManifest.xml:
<
uses-permission
android:
name
=
"
android.permission.ACCESS_FINE_LOCATION
"
/>
<
uses-permission
android:
name
=
"
android.permission.ACCESS_COARSE_LOCATION
"
/>
❌ Mistake 4: Adding Layers Before Map Loads
// ❌ BAD: Adding layers immediately
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
mapView
=
findViewById
(
R
.
id
.
mapView
)
addCustomLayers
(
)
// Map not loaded yet!
}
// ✅ GOOD: Wait for style to load
override
fun
onCreate
(
savedInstanceState
:
Bundle
?
)
{
super
.
onCreate
(
savedInstanceState
)
mapView
=
findViewById
(
R
.
id
.
mapView
)
mapView
.
getMapboxMap
(
)
.
loadStyleUri
(
Style
.
MAPBOX_STREETS
)
{
style
->
addCustomLayers
(
style
)
}
}
Testing Patterns
Unit Testing Map Logic
import
org
.
junit
.
Test
import
org
.
junit
.
Assert
.
*
import
com
.
mapbox
.
geojson
.
Point
class
MapLogicTest
{
@Test
fun
testCoordinateConversion
(
)
{
val
point
=
Point
.
fromLngLat
(
-
122.4194
,
37.7749
)
// Test your map logic without creating actual MapView
val
converted
=
MapLogic
.
convert
(
point
)
assertEquals
(
-
122.4194
,
converted
.
longitude
(
)
,
0.001
)
assertEquals
(
37.7749
,
converted
.
latitude
(
)
,
0.001
)
}
}
Instrumented Testing with Maps
import
androidx
.
test
.
ext
.
junit
.
rules
.
ActivityScenarioRule
import
androidx
.
test
.
ext
.
junit
.
runners
.
AndroidJUnit4
import
org
.
junit
.
Rule
import
org
.
junit
.
Test
import
org
.
junit
.
runner
.
RunWith
@RunWith
(
AndroidJUnit4
::
class
)
class
MapActivityTest
{
@get:Rule
val
activityRule
=
ActivityScenarioRule
(
MapActivity
::
class
.
java
)
@Test
fun
testMapLoads
(
)
{
activityRule
.
scenario
.
onActivity
{
activity
->
val
mapView
=
activity
.
findViewById
<
MapView
>
(
R
.
id
.
mapView
)
assertNotNull
(
mapView
)
}
}
}
Troubleshooting
Map Not Displaying
Checklist:
✅ Token configured in string resources?
✅ Correct package name in token restrictions?
✅ MapboxMaps dependency added to build.gradle?
✅ MapView lifecycle methods called?
✅ Internet permission in AndroidManifest.xml?
<
uses-permission
android:
name
=
"
android.permission.INTERNET
"
/>
Memory Leaks
Use Android Studio Profiler:
Run → Profile 'app' → Memory
Look for MapView instances not being garbage collected
Ensure
mapView.onDestroy()
is called
Set
mapView = null
in Fragments after destroy
Slow Performance
Common causes:
Too many markers (use clustering or symbols)
Large GeoJSON sources (use vector tiles)
Not handling lifecycle properly
Not calling
onLowMemory()
Running on emulator (use device for accurate testing)
Platform-Specific Considerations
Android Version Support
Android 6.0+ (API 23+)
Minimum supported version
Android 12+ (API 31+)
New permission handling
Android 13+ (API 33+)
Runtime notification permissions Device Optimization import android . app . ActivityManager import android . content . Context fun isLowRamDevice ( ) : Boolean { val activityManager = getSystemService ( Context . ACTIVITY_SERVICE ) as ActivityManager return activityManager . isLowRamDevice } // Adjust map quality based on device if ( isLowRamDevice ( ) ) { // Reduce detail, limit features } Screen Density val density = resources . displayMetrics . density when { density

= 4.0 -> { // xxxhdpi displays // Use highest quality } density = 3.0 -> { // xxhdpi displays // High quality } density = 2.0 -> { // xhdpi displays // Standard quality } } Reference Mapbox Maps SDK for Android API Reference Examples Navigation SDK Gradle Installation Migration Guides

返回排行榜