이번 KMP/CMP 예제는 권한 설정을 요청하는 것이다.
안드로이드하고 IOS에서는 사용자에게 권한이 필요한 요청이 있다. 주로 음성 녹음, 동영상 녹화 등이 대표적인데, 안드로이드하고 IOS의 권한 설정 순서가 약간 다르다.

안드로이드는 한 번 거부되어도 재요청이 가능하며, 이 또한 거부된 경우 앱 설정화면에서 변경을 요청해야 한다.
반면에 IOS는 한 번만 거부가 되어도 사용자는 앱 설정에서 권한을 변경해야하며, 둘의 횟수가 조금 다르다.
이걸 Android/IOS 따로 expect/actual로 PermissionManager로 각각 구현하는 것도 방법이지만, 이를 통합하고 있는 라이브러리인 moko 라이브러리로 간단하게 권한요청을 설정하는 방법을 알아보자.
참고로 이 게시글은 유튜브 영상 과 moko 공식 깃허브를 참고하여 구현을 진행했습니다.
[versions]
#...
moko = "0.20.1"
lifecycleViewModel = "2.9.4"
[libraries]
moko-permissions = { module = "dev.icerock.moko:permissions", version.ref = "moko" }
moko-permissions-compose = { module = "dev.icerock.moko:permissions-compose", version.ref = "moko" }
moko-permissions-microphone = { module = "dev.icerock.moko:permissions-microphone", version.ref = "moko" }
lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleViewModel"}
아마 유튜브 영상도 봤었다면 moko-permissions-microphone가 없을 것이다. moko 라이브러리에서 이제 특정 권한 요청에 대한 라이브러리들을 각각 추가해야하는 것 같다. 아래 이미지가 공식 깃허브에서 보여주는 각 권한에 맞는 의존성을 보여준다.(영상 따라했다가 Permission.AUDIO_RECORD가 없었음..)

이제 composeApp/build.gradle.kts에 아래와 같이 추가
kotlin {
//...
sourceSets {
//...
commonMain.dependencies {
//...
implementation(libs.lifecycle.viewmodel)
api(libs.moko.permissions.compose)
api(libs.moko.permissions.microphone)
api(libs.moko.permissions)
}
//...
}
}
microphone은 다시 보니까 api가 아닌 implementation으로 주입 받아도 될 것 같다.(공식 문서가 그럼)
먼저 Moko를 사용하는 방법은 ViewModel을 만들어 해당 부분에 권한을 요청하는 방식이다.
말로 설명하면 이해가 잘 안되니 전체 코드를 살펴보자
@Composable
@Preview
fun App() {
MaterialTheme {
//moko라이브러리를 통한 권한 설정 controller 생성
val factory = rememberPermissionsControllerFactory()
val controller = remember(factory) {
factory.createPermissionsController()
}
//이건 라이프사이클에 맞게 연결되도록 설정하는 것 같음.
BindEffect(controller)
//이제 conroller를 받은 PermissionViewModel을 생성
val viewModel = viewModel {
PermissionsViewModel(controller)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//여기서 state는 Audio_Record의 권한 상태 여부를 받고 있으며
//각 상태는 아래와 같다.
/*
PermissionState.Granted - 권한 허용
PermissionState.DeniedAlways - 권한 항상 거부
PermissionState.NotDetermined - 권한 요청이 되지 않음
PermissionState.NotGranted - 권한이 허용되지 않은 모든 상태를 포함하는 “포괄적 상태”(NotDetermined, DeniedAlways, Denied를 모두 아우른다)
PermissionState.Denied - 권한 거부
**/
when(viewModel.state) {
PermissionState.Granted -> {
Text("Record audio permission granted!")
}
PermissionState.DeniedAlways -> {
Text("Permission was permanently declined.")
Button(
onClick = {
controller.openAppSettings()
}
) {
Text("Open app Setting")
}
}
else -> {
Button(
onClick = {
viewModel.providerOrRequestRecordAudioPermission()
}
) {
Text("Request Permission")
}
}
}
}
}
}
이제 PermissionViewModel의 코드를 보자
class PermissionsViewModel(
private val controller: PermissionsController
) : ViewModel() {
//오디오 권한상태
var state by mutableStateOf(PermissionState.NotDetermined)
private set
init {
//실행 시 오디오 권한 상태의 정보를 받음
viewModelScope.launch {
state = controller.getPermissionState(Permission.RECORD_AUDIO)
}
}
fun providerOrRequestRecordAudioPermission() {
//아래 catch문은 공식 문서에 있는 예외 코드다.
//직접 설정을 하면 된다.
viewModelScope.launch {
try {
controller.providePermission(Permission.RECORD_AUDIO)
state = PermissionState.Granted
} catch (e: DeniedAlwaysException) {
state = PermissionState.DeniedAlways
} catch (e: DeniedException) {
state = PermissionState.Denied
} catch (e: RequestCanceledException) {
e.printStackTrace()
}
}
}
}
코드만 보면 간단한게 권한요청에 따른 UI나 요청/앱 설정화면 이동이 쉽게 가능하다.
안드로이드는 AndroidManifest.xml에 오디오 권한을 추가해야 한다.
composeApp > androidMain > AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--권한 추가-->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!-- ... -->
</manifest>
iosApp파일 중에 info.plist 파일이 보일 것이다 해당 파일에 아래와 같이 추가
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--이것 추가-->
<!--오디오 권한 요청시 나올 string 값이다.-->
<key>NSMicrophoneUsageDescription</key>
<string>So that others can hear you in the call, we need your microphone.</string>
</dict>
</plist>
권한 수락을 누르면 바로 끝나니 둘 다 권한 거부 요청 후 설정화면 이동 후 권한 허용을 바꾼 시나리오로 동작 진행.
안드로이드는 설정화면에서 허용으로 변경 후 돌아가도 권한 허용 Text가 뜨지 않는다.
이를 해결하기 위해 App 컴포저블에 아래와 같이 다시 Start가 될 때 권한 정보를 받는 코드를 추가하니 변경이 되었다. (Android/IOS 모두 정상 동작 됨)
//App 컴포저블에 코드 추가
LifecycleStartEffect(Unit) {
//이 함수는 viewModel init에 있는 코드를 함수화 시킨 것
viewModel.getPermissionRequest()
onStopOrDispose {}
}