使用Kotlin协程的FusedLocationProviderClient

8

我正在尝试使用FusedLocationProviderClient和Kotlin Coroutines请求新的位置。以下是我的当前设置:

class LocationProviderImpl(context: Context) : LocationProvider, CoroutineScope {

    private val TAG = this::class.java.simpleName

    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.IO

    private val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
    private val locationRequest = LocationRequest().apply {
        numUpdates = 1
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY
    }

    override suspend fun getLocation(): LatLng = suspendCoroutine {
        val locationCallback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                result.lastLocation.run {
                    val latLng = latitude at longitude
                    it.resume(latLng)
                }

                fusedLocationProviderClient.removeLocationUpdates(this)
            }
        }

        try {
            fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
        } catch (e: SecurityException) {
            throw NoLocationPermissionException()
        }
    }
}

但是在尝试请求新位置时,我收到了以下异常:

java.lang.IllegalStateException: Can't create handler inside thread that has not called Looper.prepare()

然而,如果我调用Looper.prepare()(以及最终的Looper.quit()),这是否意味着我只能调用该函数一次?欢迎任何帮助。
3个回答

3
问题出在您设置coroutineContext的方式上。请使用以下方式代替原有设置:
override val coroutineContext = Dispatchers.Main + job

如果您需要使用IO分发器,您可以显式地引入它:
withContext(Dispatchers.IO) { ... blocking IO code ... }

要挂起协程,请调用suspendCancellableCoroutine,否则您将无法从结构化并发中获得任何好处。

另一个细节是,在suspendCancellableCoroutine块中的it.resume之后不要编写任何代码。如果调度程序选择立即恢复协程,则该代码在所有协程代码运行完成(或至少在下一个挂起点之前)之前不会执行。

override fun onLocationResult(result: LocationResult) {
    fusedLocationProviderClient.removeLocationUpdates(this)
    it.resume(result.lastLocation.run { latitude to longitude })
}

最好使用Dispatchers.Default而不是IO进行非阻塞操作,例如计算。 - Vlad
1
我不认为这个评论有任何相关性。OP使用了IO调度程序,答案明确指出它仅适用于阻塞IO操作,并且没有人提到CPU密集型操作。 - Marko Topolnik

3
private val locationRequestGPS by lazy {
    LocationRequest.create()
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
            .setNumUpdates(1)
            .setExpirationDuration(1000)
}

private val locationRequestNETWORK by lazy {
    LocationRequest.create()
            .setPriority(LocationRequest.PRIORITY_LOW_POWER)
            .setNumUpdates(1)
            .setExpirationDuration(1000)
}



suspend fun getLocation(context: Context, offsetMinutes: Int = 15): Location? = suspendCoroutine { task ->
    val ctx = context.applicationContext
    if (!ctx.isPermissionValid(Manifest.permission.ACCESS_COARSE_LOCATION)
            && !ctx.isPermissionValid(Manifest.permission.ACCESS_FINE_LOCATION)) {
        task.resume(null)
    } else {
        val manager = ctx.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        if (!LocationManagerCompat.isLocationEnabled(manager)) {
            task.resume(null)
        } else {
            val service = LocationServices.getFusedLocationProviderClient(ctx)
            service.lastLocation
                    .addOnCompleteListener { locTask ->
                        if (locTask.result == null || System.currentTimeMillis() - locTask.result!!.time > offsetMinutes.minute) {
                            GlobalScope.launch(Dispatchers.Main) {
                                task.resume(locationRequest(manager, service))
                            }
                        } else {
                            task.resume(locTask.result)
                        }

                    }
        }
    }
}

suspend fun getLocationLast(context: Context): Location? = suspendCoroutine { task ->
    val ctx = context.applicationContext
    if (!ctx.isPermissionValid(Manifest.permission.ACCESS_COARSE_LOCATION)
            && !ctx.isPermissionValid(Manifest.permission.ACCESS_FINE_LOCATION)) {
        task.resume(null)
    } else {
        if (!LocationManagerCompat.isLocationEnabled(ctx.getSystemService(Context.LOCATION_SERVICE) as LocationManager)) {
            task.resume(null)
        } else {
            LocationServices.getFusedLocationProviderClient(ctx)
                    .lastLocation
                    .addOnCompleteListener { locTask ->
                        task.resume(locTask.result)
                    }
        }

    }
}

suspend fun locationRequest(locationManager: LocationManager, service: FusedLocationProviderClient): Location? = suspendCoroutine { task ->
    val callback = object : LocationCallback() {
        override fun onLocationResult(p0: LocationResult?) {
            service.removeLocationUpdates(this)
            task.resume(p0?.lastLocation)
        }
    }

    when {
        locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) -> {
            service.requestLocationUpdates(locationRequestGPS, callback, Looper.getMainLooper())
        }
        locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> {
            service.requestLocationUpdates(locationRequestNETWORK, callback, Looper.getMainLooper())
        }
        else -> {
            task.resume(null)
        }
    }
}

未找到 ctx.isPermissionValid 和 offsetMinutes.minute。 - Codelaby

0
通过使用 suspendCoroutine,您可以在运行时调用调度程序上提供的代码,以调用暂停的函数。由于大多数调度程序不在 Looper 线程上运行(几乎只有 Dispatchers.MAIN 运行),因此对 Looper.myLooper() 的调用会失败。
文档中说,您可以将 Looper.myLooper() 替换为 null,以在未指定的线程上调用回调函数。内置协程调度程序将确保将其路由到正确的线程以恢复执行。
编辑:您可能需要调用 it.intercepted().resume(latLng) 以确保结果被分派到正确的线程。我不确定 suspendCoroutine 继续是否默认拦截。
此外,您无需调用 fusedLocationProviderClient.removeLocationUpdates(this),因为您已经在 LocationRequest 中设置了更新次数为 1

谢谢您的回复!当传入null时,我再次收到相同的异常... it.intercept()方法对我不可用?! - user9990112
1
"IllegalStateException 如果 looper 为 null 并且此方法在未调用 Looper.prepare() 的线程中执行,则会抛出异常。您需要将 requestLocationUpdates 调用包装在 withContext(Dispatchers.MAIN) { ... } 块中。" - Kiskae

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接