위치 인식 앱 빌드 | Android 개발자 | Android Developers
위치 인식 앱 빌드 모바일 애플리케이션의 고유한 기능 중 하나는 위치 인식 기능입니다. 모바일 사용자는 어디에나 기기를 휴대하기 때문에 앱에 위치 인식 기능을 추가하면 사용자에게 더욱
developer.android.com
안드로이드 공식문서에서 제공하는 위치 라이브러리입니다. 기존에 사용하던 LocationManager는 생각보다 정확하지 않아서 새로운 위치기반 API로 바꾸면서 정리해보려 합니다.
총 4단계로
- 의존성 주입
- 권한 설정 및 권한 검증
- 위치 기능 구현
- 사용
입니다.
의존성 주입
구글 위치 설정 라이브러리 의존성을 app 단 build.gradle에 넣어줍니다.
//구글 위치 설정 라이브러리
implementation "com.google.android.gms:play-services-location:21.0.1"
권한 설정 및 권한 검증
ManiFest에서 정확 위치 권한과 대략적인 위치 권한을 설정해 줍니다. Anrdoid12(API 30, 31) 이상부턴 ACCESS_FINE_LOCATION을 요청하면 ACCESS_COARSE_LOCATION을 요청해야 하므로 둘 다 요청해 줍니다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
위치권한을 사용자에게 요청합니다.
fun requestMyPermission(){
if(checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION,
), 1
)
}
}
백그라운드 위치 권한
Android10(API 29) 이상에선 개발자는 런타임 시 백그라운드 위치 정보 액세스 권한을 요청하기 위해 맵 매니 페스트에서 권한을 선언해줘야 합니다.
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
권한 요청을 마쳤다면 사용자에게 백그라운드 위치 권한을 허용하도록 Setting 창으로 이동하는 Dialog를 띄워줍니다.
백그라운드 위치 권한은 앱 내에서 설정할 수 없고 앱 권한 설정에서 사용자가 직접 할당할 수 있습니다. 또한 포그라운드 위치가 할당돼 있어야 백그라운드 위치 권한이 제대로 작동합니다. 따라서 포그라운드 위치 권한이 허용된 후 Dialog로 요청해 줍니다.
- 기존 방법
AlertDialog.Builder(this)
.setTitle("백그라운드 위치권한")
.setMessage("백그라운드 위치권한을 허용해주세요")
.setNegativeButton("취소"){p0, p1 ->
p0.dismiss()
}
.setPositiveButton("허용") { p0, p1 ->
requestPermissions(
this, arrayOf(
android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
), 0
)
p0.dismiss()
}.show()
- 컴포즈
@RequiresApi(Build.VERSION_CODES.Q)
@Composable
fun showBackgroundPermissionDialog(){
val activity = LocalContext.current as MainActivity
var openAlertDialog by remember {
mutableStateOf(true)
}
if(openAlertDialog)
AlertDialog(onDismissRequest = { /*TODO*/ },
title = { Text(text = "백그라운드 위치권한")},
text = { Text(text = "백그라운드 위치권한을 허용해주세요")},
dismissButton = {
Button(onClick = {
openAlertDialog = false
}) {
Text(text = "취소")
}
},
confirmButton = {
Button(onClick = {
openAlertDialog = false
requestPermissions(activity, arrayOf(
android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
), 0 )
}){
Text(text = "허용")
}
}
)
}
여기서 주의할 점은 사용자가 2회 백그라운드 위치 권한을 설정하지 않고 뒤로 가기를 했다면, 요청해도 권한 설정으로 넘어가지 않습니다.
위 사항에 대해선 별도의 안내 문구를 띄워주는 걸 추천드립니다.
위치 기능 구현
위치 요청하는 함수는 3가지 입니다.
- getLastLocation : 캐싱돼 있는 위치를 가져옵니다.
- 장점 : 빠르다. 배터리 소모량이 적다
- 단점 : 캐싱돼 있는 위치 정보가 최신이 아닐 수도 있다.
- requestLocationUpdates : 일정한 간격으로 사용자의 위치를 파악할 수 있습니다.
- 장점 : 별도의 반복 작성 없이 사용자의 위치를 추적할 수 있다.
- 단점 : 제 때 중지하지 않으면 리소스가 낭비될 수 있다. 요청시간이 상대적으로 길다(평균 1.7124초)
- getCurrentLocation
- 장점 : 현재 위치를 가져올 수 있다
- 단점 : 콜백 형식이라 별도의 데이터를 받을 객체나 리스너가 필요하다
위치 요청
클래스 생성
클래스에 위도와 경도 변수를 가진 companion object와 FusedLocationClient를 생성해 줍니다.
class MyLocationManager {
companion object{
var currentLatitude = 0.0
var currentLongitude = 0.0
}
val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(applicationContext())
...
}
위치 결과를 받을 콜백 만들어 줍니다.
private val locationCallBack = object : LocationCallback() {
override fun onLocationAvailability(p0: LocationAvailability){
val a = p0.isLocationAvailable
Log.d(TAG, "Location Availability : $a")
}
override fun onLocationResult(locationResult: LocationResult) {
currentLatitude = locationResult.lastLocation?.latitude ?: 0.00
currentLongitude = locationResult.lastLocation?.longitude ?: 0.00
}
}
마지막 위치를 받을 수 있도록 빌더패턴으로 업데이트를 요청해 줍니다
fun getLastLocationClient(){
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000).build()
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallBack, Looper.getMainLooper())
}
우선순위는
- PRIORITY_HIGH_ACCURACY : 아주 높은 정확성 + 배터리 많이 닳음
- PRIORITY_BALANCED_POWER_ACCURACY : 전력사용 최적화 + 정확한 위치
- PRIORITY_LOW_POWER : 배터리를 저전력으로 사용하지만 도시 수준의 정확도를 제공
- PRIORITY_PASSIVE : 다른 앱에서 이미 연산되어 있는 위치정보를 수동적으로 수신
PRIORITY_HIGH_ACCURACY와 PRIORITY_BALANCED_POWER_ACCURACY 중 선택해서 설정해 주면 됩니다.
두 번째 패러미터인 간격은 실시간 위치가 필요한 것이 아니라면 5 ~ 10초 정도로 설정해 주면 됩니다.
지연시간은 기본 값에서 따로 설정하지 않았습니다.
지연 시간
setMaxWaitTime() 메서드를 사용하여 지연 시간을 지정할 수 있습니다. 일반적으로는 setInterval() 메서드에 지정된 간격보다 몇 배 큰 값을 전달합니다. 이 설정은 위치 정보 전달을 지연하고 여러 위치 업데이트를 일괄 전달할 수 있습니다.
이 두 가지 변경사항은 배터리 소모를 최소화하는 데 도움이 됩니다.
앱에서 즉시 위치 정보가 필요하지 않은 경우 setMaxWaitTime() 메서드에 최대한 큰 값을 전달하여 지연 시간을 늘리는 대신 더 많은 데이터를 제공하고 배터리 효율을 높여야 합니다.
지오펜스를 사용할 경우 앱은 setNotificationResponsiveness() 메서드에 큰 값을 전달하여 전력을 절약해야 합니다. 5분 이상의 값을 사용하는 것이 좋습니다.
위 getLastLocation은 이미 반복하고 있어서 별도의 반복 호출이 필요 없습니다.
패러미터로 메인 스레드에서 도는 루프를 넘겨 메인스레드에서 반복 실행하고 있습니다.
MyLocationManager().getLastLocationClient()
val a = Timer()
a.scheduleAtFixedRate(object : TimerTask(){
override fun run() {
Log.d("위도 경도", "${MyLocationManager.currentLatitude} ${MyLocationManager.currentLongitude}")
}
override fun cancel(): Boolean {
return super.cancel()
}
override fun scheduledExecutionTime(): Long {
return super.scheduledExecutionTime()
}
},0, 3000)
Timer로 일정 시간 동안 반복호출 하면 잘 찍히는 걸 확인할 수 있습니다.
requestLocationUpdates는 업데이트를 중단시키기 않으면 계속 반복해서 메모리 누수의 원인이 되므로 사용했다면 중단시켜야 합니다.
fun stopLocationupdate() = fusedLocationClient.removeLocationUpdates(locationCallBack)
현재 위치받기
현재 위치를 받는 함수는 콜백 형식을 취하고 있기 때문에 뷰모델을 받아서 LiveData에 넣어주거나 Intent의 action으로 알리는 방법 등을 선택할 수 있습니다.
fun setCurrentLocationRequest() = CurrentLocationRequest.Builder()
.setDurationMillis(30000)
.setPriority(Priority.PRIORITY_BALANCED_POWER_ACCURACY)
.build()
fun getCurrentLocation(){
val token = object: CancellationToken(){
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken = CancellationTokenSource().token
override fun isCancellationRequested(): Boolean = false
}
fusedLocationClient.getCurrentLocation(setCurrentLocationRequest(), token)
.addOnSuccessListener { location ->
if(location != null) {
Log.d(TAG, "Location is Success")
currentLatitude = location.latitude
currentLongitude = location.longitude
Log.d("위도 경도", "${currentLatitude} ${currentLongitude}")
} else{
Log.d(TAG, "location is null")
}
}
.addOnFailureListener {e ->
Log.d(TAG, "Location is Failed by $e")
}
.addOnCanceledListener {
Log.d(TAG, "Location is Canceled")
}
.addOnCompleteListener {
Log.d(TAG, "Location is Completed")
}
}
코드 전문
class MyLocationManager {
companion object{
var currentLatitude = 0.0
var currentLongitude = 0.0
}
val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(MyApplication.applicationContext())
private val TAG = javaClass.simpleName
private val locationCallBack = object : LocationCallback() {
override fun onLocationAvailability(p0: LocationAvailability){
val a = p0.isLocationAvailable
Log.d(TAG, "Location Availability : $a")
}
override fun onLocationResult(locationResult: LocationResult) {
currentLatitude = locationResult.lastLocation?.latitude ?: 0.00
currentLongitude = locationResult.lastLocation?.longitude ?: 0.00
}
}
fun getLastLocationClient(){
val lastLocationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000).build()
fusedLocationClient.requestLocationUpdates(lastLocationRequest, locationCallBack, Looper.getMainLooper())
}
fun setCurrentLocationRequest() = CurrentLocationRequest.Builder()
.setDurationMillis(30000)
.setPriority(Priority.PRIORITY_BALANCED_POWER_ACCURACY)
.build()
fun getCurrentLocation(){
val token = object: CancellationToken(){
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken = CancellationTokenSource().token
override fun isCancellationRequested(): Boolean = false
}
fusedLocationClient.getCurrentLocation(setCurrentLocationRequest(), token)
.addOnSuccessListener { location ->
if(location != null) {
Log.d(TAG, "Location is Success")
currentLatitude = location.latitude
currentLongitude = location.longitude
Log.d("위도 경도", "${currentLatitude} ${currentLongitude}")
} else{
Log.d(TAG, "location is null")
}
}
.addOnFailureListener {e ->
Log.d(TAG, "Location is Failed by $e")
}
.addOnCanceledListener {
Log.d(TAG, "Location is Canceled")
}
.addOnCompleteListener {
Log.d(TAG, "Location is Completed")
}
}
fun stopLocationupdate() = fusedLocationClient.removeLocationUpdates(locationCallBack)
}
class MyApplication: Application() {
init {
instance = this
}
companion object {
lateinit var instance: MyApplication
fun applicationContext(): Context {
return instance.applicationContext
}
}
}
'안드로이드 > 응용' 카테고리의 다른 글
다국어 지원 (Wear OS) (0) | 2023.11.17 |
---|---|
SingleTon Holder로 편하게 SingleTon 쓰기 (0) | 2023.09.22 |
ApplicationContext 일반 클래스에서 쓰기 (0) | 2023.09.22 |