#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) { ... }
блока.