mapbox-android-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Mapbox Android Integration Patterns

Mapbox Android集成模式

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

这是适用于Android平台的Mapbox Maps SDK官方集成模式,涵盖Kotlin、Jetpack Compose、View系统、规范的生命周期管理、令牌处理、离线地图及移动端专属优化方案。
以下场景适用本技能:
  • 在新项目或现有项目中搭建Mapbox Maps SDK for Android环境
  • 将地图与Jetpack Compose或View系统集成
  • 实现规范的生命周期管理与资源清理
  • 在Android应用中安全管理令牌
  • 处理离线地图与缓存相关需求
  • 集成Navigation SDK
  • 优化电池续航与内存占用
  • 调试崩溃、内存泄漏或性能问题

Core Integration Patterns

核心集成模式

Jetpack Compose Pattern (Modern)

Jetpack Compose模式(现代化方案)

Modern approach using Jetpack Compose and Kotlin
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
基于Jetpack Compose与Kotlin的现代化实现方式
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 ->
            // 状态变更时更新相机
            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()
        }
    }

    // 感知生命周期的资源清理
    DisposableEffect(mapView) {
        onDispose {
            mapView.onDestroy()
        }
    }

    return mapView
}

// 在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 ->
            // 添加数据源与图层
        }
    )
}
核心要点:
  • 使用
    AndroidView
    在Compose中集成MapView
  • 使用
    remember
    确保MapView在重组时保留
  • 使用
    DisposableEffect
    实现规范的生命周期清理
  • update
    代码块中处理状态更新

View System Pattern (Classic)

View系统模式(传统方案)

Traditional Android View system with proper lifecycle
kotlin
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):
xml
<?xml version="1.0" encoding="utf-8"?>
<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)
遵循生命周期规范的传统Android View系统实现
kotlin
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 ->
            // 地图加载完成,添加数据源与图层
            setupMap(style)
        }

        // 添加点击监听器
        mapView.getMapboxMap().addOnMapClickListener { point ->
            handleMapClick(point)
            true
        }
    }

    private fun setupMap(style: Style) {
        // 添加自定义数据源与图层
    }

    private fun handleMapClick(point: Point) {
        // 处理地图点击事件
    }

    // 关键:调用对应生命周期方法以实现规范清理
    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布局文件(activity_map.xml):
xml
<?xml version="1.0" encoding="utf-8"?>
<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>
核心要点:
  • 在Activity对应生命周期方法中调用
    mapView.onStart()
    onStop()
    onDestroy()
    onLowMemory()
  • 等待样式加载完成后再添加图层
  • 使用lateinit var存储MapView引用(将在onCreate中初始化)

Fragment Pattern

Fragment模式

kotlin
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

kotlin
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) {
        // 添加数据源与图层
    }

    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()
    }
}
核心要点:
  • onDestroyView()
    中将
    mapView
    置为null以避免内存泄漏
  • 使用可空类型
    mapView?
    保证安全性
  • 合理调用生命周期方法

Token Management

令牌管理

✅ Recommended: String Resources with BuildConfig

✅ 推荐方案:结合BuildConfig的字符串资源

1. Add to
local.properties
(DO NOT commit):
properties
undefined
1. 添加至
local.properties
(请勿提交至版本控制):
properties
undefined

local.properties (add to .gitignore)

local.properties(需添加至.gitignore)

MAPBOX_ACCESS_TOKEN=pk.your_token_here

**2. Configure in `build.gradle.kts` (Module):**

```kotlin
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
:
gitignore
local.properties
4. Usage in code:
kotlin
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
MAPBOX_ACCESS_TOKEN=pk.your_token_here

**2. 在`build.gradle.kts`(Module层级)中配置:**

```kotlin
android {
    defaultConfig {
        // 从local.properties读取配置
        val properties = Properties()
        properties.load(project.rootProject.file("local.properties").inputStream())

        buildConfigField(
            "String",
            "MAPBOX_ACCESS_TOKEN",
            "\"${properties.getProperty("MAPBOX_ACCESS_TOKEN", "")}\""
        )

        // 同时添加至资源文件供SDK使用
        resValue(
            "string",
            "mapbox_access_token",
            properties.getProperty("MAPBOX_ACCESS_TOKEN", "")
        )
    }

    buildFeatures {
        buildConfig = true
    }
}
3. 添加至
.gitignore
gitignore
local.properties
4. 代码中的使用方式:
kotlin
import com.yourapp.BuildConfig

// SDK会自动从资源文件中读取令牌
// 若已配置在字符串资源中,无需手动设置

// 或者通过代码直接获取:
val token = BuildConfig.MAPBOX_ACCESS_TOKEN
该模式优势:
  • 令牌不会出现在源代码或版本控制中
  • 适用于本地开发与CI/CD流程(通过环境变量注入)
  • 构建时自动注入
  • 无硬编码密钥风险

❌ Anti-Pattern: Hardcoded Tokens

❌ 反模式:硬编码令牌

kotlin
// ❌ NEVER DO THIS - Token in source code
MapboxOptions.accessToken = "pk.YOUR_MAPBOX_TOKEN_HERE"

