Android/실전 회고

[Android/Kotlin] Jetpack Compose, Navigation component, BottomNavigation 사용하여 화면이동 세팅

노소래 2022. 12. 9. 09:25


사용하게된 계기

첫 회사 입사 후 일을 배우는 것에만 집중하다가, 더 많은 성창을 위해 사이드 프로젝트를 시작했다.

내 사이드 프로젝트의 목표는 기술 경험이 중점이었다.

그래서 회사에서 아직 활발하게 쓰지않는 새로운 기술스택을 사용하기로 했고,

UI 툴킷으로 Jetpack Compose 는 내 사이드 프로젝트에서 필수 기술이 되었다.

초반에 컴포즈 공부하고 프로젝트 초반 세팅을 하던 와중에 네비게이션 가능인가? 였는데 역시나였다.

그렇게 Navigation - compose 를 사용하게 되었다.


단순 사용법

단순 사용법은 링크를 확인하는 게 좋을 것 같다.

아래 부터는 바텀 네이게이션을 통합하면서 했던 고민과, Destination 관리, 경로 관련 주의 점을 말하려고 한다.

공식문서 - Compose를 통해 이동

코드랩 - Jetpack Compose 탐색


고민

1. Destination 데이터 관리

sealed class 를 상속하는 Destination, enum 으로정리하는 Destination, interface 를 상속하는 Destination, Destination 마다마다 file 하나 등등

나름 다양한 레퍼런스를 참고했는데

내가 하려는 사이드 프로젝트 규모에서는 피쳐별로 모듈을 가져가지 않을 거라는 점과 취향을 타서 interface를 상속하는 Destinatoin을 만들어서 정보를 깔끔하게저장하는 방식을 정했다.

이 방식은 위 코드랩 Rally 프로젝트를 참고했다. 

interface RallyDestination {
    val icon: ImageVector
    val route: String
}

object Overview : RallyDestination {
    override val icon = Icons.Filled.PieChart
    override val route = "overview"
}

object Accounts : RallyDestination {
    override val icon = Icons.Filled.AttachMoney
    override val route = "accounts"
}

// ... 출처 : Rally
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = modifier
    ) {
        composable(route = Overview.route) {
            OverviewScreen()
        }
        composable(route = Accounts.route) {
            AccountsScreen()
        }
		// ... 출처 : Rally
	}

2. BottomNavigation 통합 - NavHost 를 두 개 만들기

Navigation 을 세팅할 때 BottomNavigation 내의 이동과, BottomNavigation 밖으로의 이동을 어떻게 처리할지 고민했다.

아래 사진과 같은 바텀네비게이션이 있다고 해보자. (출처: Material 사이트 이미지)

 바텀네비게이션을 담은 Screen 이 있고 그 밖의 Screen이 있을 것이다. 

바텀네비게이션을 담은 Screen 을 그 밖의 Screen 과 동일

 

고민 끝에 NavHost 를 두 개 만들기를 선택했다.

즉, BottomNavigation 을 담은 Screen 에도 또 하나의 NavHost 를 만드는 것이다.

이유는 다음과 같다. 

 

2-1. 이유 (장점)

1. 바텀네비게이션을 담은 Screen Destination 에 대해서  NavGraphBuilder.composable 이 아닌 NavGraphBuilder.navigation 을 사용하려하니, if 문으로 BottomNavigation 의 가시성(visivbility)을 조절해줘야한다.

기존 View 시스템에서 그렇게 해본 적이 있는데, 바텀네비 밖 화면으로 이동 시 덜컹거리는 게 사용자경험에 좋지 못한 영향을 미친다고 생각했기 때문이다. 

 

2. 1번 이유와 함께 바텀네비게이션을 담은 Screen 과 그 밖의 Screen 들 간의 네비게이션 애니메이션을 고려했다. 바텀네비게이션을 담은 화면 통째로 네비게이션 애니메이션이 구현되길 바랐기 때문이다.

 

두 개의 NavHost 를 만드는 것은 위 두 가지를 해결해준다.

물론, 단점도 생각나고 더 고민해볼 점도 생각난다.

2-2. 단점

당장 생각나는 단점은 두 가지이다.

1. 바텀 탭은 많아봐야 5~6개의 Screen 이동이 될텐데 그것때문에 굳이 NavHost 를 추가해야하는가? 하는 의문에서 오는 스트레스

