24、Location KTX:位置请求与地理围栏的KTX扩展

位置服务,说白了就是让你的App知道用户在哪。

我最早做位置相关功能时,还是用LocationManager那一套。注册监听器、处理回调、手动管理生命周期……代码写起来又臭又长。后来Google出了FusedLocationProviderClient,好了一些,但依然不够优雅。

直到KTX扩展出现,我才觉得「嗯,这才像Kotlin该有的样子」。

为什么需要Location KTX?

原生Location API有几个痛点:

  • 回调地狱:每次请求位置都要写一个LocationCallback,里面再套各种逻辑
  • 生命周期耦合:忘记在onStop里移除监听,就会内存泄漏
  • 权限处理繁琐:位置权限的请求和回调是分开的,容易漏掉

KTX扩展用协程解决了这些问题。你想想看,用suspend函数替代回调,用Flow替代监听器,代码瞬间清爽了。

核心思想:Location KTX 将异步位置请求封装为协程挂起函数,将连续位置更新封装为 Flow,让开发者用同步思维写异步代码。

基础用法:单次位置请求

先看最常用的场景——获取一次当前位置。

// 传统方式
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
    // 处理位置
}.addOnFailureListener { e ->
    // 处理错误
}

// KTX 方式
val location = fusedLocationClient.awaitLastLocation()

就一行?对,就一行。awaitLastLocation()是一个挂起函数,直接返回Location?。我在项目中第一次用这个时,差点把旧代码全删了重写。

不过要注意:awaitLastLocation()返回的是缓存位置,不一定是最新的。如果你需要强制获取最新位置,用这个:

val location = fusedLocationClient.awaitCurrentLocation(
    CurrentLocationRequest.Builder()
        .setPriority(Priority.PRIORITY_HIGH_ACCURACY)
        .setDurationMillis(5000)
        .build()
)

这里我设置了一个5秒的超时。如果5秒内拿不到高精度位置,就会返回一个精度较低的位置,或者抛出异常。

我的习惯:在需要精确位置的场景(比如导航启动时),我会用awaitCurrentLocation;在只需要大概位置的场景(比如显示城市),用awaitLastLocation就够了,省电。

连续位置更新:用Flow监听

如果你需要持续跟踪用户位置(比如运动轨迹记录),传统做法是注册LocationCallback,然后在回调里处理。KTX把它变成了Flow:

fusedLocationClient
    .locationUpdates(LocationRequest.Builder(5000).build())
    .catch { e ->
        // 处理异常
        emit(emptyList())
    }
    .collect { locations ->
        // 每次收到位置更新,这里都会执行
        updateUI(locations.last())
    }

Flow的好处是什么?你可以用协程的取消机制自动停止监听。比如在ViewModelviewModelScope里收集,当页面销毁时,协程自动取消,监听也就停了。再也不用手动调用removeLocationUpdates了。

我曾经在一个运动App里用过这个模式。用户开始跑步时启动Flow收集,暂停时用Job.cancel()停止,继续时重新收集。代码量比之前少了至少一半。

地理围栏:从回调到协程

地理围栏(Geofencing)是另一个典型场景。原生API需要注册BroadcastReceiverPendingIntent,配置起来相当繁琐。

KTX扩展提供了awaitAddGeofences()awaitRemoveGeofences()两个挂起函数:

val geofencingClient = LocationServices.getGeofencingClient(this)

// 添加地理围栏
val geofence = Geofence.Builder()
    .setRequestId("home_zone")
    .setCircularRegion(39.9042, 116.4074, 100f)
    .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
    .setExpirationDuration(Geofence.NEVER_EXPIRE)
    .build()

val geofenceRequest = GeofencingRequest.Builder()
    .addGeofence(geofence)
    .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
    .build()

try {
    geofencingClient.awaitAddGeofences(geofenceRequest, geofencePendingIntent)
    Log.d("Geofence", "地理围栏添加成功")
} catch (e: SecurityException) {
    Log.e("Geofence", "缺少位置权限", e)
}

这里有个坑:geofencePendingIntent还是需要你自己创建,因为地理围栏的触发是跨进程的,必须用PendingIntent。KTX只封装了添加和移除的异步调用,没有封装触发逻辑——这个确实没法封装,因为触发是在系统服务里发生的。

我曾经踩过的坑:地理围栏的精度受设备硬件限制。在室内或高楼林立的区域,围栏触发可能延迟甚至不触发。不要把它当作精确的「电子围栏」,它更适合做「区域提醒」这类非关键场景。

权限处理的协程化

位置权限的请求在KTX里也有优化。虽然权限本身不属于Location KTX,但结合ActivityResultContracts和协程,可以写出很流畅的代码:

class LocationViewModel : ViewModel() {
    private val _location = MutableStateFlow<Location?>(null)
    val location: StateFlow<Location?> = _location.asStateFlow()

    fun requestLocation(activity: Activity) {
        viewModelScope.launch {
            // 先检查权限
            if (hasFineLocationPermission(activity).not()) {
                // 请求权限,这里用 ActivityResultLauncher
                return@launch
            }
            // 获取位置
            val loc = LocationServices
                .getFusedLocationProviderClient(activity)
                .awaitCurrentLocation(...)
            _location.value = loc
        }
    }
}

你看,整个流程从「检查权限 → 请求权限 → 获取位置」变成了一个顺序执行的协程。没有回调嵌套,没有状态机,就是从上往下写。

知识体系总览

下面这张图总结了Location KTX的核心扩展点:

Location KTX 扩展体系 FusedLocationProviderClient 单次位置请求 awaitLastLocation() awaitCurrentLocation() 返回 Location? 或抛出异常 连续位置更新 locationUpdates() 返回 Flow<Location> 协程取消即停止监听 地理围栏管理 awaitAddGeofences() awaitRemoveGeofences() 需配合 PendingIntent 核心优势:协程化 → 消除回调 → 生命周期安全 → 代码量减少 50%+ 适用于:导航、运动记录、位置签到、区域提醒等场景

实际项目中的最佳实践

我在一个外卖配送App里大量使用了Location KTX。总结几条经验:

  1. 区分前台和后台定位:前台用locationUpdates的Flow,后台用PendingIntent + WorkManager。KTX不直接支持后台定位,但你可以把Flow收集到的数据通过WorkManager持久化。
  2. 合理设置更新间隔:不要为了省电把间隔设得太长,也不要为了精度设得太短。我个人习惯:导航场景1秒,运动场景5秒,签到场景30秒。
  3. 处理权限拒绝:用户拒绝权限后,awaitCurrentLocation会抛出SecurityException。记得用try-catch捕获,并引导用户去设置页开启权限。
  4. 测试时用模拟位置:在模拟器上测试地理围栏时,记得开启「允许模拟位置」选项。我因为这个浪费了半天时间——围栏死活不触发,最后发现是模拟器设置问题。

一个小技巧:如果你需要同时监听多个位置源(比如GPS和网络),可以用merge操作符合并多个Flow。这样不管哪个源更新了,你都能收到通知。

总结

Location KTX没有引入新功能,它只是把已有的API用Kotlin的方式重新包装了一遍。但就是这一层包装,让代码的可读性和可维护性提升了一个档次。

说白了,好的扩展库就是让你感觉「本来就应该这样写」。Location KTX做到了这一点。


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