레이블이 navigation인 게시물을 표시합니다. 모든 게시물 표시
레이블이 navigation인 게시물을 표시합니다. 모든 게시물 표시

Jetpack Navigation + BottomNavigationBar 구현하기

1. Jetpack Navigation


사실 Jetpack Navigation에 대해서는 이전에도 한번 포스트를 쓴 적이 있다.

Navigation은 크게 3가지 요소로 이루어져 있다.


NavController

- 대상(즉, 앱의 화면) 간 이동을 담당하는데, 탐색 동작을 처리하는 역할

- 대상을 탐색하고 딥 링크를 처리하며 대상 백 스택을 관리하는 등의 여러 메소드를 제공


NavGraph

- 이동할 컴포저블 대상의 매핑을 담당

- 앱 내의 화면 연결을 보여주는 일종의 지도 역할을 한다.


NavHost

- 현재 탐색 대상(navigation destination)이 포함된 UI 요소

- NavGraph의 현재 대상을 표시하는 컨테이너 역할을 하는 컴포저블


2. BottomNavigationBar 구현하기

이제 위에서 설명한 Navigation과 Compose를 활용해 BottomNavigationBar를 구현해보자.

gradle에 라이브러리 추가

dependencies {
    implementation("androidx.navigation:navigation-compose:2.7.7")
}


Bottom Navigation으로 이동할 화면 생성

필자는 여기서 Home, Rating, Profile 3개의 화면을 만들어주었다.


@Composable
fun HomeScreen() {
    Text(text = "Home")
}

@Composable
fun RatingScreen() {
    Text(text = "Rating")
}

@Composable
fun ProfileScreen() {
    Text(text = "Profile")
}


BottomNavItem 생성.

Bottom Navigation으로 이동할 item 객체 클래스 BottomNavItem 클래스를 생성하고,

그 자식으로 Home, Rating, Profile 을 추가한다.

sealed class BottomNavItem(
    @StringRes val title: Int,
    @DrawableRes val icon: Int,
    val screenRoute: String
) {
    object Home : BottomNavItem(R.string.home, R.drawable.ic_home, NavScreens.Home.name)
    object Rating : BottomNavItem(R.string.rating, R.drawable.ic_rating, NavScreens.Rating.name)
    object Profile : BottomNavItem(R.string.profile, R.drawable.ic_profile, NavScreens.Profile.name)
}


NavHost, NavGraph 생성.

NavHost 함수의 NavGraphBuilder 를 통해서 NavGraph를 생성할 수 있다.

@Composable
private fun MyNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController,
    startDestination: String
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        ...

        composable(route = NavScreens.Home.name) {
            HomeScreen(
                onSearchClicked = { navController.navigate(NavScreens.Search.name) }
            )
        }

        composable(route = NavScreens.Rating.name) {
            RatingScreen()
        }

        composable(route = NavScreens.Profile.name) {
            ProfileScreen()
        }

        composable(route = NavScreens.Search.name) {
            SearchScreen()
        }
    }
}


Bottom Navigation 생성
@Composable
private fun MyBottomNavigation(
    modifier: Modifier = Modifier,
    navController: NavHostController
) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    val items = listOf(
        BottomNavItem.Home,
        BottomNavItem.Rating,
        BottomNavItem.Profile
    )

    NavigationBar {
        items.forEach { item ->
            NavigationBarItem(
                selected = currentRoute == item.screenRoute,
                label = {
                    Text(
                        text = stringResource(id = item.title),
                        style = TextStyle(
                            fontSize = 12.sp
                        )
                    )
                },
                icon = {
                    Icon(
                        painter = painterResource(id = item.icon),
                        contentDescription = stringResource(id = item.title)
                    )
                },
                onClick = {
                    navController.navigate(item.screenRoute) {
                        navController.graph.startDestinationRoute?.let {
                            popUpTo(it) { saveState = true }
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                },
            )
        }
    }

}


BottomNavigationBar를 그리는 앱 화면 구성

@Composable
fun MyApp() {
    val navController = rememberNavController()

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            ...
        },
        bottomBar = {
            MyBottomNavigation(navController = navController)
        }
    ) {
        Box(modifier = Modifier.padding(it)) {
            MyNavHost(
                navController = navController,
                startDestination = NavScreens.Home.name
            )
        }
    }
}



