Rally는 초기에 Navigation을 사용하지 않는 기존 앱이다. 네비게이션으로 마이그레이션하는 몇 가지 단계는 다음과 같다.

  1. 네비게이션 의존성 추가
  2. NavController 및 NavHost 설정하기
  3. 목적지 루트 준비하기
  4. 기존 목적지 메커니즘을 네비게이션 루트로 변경하기

네비게이션 의존성 추가하기

app/build.gradle 찾아서 열자. dependencies 부분에 navigation-compose 의존성을 추가하자.

최신버전은 공식문서를 참조하자

dependencies {
  implementation "androidx.navigation:navigation-compose:2.4.0-beta04"
  ...
}

이제 프로젝트를 sync하면 컴포즈에서 네비게이션을 사용할 준비가 되었다.

NavController 설정하기

컴포즈에서 네비게이션을 사용할 때 NavController는 중심이 되는 컴포넌트다. 백 스택 항목을 추적하고, 스택을 앞으로 이동하고, 백 스택 조작을 활성화하고, 화면 상태(Screen states) 간 네비게이팅 한다. NavController는 네비게이션의 중심이기 때문에, 목적지로 이동하려면 NavController를 먼저 생성해야 한다.

우리는 컴포즈내에서 NavController의 하위 클래스인 NavHostController로 작업하고 있다. rememberNavController() 함수를 사용하여 NavController 얻자. 이는 NavController를 생성하고 컴포지션내에 저장한다. remember Savable을 사용하면 구성 변경(예:화면회전)에서도 NavController가 살아남는다. NavController는 단일 NavHost 컴포저블과 연관되어있다. NavHost는 NavController를 컴포저블 목적지들이 명시되어있는 네비게이션 그래프와 연결한다.

이 코드랩에서는 RallyApp내에서 NavController를 얻고, 저장한다. NavController는 애플리케이션 전체에 대한 최상위(root) 컴포저블이다. RallyActivity.kt에서 이를 찾을 수 있다.

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
        val navController = rememberNavController()
        Scaffold(...
}

목적지를 위한 루트 준비하기(Prepare routes for destinations)

개요(Overview)

Rally 앱은 3가지 화면을 갖고 있다.

  1. 개요(Overview) – 전체 금융 거래 개요 및 알림
  2. 계좌(Accounts) – 기존 계좌에 대한 이해
  3. 청구서(Bills) – 예정된 비용
Screenshot of the overview screen containing information on Alerts, Accounts and Bills.Screenshot of the Accounts Screen, containing information on several accounts.Screenshot of the Bills Screen, containing information on several outgoing bills.

모든 세 화면들은 컴포저블을 사용하여 만들어졌다. RallyScreen.kt를 살펴보자. 이 파일에 세 화면은 이 선언되어있다. 나중에 Overview를 시작 목적지로하여, 이 세화면들을 네비게이션의 목적지로 매핑할 것이다. 또한 RallyScreen으로부터 컴포저블을 NavHost로 이동시킬 것이다. 지금 당장은 RallyScreen을 손대지 않은채로 그냥 두자.

컴포즈내에서 네비게이션을 사용할 때, 경로(루트)는 문자열로 표현된다. 이러한 문자열들을 URL 또는 딥링크라고 생각할 수 있다. 이 코드랩에서는 각 RallyScreen 아이템을 경로로하여 name속성을 사용해보자. 예를들면 RallyScreen.Overview.name 이라고 사용할 수 있다.

준비(Preparation)

RallyActivity.kt에서 RallyApp 컴포저블로 돌아온 뒤, 화면의 컨텐츠를 포함하고 있는 Box를 새로 생성한 NavHost로 교체하자. 이전 단계에서 우리가 생성한 navController를 전달한다. NavHost는 또한 startDestination이 필요하다. RallyScreen.Overview.name으로 설정하자. 또한 Modifier를 생성하여 padding을 적용할 수 있도록 NavHost에 전달한다.

