Compose 권한 요청 by accompanist - YangJJune/U-Compass GitHub Wiki

런타임 권한 요청

기본 원칙

  • 권한이 필요한 기능을 사용할 때에 권한을 요청해야 함.
  • 권한 요청의 근거를 설명하는 화면에서 취소하는 옵션을 항상 제공해야 함. (선택지를 제공해야함.)
  • 사용자가 필요한 권한을 거부하거나 취소하더라도, 앱을 계속 사용할 수 있도록 그 때의 플로우도 고려해야 함.

권한 요청 workflow

런타임 권한을 선언하고 요청하기 전에, 앱에서 해당 작업이 필요한 지 점검해야 합니다. 그렇게 해서 불필요한 권한 요청을 하지 않도록 합니다.

  1. manifest.xml 파일에 권한 선언
  2. 권한이 필요한 기능에 접근할 때 런타임 권한 요청을 하도록 UX 를 설계
  3. 사용자가 이미 런타임 권한을 부여했는지 확인
    • 승인이 되었다면 기능을 실행함
    • 권한이 필요한 작업을 실행할 때마다 권한이 있는지 확인해야 함.
  4. 승인이 안 되어있다면 권한이 필요한 이유를 설명
    • 액세스하려는 데이터가 무엇인지, 사용자에게 권한을 승인하면 얻을 수 있는 이점이 뭔지 설명해야 함.
  5. 런타임 권한 요청 후 사용자의 응답을 확인
    • 승인되었다면 기능을 실행함.

    • 거절되었다면 권한이 필요한 기능을 사용하지 않아도 앱을 사용할 수 있도록 설계함.

      앱의 기능을 Gracefully degrade 한다고 합니다.

Accompanist

사용법

  • rememberPermissionState
  • rememberMultiplePermissionsState

각각 한 개, 여러 개의 권한을 요청할 수 있는 권한 요청 API 입니다.

위치 권한 설정 예제

  1. Manifest 권한 설정
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

  1. 위치 권한을 요청하는 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("권한 요청하기")
        }
    }
    

카메라 권한 요청 예제

  1. Manifest 권한 설정
<uses-permission android:name="android.permission.CAMERA" />
  1. 카메라 권한 요청 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("권한 요청하기")
        }
    }
}