위치정보에 권한에 대해 알기 전에 사전 지식이 필요합니다!
출처 - 안드로이드 공식문서
사진을 보면 알 수 있듯이 안드로이드에서 대략적인 위치, 정확한 위치 2가지 권한을 모두 요청하는 것을 권하는 것을 확인할 수 있습니다.
ACCESS_FINE_LOCATION: 정확한 위치
ACCESS_COARSE_LOCATOIN: 대략적인 위치
이 상수값들을 기억하고 시작하겠습니다.
매니페스트 설정을 해줍니다
<?xml version="1.0" encoding="utf-8"?>
<manifest
...
<!-- Always include this permission -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Include only if your app benefits from precise location access. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
다음은 gradle 설정을 해주겠습니다
dependencies {
implementation("com.google.android.gms:play-services-location:play-services-location:21.1.0")
...
먼저 결과화면부터 확인하고 가보겠습니다.
앱의 흐름은 다음과 같습니다.
이제 코드를 볼까요?
해당 앱은 홈화면에서 진행됩니다.
먼저 HomeScreen 함수부터 보겠습니다.
[HomeScreen.kt]
@OptIn(ExperimentalPermissionsApi::class)
@RequiresPermission(
anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]
)
@Composable
fun HomeScreen(
navController: NavController
){
val context = LocalContext.current
var showPermissionDialog by remember {
mutableStateOf(false)
}
val locationProviderClient = remember {
LocationServices.getFusedLocationProviderClient(context)
}
val permissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
val permissionState = rememberMultiplePermissionsState(permissions = permissions)
// 설정한 permission List 중 첫번째 권한이 현재 포함되어 있는지
val allRequiredPermission =
permissionState.revokedPermissions.none { it.permission in permissions.first() }
// 만약 권한이 존재한다면 바로 LocationButton 표시
if(allRequiredPermission) {
LocationButton(
locationProviderClient = locationProviderClient,
userPreciseLocation =
permissionState.permissions
.filter { it.status.isGranted }
.map { it.permission }
.contains(Manifest.permission.ACCESS_FINE_LOCATION)
)
}else{
// 위치권한 정보가 없다면 권한요청 버튼 표시
Button(
// 다이얼로그 표시 시작
onClick = { showPermissionDialog = true }
) {
Text(text = "Click")
}
}
// 다이얼로그 표시
if(showPermissionDialog){
LocationDialog(permissionState = permissionState){
showPermissionDialog = it
}
}
}
설명이 필요할 것 같은 부분은 주석을 달았습니다!
참고로 함수 위에 선언된 어노테이션은 아직 안정화가 이뤄지지 않은 위치 관련 Api를 쓰기위한 것과 해당 함수를 사용하기 위해 필요한 권한정보들을 작성한 코드입니다.
생소한 코드부터 확인해보겠습니다.
LocationServices는 뭘까요?
val locationProviderClient = remember {
LocationServices.getFusedLocationProviderClient(context)
}
바로 Google Play Service에서 제공해주는 API라고 볼 수 있습니다!
해당 API에 대해 자세히 알고싶으신 분들은 Google Play Service를 참고해주시면 될 것 같습니다!
간단히 설명드리면 해당 API는 위치에 관한 클라이언트라고 볼 수 있습니다.
즉 해당 변수를 통해 사용자의 위치에 관한 값들을 얻을 수 있다는 것이죠!
위치를 실제로 조회하는 코드를 보기전에, 다이얼로그 코드부터 확인해보겠습니다.
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun LocationDialog(
permissionState: MultiplePermissionsState,
showPermissionDialog: (Boolean) -> Unit
){
AlertDialog(
onDismissRequest = { showPermissionDialog(false) },
title = {
Text(text = "위치정보 권한 요청")
},
text = {
Text(text = "PlacePick 서비스를 원활하게 이용하기 위해 위치를 설정해보세요!")
},
confirmButton = {
TextButton(
onClick = {
showPermissionDialog(false)
permissionState.launchMultiplePermissionRequest() // 위치권한 요청
}
) {
Text(text = "확인")
}
},
dismissButton = {
TextButton(onClick = { showPermissionDialog(false) }) {
Text(text = "취소")
}
}
)
}
위치 권한을 사용자에게 요청하는 부분을 보시면 아시겠지만 굉장히 간단합니다.
주석을 단 부분을 보시면 실제로 저 코드에서 안드로이드 OS가 사용자에게 권한을 묻는 시스템창이 뜬다고 생각하시면 됩니다!
이제 실제 사용자 위치를 조회하는 코드를 보겠습니다.
@RequiresPermission(
anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]
)
@Composable
fun LocationButton(
locationProviderClient: FusedLocationProviderClient,
userPreciseLocation: Boolean
){
val scope = rememberCoroutineScope()
var locationInfo by remember {
mutableStateOf("")
}
Column {
IconButton(
onClick = {
scope.launch(Dispatchers.IO) {
val priority = if (userPreciseLocation){
Priority.PRIORITY_HIGH_ACCURACY
} else {
Priority.PRIORITY_BALANCED_POWER_ACCURACY
}
val result = locationProviderClient.getCurrentLocation(
priority,
CancellationTokenSource().token,
).await()
result?.let { fetchedLocation->
locationInfo = "let: ${fetchedLocation.latitude} long: ${fetchedLocation.longitude}"
}
}
}
) {
Icon(imageVector = Icons.Default.LocationOn, contentDescription = null)
}
Text(text = locationInfo)
}
}
사용자가 권한을 요청한 뒤 버튼입니다.
해당 버튼을 누르면 사용자의 위치값(위도, 경도)을 얻어오고 그 값을 화면에 뿌려주는 간단한 코드입니다!
여기서도 생소한 코드를 살펴보겠습니다.
val priority = if (userPreciseLocation){
Priority.PRIORITY_HIGH_ACCURACY
} else {
Priority.PRIORITY_BALANCED_POWER_ACCURACY
}
참고로 userPreciseLocation은 사용자가 정확한 위치값을 동의 여부를 저장한 변수입니다.
Priority가 무엇일까요? Priority관련 공식문서 설명보러가기
자세한 설명은 공식문서를 읽어보시면 됩니다!
간략하게 설명드리면 Priority, 직역하면 우선순위를 설정하는 값이라고 이해하시면 될 것 같습니다.
해당 우선순위 값의 프로퍼티가 Int로 되어있어 코드의 원형을 살펴봤습니다!
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.SOURCE)
public @interface Priority {
int PRIORITY_HIGH_ACCURACY = 100;
int PRIORITY_BALANCED_POWER_ACCURACY = 102;
int PRIORITY_LOW_POWER = 104;
int PRIORITY_PASSIVE = 105;
}
각 우선순위에 따라 100부터 105까지 설정되어있는 것을 알 수 있습니다.
이 값들을 활용해 추후에 배터리 소모도 최적화 할 수 있을 것 같습니다!
다음 코드를 보겠습니다.
val result = locationProviderClient.getCurrentLocation(
priority,
CancellationTokenSource().token,
).await()
result?.let { fetchedLocation->
locationInfo = "let: ${fetchedLocation.latitude} long: ${fetchedLocation.longitude}"
}
locationProviderClinet로 현재 위치를 참조하려고 합니다.
여기서 들어가는 파라미터값 priority는 위치의 정확성에 관한 값이고, 두번째 파라미터는 요청을 취소하는데 사용하는 토큰값입니다.
출처 - Google Play Service
요청을 취소하는 토큰값은 글을 읽어보면 알 수 있듯이 사용자 위치정보를 얻는데 실패하는 경우에 관한 값임을 알 수 있습니다.
위치정보에 관한 코드는 생각보다 굉장히 단순합니다
하지만 잘모르고 썻다간 자원낭비, 권한관리 등등.. 을 고려하지 못하고 코드를 작성할 수 있을 것 같습니다! (이전 프로젝트에 제가 그랬거든요...😂)
그리고 이번 기회를 통해 공식문서에 대한 중요성을 한번더 느끼게되었습니다..
아직 Compose에 위치권한 레퍼런스가 많이 없기도하구 이런 민감정보는 공식문서를 더 꼼꼼히 봐야 추후 배포할때도 문제가 없더라구요
아무튼 도움이 되셨길 바랍니다!
참고한 샘플코드 git 주소 - platform-samples