일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Rxjava Observable
- 큐 자바 코드
- 2022 플러터 안드로이드 스튜디오
- 스택 큐 차이
- 안드로이드 os 구조
- rxjava cold observable
- ANR이란
- ar vr 차이
- rxjava disposable
- 안드로이드 라이선스 종류
- 객체
- 2022 플러터 설치
- 서비스 vs 쓰레드
- jvm 작동 원리
- 스택 자바 코드
- 플러터 설치 2022
- 안드로이드 유닛 테스트 예시
- 안드로이드 유닛테스트란
- 안드로이드 라이선스
- jvm이란
- 멤버변수
- 클래스
- android ar 개발
- 서비스 쓰레드 차이
- rxjava hot observable
- 안드로이드 레트로핏 crud
- 자바 다형성
- 안드로이드 레트로핏 사용법
- android retrofit login
- 안드로이드 유닛 테스트
- Today
- Total
나만을 위한 블로그
[Android] 내 위치 정보 가져와서 사용하는 법 (with. Hilt) 본문
※ 모든 코드는 예시 코드기 때문에 실제로 쓰려면 반드시 리팩토링, 예외처리를 추가한다
위치 정보를 활용하는 기능 구현 중 액티비티, 프래그먼트 곳곳에 위치 권한을 요청하고 허용, 거절 상태에 따라 분기되는 함수가 보였다.
Base 클래스에 빼자니 필요없는 화면도 있어서 아닌 거 같고, 뷰모델에 박아두고 쓰자니 자유롭게 재사용할 수 없는 느낌이라 아닌 거 같아서 이리저리 시험해보다 괜찮아 보이는 방법을 찾은 것 같아 포스팅한다.
코드부터 본다.
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.tasks.CancellationToken
import com.google.android.gms.tasks.CancellationTokenSource
import com.google.android.gms.tasks.OnTokenCanceledListener
import com.example.data.model.LatdLntd
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
class GlobalLocationManager @Inject constructor(
private val fusedLocationProviderClient: FusedLocationProviderClient,
@ApplicationContext private val context: Context,
) {
private val _currentLocation = MutableStateFlow<LatdLntd?>(null)
val currentLocation: StateFlow<LatdLntd?> = _currentLocation.asStateFlow()
fun updateLocation(
onLocationCanceled: (() -> Unit)? = null,
onLocationSuccess: ((LatdLntd) -> Unit)? = null,
onLocationError: ((Exception) -> Unit)? = null,
onLocationComplete: (() -> Unit)? = null,
) {
if (checkLocationPermission()) {
fusedLocationProviderClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
val cachedLocation = LatdLntd(location.latitude, location.longitude)
_currentLocation.value = cachedLocation
onLocationSuccess?.invoke(cachedLocation)
} else {
requestCurrentLocation(onLocationSuccess, onLocationCanceled, onLocationError, onLocationComplete)
}
}
.addOnFailureListener { exception ->
requestCurrentLocation(onLocationSuccess, onLocationCanceled, onLocationError, onLocationComplete)
}
} else {
onLocationError?.invoke(SecurityException("위치 권한 거절됨"))
}
}
private fun requestCurrentLocation(
onLocationSuccess: ((LatdLntd) -> Unit)? = null,
onLocationCanceled: (() -> Unit)? = null,
onLocationError: ((Exception) -> Unit)? = null,
onLocationComplete: (() -> Unit)? = null,
) {
if (checkLocationPermission()) {
fusedLocationProviderClient.getCurrentLocation(
LocationRequest.PRIORITY_HIGH_ACCURACY,
object : CancellationToken() {
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken {
onLocationCanceled?.invoke()
return CancellationTokenSource().token
}
override fun isCancellationRequested(): Boolean = false
}
).addOnSuccessListener { location ->
if (location != null) {
val newLocation = LatdLntd(location.latitude, location.longitude)
_currentLocation.value = newLocation
onLocationSuccess?.invoke(newLocation)
}
}.addOnFailureListener { exception ->
onLocationError?.invoke(exception)
}.addOnCompleteListener {
onLocationComplete?.invoke()
}
}
}
fun checkLocationPermission(): Boolean =
ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
data class LatdLntd(
val latd: Double?,
val lntd: Double?,
)
module 설정도 잊지 않고 해 준다.
import android.content.Context
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.example.data.GlobalLocationManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object LocationModule {
@Provides
@Singleton
fun provideFusedLocationProviderClient(@ApplicationContext context: Context): FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
@Provides
@Singleton
fun provideGlobalLocationManager(
fusedLocationProviderClient: FusedLocationProviderClient,
@ApplicationContext context: Context
): GlobalLocationManager = GlobalLocationManager(fusedLocationProviderClient, context)
}
hilt를 쓰기 때문에 이렇게 설정했다면 프래그먼트, 액티비티 어디서든 GlobalLocationManager 의존성을 주입받아서 사용하면 된다.
만약 런처를 통해 권한 허용 여부를 판단한다면 아래처럼 할 수 있다.
@AndroidEntryPoint
class ExampleFragment : Fragment() {
@Inject
lateinit var globalLocationManager: GlobalLocationManager
...
private val permissionCheckLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val isCoarseLocationGranted = permissions[Manifest.permission.ACCESS_COARSE_LOCATION]
val isFineLocationGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION]
if (isCoarseLocationGranted == true && isFineLocationGranted == true) {
globalLocationManager.updateLocation()
// ...
}
// ...
permissions 리시버 변수를 통해 위치 권한인 ACCESS_COARSE_PERMISSION, ACCESS_FINE_LOCATION이 허용되었는지 알 수 있고, 이 값들이 모두 true라면 updateLocation()을 호출하는 식이다.
또는 앱을 나오고 설정 앱에서 위치 권한을 허용할 수도 있다. 이 경우 onResume()을 활용하면 대부분 잘 먹힐 것이다.
override fun onResume() {
super.onResume()
if (globalLocationManager.checkLocationPermission()) {
globalLocationManager.updateLocation()
}
}
이렇게 하면 생각보다 빠르게 위도, 경도 값을 받아오는 걸 볼 수 있다.
그럼 이게 어떻게 가능한 건가? GlobalLocationManager의 updateLocation()을 확인한다.
fun updateLocation(
onLocationCanceled: (() -> Unit)? = null,
onLocationSuccess: ((LatdLntd) -> Unit)? = null,
onLocationError: ((Exception) -> Unit)? = null,
onLocationComplete: (() -> Unit)? = null,
) {
if (checkLocationPermission()) {
fusedLocationProviderClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
val cachedLocation = LatdLntd(location.latitude, location.longitude)
_currentLocation.value = cachedLocation
onLocationSuccess?.invoke(cachedLocation)
} else {
requestCurrentLocation(onLocationSuccess, onLocationCanceled, onLocationError, onLocationComplete)
}
}
.addOnFailureListener { exception ->
requestCurrentLocation(onLocationSuccess, onLocationCanceled, onLocationError, onLocationComplete)
}
} else {
onLocationError?.invoke(SecurityException("위치 권한 거절됨"))
}
}
private fun requestCurrentLocation(
onLocationSuccess: ((LatdLntd) -> Unit)? = null,
onLocationCanceled: (() -> Unit)? = null,
onLocationError: ((Exception) -> Unit)? = null,
onLocationComplete: (() -> Unit)? = null,
) {
if (checkLocationPermission()) {
fusedLocationProviderClient.getCurrentLocation(
LocationRequest.PRIORITY_HIGH_ACCURACY,
object : CancellationToken() {
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken {
onLocationCanceled?.invoke()
return CancellationTokenSource().token
}
override fun isCancellationRequested(): Boolean = false
}
).addOnSuccessListener { location ->
if (location != null) {
val newLocation = LatdLntd(location.latitude, location.longitude)
_currentLocation.value = newLocation
onLocationSuccess?.invoke(newLocation)
}
}.addOnFailureListener { exception ->
onLocationError?.invoke(exception)
}.addOnCompleteListener {
onLocationComplete?.invoke()
}
}
}
먼저 fusedLocationProviderClient.lastLocation을 통해 성공 시 location 값을 가져오고, 만약 이 location이 null이라면 requestCurrentLocation()을 호출해 새 위치 정보를 얻는 흐름이다.
여기서 lastLocation은 fusedLocationProviderClient를 만든 후 마지막으로 알려진(=캐싱된) 위치를 가져온다.
위치 서비스 클라이언트를 만든 후 마지막으로 알려진 유저 기기의 위치를 가져올 수 있다. 앱이 여기 연결된 경우 통합 위치 정보 제공자의 getLastLocation()을 써서 기기 위치를 가져올 수 있다. 이 호출로 리턴되는 위치의 정밀도는 위치 정보 액세스 권한 요청 방법 가이드에 설명된 대로 매니페스트에 입력한 권한 설정에 따라 결정된다
fusedLocationClient.lastLocation
.addOnSuccessListener { location : Location? ->
// Got last known location. In some rare situations this can be null.
}
getLastLocation()은 지리적 위치의 위도, 경도 좌표가 있는 Location 객체를 가져오는 데 쓸 수 있는 Task를 리턴한다. 아래 상황에선 Location 객체가 null일 수 있다
- 기기 설정에서 위치가 사용 중지된 경우. 위치를 사용 중지하면 캐시도 지워지므로, 이전에 마지막 위치를 가져온 경우에도 결과는 null일 수 있다
- 기기에서 위치를 기록한 적이 없다(새 핸드폰이거나 기본 설정으로 복원된 핸드폰인 경우)
- 기기의 구글 플레이 서비스가 재시작됐고, 서비스 재시작 후 위치를 요청한 활성 통합 위치 정보 제공자 클라이언트가 없다. 이런 상황이 발생하지 않도록 새 클라이언트를 만들고 직접 위치 업데이트를 요청할 수 있다...(중략)...FusedLocationProviderClient는 기기 위치 정보를 가져오는 여러 메서드를 제공한다. 앱의 사용 사례에 따라 둘 중 하나를 선택한다
- getLastLocation() : 위치 추정치를 더 빨리 가져오고 앱의 배터리 사용량을 최소화한다. 그러나 최근에 다른 클라이언트가 적극적으로 위치를 사용하지 않았다면 위치 정보가 최신이 아닐 수 있다
- getCurrentLocation() : 더 최신 상태고 정확한 위치를 일관되게 가져온다. 그러나 이 메서드를 쓰면 기기에서 활성 위치 계산이 발생할 수 있다. 이 메서드는 가능한 경우 최신 위치를 가져오는 데 권장되는 방법이며 requestLocationUpdates()를 써서 직접 위치 업데이트를 시작, 관리하는 것보다 안전하다. requestLocationUpdates()를 호출하는 경우 위치를 쓸 수 없거나 최신 위치를 가져온 후 요청이 정확하게 중지되지 않으면 앱이 전력을 많이 소모할 수 있다
이런 특징이 있기 때문에 먼저 lastLocation으로 빠르게 캐싱된 위치값을 가져온다. 그리고 여러 이유 때문에 Location이 null이라면 getCurrentLocation()으로 보다 정밀한 위도, 경도 값을 얻는다.
getCurrentLocation()까지 호출하게 되면 네트워크 환경에 따라 시간이 제법 걸릴 수도 있어서 프로그레스 바를 띄우는 등 로딩 처리를 할 필요가 있겠다.
lastLocation으로 가져오는 마지막 위치 정보는 gps, 와이파이, 5G 등 여러 경로를 통해 가져오며 안드로이드 시스템이 비동기적으로 자동 갱신해 준다. 또한 위치 정보는 당연히 유저가 gps 사용을 해제했거나 위치 권한을 거절했다면 가져올 수 없다. 유저가 막아놨으니 어쩔 수 없다. 해킹해서 가져올 건가?
onLocationCanceled 등 함수 타입 파라미터는 호출하는 곳에서 성공, 실패, 완료 케이스를 처리하기 위해 넣었다. 굳이 외부 함수 호출을 이 클래스에서 할 필요는 없기 때문에 이렇게 만들었다. 필요없으면 없애고 쓰면 된다.
'Android' 카테고리의 다른 글
[Android] 현재 내 위치 가져오는 법 (0) | 2024.11.13 |
---|---|
[Android] 초기 데이터 로드 2 : 의문점 해소하기 (0) | 2024.10.13 |
[Android] 단위 테스트 시 Stub, Mock, Fake, Spy 선택 기준 (0) | 2024.10.11 |
[Android] 경쟁 상태(Race Condition)란? (0) | 2024.09.29 |
[Android] 초기 데이터 로드 : LaunchedEffect vs ViewModel (0) | 2024.09.23 |