Jetpack Compose Navigation 정리

Navigation 구성요소

Navigation은 크게 3가지 구성요소로 이뤄진다.


NavController: 대상(즉, 앱의 화면) 간 이동을 담당한다. 

NavGraph: 이동할 컴포저블 대상을 매핑을 담당

NavHost: NavGraph의 현재 대상을 표시하는 컨테이너 역할을 하는 컴포저블


NavController

NavController는 Navigation 구성요소의 중심 API로, 스테이트풀(Stateful)이며 앱의 화면과 각 화면 상태를 구성하는 컴포저블의 백 스택을 추적한다. 컴포즈 환경에서 NavController는 rememberNavController()를 이용하여 가져올 수 있다.

val navController = rememberNavController()

rememberNavController()를 호출하여 NavContoller 인스턴스를 생성할 때 유의해야 할 점은 상태 호이스팅(State Hoisting)에 유의해야 한다. 컴포저블 계층 구조에서 NavController를 만드는 위치는 이를 참조해야 하는 모든 컴포저블이 액세스할 수 있는 곳이어야 한다. 이러한 구조가 상태 호이스팅의 원칙을 준수하는 것이다.

왜냐하면 Navigation과 관련된 상태 정보를 스크린과 분리하며, NavController가 생성된 곳에 있는 상태 정보를 여러 스크린이 공유할 수 있기 때문이다.


 

NavGraph

NavGraph는 ID로 가져올 수 있는 NavDestination 노드의 집합이다. NavGraph는 '가상' 대상 역할을 합니다. NavGraph 자체는 백 스택에 나타나지 않지만 NavGraph로 이동하면 시작 대상이 백 스택에 추가된다. 새로운 NavGraph는 대상을 추가하고 시작 경로을 설정할 때까지 유효하지 않다. 대상을 추가하는 방법은 NavGraphBuilder.composable() 함수를 이용한다. 자세한 내용은 아래에서 설명한다.


NavHost

NavController는 하나의 NavHost와 연관되어 있어야 한다. NavHost는 NavController와 Navigation Graph를 연결하는 역할을 한다. 

지정된 NavGraphBuilder 내의 모든 컴포저블을 제공된 navController에서 탐색할 수 있다. 즉 각 NavController를 단일 NavHost 컴포저블과 연결해야 한다. NavHost는 내부적으로 구성 가능한 대상을 지정하는 NavGraph와 NavController를 연결한다. 이 연결을 통해 NavController는 이동한 컴포저블 대상을 파악하고 이동할 수 있게 된다.

화면 전환을 하면서 NavHost 안의 내용은 자동적으로 recompose된다.


public fun NavHost( navController: NavHostController, startDestination: String, modifier: Modifier = Modifier, route: String? = null, builder: NavGraphBuilder.() -> Unit ) NavHost의 원형을 살펴보면, 위와 같다

navController: NavHostController 클래스의 인스턴스이다. navigate() 메서드를 호출하여 다른 대상으로 이동하는 등의 방식으로 화면 간에 이동하는 데 이 객체를 사용할 수 있다.

startDestination: 앱에서 NavHost를 처음 표시할 때 기본적으로 표시되는 대상을 정의하는 문자열 경로이다.

builder: NavGraphBuilder로 그래프를 생성하기 위해 사용되는 빌더이다. 여기에 NavGraphBuilder의 composable() 함수를 이용하여 경로 대상을 정의 할 수 있다.


 

Compose Navigation 적용하기

의존성 추가

 (https://developer.android.com/jetpack/androidx/releases/navigation#declaring_dependencies)

dependencies {
    def nav_version = "2.5.3"
    implementation "androidx.navigation:navigation-compose:$nav_version"
}


경로 추가

앱에서 경로를 추가해야 한다. 경로는 탐색의 기본 개념 중 하나이다. 경로는 Destination에 상응하는 문자열이다. 보통 enum 클래스를 사용하여 경로를 정의하는 편이다.


enum class TestScreen() {
    First,
    Second,
    Third
}


NavController 추가

NavController는 위에서 설명했듯이 상태 호이스팅에 유의하여 rememberNavController() 를 이용하여 선언할 수 있다.


@Composable
fun TestApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()
    ...
}

  

NavHost 추가 및 경로 정의

