Compose 권한 요청 by accompanist - YangJJune/U-Compass GitHub Wiki
런타임 권한 요청
기본 원칙
- 권한이 필요한 기능을 사용할 때에 권한을 요청해야 함.
- 권한 요청의 근거를 설명하는 화면에서 취소하는 옵션을 항상 제공해야 함. (선택지를 제공해야함.)
- 사용자가 필요한 권한을 거부하거나 취소하더라도, 앱을 계속 사용할 수 있도록 그 때의 플로우도 고려해야 함.
권한 요청 workflow
런타임 권한을 선언하고 요청하기 전에, 앱에서 해당 작업이 필요한 지 점검해야 합니다. 그렇게 해서 불필요한 권한 요청을 하지 않도록 합니다.
- manifest.xml 파일에 권한 선언
- 권한이 필요한 기능에 접근할 때 런타임 권한 요청을 하도록 UX 를 설계
- 사용자가 이미 런타임 권한을 부여했는지 확인
- 승인이 되었다면 기능을 실행함
- 권한이 필요한 작업을 실행할 때마다 권한이 있는지 확인해야 함.
- 승인이 안 되어있다면 권한이 필요한 이유를 설명
- 액세스하려는 데이터가 무엇인지, 사용자에게 권한을 승인하면 얻을 수 있는 이점이 뭔지 설명해야 함.
- 런타임 권한 요청 후 사용자의 응답을 확인
-
승인되었다면 기능을 실행함.
-
거절되었다면 권한이 필요한 기능을 사용하지 않아도 앱을 사용할 수 있도록 설계함.
앱의 기능을 Gracefully degrade 한다고 합니다.
-
Accompanist
사용법
rememberPermissionState
rememberMultiplePermissionsState
각각 한 개, 여러 개의 권한을 요청할 수 있는 권한 요청 API 입니다.
위치 권한 설정 예제
- Manifest 권한 설정
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
- 위치 권한을 요청하는 Composable 함수
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun LocationPermissionScreen() {
val context = LocalContext.current
// 위치 권한 목록 정의
val locationPermissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
// 여러 권한을 한번에 처리하기 위한 상태 관리
val multiplePermissionsState = rememberMultiplePermissionsState(
permissions = locationPermissions
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when {
// 모든 권한이 허용된 경우
multiplePermissionsState.allPermissionsGranted -> {
Text("위치 권한이 허용되었습니다!")
}
// 권한이 거부된 경우 사용자에게 이유 설명
multiplePermissionsState.shouldShowRationale -> {
Text("위치 기능을 사용하기 위해서는 위치 권한이 필요합니다.")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
multiplePermissionsState.launchMultiplePermissionRequest()
}) {
Text("권한 허용하기")
}
}
// 처음 권한 요청하는 경우
else -> {
Text("이 앱은 위치 기반 서비스를 제공하기 위해 위치 권한이 필요합니다.")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
multiplePermissionsState.launchMultiplePermissionRequest()
}) {
Text("권한 요청하기")
}
}
}
}
}
- 위치 권한 목록을 정의하고, 해당 권한들이 2개 이상이기 때문에
rememberMultiplePermissionsState()
를 사용함.- COARSE Location 은 대략적인 위치를 요청하는 권한
- FINE Location 은 정확한 위치를 요청하는 권한
// 위치 권한 목록 정의
val locationPermissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
// 여러 권한을 한번에 처리하기 위한 상태 관리
val multiplePermissionsState = rememberMultiplePermissionsState(
permissions = locationPermissions
)
-
권한 여부에 따라서 분기 처리하기
- 승인된 경우
// 모든 권한이 허용된 경우 multiplePermissionsState.allPermissionsGranted -> { Text("위치 권한이 허용되었습니다!") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { // 위치 정보 가져오기 getUserLocation(fusedLocationClient, context) }) { Text("현재 위치 가져오기") } }
- 권한이 거부된 경우
// 권한이 거부된 경우 사용자에게 이유 설명 multiplePermissionsState.shouldShowRationale -> { Text("위치 기능을 사용하기 위해서는 위치 권한이 필요합니다.") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { multiplePermissionsState.launchMultiplePermissionRequest() }) { Text("권한 허용하기") } }
- 처음 요청하는 경우
// 처음 권한 요청하는 경우 else -> { Text("이 앱은 위치 기반 서비스를 제공하기 위해 위치 권한이 필요합니다.") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { multiplePermissionsState.launchMultiplePermissionRequest() }) { Text("권한 요청하기") } }
카메라 권한 요청 예제
- Manifest 권한 설정
<uses-permission android:name="android.permission.CAMERA" />
- 카메라 권한 요청 Composable 함수
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPermissionScreen() {
// 단일 권한 상태 관리
val cameraPermissionState = rememberPermissionState(
permission = Manifest.permission.CAMERA
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when {
// 권한이 허용된 경우
cameraPermissionState.status.isGranted -> {
Text("카메라 권한이 허용되었습니다.")
}
// 권한이 거부된 경우 사용자에게 이유 설명
cameraPermissionState.status.shouldShowRationale -> {
Text("카메라 기능을 사용하기 위해서는 카메라 권한이 필요합니다. 권한을 허용해주세요.")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
cameraPermissionState.launchPermissionRequest()
}) {
Text("권한 허용하기")
}
}
// 처음 권한 요청하는 경우
else -> {
Text("이 앱은 카메라 기능을 제공하기 위해 카메라 권한이 필요합니다.")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
cameraPermissionState.launchPermissionRequest()
}) {
Text("권한 요청하기")
}
}
}
}
}
- 이번엔 권한이 1개이기 때문에
rememberPermissionState()
사용
// 단일 권한 상태 관리
val cameraPermissionState = rememberPermissionState(
permission = Manifest.permission.CAMERA
)
-
권한 여부에 따라서 분기 처리하기
- 승인된 경우
// 권한이 허용된 경우 cameraPermissionState.status.isGranted -> { Text("카메라 권한이 허용되었습니다!") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { // 카메라 기능 실행 코드 }) { Text("카메라 열기") } }
- 권한이 거부된 경우
// 권한이 거부된 경우 사용자에게 이유 설명 cameraPermissionState.status.shouldShowRationale -> { Text("카메라 기능을 사용하기 위해서는 카메라 권한이 필요합니다. 권한을 허용해주세요.") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { cameraPermissionState.launchPermissionRequest() }) { Text("권한 허용하기") } }
- 처음 요청하는 경우
// 처음 권한 요청하는 경우 else -> { Text("이 앱은 카메라 기능을 제공하기 위해 카메라 권한이 필요합니다.") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { cameraPermissionState.launchPermissionRequest() }) { Text("권한 요청하기") } }
2회 이상 거부 구분하기
사용자가 처음 권한을 거부한 경우와 2회 이상 거부한 겨우를 구분해야 합니다.
처음 거부한 경우에는 shouldShowRationale()
을 통해서 권한이 필요한 이유를 설명하고 다시 서비스 내에서 권한 허용 팝업을 호출할 수 있습니다.
하지만 그 이상 거부했을 경우(shouldShowRationale()
을 실행했음에도 불구하고 거부했을 경우), 사용자를 앱 설정으로 이동시켜 수동으로 권한을 허용하도록 안내해야 합니다.
var showRationale by remember { mutableStateOf(false) }
// 최초 요청이거나 , shouldShowRationale 도 거절한 경우
if(!multiplePermissionsState.allPermissionsGranted &&
!multiplePermissionsState.shouldShowRationale) {
// 2번 이상 거절
if (showRationale) {
Text("[2회 거절] 권한이 필요합니다. 앱 설정에서 권한을 허용해주세요.")
Button(onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}) {
Text("설정으로 이동")
}
// 최초 요청
} else {
Text("최초 요청")
Button(onClick = {
multiplePermissionsState.launchMultiplePermissionRequest()
showRationale = true
}) {
Text("권한 요청하기")
}
}
}