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的好处是什么?你可以用协程的取消机制自动停止监听。比如在ViewModel的viewModelScope里收集,当页面销毁时,协程自动取消,监听也就停了。再也不用手动调用removeLocationUpdates了。
我曾经在一个运动App里用过这个模式。用户开始跑步时启动Flow收集,暂停时用Job.cancel()停止,继续时重新收集。代码量比之前少了至少一半。
地理围栏:从回调到协程
地理围栏(Geofencing)是另一个典型场景。原生API需要注册BroadcastReceiver或PendingIntent,配置起来相当繁琐。
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的核心扩展点:
实际项目中的最佳实践
我在一个外卖配送App里大量使用了Location KTX。总结几条经验:
- 区分前台和后台定位:前台用
locationUpdates的Flow,后台用PendingIntent+WorkManager。KTX不直接支持后台定位,但你可以把Flow收集到的数据通过WorkManager持久化。 - 合理设置更新间隔:不要为了省电把间隔设得太长,也不要为了精度设得太短。我个人习惯:导航场景1秒,运动场景5秒,签到场景30秒。
- 处理权限拒绝:用户拒绝权限后,
awaitCurrentLocation会抛出SecurityException。记得用try-catch捕获,并引导用户去设置页开启权限。 - 测试时用模拟位置:在模拟器上测试地理围栏时,记得开启「允许模拟位置」选项。我因为这个浪费了半天时间——围栏死活不触发,最后发现是模拟器设置问题。
一个小技巧:如果你需要同时监听多个位置源(比如GPS和网络),可以用merge操作符合并多个Flow。这样不管哪个源更新了,你都能收到通知。
总结
Location KTX没有引入新功能,它只是把已有的API用Kotlin的方式重新包装了一遍。但就是这一层包装,让代码的可读性和可维护性提升了一个档次。
说白了,好的扩展库就是让你感觉「本来就应该这样写」。Location KTX做到了这一点。