NavHost를 추가하고 내부에 NavGraphBuilder.composable() 함수를 이용하여 경로를 정의한다. composable은 2개의 매개변수가 필요하다.


route: 경로 이름에 해당하는 문자열입니다. 모든 고유 문자열을 사용할 수 있습니다. CupcakeScreen enum의 상수 이름 속성을 사용합니다.

content: 여기에서 특정 경로에 표시할 컴포저블을 호출할 수 있습니다.


route는 위에서 정의한 경로 (enum class를 사용했었다.)를 이용하여 route 매개변수를 전달하고, content는 컴포저블 함수로 Screen 등 표시할 컨텐츠가 포함된다.

@Composable
fun TestApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()

    ...


    NavHost(
        navController = navController,
        startDestination = TestScreen.First.name,
        modifier = modifier.padding(innerPadding)

    ) {

        composable(route = TestScreen.First.name) {
            FirstScreen(
                ...
            )
        }

        composable(route = TestScreen.Second.name) {
            SecondScreen(
                ...
            )
        }

        composable(route = TestScreen.Third.name) {
            ThirdScreen(
                ...
            )
        }
    }
}
 

NavHost 의 매개변수를 살펴보면, 위에서 생성한 navController를 전달한다. 그리고 처음 시작점인 startDestination으로 TestScreen.First.name을 이용해 첫 경로를 설정하여 준다.

 

경로 간 이동하기 (navigate())

경로 간 이동을 위해 navController의 navigate() 함수를 이용한다. 아래 코드를 보면, FirstScreen의 버튼 클릭 이벤트에서 SecondScreen으로 이동하는 코드이다.

navigate(route)를 통해 destination을 인자로 주어 원하는 화면으로 전환이 가능하다. 이 때, 화면 전환과 함께 back stack에 전환 전의 화면이 추가된다. 다음과 같이 사용한다.


@Composable
fun TestApp(modifier: Modifier = Modifier){

    val navController = rememberNavController()
    ...

    NavHost(
        navController = navController,
        startDestination = TestScreen.First.name,
        modifier = modifier.padding(innerPadding)

    ) {

        composable(route = TestScreen.First.name) {

            FirstScreen(

                ...,

                onNextButtonClicked = {
                    navController.navigate(TestScreen.Second.name)

                }
            )

        }

        composable(route = TestScreen.Second.name) {
            SecondScreen(

                ...

            )
        }
    }

}
  

navController.navigate() 메서드의 옵션들
다양한 옵션들을 이용하여 back stack을 관리할 수 있다. 

- popUpTo(route) 
navigate에 전달한 인자에 해당하는 화면으로 전환하기 전에, route에 해당하는 화면이 나오기 전까지의 back stack을 뺀다. inclusive를 true로 하면 route에 해당하는 화면까지 back stack에서 뺀다. 

- launchSingleTop = true 
route에 해당하는 화면이 아닌 경우에만 route로 화면이 전환된다. back stack에 동일한 route가 중복으로 있는 것을 방지한다.

navController.navigate(item.route) {
    // 항목을 선택할 때 백스택에 대상스택이 많이 생성되는 것을 방지하기 위해 그래프의 시작 대상까지 팝업
    navController.graph.startDestinationRoute?.let { route ->
        popUpTo(route) {
            saveState = true
        }
    }
    
    // 백 스택에 동일한 route가 중복으로 있는 것을 방지
    launchSingleTop = true
    
    // 이전에 선택한 항목을 다시 선택할 때 상태 복원
    restoreState = true
}



navController.popBackStack()
- 이전화면으로 돌아갈때

navController.popBackStack(route, inclusive)
- route: 돌아가고 싶은 화면의 route
- inclusive: Boolean값. true이면 특정한 route를 삭제, false이면 모든 destination을 pop하고 start destination으로 이동

navController.popBackStack(
    TestScreen.First.name,
    false
)


참고

https://developer.android.com/jetpack/compose/navigation

Jetpack Compose 키보드작업처리

사용자가 수정 가능한 텍스트 구성요소(예:  TextField )에 포커스를 맞추면 기기에 하드웨어 키보드가 연결되어 있는 경우 모든 입력은 시스템에 의해 처리됩니다. 주요 이벤트를 처리하여 단축키를 제공할 수 있습니다. 주요 이벤트 Compose에서...