질문 정리

Compose BackHander, 앱 종료가 안되는 이유

five2week 2024. 6. 5. 19:52

문제 상황

테스트를 하는데, 앱 종료가 불가능한 오류를 발견했습니다. 문제의 원인은 다음 코드에서 확인할 수 있습니다. 과거에 작성한 BackHandler의 enabled 조건이 문제였습니다.

navController = rememberNavController()
BackHandler(
    enabled = navController.currentBackStackEntryAsState().value?.destination?.id != navController.graph.startDestinationId,
) {
    navController.popBackStack()
}

 

위 코드에 따르면 BackHandler 컴포넌트는 NavController의 현재 백스택 항목이 시작 목적지가 아닐 때만 활성화됩니다. 따라서 시작 화면에서 뒤로 가기 버튼을 누르면 아무런 반응이 없습니다. 왜냐하면 시작 화면에서는 현재 백스택 항목의 목적지 ID가 시작 목적지 ID와 같기 때문입니다.

 

이는 사용자가 앱의 시작 화면에서 뒤로 가기 버튼을 눌렀을 때, 앱이 종료되어야 한다는 기대와 맞지 않습니다. 따라서 이 부분은 수정이 필요합니다. 이와 함께 BackHandler에 대해서도 자세히 알아보겠습니다.

BackHandler

과거의 XML 기반 UI 설계에서는 주로 Activity의 onBackPressed() 메서드를 오버라이드하여 뒤로 가기 버튼의 동작을 제어했습니다. 사용자가 뒤로 가기 버튼을 누를 때마다 이 메서드가 호출되고, 개발자는 이 메서드 내에서 원하는 동작을 정의할 수 있었습니다.

 

Jetpack Compose에서는 BackHandler API를 사용하여 이와 유사한 기능을 구현합니다. BackHandler는 컴포저블 함수 안에서 사용할 수 있으며, 특정 조건에서 사용자의 뒤로 가기 동작을 가로채 필요한 작업을 수행할 수 있습니다.

수정된 구현 방법

BackHandler 내에서 navController.popBackStack()을 한 번만 호출하고, 만약 백스택이 비어 있다면 (false를 반환하는 경우) 앱을 종료하는 finish() 메소드를 호출합니다.

navController = rememberNavController()

BackHandler(enabled = true) {
    // 현재 백스택에서 뒤로 갈 수 있는지 시도하고, 더 이상 뒤로 갈 수 없으면 앱을 종료
    if (!navController.popBackStack()) {
        finish()  // 백스택이 비어 있을 경우, 액티비티를 종료하여 앱을 종료한다.
    }
}

 

이 코드는 BackHandler를 사용하여 뒤로가기 버튼이 항상 활성화되도록 설정합니다. navController.popBackStack() 메소드는 현재 백스택에서 이전 항목으로 이동하려고 시도하고, 백스택이 비어 있으면 false를 반환합니다. 백스택에 더 이상 항목이 없으면, 즉 더 이상 뒤로 갈 곳이 없으면 finish()를 호출하여 액티비티를 종료하고 결과적으로 앱을 종료합니다.

BackHandler 여러 개 선언하기

예를 들어, 어떤 화면에서 두 개의 다른 조건에 따라 뒤로 가기 버튼을 다르게 처리해야 할 경우, 각 조건에 맞는 BackHandler를 배치할 수 있습니다.

@Composable
fun MyScreen() {
    val showDialog = remember { mutableStateOf(false) }

    // 조건 A에 대한 BackHandler
    BackHandler(enabled = showDialog.value) {
        // 대화 상자 표시 상태를 변경
        showDialog.value = false
    }

    // 기본 BackHandler
    BackHandler {
        // 기본 동작으로 화면을 종료
        finish()
    }

    if (showDialog.value) {
        AlertDialog(
            onDismissRequest = { showDialog.value = false },
            title = { Text("Important") },
            text = { Text("Do you really want to exit?") },
            confirmButton = {
                Button(onClick = { finish() }) {
                    Text("Yes")
                }
            },
            dismissButton = {
                Button(onClick = { showDialog.value = false }) {
                    Text("No")
                }
            }
        )
    } else {
        MainContent()
    }
}

showDialog 상태가 참일 때, 첫 번째 BackHandler가 활성화되어 사용자가 뒤로 가기 버튼을 누르면 대화 상자를 닫습니다. 만약 showDialog가 거짓이면, 두 번째 BackHandler가 기본적으로 활성화되어 앱을 종료합니다.

 

BackHandler가 여러 개 있을 경우, 모든 BackHandler가 순차적으로 호출되는 것이 아니라, 가장 가까운 하나, 즉 UI 트리에서 가장 깊은 위치에 있는 BackHandler가 먼저 반응합니다. 이 BackHandler가 뒤로 가기 이벤트를 처리하면, 이벤트는 여기서 소비되고 더 이상 상위로 전파되지 않습니다.

 

또한 조건부 BackHandler와 기본 BackHandler가 함께 있다면, 조건부 BackHandler가 먼저 불려집니다. 이는 조건부 BackHandler가 UI 트리 내에서 더 구체적인 위치에 배치되거나 특정 조건에 의해 활성화되기 때문입니다. 이러한 조건이 충족되면, 해당 BackHandler는 뒤로 가기 이벤트를 가로채고 처리합니다. 처리가 완료되면 이벤트는 소비되어 기본 BackHandler까지 도달하지 않습니다.

 

참고한 사이트

 

Android BackHander

 

androidx.activity.compose  |  Android Developers

androidx.compose.desktop.ui.tooling.preview

developer.android.com

 

시스템 뒤로 버튼 처리

 

Compose 및 기타 라이브러리  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose 및 기타 라이브러리 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose에서는 자주 이용하는

developer.android.com