2. navController 를 두 개 관리해야한다는 단점.  Screen 스택을 추적하는 케이스를 생각해보자. 복잡성이 증가하여 벌써 머리가 아프다..

 

2-3. 더 고민해볼 점

앱 규모가 커져 피쳐별로 나누는 멀티모듈 환경에선 어떻게 처리할지이다. 당장 여기서 다루긴 어려워서 우선 패스이다.

 

2-4. nowinandroid, codelab(Rally) 에선 어떻게 처리?

BottomNavigation 통합에 대해 쓰고 싶었는데 내가 원하는 케이스가 있지 않았다.

여기서 내가 원하는 케이스를 다시 말해보자면 바텀네비게이션을 담은 화면에서 그 밖의 화면으로 이동이다.

nowinandroid, Rally 모두 탭이 계속 유지된다.. 즉 내가 원하는 케이스를 발견하지 못했다.

 

2-5. 코드로 이해

MainScreen 이 바텀네비게이션을 담은 Screen 이다. 

그리고 MainScreen 도 NavHost를 가지고 있다.

MainScreen 의 파라미터 onNavOutEvent 는 route 를 인자로 받는 콜백함수이다. 바텀네비게이션을 담은 Screen 밖으로 나갈 때 사용된다.

아래 코드는 사이드 프로젝트에서 네비게이션 초반세팅할 때의 네비게이션 관련 코드 일부이다.

@Composable
fun MuntamNavHost(
    navController: NavHostController,
    startDestination: MutamDestination = Main,
    modifier: Modifier
) {
    NavHost(
        navController = navController,
        modifier = modifier,
        startDestination = startDestination.route
    ) {
        composable(
            route = Main.route
        ) {
            MainScreen { route ->
                navController.navigate(route) {
                    popUpTo(Main.route) {
                        saveState = true
                    }
                }
            }
        }
        
        composable(
            route = SubjectAdd.route
        ) {
            SubjectAddScreen()
        }
        
        // ... 생략
        
    }
}
@Composable
fun MainScreen(
    onNavOutEvent: (route: String) -> Unit
) {
	// ... 생략

    Scaffold(
        bottomBar = {
            MuntamBottomNavigation(
                currentDestination = currentDestination,
                onNavInEvent = { route ->
                    bottomNavController.navigateSingleTopTo(route)
                }
            )
        }
    ) { innerPadding ->
        MainBottomNavHost(
            navController = bottomNavController,
            modifier = Modifier.padding(innerPadding),
            onNavOutEvent = { route ->
                onNavOutEvent(route)
            }
        )
    }
}
@Composable
fun MainBottomNavHost(
    navController: NavHostController,
    startDestination: MuntamBottomDestination = Subjects,
    modifier: Modifier,
    onNavOutEvent: (route: String) -> Unit
) {
    NavHost(
        navController = navController,
        modifier = modifier,
        startDestination = startDestination.route,
    ) {
        composable(
            route = Subjects.route
        ) {
            SubjectsScreen(
                onNavOutEvent = { route ->
                    onNavOutEvent(route)
                }
            )
        }
        // ... 생략
    }
}


사용하다가 알게된 작은 주의할 점

경로 구분자가 / 이기 때문에 argument 이름으로 / 를 넣으면 안된다!!

넣으려면 커스텀으로 컨버터를 만들어야할 것으로 보인다.

예를 들어 / 를 _, | 등으로 바꿔서 전달했다가 다시 / 로 바꿔주는 것이다.

 

마치며

이미 서비스를 하고 있는 회사에 들어가서 경험할 수 없는 초기 프로젝트 세팅을 진행하는데 꽤 재미를 느끼고 있다.

그 초기 세팅 중 하나가 네비게이션 세팅인데, 눈에 보이는 것이라 그런지 특히 더 재밌는 것 같다.

프로젝트를 완성하고 다음에 네비게이션 관련해서 쓸 글은

뷰모델 관리, 딥링크, 네비게이션 애니메이션, 현재 사이드 프로젝트에서 네비게이션 관련 개선점 등이 될 것 같고

회사에서 피쳐단위 멀티모듈을 경험해보게 된다면 멀티모듈일 때의 navigation 에 대해서 쓸 예정이다.