Scaffold(...) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name,
            modifier = Modifier.padding(innerPadding)
        ) { ... }

이제 네비게이션 그래프를 정의 할 수 있다. NavHost가 이동할 수 있는 목적지들은 다른 목적지들을 수용할 준비가 되었다. NavHost의 마지막 매개변수에 제공되는 NavGraphBuilder를 사용하여 이 작업을 수행한다. NavGraphBuilder는 후행 람다식 표현으로 목적지를 선언할 수 있다. Navigation Compose 아티팩트는 NavGraphBuilder.composable 확장 함수를 제공한다. 이 함수를 그래프에서 목적지를 이동을 정의하는 데 사용한다.

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name,
    modifier = Modifier.padding(innerPadding)

) { 
    composable(RallyScreen.Overview.name) { ... }
}

지금 당장은 화면 이름을 컴포저블의 내용으로 Text를 임시 설정한다. 다음 단계에서는 기존 컴포저블을 사용한다.

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name
    modifier = Modifier.padding(innerPadding)
) { 
    composable(RallyScreen.Overview.name) {
      Text(text = RallyScreen.Overview.name)
    }

    // TODO: 다른 두개의 화면을 추가하자
}

이제 Scaffold에서 currentScreen.content 호출을 제거하고 앱을 실행하자. 시작 목적지의 이름과 위의 탭이 표시됩니다.

최종적인 코드는 다음과 같다.

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name,
    modifier = Modifier.padding(innerPadding)
) {
    composable(RallyScreen.Overview.name) {
      Text(RallyScreen.Overview.name)
    }
    composable(RallyScreen.Accounts.name) {
        Text(RallyScreen.Accounts.name)
    }
    composable(RallyScreen.Bills.name) {
        Text(RallyScreen.Bills.name)
    }
}

NavHost는 이제 Scaffold 내의 Box를 대체할 수 있다. innerPadding을 그대로 유지하려면 Modifier를 NavHost에 전달하자.

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        // FIXME: 이 중복된 코드는 나중에 제거 된다.
        var currentScreen by rememberSaveable {
            mutableStateOf(RallyScreen.Overview)
        }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen -> currentScreen = screen },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            NavHost(
                navController = navController,
                startDestination = RallyScreen.Overview.name,
                modifier = Modifier.padding(innerPadding)) {
            }
        }
    }
}

이 시점에서 Top bar는 아직 연결되지 않았으므로, 탭을 클릭해도 표시된 컴포저블이 변경되지 않는다. 다음 단계에서 이를 처리하자.

네비게이션 바 상태 변경을 완전히 통합하기

이 단계에서는 RallyTabRow를 연결하고, 현재 수동 네비게이션 코드를 삭제하게 된다. 이 단계를 완료하면 네비게이션 컴포넌트가 완전히 라우팅(목적지 이동)을 처리한다.

Note: 코드를 테스트 가능하게 만들려면, navController를 전달하지 않는 것이 좋다. 이 코드랩에서는 단일 책임 지점에서 이동할 수 있도록 콜백을 제공한다.

RallyActivity에서 탭을 클릭하면, RallyTabRow 컴포저블에 onTabSelected라는 콜백이 수신됨을 알 수 있다. 선택한 화면으로 이동하기 위해 navController를 사용하도록 코드를 업데이트하자.

다음은 네비게이션을 사용하여, TabRow를 통해 화면으로 이동하는 코드를 보여준다.

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        // FIXME: 이는 중복된 코드로 나중에 제거 한다.
        var currentScreen by rememberSaveable {
            mutableStateOf(RallyScreen.Overview)
        }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen ->
                        navController.navigate(screen.name)
                },
                    currentScreen = currentScreen,
                )
            }

이 변경으로 currentScreen은 더 이상 업데이트되지 않는다. 즉, 선택한 아이템의 확장 및 축소가 되지 않는다. 이 동작을 다시 활성화하려면 currentScreen 속성도 업데이트해야 한다. 다행히도 Navigation은 백 스택을 유지하고, 현재 백 스택 항목을 State로 제공할 수 있다. 이 State를 사용하면 백 스택의 변경 사항에 대응할 수 있다. 경로에 대한 현재 백 스택 항목을 쿼리할 수도 있다.