kotlin
// ❌ 绝对禁止此操作 - 令牌直接写入源代码
MapboxOptions.accessToken = "pk.YOUR_MAPBOX_TOKEN_HERE"

Memory Management and Lifecycle

内存管理与生命周期

✅ Proper Lifecycle Management

✅ 规范的生命周期管理

kotlin
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()
    }
}
kotlin
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()
    }
}

// 在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)

        // 自动处理生命周期
        lifecycle.addObserver(lifecycleObserver)
    }

    override fun onLowMemory() {
        super.onLowMemory()
        lifecycleObserver.onLowMemory()
    }
}

✅ ViewModel Pattern

✅ ViewModel模式

kotlin
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

kotlin
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 {
            // 从仓库加载数据
            // 数据就绪后更新状态
        }
    }
}
优势:
  • 配置变更时状态得以保留
  • 业务逻辑与UI解耦
  • 感知生命周期
  • 易于测试

Offline Maps

离线地图

Download Region for Offline Use

下载指定区域供离线使用

kotlin
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
kotlin
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 {
        // 粗略估算:平均每个瓦片约50 KB
        val tileCount = estimateTileCount(bounds, minZoom, maxZoom)
        return tileCount * 50_000L // 字节
    }

    private fun estimateTileCount(
        bounds: CoordinateBounds,
        minZoom: Int,
        maxZoom: Int
    ): Long {
        // 简化的瓦片数量估算逻辑
        var count = 0L
        for (zoom in minZoom..maxZoom) {
            val tilesAtZoom = Math.pow(4.0, zoom.toDouble()).toLong()
            count += tilesAtZoom
        }
        return count
    }
}
核心注意事项:
  • 电池影响:下载过程会消耗大量电池电量
  • 存储限制:需监控可用磁盘空间
  • 缩放级别:缩放级别越高,瓦片数量越多,占用存储越大
  • 网络类型:区分WiFi与蜂窝网络

Check Available Storage

检查可用存储空间

kotlin
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
}

kotlin
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 // 预留2倍缓冲空间
}

Navigation SDK Integration

Navigation SDK集成

Basic Navigation Setup

基础导航配置

kotlin
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)

kotlin
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)

        // 初始化Navigation SDK
        mapboxNavigation = MapboxNavigationProvider.create(
            NavigationOptions.Builder(this)
                .accessToken(getString(R.string.mapbox_access_token))
                .build()
        )

        setupObservers()
    }

    private fun setupObservers() {
        // 监听路线更新
        mapboxNavigation.registerRoutesObserver(object : RoutesObserver {
            override fun onRoutesChanged(result: RoutesUpdatedResult) {
                val routes = result.navigationRoutes
                if (routes.isNotEmpty()) {
                    // 在地图上显示路线
                    showRouteOnMap(routes.first())
                }
            }
        })

        // 监听导航进度
        mapboxNavigation.registerRouteProgressObserver(object : RouteProgressObserver {
            override fun onRouteProgressChanged(routeProgress: RouteProgress) {
                // 更新UI展示进度
                val distanceRemaining = routeProgress.distanceRemaining
                val durationRemaining = routeProgress.durationRemaining
            }
        })
    }

    fun startNavigation(destination: Point) {
        // 请求路线
        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
                ) {
                    // 处理错误
                }

                override fun onCanceled(
                    routeOptions: RouteOptions,
                    routerOrigin: RouterOrigin
                ) {
                    // 处理取消请求
                }
            }
        )
    }

    private fun showRouteOnMap(route: NavigationRoute) {
        // 在地图上绘制路线
    }

    override fun onDestroy() {
        super.onDestroy()
        mapboxNavigation.onDestroy()
    }
}
Navigation SDK功能:
  • 逐向导航指引
  • 语音播报指令
  • 路线进度追踪
  • 重新规划路线
  • 实时路况导航
  • 离线导航(需配合离线区域使用)

Mobile Performance Optimization

移动端性能优化

Battery Optimization

电池优化

kotlin
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)
    }
}
kotlin
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()
        }

        // 注册广播接收器监听省电模式变更
        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() {
        // 降低帧率
        mapView.getMapboxMap().setMaximumFps(30)

        // 禁用3D功能
        // 降低瓦片质量
    }

    private fun enableNormalMode() {
        mapView.getMapboxMap().setMaximumFps(60)
    }
}

Memory Optimization

内存优化

kotlin
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 { }
        }
    }
}
kotlin
override fun onLowMemory() {
    super.onLowMemory()
    mapView.onLowMemory()

    // 清理地图缓存
    mapView.getMapboxMap().clearData { result ->
        if (result.isValue) {
            Log.d("Map", "缓存已清理")
        }
    }
}

override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)

    when (level) {
        ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
        ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
            // 清理非必要数据
            mapView.getMapboxMap().clearData { }
        }
    }
}

Network Optimization

网络优化

kotlin
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
    }
}

