2024. 2. 7. 16:01ㆍIT/Android
<미리 보기>
<소스 코드>
<정리>
Navigation 구성 요소.
1) NavController - 대상(화면) 간 이동을 담당한다.
2) NavGraph - 화면 간 이동의 경로 설정을 담당한다.
3) NavHost - NavController, NavGraph를 근거하여 화면 간의 전환, 데이터 전달, 백 스택 처리들을 편리하게 처리할 수 있는 탐색 구조가 명확한 컴포저블.
경로 설정 - 각각의 화면을 나타내는 컴포지션들에 URL처럼 고유한 문자열 경로를 지정해줄 수 있는데, enum 클래스를 사용하면 가독성이 좋아지고 오타로 인한 버그를 방지할 수 있다.
enum class CupcakeScreen(@StringRes val title: Int) {
Start(title = R.string.app_name),
Flavor(title = R.string.choose_flavor),
Pickup(title = R.string.choose_pickup_date),
Summary(title = R.string.order_summary)
}
NavHost 컴포저블의 대략적인 구조.
NavHost(
navController = /* 화면 이동에 사용할 탐색 컨트롤러 */
startDestination = /* 처음 화면에 보여질 경로 */
modifier = Modifier
) {
//각각의 화면은 composable()로 정의한다. 따라서 화면이 4개면 총 composable() 함수 4개가 존재.
composable(route = /* 해당 화면에 대한 경로 */) {
/* 하나의 화면이 될 구성 가능한 함수 */
}
}
rememberNavController() - 탐색 컨트롤러 NavController의 서브 클래스 NavHostController 인스턴스를 생성한다.
NavController.navigate() - 파라미터로 전달된 경로(route)로 화면이 이동된다.
백 스택(Back Stack) - 사용자가 앱과 상호작용을 하면서 화면을 이동하면 해당 경로들은 처음 화면의 경로 위에 켜켜히 쌓이게 되는데 이렇게 쌓인 경로들을 백 스택이라 부른다. 참고로 뒤로 가기 버튼을 누르면 이전 화면으로의 이동과 동시에 백 스택 가장 상단에 위치한 현재 화면의 경로는 삭제된다.
NavController.popBackStack() - 첫 번째 파라미터(route)는 돌아갈 경로, 두 번째 파라미터(inclusive)는 불리언 값을 받는다.
1) 두 번째 파라미터(inclusive)가 true인 경우 - 첫 번째 파라미터로 받은 경로를 포함하여 백 스택을 비운다.
2) 두 번째 파라미터(inclusive)가 false인 경우 - 첫 번째 파라미터로 받은 경로를 제외하고 백 스택을 비운다.
*만약 첫 번째 파라미터로 첫 화면의 경로를 전달하고, 두 번째 파라미터로 true를 전달하면 백 스택에 어떤 경로도 남아 있지 않아 오류가 발생할 수 있으니 주의해야 한다. 예를 들어 백스택 상태가 **[A, B, C]**인 상황에서 popBackStack(A, true)를 실행하면 백스택에 아무것도 남지 않게 되어 빈화면을 출력한다.
NavController.currentBackStackEntryAsState() - 백 스택 가장 상단에 위치한 현재 화면의 경로를 추적하는 메서드.
NavController.previousBackStackEntry - 현재 화면을 기준으로 직전 백 스택 항목만을 반환하는 프로퍼티.
NavController.navigateUp() - 이전 화면으로 이동하는 메서드로, 예를 들어 백스택 상태가 **[A, B, C]**인 상황에서 navigateUp()을 실행하면 백스택 상태는 **[A, B]**가 되고 화면 B를 출력하게 된다. 만약 백스택에 상위 경로 없이 단 하나의 경로만 존재한다면 동작하지 않는 특징이 있다.
인텐트(Intent) - 액티비티, 브로드캐스트 리시버, 서비스 같은 앱 구성 요소 간 통신을 용이하게 하고 다양한 앱 컴포넌트 간의 상호작용을 가능하게 하는 메시지 객체.
private fun shareOrder(context: Context, subject: String, summary: String) {
val intent = Intent(Intent.ACTION_SEND).apply { //데이터를 담아 공유할 수 있는 인텐트 생성.
type = "text/plain" //인텐트와 함께 전달되는 데이터 유형.
putExtra(Intent.EXTRA_SUBJECT, subject) //제목이 되는 데이터를 키와 함께 인텐트에 추가.
putExtra(Intent.EXTRA_TEXT, summary) //본문이 되는 데이터를 키와 함께 인텐트에 추가.
}
context.startActivity( //해당 컨텍스트에서 새로운 액티비티를 실행.
Intent.createChooser( //해당 액티비티에서 실행될 인텐트 설정.
intent,
context.getString(R.string.new_cupcake_order)
)
)
}
plurals 리소스 - 특정 양에 대해 다양한 문자열 표현을 제공할 수 있는 방법으로 해당 범위에 적합한 문자열이 반환된다.
<resources>
<plurals name="cupcakes">
<item quantity="one">%d cupcake</item> //1
<item quantity="few">%d cupcakes</item> //2~4
<item quantity="many">%d cupcakes</item> //5~10
<item quantity="other">%d cupcakes</item> //그 외.
</plurals>
</resources>
val numberOfCupcakes = resources.getQuantityString(
R.plurals.cupcakes,
orderUiState.quantity,
orderUiState.quantity
orderUiState.quantity
orderUiState.quantity
)
계측 테스트(Instrumentation Test)
1) 탐색 테스트 - 앱과 상호 작용을 통한 탐색 경로가 일치하는지 확인.
*@Before - 해당 주석을 설정한 메서드는 테스트 메서드(@Test) 보다 먼저 실행되므로 초기화 같은 반복적인 코드를 적어두는 것이 좋다. 각각의 테스트 메서드(@Test)는 독립적으로 실행되기 때문에, 항상 @Before 주석이 붙은 메서드가 실행된 상태에서 시작된다.
@Before
fun setupCupcakeNavHost() {
composeTestRule.setContent { //테스트 하기 위한 초기화 작업, 컴포지션 설정.
navController = TestNavHostController(LocalContext.current).apply {
navigatorProvider.addNavigator(ComposeNavigator())
} //TestNavHostController에는 기본 네비게이터를 추가해야 사용할 수 있다.
CupcakeApp(navController = navController)
}
}
@Test
fun cupcakeNavHost_verifyStartDestination() {
navController.assertCurrentRouteName(CupcakeScreen.Start.name)
} //처음 시작 화면의 경로를 확인하는 메서드.
@Test
fun cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() {
val backText = composeTestRule.activity.getString(R.string.back_button)
composeTestRule.onNodeWithContentDescription(backText).assertDoesNotExist()
} //처음 시작 화면에서 위로 버튼이 없음을 확인하는 메서드.
...
2) 단일 화면 테스트 - 해당 화면의 UI 요소와 그 속성이 설정과 일치하는지 확인.
@Test
fun selectOptionScreen_verifyContent() {
val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
val subtotal = "$100"
composeTestRule.setContent {
SelectOptionScreen(subtotal = subtotal, options = flavors)
}
flavors.forEach { flavor ->
composeTestRule.onNodeWithText(flavor).assertIsDisplayed()
}
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(
R.string.subtotal_price,
subtotal
)
).assertIsDisplayed()
composeTestRule.onNodeWithStringId(R.string.next).assertIsNotEnabled()
}
'IT > Android' 카테고리의 다른 글
Jetpack Compose - Adaptive app with dynamic navigation (2) | 2024.02.14 |
---|---|
Jetpack Compose - Navigation Practice (0) | 2024.02.07 |
Jetpack Compose - Architecture(ViewModel) (0) | 2024.02.04 |
Jetpack Compose - Activity lifecycle, remember, rememberSaveable (0) | 2024.02.01 |
Jetpack Compose - Material Design Practice (0) | 2024.01.30 |