[Android][KMP/CMP] moko 라이브러리를 이용한 권한 요청 설정하기

윤찬·2025년 9월 30일

Android

목록 보기
30/38

이번 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 권한 사용해보기

먼저 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나 요청/앱 설정화면 이동이 쉽게 가능하다.


Android/IOS 권한 설정

1. Android

안드로이드는 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>

2. IOS

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>

실행 결과

권한 수락을 누르면 바로 끝나니 둘 다 권한 거부 요청 후 설정화면 이동 후 권한 허용을 바꾼 시나리오로 동작 진행.

1. Android

2. IOS

안드로이드는 설정화면에서 허용으로 변경 후 돌아가도 권한 허용 Text가 뜨지 않는다.
이를 해결하기 위해 App 컴포저블에 아래와 같이 다시 Start가 될 때 권한 정보를 받는 코드를 추가하니 변경이 되었다. (Android/IOS 모두 정상 동작 됨)


//App 컴포저블에 코드 추가
       LifecycleStartEffect(Unit) {
       		//이 함수는 viewModel init에 있는 코드를 함수화 시킨 것
            viewModel.getPermissionRequest()
            onStopOrDispose {}
        }
profile
좋은 개발자가 되기까지

0개의 댓글