คอมโพเนนต์การนำทางรองรับแอปพลิเคชัน Jetpack Compose คุณสามารถไปยังส่วนต่างๆ ของคอมโพสิเบิลต่างๆ ไปพร้อมๆ กับใช้ประโยชน์จากโครงสร้างพื้นฐานและฟีเจอร์ของคอมโพเนนต์การนำทาง
ตั้งค่า
หากต้องการรองรับ Compose ให้ใช้ Dependency ต่อไปนี้ในไฟล์ build.gradle
ของโมดูลแอป
Groovy
dependencies { def nav_version = "2.8.4" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.4" implementation("androidx.navigation:navigation-compose:$nav_version") }
เริ่มต้นใช้งาน
เมื่อใช้การนําทางในแอป ให้ใช้โฮสต์การนําทาง กราฟ และตัวควบคุมการนําทาง ดูข้อมูลเพิ่มเติมได้ที่ภาพรวมการนําทาง
สร้าง NavController
ดูข้อมูลเกี่ยวกับวิธีสร้าง NavController
ใน "เขียน" ได้ที่ส่วน "เขียน" ของสร้างตัวควบคุมการนําทาง
สร้าง NavHost
ดูข้อมูลเกี่ยวกับวิธีสร้าง NavHost
ใน "เขียน" ได้ที่ส่วน "เขียน" ของออกแบบกราฟการนําทาง
ไปที่คอมโพสิเบิล
ดูข้อมูลเกี่ยวกับการไปยัง Composable ได้ที่ไปยังปลายทางในเอกสารประกอบสถาปัตยกรรม
ไปยังส่วนต่างๆ ด้วยอาร์กิวเมนต์
ดูข้อมูลเกี่ยวกับการส่งอาร์กิวเมนต์ระหว่างปลายทางแบบคอมโพสิเบิลได้ที่ส่วนคอมโพสิเบิลของออกแบบกราฟการนําทาง
ดึงข้อมูลที่ซับซ้อนเมื่อนำทาง
ขอแนะนำอย่างยิ่งว่าอย่าส่งออบเจ็กต์ข้อมูลที่ซับซ้อนเมื่อไปยังส่วนต่างๆ แต่ให้ส่งข้อมูลที่จำเป็นขั้นต่ำ เช่น ตัวระบุที่ไม่ซ้ำกันหรือรหัสรูปแบบอื่นๆ แทน โดยใช้เป็นอาร์กิวเมนต์เมื่อดำเนินการไปยังส่วนต่างๆ
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
ออบเจ็กต์ที่ซับซ้อนควรจัดเก็บเป็นข้อมูลในแหล่งข้อมูลเดียวที่เชื่อถือได้ เช่น ชั้นข้อมูล เมื่อไปถึงปลายทางหลังจากไปยังส่วนต่างๆ แล้ว คุณสามารถดาวน์โหลดข้อมูลที่จําเป็นจากแหล่งข้อมูลเดียวโดยใช้รหัสที่ส่งผ่าน หากต้องการเรียกข้อมูลอาร์กิวเมนต์ใน ViewModel
ที่รับผิดชอบการเข้าถึงชั้นข้อมูล ให้ใช้ SavedStateHandle
ของ ViewModel
ดังนี้
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
วิธีนี้ช่วยป้องกันไม่ให้ข้อมูลสูญหายในระหว่างการเปลี่ยนแปลงการกําหนดค่าและความคลาดเคลื่อนเมื่อมีการอัปเดตหรือเปลี่ยนแปลงออบเจ็กต์ที่เป็นปัญหา
ดูคำอธิบายโดยละเอียดเพิ่มเติมเกี่ยวกับเหตุผลที่ควรหลีกเลี่ยงการส่งข้อมูลที่ซับซ้อนเป็นอาร์กิวเมนต์ รวมถึงรายการประเภทอาร์กิวเมนต์ที่รองรับได้ที่ส่งข้อมูลระหว่างปลายทาง
Deep Link
Navigation Compose รองรับ Deep Link ที่กําหนดเป็นส่วนหนึ่งของฟังก์ชัน composable()
ได้ด้วย พารามิเตอร์ deepLinks
จะยอมรับรายการออบเจ็กต์ NavDeepLink
ซึ่งสร้างได้อย่างรวดเร็วโดยใช้เมธอด navDeepLink()
ดังนี้
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}
Deep Link เหล่านี้ช่วยให้คุณเชื่อมโยง URL, การดำเนินการ หรือประเภท mime ที่เฉพาะเจาะจงกับคอมโพสิเบิลได้ โดยค่าเริ่มต้น Deep Link เหล่านี้จะไม่แสดงในแอปภายนอก หากต้องการทำให้ Deep Link เหล่านี้พร้อมใช้งานภายนอก คุณต้องเพิ่มองค์ประกอบ <intent-filter>
ที่เหมาะสมลงในไฟล์ manifest.xml
ของแอป หากต้องการเปิดใช้ Deep Link ในตัวอย่างข้างต้น คุณควรเพิ่มข้อมูลต่อไปนี้ภายในองค์ประกอบ <activity>
ของไฟล์ Manifest
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
การนำทางจะลิงก์ในรายละเอียดไปยังคอมโพสิชันนั้นโดยอัตโนมัติเมื่อแอปอื่นทริกเกอร์ Deep Link
คุณยังใช้ Deep Link เดียวกันเหล่านี้เพื่อสร้าง PendingIntent
ที่มี Deep Link ที่เหมาะสมจากคอมโพสิเบิลได้ด้วย
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/profile/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
จากนั้นคุณก็ใช้ deepLinkPendingIntent
นี้เหมือนกับ PendingIntent
อื่นๆ เพื่อเปิดแอปที่ปลายทางของ Deep Link ได้
การนำทางแบบซ้อน
ดูข้อมูลเกี่ยวกับวิธีสร้างกราฟการนําทางที่ซ้อนกันได้ที่กราฟที่ซ้อนกัน
การผสานรวมกับแถบนําทางด้านล่าง
การกําหนด NavController
ที่ระดับสูงขึ้นในลําดับชั้นแบบคอมโพสิเบิลจะช่วยให้คุณเชื่อมต่อการนําทางกับคอมโพเนนต์อื่นๆ เช่น คอมโพเนนต์การนําทางด้านล่างได้ ซึ่งจะช่วยให้คุณไปยังส่วนต่างๆ ได้โดยการเลือกไอคอนในแถบด้านล่าง
หากต้องการใช้คอมโพเนนต์ BottomNavigation
และ BottomNavigationItem
ให้เพิ่มการพึ่งพา androidx.compose.material
ลงในแอปพลิเคชัน Android
Groovy
dependencies { implementation "androidx.compose.material:material:1.7.5" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.7.5") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
หากต้องการลิงก์รายการในแถบนําทางด้านล่างกับเส้นทางในกราฟการนําทาง เราขอแนะนําให้กําหนดคลาส เช่น TopLevelRoute
ที่แสดงที่นี่ ซึ่งมีคลาสเส้นทางและไอคอน
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
จากนั้นใส่เส้นทางเหล่านั้นไว้ในรายการที่ BottomNavigationItem
สามารถใช้ได้
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
ในคอมโพสิเบิล BottomNavigation
ให้รับ NavBackStackEntry
ในปัจจุบันโดยใช้ฟังก์ชัน currentBackStackEntryAsState()
รายการนี้จะช่วยให้คุณเข้าถึง NavDestination
ปัจจุบันได้ จากนั้นระบบจะกำหนดสถานะที่เลือกของ BottomNavigationItem
แต่ละรายการโดยเปรียบเทียบเส้นทางของรายการนั้นกับเส้นทางของปลายทางปัจจุบันและปลายทางหลักของปลายทางนั้นเพื่อจัดการกรณีที่คุณใช้การนําทางที่ฝังโดยใช้ลําดับชั้น NavDestination
นอกจากนี้ ระบบยังใช้เส้นทางของรายการเพื่อเชื่อมต่อ Lambda onClick
กับการเรียกใช้ navigate
เพื่อให้การแตะรายการนําไปยังรายการนั้น เมื่อใช้ Flag saveState
และ restoreState
ระบบจะบันทึกและกู้คืนสถานะและสแต็กย้อนกลับของรายการนั้นอย่างถูกต้องเมื่อคุณสลับระหว่างรายการการนำทางด้านล่าง
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
ในส่วนนี้ คุณใช้ประโยชน์จากNavController.currentBackStackEntryAsState()
เมธอดเพื่อยกสถานะ navController
ออกจากฟังก์ชัน NavHost
และแชร์กับคอมโพเนนต์ BottomNavigation
ซึ่งหมายความว่า
BottomNavigation
จะมีสถานะล่าสุดโดยอัตโนมัติ
ความสามารถในการทำงานร่วมกัน
หากต้องการใช้คอมโพเนนต์การนำทางกับ Compose คุณมี 2 ตัวเลือกดังนี้
- กำหนดกราฟการนำทางด้วยคอมโพเนนต์การนำทางสำหรับข้อมูลโค้ด
- กําหนดกราฟการนําทางด้วย
NavHost
ในคอมโพซโดยใช้ปลายทางคอมโพซ ซึ่งจะทำได้ในกรณีที่หน้าจอทั้งหมดในกราฟการนําทางเป็นแบบคอมโพสิเบิลเท่านั้น
ดังนั้น คําแนะนําสําหรับแอป Compose และ Views แบบผสมคือให้ใช้คอมโพเนนต์การนําทางตาม Fragment จากนั้น ข้อมูลโค้ดจะเก็บหน้าจอแบบ View, หน้าจอเขียน และหน้าจอที่ใช้ทั้ง View และเขียน เมื่อเนื้อหาของ Fragment แต่ละรายการอยู่ใน Compose แล้ว ขั้นตอนถัดไปคือการเชื่อมโยงหน้าจอทั้งหมดเหล่านั้นเข้าด้วยกันด้วย Navigation Compose และนํา Fragment ทั้งหมดออก
ไปยังส่วนต่างๆ จาก Compose ด้วย Navigation สำหรับส่วนย่อย
หากต้องการเปลี่ยนปลายทางภายในโค้ด Compose คุณต้องแสดงเหตุการณ์ที่คอมโพสิเบิลใดก็ได้ในลําดับชั้นสามารถส่งผ่านและทริกเกอร์ได้ ดังนี้
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
ในข้อมูลโค้ดโค้ดโค้ด คุณจะสร้างบริดจ์ระหว่าง Compose กับคอมโพเนนต์การนําทางที่อิงตามข้อมูลโค้ดโค้ดโดยค้นหา NavController
และไปยังปลายทาง ดังนี้
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
หรือจะส่ง NavController
ลงตามลําดับชั้นการเขียนก็ได้
อย่างไรก็ตาม การเปิดเผยฟังก์ชันแบบง่ายจะนํากลับมาใช้ซ้ำและทดสอบได้มากกว่า
การทดสอบ
แยกโค้ดการนำทางออกจากปลายทางแบบคอมโพสิเบิลเพื่อเปิดใช้การทดสอบคอมโพสิเบิลแต่ละรายการแยกต่างหากจากคอมโพสิเบิล NavHost
ซึ่งหมายความว่าคุณไม่ควรส่ง navController
ไปยังคอมโพสิเบิลโดยตรง แต่ให้ส่งการเรียกกลับการนำทางเป็นพารามิเตอร์แทน ซึ่งจะช่วยให้คอมโพสิเบิลทั้งหมดสามารถทดสอบแยกกันได้ เนื่องจากไม่จําเป็นต้องใช้อินสแตนซ์ของ navController
ในการทดสอบ
ระดับการสื่อให้เข้าใจโดยอ้อมที่ composable
lambda มีให้จะช่วยให้คุณแยกโค้ดการนำทางออกจากคอมโพสิเบิลได้ ซึ่งจะทำงานได้ 2 แบบดังนี้
- ส่งเฉพาะอาร์กิวเมนต์ที่แยกวิเคราะห์แล้วไปยังคอมโพสิเบิล
- ส่ง Lambda ที่คอมโพสิเบิลควรทริกเกอร์เพื่อไปยังส่วนต่างๆ แทน
NavController
ตัวอย่างเช่น คอมโพสิเบิล ProfileScreen
ที่รับ userId
เป็นอินพุตและอนุญาตให้ผู้ใช้ไปยังหน้าโปรไฟล์ของเพื่อนอาจมีลายเซ็นดังนี้
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
วิธีนี้ช่วยให้คอมโพสิชัน ProfileScreen
ทํางานแยกจากการนำทางได้ จึงทดสอบแยกกันได้ แลมดา composable
จะรวมตรรกะขั้นต่ำที่จำเป็นเพื่อเชื่อมช่องว่างระหว่าง Navigation API กับคอมโพสิเบิล
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
เราขอแนะนำให้เขียนการทดสอบที่ครอบคลุมข้อกำหนดการนำทางของแอปโดยทดสอบ NavHost
, การนําทางที่ส่งไปยังคอมโพสิเบิล รวมถึงคอมโพสิเบิลหน้าจอแต่ละรายการ
การทดสอบ NavHost
หากต้องการเริ่มทดสอบ NavHost
ให้เพิ่มข้อกําหนดในการทดสอบการนําทางต่อไปนี้
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
ตัด NavHost
ของแอปในคอมโพสิเบิลที่ยอมรับ NavHostController
เป็นพารามิเตอร์
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
ตอนนี้คุณสามารถทดสอบ AppNavHost
และตรรกะการนําทางทั้งหมดที่กําหนดไว้ภายใน NavHost
ได้โดยส่งอินสแตนซ์ของอาร์ติแฟกต์การทดสอบการนําทาง TestNavHostController
การทดสอบ UI ที่ยืนยันปลายทางเริ่มต้นของแอปและ NavHost
จะมีลักษณะดังนี้
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Before
fun setupAppNavHost() {
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavHost(navController = navController)
}
}
// Unit test
@Test
fun appNavHost_verifyStartDestination() {
composeTestRule
.onNodeWithContentDescription("Start Screen")
.assertIsDisplayed()
}
}
การทดสอบการไปยังส่วนต่างๆ
คุณสามารถทดสอบการใช้งานการนําทางได้หลายวิธี เช่น คลิกองค์ประกอบ UI แล้วตรวจสอบจุดหมายที่แสดง หรือเปรียบเทียบเส้นทางที่คาดไว้กับเส้นทางปัจจุบัน
เนื่องจากคุณต้องการทดสอบการติดตั้งใช้งานแอปที่เป็นรูปธรรม คุณจึงควรคลิก UI หากต้องการดูวิธีทดสอบสิ่งนี้ควบคู่ไปกับฟังก์ชันคอมโพสิเบิลแต่ละรายการแยกกัน โปรดดู Codelab การทดสอบใน Jetpack Compose
นอกจากนี้ คุณยังใช้ navController
เพื่อตรวจสอบการยืนยันได้โดยเปรียบเทียบเส้นทางปัจจุบันกับเส้นทางที่คาดไว้ โดยใช้ navController
's currentBackStackEntry
ดังนี้
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
ดูคําแนะนําเพิ่มเติมเกี่ยวกับพื้นฐานการทดสอบ Compose ได้ที่การทดสอบเลย์เอาต์ Compose และการทดสอบใน Jetpack Compose ของ Codelab ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบขั้นสูงของโค้ดการนำทางได้ที่คู่มือทดสอบการนําทาง
ดูข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับการไปยังส่วนต่างๆ ใน Jetpack ได้ที่เริ่มต้นใช้งานคอมโพเนนต์การไปยังส่วนต่างๆ หรือเข้าร่วมโค้ดแล็บการไปยังส่วนต่างๆ ใน Jetpack Compose
ดูวิธีออกแบบการนําทางของแอปให้ปรับขนาด การวางแนว และรูปแบบของหน้าจอที่แตกต่างกันได้ที่การนําทางสําหรับ UI ที่ปรับเปลี่ยนตามอุปกรณ์
หากต้องการดูข้อมูลเกี่ยวกับการใช้งานการนําทางของ Compose ขั้นสูงขึ้นในแอปแบบโมดูล รวมถึงแนวคิดต่างๆ เช่น การฝังกราฟและการผสานรวมแถบนําทางด้านล่าง โปรดดูแอป Now in Android ใน GitHub
ตัวอย่าง
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- Material Design 2 ในเครื่องมือเขียน
- ย้ายข้อมูล Jetpack Navigation ไปยัง Navigation Compose
- Where to hoist state