如何用Kotlin实现Android ACCESS_FINE_LOCATION精确定位功能?
Android ACCESS_FINE_LOCATION 精确定位 Kotlin 实现方案
两类实现方案适用场景
1. 改进版生产环境实现
该方案逻辑完善,核心优势如下:
- 绑定Activity生命周期,应用暂停或停止时自动终止GPS更新,降低不必要功耗,符合隐私合规要求
- 内置10秒GPS看门狗计时器,可检测定位异常,适配Android Studio模拟器定位不稳定的问题
- 权限校验逻辑完整,异常处理覆盖全面
适用场景:正式上线的生产环境应用,尤其是导航、运动记录、实时位置上报类需要长期依赖定位的APP。
const val PERMISSIONS_REQUEST_ACCESS_LOCATION = 99 // 任意正整数即可 class MainActivity : AppCompatActivity() { private lateinit var tvCurrentTime : TextView private lateinit var tvSpeed : TextView private lateinit var tvBearing : TextView private var myTimer: Timer = Timer() private var sHourMinuteSecond : String = "00:00:00" private lateinit var fusedLocationClient : FusedLocationProviderClient private lateinit var locationCallback: LocationCallback private var gpsWatchDog : Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tvCurrentTime = findViewById<View>(R.id.show_time_textview) as TextView tvSpeed = findViewById<View>(R.id.speed_textview) as TextView tvBearing = findViewById<View>(R.id.bearing_textview) as TextView fusedLocationClient = LocationServices.getFusedLocationProviderClient(this@MainActivity) as FusedLocationProviderClient locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { super.onLocationResult(locationResult) processNewLocation(locationResult.lastLocation) // 仅处理最新的定位结果 } } } private fun checkPermissionsForGPSUpdates() { if ((ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) || (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)) { // Android系统要求同时申请模糊和精确定位权限 writeToLog("checkSelfPermission 权限未授予,发起权限申请") ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), PERMISSIONS_REQUEST_ACCESS_LOCATION) // 申请权限时会触发OnPause() // 用户授予权限后会回调OnResume(),会再次调用checkPermissionsForGPSUpdates()启动定位 return } if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { // 仅在获得精确定位权限时启动定位 writeToLog("ACCESS_FINE_LOCATION 权限已授予,启动GPS更新") val locationRequest: LocationRequest = LocationRequest.create().apply { interval = 1000 fastestInterval = 500 priority = LocationRequest.PRIORITY_HIGH_ACCURACY } try { if (!fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()).isSuccessful) { writeToLog("requestLocationUpdates() 调用失败") // 相关问题反馈多为谷歌服务错误 } } catch (unlikely: SecurityException) { writeToLog("定位权限丢失,无法启动定位更新: $unlikely") } } else { writeToLog("权限未授予,退出") Toast.makeText(this@MainActivity, "应用需要GPS权限才能运行" , Toast.LENGTH_LONG).show() finish() } } private fun updateTimeString() { // 由计时器调用,请勿在此处直接修改UI val systemTime = GregorianCalendar() sHourMinuteSecond = SimpleDateFormat("HH:mm:ss").format(systemTime.timeInMillis) // 使用runOnUiThread方法更新界面 this.runOnUiThread(updateDisplay) } private val updateDisplay = Runnable { // 该方法运行在UI线程 // 为避免模拟器崩溃,逻辑尽量精简 tvCurrentTime.text = sHourMinuteSecond if (++gpsWatchDog > 10) { tvBearing.text = "无GPS信号" } } private fun processNewLocation(latestLocation : Location) { // 收到定位更新,重置看门狗计时器 gpsWatchDog = 0 // 处理最新的定位结果 // 示例为展示GPS时间 val sSpeed = SimpleDateFormat("HH:mm:ss").format(latestLocation.time) tvSpeed.text = sSpeed val sBearing = latestLocation.bearing.roundToInt().toString() // 不保留小数 tvBearing.text = sBearing } override fun onResume() { super.onResume() writeToLog("调用 onResume()") // 校验权限后启动GPS更新 checkPermissionsForGPSUpdates() val currentSystemTime = System.currentTimeMillis() // 启动计时器更新时间 myTimer = Timer() val delayToNextWholeSecond = ((currentSystemTime / 1000) + 1) * 1000 - currentSystemTime // 对齐整秒更新 myTimer.scheduleAtFixedRate(object : TimerTask() { override fun run() { updateTimeString() } }, delayToNextWholeSecond, 1000) } override fun onPause() { super.onPause() writeToLog("调用 onPause()") myTimer.cancel() // 停止计时器 myTimer.purge() fusedLocationClient.removeLocationUpdates(locationCallback) } override fun onStop() { super.onStop() writeToLog("调用 onStop()") myTimer.cancel() myTimer.purge() fusedLocationClient.removeLocationUpdates(locationCallback) } private fun writeToLog(message : String) { Log.d("SmallGPSDemo", message) } }
2. 极简版演示实现
该方案代码精简,核心优势如下:
- 采用新版Activity Result API申请权限,逻辑直观易读
- 无冗余代码,可快速验证定位功能可用性
适用场景:Demo开发、功能验证、学习测试场景,不建议直接用于生产环境,未绑定生命周期,退后台后依然会持续请求定位,耗电且不符合合规要求。
class MainActivity : AppCompatActivity() { private var debugTextView : TextView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) debugTextView = findViewById<View>(R.id.debug_textview) as TextView // 校验权限并启动定位更新 requestPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION) } private fun processNewLocation(latestLocation : Location) { // 处理定位结果 debugTextView!!.text = SimpleDateFormat("hh:mm:ss.SSS").format(System.currentTimeMillis()) } @SuppressLint("MissingPermission") private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { // 权限已授予,启动GPS更新 val locationRequest: LocationRequest = LocationRequest.create().apply { interval = 1000 fastestInterval = 500 priority = LocationRequest.PRIORITY_HIGH_ACCURACY } val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { super.onLocationResult(locationResult) for (location in locationResult.locations) { processNewLocation(location) // 配置仅返回单个定位结果,遍历兼容异常情况 } } } val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this@MainActivity) fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()) } else { Log.d("DEBUG", "权限被拒绝 ") Toast.makeText(this@MainActivity, "应用需要GPS权限才能运行", Toast.LENGTH_LONG).show() finish() } } }
必备配置:AndroidManifest 权限声明
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.reallysmallgpscode"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ReallySmallGPSCode"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
不同Android系统版本适配优化建议
- Android 6.0 (API 23)及以上:必须动态申请定位权限,不可仅在Manifest中声明,用户拒绝权限后需给出友好引导,避免直接闪退。
- Android 10 (API 29)及以上:如果需要后台定位,需额外申请
ACCESS_BACKGROUND_LOCATION权限,且必须先获得前台定位权限后再申请后台权限,同时要向用户明确说明后台定位的用途,否则会被Google Play审核驳回。 - Android 12 (API 31)及以上:如果精确定位不是应用核心功能,可同时提供模糊定位选项供用户选择,申请权限时可同时请求
ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION,允许用户自主选择授权精度。 - Android 13 (API 33)及以上:如果应用需要后台持续定位,需提交谷歌位置权限使用说明审核,建议搭配前台服务+通知栏提示,明确告知用户正在使用定位功能。
内容的提问来源于stack exchange,提问作者RatherBeSailing
相关产品推荐
相关产品推荐

