You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android MVVM下LocationCallback未触发 地图无法定位问题

问题根因

LocationCallback不触发、地图无法自动定位是4个代码逻辑错误导致的,和MVVM分层本身无关:

  • 持续位置监听没有在页面进入时启动:注册连续位置更新的trackUser()被你设为私有,只有点击开始跑步按钮触发startTracking()时才会调用,进入页面时仅调用了获取缓存位置的getUserLocation(),自然不会触发持续回调的LocationCallback
  • 初始位置获取存在空指针风险:fusedLocationProviderClient.lastLocation在设备重启、位置服务刚开启、从未获取过位置的场景下会返回null,你代码里直接调用location.latitude没有判空,会直接静默崩溃,拿不到初始位置
  • 计步器逻辑存在实例错误:每次调用RPMLiveData()都会新建一个内部类实例,注册、注销监听器操作的不是同一个对象,后续会导致计步不准、内存泄漏
  • 权限注册逻辑有缺陷:你用activity?.registerForActivityResult注册权限回调,在Fragment附着状态变化时容易空指针,且仅在MainActivity申请权限无法覆盖Fragment内的权限校验场景
修复步骤

1. 修正ViewModel层位置逻辑

修改RunSessionViewModel.kt中对应代码,补全空判断、修复实例创建问题、增加生命周期兜底:

// 替换原有getUserLocation方法,增加空判断和无缓存位置的兜底请求
@SuppressLint("MissingPermission")
fun getUserLocation() {
    mFusedLocationProviderClient.lastLocation.addOnSuccessListener { location ->
        location?.let {
            val latLng = LatLng(it.latitude, it.longitude)
            _locations.add(latLng)
            _liveLocation.postValue(latLng)
        } ?: run {
            // 无缓存位置时发起单次位置请求
            val singleLocationRequest = LocationRequest.create().apply {
                priority = PRIORITY_HIGH_ACCURACY
                numUpdates = 1
            }
            mFusedLocationProviderClient.requestLocationUpdates(
                singleLocationRequest,
                object : LocationCallback(){
                    override fun onLocationResult(result: LocationResult) {
                        super.onLocationResult(result)
                        result.lastLocation?.let {loc ->
                            val latLng = LatLng(loc.latitude, loc.longitude)
                            _locations.add(latLng)
                            _liveLocation.postValue(latLng)
                        }
                        mFusedLocationProviderClient.removeLocationUpdates(this)
                    }
                },
                Looper.getMainLooper()
            )
        }
    }
    .addOnFailureListener { failure ->
        Timber.d("lastLocation failure ${failure.message}")
    }
}

// 将trackUser方法改为公开,增加重复注册判断和空判断
@SuppressLint("MissingPermission")
fun trackUser() {
    // 先移除已有回调,避免重复注册
    mFusedLocationProviderClient.removeLocationUpdates(locationCallback)
    val locationRequest = LocationRequest.create().apply {
        priority = PRIORITY_HIGH_ACCURACY
        interval = 5000L
        fastestInterval = 2000L
        smallestDisplacement = 5f // 静止时不触发回调,减少耗电
    }
    mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
}

// 修复RPMLiveData重复创建问题,改用懒加载单例
private val rpmLiveData by lazy { RPMLiveData() }
fun setupStepCounter() = rpmLiveData.setupStepCounter()
fun unloadStepCounter() = rpmLiveData.unloadStepCounter()

// 给locationCallback增加空判断
private val locationCallback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
        super.onLocationResult(result)
        val currentLocation = result.lastLocation ?: return
        val latLng = LatLng(currentLocation.latitude, currentLocation.longitude)

        val lastLocation = _locations.lastOrNull()
        if (lastLocation != null) {
            distance += SphericalUtil.computeDistanceBetween(lastLocation, latLng).roundToInt()
            _liveDistance.postValue(distance)
        }

        _locations.add(latLng)
        _liveLocation.postValue(latLng)
        _liveLocations.postValue(locations)
    }
}

// 重写生命周期方法,页面销毁时自动释放资源
override fun onCleared() {
    super.onCleared()
    mFusedLocationProviderClient.removeLocationUpdates(locationCallback)
    unloadStepCounter()
}

记得同步修改stopTracking方法里的计步器注销逻辑,替换为unloadStepCounter(),不要直接新建RPMLiveData实例。

2. 修正Fragment层权限和调用逻辑

修改RunTrackerFragment.kt中权限注册相关代码:

// 直接在Fragment上注册权限回调,不要依赖activity的注册方法
private val locationPermissionProviderContract = registerForActivityResult(
    ActivityResultContracts.RequestPermission()) { granted ->
    if (granted) {
        viewModel.getUserLocation()
        // 权限通过时如果已经处于跑步状态,直接启动位置追踪
        if (binding.btnStartStop.text == getString(R.string.stop_label)) {
            viewModel.trackUser()
        }
    } else {
        Toast.makeText(requireContext(), "需要位置权限才能记录跑步轨迹", Toast.LENGTH_SHORT).show()
    }
}

private val activityRecognitionPermissionProviderContract = registerForActivityResult(
    ActivityResultContracts.RequestPermission()) { granted ->
    if (granted) {
        viewModel.setupStepCounter()
    }
}

// 替换requestActivityRecognition里的实例化调用
fun requestActivityRecognition() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        activityRecognitionPermissionProviderContract.launch(Manifest.permission.ACTIVITY_RECOGNITION)
    } else {
        viewModel.setupStepCounter()
    }
}

// 修改stopTracking方法,调用ViewModel统一暴露的注销方法
private fun stopTracking() {
    viewModel.stopTracking()
    viewModel.unloadStepCounter()
    binding.container.txtTime.stop()
}

3. 清单文件校验

确认AndroidManifest.xml中已经声明以下权限,否则动态申请无法生效:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<!-- Android10以上需要后台追踪时添加,前台跑步使用可省略 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

测试时确认设备位置服务处于开启状态,模拟器需要手动在应用设置中授予位置权限,不要完全依赖MainActivity中的权限申请逻辑。

修复完成后进入页面会自动拉取当前位置并移动地图视角,点击开始跑步后LocationCallback会按配置间隔持续返回位置更新,自动绘制跑步轨迹。

内容的提问来源于stack exchange,提问作者Isaac Chen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 18:09:23