[Android] Navigation 중복 이동 방지

김방토·2025년 4월 15일
0

Android

목록 보기
7/10

코틀린 확장함수 최고야!🥹

문제상황

앱 테스트 중, 어느 화면에 진입 시 자꾸 같은 화면을 두 번 호출하는 것을 알아차렸다.
backPress 해보니 애니매이션과 함께 같은 화면이 또 나온다. 스택에 같은 화면이 또 쌓였다는 소리겠지.
뒤로가기를 눌렀는데 같은 화면이 또 나온다는건, 사실 UX 측면에서 말이 안되는 소리기 때문에 해결해야겠다는 생각이 들었다.

해결방법 모색하기

가장 원초적인 해결방법, launchSingleTop

navController.navigate(route) {
	launchSingleTop = true
}

스택 위에 같은 화면이 또 쌓이면 쌓지 않고 재사용 하겠다는 뜻이다.

그런데 좀 찝찝한게

같은 화면을 또 쌓지 않고 재사용 하겠다는 소리는, 이동을 하기는 하겠다는 소리가 된다.
그래서 같은 화면으로 이동을 시도하면(나의 경우는 바텀 네비게이션의 현재 화면에 해당하는 탭을 눌러서 시도) 화면 전환 애니메이션도 그렇고, Data Fetching도 계속 시도된다.
그냥 네비게이션 스택에 또 안쌓겠다는 소리일 뿐이다...

그럼 같은 화면일때는

아예 이동 자체를 시도하지 않는것도 괜찮겠다는 생각이 들었다.
(새로고침은 같은 페이지를 두번 이상 누르는게 아니고, 새로고침이라는 명확한 액션을 취해야만 되도록 하는게 더 좋을 것 같았기 때문이다.)

같은 화면일땐 이동하지 않도록 하자!

HOW?

일단 현재 화면과, 이동할 화면에 대한 정보를 가져와 둘을 비교해야 한다.
이동할 화면에 대한 정보는 navigate 할 때 받을거니까, 현재 화면에 대한 정보를 가져와 둘을 비교해보려 한다.

navigate할때 이동할 화면의 route name이 아니고, Serializable 객체를 지정해 놓았기 때문에, string으로 출력하면 이렇게 나오게 된다.

@param route: T
val destination = route.toString()
Log.d("navigate - destination", "destination : $destination")
// destination : ScheduleMain

그리고 현재 화면에 대한 정보를 출력하면 이렇게 나온다.

val currentRoute = navController.currentDestination?.route
Log.d("navigate - currentRoute", "currentRoute : $currentRoute")
// currentRoute : com.kitching.app.navgraph.Route.ScheduleGraph.ScheduleMain

현재 화면의 route를 String으로 가져오기 때문에, 목적지도 String으로 맞춘 후 입맛대로 비교하면 되겠다.

이렇게 쉽게 끝날리가

사실은 이렇게 나왔다.
(목적지가 그래프라면 이렇게 나올 것이다.)

destination : ScheduleGraph
currentRoute : com.kitching.app.navgraph.Route.ScheduleGraph.ScheduleMain

나는 Bottom bar에서 목적지를 각각의 탭에 맞는 "네비게이션 그래프"로 해 두었다.
그러면 목적지는 그래프인데, 실제 화면은 startDestination이 되니 비교가 의미가 없어져버린다.

@Serializable
sealed interface BottomTab {
	fun getStartDestination(): Route
}

그래서 BottomTab이라는 interface로 구현한 내 그래프들에게 함수도 구현하라고 했다. 이 참에 startDestination도 좀 더 명확하게 하고 좋지 뭐.

navigation<Route.ScheduleGraph>(
	startDestination = Route.ScheduleGraph.getStartDestination() // ✨
	) {
        composable<Route.ScheduleGraph.ScheduleMain> {
            ScheduleMainScreen()
        }
    }

표시한 부분도 저렇게 바꿔주었다. 이제 startDestination 관리도 좀 더 편안해 질 것이다!

모든 화면 이동시에 비교하자

모든 화면 이동 시에 비교해서 쓸모없는 navigate를 막고, 나아가 쓸모없는 Data Fetching도 막으면 좋을거라는 생각이 들었다.
무엇보다 화면 전환 애니메이션이 나오면서 같은 화면이 또 나오는 상황이 반복된다고 생각하면 너무 끔찍했다. 사용자가 뭐라고 생각하겠냐고! (앱이 버벅댄다고 생각하겠지! 그러면 속으로 개발자 욕을 하겠지!)

그래서 navigate 대신 쓸 확장 함수를 만들어 모든 navigate 시에 이 함수를 쓰도록 하면 좋을 것 같았다.

/**
 * 이동할 경로가 현재 경로와 같으면 이동하지 않고,
 * 스택의 마지막이 경로와 같으면 재사용하는 navigate 함수
 *
 * 만약 이동할 경로가 그래프라면,
 * 해당 그래프의 startDestination과 비교하여 중복 이동을 방지
 *
 * @param route 이동할 경로
 * @param builder 추가적인 Navigation 옵션 설정
 */
fun <T : Any> NavController.navIfNew(
    route: T,
    builder: (NavOptionsBuilder.() -> Unit)? = null
) {
    val currentRoute = this.currentDestination?.route?.substringAfterLast(".")
    val routeString = if(route is Route.BottomTab) {
        route.getStartDestination().toString()
    } else route.toString()
    Log.d("navigate - currentRoute", "currentRoute : $currentRoute")
    Log.d("navigate - destination", "destination : $routeString")
    if(currentRoute != routeString) {
        this.navigate(route = route) {
            launchSingleTop = true
            builder?.invoke(this)
        }
    }
}

NavController의 navigate 대신, 새로운 화면인지 판단한 후 navigate 할 함수를 만들었다. 세상에서 함수 이름 짓는게 제일 힘들어...
이름이 직관적이려면 팔만대장경이어야 하고, 이름이 깔끔하면 좀 모호해지는 경향이 있는데(그 둘을 다 잡으면 작명소 차려도 될 것 같다) 그냥 후자를 택하고 설명을 좀 자세히 쓰기로 했다.

특히 이런 확장함수 만들때는 나 혼자 쓰는게 아니니까 더 고심해서 설명을 쓰곤 한다. 읽었을때 딱 알아야 하는것도 중요하니까! 아니면 만든 의미가 없으니까!

끝!

바텀바에 레시피 아이콘을 난타해도 화면이동, 전환, Fetching이 되지 않는다!😎

profile
🍅 준비된 안드로이드 개발자 📱

0개의 댓글