TabRow 화면 선택을 Navigation으로 마이그레이션하는 작업을 마치려면, 다음과 같이 네비게이션 백스택을 사용하도록 currentScreen을 업데이트하자.

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        val navController = rememberNavController()
        val backstackEntry = navController.currentBackStackEntryAsState()
        val currentScreen = RallyScreen.fromRoute(
            backstackEntry.value?.destination?.route
        )
        ...
    }
}

이 시점에서 앱을 실행하면, 탭을 사용하여 화면 간에 전환할 수 있지만 표시되는 것은 화면 이름뿐이다. 화면이 표시되기 전에 RallyScreen을 네비게이션으로 마이그레이션해야 한다.

RallyScreen을 네비게이션으로 마이그레이션하기

이 단계를 완료하면 컴포저블이 RallyScreen 열거형에서 완전히 분리되고 NavHost로 이동된다. RallyScreen은 화면의 아이콘과 제목을 제공하기 위해서만 존재한다.

RallyScreen.kt를 열고, 각 화면의 body 구현을 RallyApp의 NavHost 내의 해당 컴포저블로 이동한다.

NavHost(
    navController = navController,
    startDestination = Overview.name,
    modifier = Modifier.padding(innerPadding)
) {
    
    composable(Overview.name) {
        OverviewBody()
    }
    composable(Accounts.name) {
        AccountsBody(accounts = UserData.accounts)
    }
    composable(Bills.name) {
        BillsBody(bills = UserData.bills)
    }
}

이 시점에서, 다음 코드를 남길 RallyScreen에서 콘텐츠 함수, body 매개변수 및 그 사용법들을 안전하게 제거할 수 있다.

enum class RallyScreen(
    val icon: ImageVector,
) {
    Overview(
        icon = Icons.Filled.PieChart,
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
    );

    companion object {
        ...
    }
}

앱을 다시 실행하면, 원래나오던 3개의 화면이 표시되고, TabRow를 통해 화면간 이동할 수 있다.

Note: 위의 변경 사항으로 이제 네비게이션 컴포넌트를 통해 뒤로(back)이동이 지원됩니다. 화면 사이를 전환한 다음 back 버튼을 누르면 스택이 팝업되고, 이전 목적지로 이동한다.

OverviewScreen에서 클릭 활성화하기

이 코드랩에서 OverviewBody의 클릭 이벤트는 처음에 무시되었다. 이는 “SEE ALL” 버튼을 클릭할 수 있었지만, 아무데도 가지 않았음을 의미한다.

Screen recording of the overview screen, scrolling to eventual click destinations, and attempting to click. Clicks don't work as they aren't implemented yet.

자 이제 고쳐보자!

OverviewBody는 클릭 이벤트에 대한 콜백으로 여러 함수를 허용할 수 있다. onClickSeeAllAccounts 및 onClickSeeAllBills를 구현하여 관련 목적지로 이동해 보자.

“SEE ALL” 버튼을 클릭할 때 이동을 활성화하려면, navController를 사용하여 Account 또는 Bill 화면으로 이동한다. RallyActivity.kt를 열고 NavHost에서 OverviewBody를 찾아 navigate 호출을 추가한다.

OverviewBody(
    onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
    onClickSeeAllBills = { navController.navigate(Bills.name) },
)

이제 OverviewBody에 대한 클릭 이벤트 동작을 쉽게 변경할 수 있게 되었다. 네비게이션 계층 구조의 최상위 레벨에서 navController를 유지한채로 OverviewBody에 직접 전달하지 않으면, 이러한 작업을 수행할 때 기존 navController에 의존하지 않고도 OverviewBody를 분리하여 미리보기하거나 테스트하기 쉽다.

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.