FusedLocationProviderClient с сопрограммами Kotlin

#android #kotlin #fusedlocationproviderapi #kotlin-coroutines

#Android #kotlin #fusedlocationproviderapi #kotlin-сопрограммы

Вопрос:

Я пытаюсь запросить новое местоположение с помощью сопрограмм FusedLocationProviderClient и Kotlin. Это моя текущая настройка:

 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() в конечном итоге), разве это не означало бы, что я могу вызвать функцию только один раз?

Любая помощь приветствуется.

Ответ №1:

Проблема в том, как вы настраиваете свой coroutineContext . Используйте это вместо:

 override val coroutineContext = Dispatchers.Main   job
  

Если вам когда-нибудь понадобится IO диспетчер, вы можете запросить его явно:

 withContext(Dispatchers.IO) { ... blocking IO code ... }
  

Чтобы приостановить сопрограмму, вызовите suspendCancellableCoroutine , иначе вы не получите никакой пользы от структурированного параллелизма.

Еще одна деталь, не пишите никакого кода после it.resume в suspendCancellableCoroutine блоке. Если диспетчер решит возобновить сопрограмму немедленно, в resume вызове, этот код не будет выполняться до тех пор, пока не будет выполнен весь код сопрограммы (или, по крайней мере, до следующей точки приостановки).

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

Комментарии:

1. Лучше использовать Dispatchers.Default вместо ввода-вывода для неблокирующих операций, например, вычислений

2. Я не вижу значимости этого комментария. OP использовал IO dispatcher, ответ явно указывает, что он предназначен только для блокирования операций ввода-вывода, и никто не упоминал операции, требующие больших затрат процессора.

Ответ №2:

 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)
            amp;amp; !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)
            amp;amp; !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)
        }
    }
}
  

Комментарии:

1. Не найдены ctx.isPermissionValid и offsetMinutes.minute

Ответ №3:

С помощью suspendCoroutine вы вызываете предоставленный код в диспетчере, который вызывал приостановленную функцию во время выполнения. Поскольку большинство диспетчеров не работают в циклических потоках (в значительной степени только Dispatchers.MAIN работает), вызов Looper.myLooper() завершается неудачей.

В документации говорится, что вы можете заменить Looper.myLooper() на null , чтобы вызвать обратный вызов в неуказанном потоке. Затем встроенный диспетчер сопрограммы убедится, что она перенаправлена в правильный поток для возобновления выполнения.

РЕДАКТИРОВАТЬ: возможно, вам потребуется вызвать it.intercepted().resume(latLng) , чтобы убедиться, что результат отправлен в правильный поток. Я не совсем уверен, перехватывается ли suspendCoroutine продолжение по умолчанию.

Кроме того, вам не нужно вызывать, fusedLocationProviderClient.removeLocationUpdates(this) потому что вы уже установили количество обновлений в LocationRequest to 1 .

Комментарии:

1. Спасибо за ваш ответ! При передаче в null я снова получаю то же исключение… Метод it.intercept() мне недоступен?!

2. «Исключение IllegalStateException, если looper имеет значение null и этот метод выполняется в потоке, который не вызывал Looper.prepare()». Вам нужно будет обернуть requestLocationUpdates вызов внутри withContext(Dispatchers.MAIN) { ... } блока.