kotlin
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环境下使用全质量配置
                        enableHighQuality()
                    }
                    capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
                        // 蜂窝网络下降低数据消耗
                        enableLowDataMode()
                    }
                }
            }
        }

        connectivityManager.registerDefaultNetworkCallback(networkCallback)
    }

    private fun enableHighQuality() {
        // 使用全分辨率瓦片
    }

    private fun enableLowDataMode() {
        // 降低瓦片分辨率
        // 限制预加载
    }
}

Common Mistakes and Solutions

常见错误与解决方案

❌ Mistake 1: Not Calling Lifecycle Methods

❌ 错误1:未调用生命周期方法

kotlin
// ❌ 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()
    }
}
kotlin
// ❌ 错误示例:未管理MapView生命周期
class MapActivity : AppCompatActivity() {
    private lateinit var mapView: MapView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mapView = findViewById(R.id.mapView)
        // 未调用任何生命周期方法!
    }
}

// ✅ 正确示例:规范的生命周期管理
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

❌ 错误2:Fragment中的内存泄漏

kotlin
// ❌ 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
    }
}
kotlin
// ❌ 错误示例:Fragment中未清理MapView
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
    }
    // 未做任何清理!
}

// ✅ 正确示例:规范的清理操作
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 // 避免内存泄漏
    }
}

❌ Mistake 3: Ignoring Location Permissions

❌ 错误3:忽略位置权限

kotlin
// ❌ 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:
xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
kotlin
// ❌ 错误示例:未检查权限就启用定位
mapView.location.enabled = true

// ✅ 正确示例:申请并检查权限
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 -> {
                // 处理权限被拒绝的情况
            }
        }
    }

    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
    }
}
需添加至AndroidManifest.xml:
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

❌ 错误4:地图加载完成前添加图层

kotlin
// ❌ 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)
    }
}

kotlin
// ❌ 错误示例:立即添加图层
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    mapView = findViewById(R.id.mapView)
    addCustomLayers() // 地图尚未加载完成!
}

// ✅ 正确示例:等待样式加载完成后再操作
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

地图逻辑单元测试

kotlin
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)
    }
}
kotlin
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)

        // 无需创建实际MapView即可测试地图逻辑
        val converted = MapLogic.convert(point)

        assertEquals(-122.4194, converted.longitude(), 0.001)
        assertEquals(37.7749, converted.latitude(), 0.001)
    }
}

Instrumented Testing with Maps

基于地图的仪器化测试

kotlin
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)
        }
    }
}

kotlin
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:
  1. ✅ Token configured in string resources?
  2. ✅ Correct package name in token restrictions?
  3. ✅ MapboxMaps dependency added to build.gradle?
  4. ✅ MapView lifecycle methods called?
  5. ✅ Internet permission in AndroidManifest.xml?
xml
<uses-permission android:name="android.permission.INTERNET" />
检查清单:
  1. ✅ 令牌是否已配置在字符串资源中?
  2. ✅ 令牌限制中的包名是否正确?
  3. ✅ 项目build.gradle中是否已添加MapboxMaps依赖?
  4. ✅ 是否调用了MapView的生命周期方法?
  5. ✅ AndroidManifest.xml中是否添加了网络权限?
xml
<uses-permission android:name="android.permission.INTERNET" />

Memory Leaks

内存泄漏

Use Android Studio Profiler:
  1. Run → Profile 'app' → Memory
  2. Look for MapView instances not being garbage collected
  3. Ensure
    mapView.onDestroy()
    is called
  4. Set
    mapView = null
    in Fragments after destroy
使用Android Studio Profiler排查:
  1. 运行 → Profile 'app' → Memory
  2. 查找未被垃圾回收的MapView实例
  3. 确保已调用
    mapView.onDestroy()
  4. 在Fragment销毁后将
    mapView = null

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)

常见原因:
  • 标记点过多(使用聚类或符号图层优化)
  • GeoJSON数据源过大(使用矢量瓦片替代)
  • 未正确处理生命周期
  • 未调用
    onLowMemory()
  • 在模拟器上运行(使用真实设备进行精准测试)

Platform-Specific Considerations

平台专属注意事项

Android Version Support

Android版本支持

  • Android 6.0+ (API 23+): Minimum supported version
  • Android 12+ (API 31+): New permission handling
  • Android 13+ (API 33+): Runtime notification permissions
  • Android 6.0+(API 23+):最低支持版本
  • Android 12+(API 31+):全新权限处理逻辑
  • Android 13+(API 33+):运行时通知权限

Device Optimization

设备优化

kotlin
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
}
kotlin
import android.app.ActivityManager
import android.content.Context

fun isLowRamDevice(): Boolean {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return activityManager.isLowRamDevice
}

// 根据设备配置调整地图质量
if (isLowRamDevice()) {
    // 降低细节展示,限制功能
}

Screen Density

屏幕密度适配

kotlin
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
    }
}

kotlin
val density = resources.displayMetrics.density
when {
    density >= 4.0 -> {
        // xxxhdpi屏幕
        // 使用最高质量配置
    }
    density >= 3.0 -> {
        // xxhdpi屏幕
        // 高质量配置
    }
    density >= 2.0 -> {
        // xhdpi屏幕
        // 标准质量配置
    }
}

Reference

参考资料