26、LiveData与Location:位置更新监听、使用LiveData管理位置数据、实战:地图定位

各位同学,今天我们来聊一个非常接地气的话题——位置更新。说实话,我在做Android开发的头两年,最怕的就是跟Location打交道。为什么?因为坑太多了。权限、生命周期、定位不准、耗电……随便一个都能让你调试到怀疑人生。

但有了LiveData之后,情况就大不一样了。LiveData天生就是为这种「持续变化的数据」而生的。位置信息说白了就是一个不断变化的值,你用LiveData去包装它,生命周期自动管理,数据自动分发,简直不要太爽。

好,我们直接进入正题。

为什么用LiveData管理位置数据?

你想想看,位置更新有几个特点:

  • 持续变化——用户一走动,经纬度就变了
  • 多个观察者——地图要显示、附近商家要刷新、轨迹要记录
  • 生命周期敏感——Activity退到后台就别再浪费电去定位了

LiveData恰好完美匹配这些需求。它只在活跃生命周期内分发数据,不会出现「页面已经销毁了还在回调」的尴尬情况。我在项目中遇到过好几次,没用LiveData之前,定位回调里还在更新UI,结果Activity已经finish了,直接崩掉。嗯,那种感觉,谁崩谁知道。

核心架构:LocationLiveData

我们直接封装一个 LocationLiveData,把FusedLocationProviderClient的监听逻辑藏进去。对外暴露一个LiveData,谁用谁订阅。

核心思路:继承LiveData,在onActive()时注册定位监听,在onInactive()时取消监听。这样LiveData没有活跃观察者时,定位服务自动停止,省电又省心。

class LocationLiveData(context: Context) : LiveData<Location>() {

    private val fusedLocationClient = 
        LocationServices.getFusedLocationProviderClient(context)

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) {
            result.lastLocation?.let { location ->
                postValue(location)
            }
        }
    }

    override fun onActive() {
        super.onActive()
        val request = LocationRequest.create().apply {
            interval = 5000          // 5秒一次
            fastestInterval = 2000   // 最快2秒
            priority = Priority.PRIORITY_HIGH_ACCURACY
        }
        fusedLocationClient.requestLocationUpdates(
            request,
            locationCallback,
            Looper.getMainLooper()
        )
    }

    override fun onInactive() {
        super.onInactive()
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }
}

这段代码看起来简单,但有几个细节我要强调一下:

  • postValue() 而不是 setValue()——因为回调可能不在主线程,postValue是线程安全的
  • onActive()onInactive() 是LiveData的生命周期钩子,系统自动调用
  • 定位请求的interval别设太短,5秒一次已经够用了,太频繁只会让手机发烫

小技巧:如果你需要获取最后一次已知位置(不用实时更新),可以先用 fusedLocationClient.lastLocation 拿一个快照,再决定是否开启实时监听。我一般会先拿快照展示地图,再慢慢等实时更新。

在ViewModel中使用

ViewModel里怎么用?很简单,把LocationLiveData暴露出去就行。

class LocationViewModel(application: Application) : AndroidViewModel(application) {

    private val locationLiveData = LocationLiveData(application)

    fun getLocationUpdates(): LiveData<Location> = locationLiveData

    fun refreshLocation() {
        // 手动触发一次位置刷新
        val fusedClient = LocationServices.getFusedLocationProviderClient(getApplication())
        fusedClient.lastLocation.addOnSuccessListener { location ->
            location?.let { locationLiveData.postValue(it) }
        }
    }
}

这里有个小坑——AndroidViewModel 需要传入Application,因为LocationLiveData需要Context来获取定位服务。我个人习惯把这种依赖通过构造器传进去,而不是在ViewModel内部硬编码,方便以后做单元测试。

实战:地图定位展示

好,理论说完了,我们来写一个实际的地图定位页面。假设你用的是百度地图或高德地图,核心逻辑是一样的:

  1. 在Activity/Fragment中观察LocationLiveData
  2. 每次位置更新时,移动地图中心点
  3. 添加一个定位图标标记
class MapActivity : AppCompatActivity() {

    private lateinit var viewModel: LocationViewModel
    private lateinit var mapView: MapView
    private lateinit var baiduMap: BaiduMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_map)

        viewModel = ViewModelProvider(this).get(LocationViewModel::class.java)
        mapView = findViewById(R.id.mapView)
        baiduMap = mapView.map

        // 观察位置变化
        viewModel.getLocationUpdates().observe(this, Observer { location ->
            location?.let { updateMapLocation(it) }
        })
    }

    private fun updateMapLocation(location: Location) {
        val latLng = LatLng(location.latitude, location.longitude)
        val update = MapStatusUpdateFactory.newLatLngZoom(latLng, 18f)
        baiduMap.animateMapStatus(update)

        // 添加定位标记
        val overlay = MyLocationOverlay(baiduMap)
        overlay.setLocationData(location)
        baiduMap.setMyLocationOverlay(overlay)
    }

    override fun onResume() {
        super.onResume()
        mapView.onResume()
    }

    override fun onPause() {
        super.onPause()
        mapView.onPause()
    }

    override fun onDestroy() {
        super.onDestroy()
        mapView.onDestroy()
    }
}

这段代码里,observe(this, Observer) 中的 this 就是LifecycleOwner。当Activity进入后台,LiveData自动停止分发;回到前台,自动恢复。你什么都不用管。

注意:地图SDK的MapView本身也有自己的生命周期(onResume/onPause/onDestroy),千万别忘了调用。我曾经因为忘了调onResume,地图一片空白,查了半天才发现是生命周期没同步。

SVG:位置更新核心流程

下面这张图展示了整个数据流的走向,从定位服务到UI更新,LiveData在中间起到了「生命周期感知的桥梁」作用。

位置更新核心数据流 FusedLocationClient 系统定位服务 onLocationResult LocationLiveData onActive / onInactive postValue LocationViewModel 暴露LiveData observe MapActivity LifecycleOwner 地图中心移动 + 标记 关键特性 • 生命周期感知 • 自动取消注册 • 数据粘性分发 • 线程安全 • 避免内存泄漏

避坑指南

最后,我把自己踩过的坑总结一下,你们遇到类似问题可以直接翻到这里:

  • 权限问题:Android 6.0以上需要动态申请 ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION。别忘了在定位前检查权限,否则直接崩。
  • 模拟器定位:模拟器默认位置在Google总部,你可以在Extended Controls里手动设置经纬度,或者用GPS模拟文件。
  • 定位精度:室内定位用WiFi和基站,误差几十米很正常。别指望GPS在室内也能准,那是物理限制。
  • 电量优化:如果只是展示用户当前位置,用 PRIORITY_BALANCED_POWER_ACCURACY 就够了,别一上来就开高精度。

我曾经做过一个外卖App,定位间隔设成了1秒一次,结果用户反馈手机发烫、电量狂掉。后来改成10秒一次,配合最后已知位置缓存,体验反而更好。有时候「少即是多」。

好了,这一章的内容就到这里。位置更新用LiveData来管理,说白了就是「把复杂留给框架,把简单留给业务」。你只需要关注位置数据来了之后怎么用,至于什么时候注册、什么时候注销,LiveData帮你搞定。


公众号:蓝海资料掘金营,微信deep3321