คอมโพเนนต์การนำทางรองรับแอปพลิเคชัน Jetpack Compose คุณสามารถไปยังมาใน Composable ได้ในขณะที่ใช้ประโยชน์จากโครงสร้างพื้นฐานและฟีเจอร์ของคอมโพเนนต์ Navigation
ดูไลบรารีการนำทางเวอร์ชันอัลฟ่าล่าสุดที่สร้างขึ้นสำหรับ Compose โดยเฉพาะได้ในเอกสารประกอบเกี่ยวกับการนำทาง 3
ตั้งค่า
หากต้องการรองรับ Compose ให้ใช้ทรัพยากร Dependency ต่อไปนี้ใน
build.gradle ของโมดูลแอป
Groovy
dependencies { def nav_version = "2.9.3" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.3" implementation("androidx.navigation:navigation-compose:$nav_version") }
เริ่มต้นใช้งาน
เมื่อใช้การนำทางในแอป ให้ใช้โฮสต์การนำทาง กราฟ และ ตัวควบคุม ดูข้อมูลเพิ่มเติมได้ที่ภาพรวมการนำทาง
สร้าง NavController
ดูข้อมูลเกี่ยวกับวิธีสร้าง NavController ใน Compose ได้ที่ส่วน Compose ของสร้างตัวควบคุมการนำทาง
สร้าง NavHost
ดูข้อมูลเกี่ยวกับวิธีสร้าง NavHost ใน Compose ได้ที่ส่วน Compose
ของออกแบบกราฟการนำทาง
ไปยัง Composable
ดูข้อมูลเกี่ยวกับการไปยัง Composable ได้ที่หัวข้อไปยังปลายทางในเอกสารประกอบเกี่ยวกับสถาปัตยกรรม
ไปยังส่วนต่างๆ ด้วยอาร์กิวเมนต์
ดูข้อมูลเกี่ยวกับการส่งอาร์กิวเมนต์ระหว่างปลายทางที่ใช้ Composable ได้ที่ส่วน Compose ของออกแบบกราฟการนำทาง
ดึงข้อมูลที่ซับซ้อนเมื่อนำทาง
เราขอแนะนำอย่างยิ่งว่าไม่ควรส่งออบเจ็กต์ข้อมูลที่ซับซ้อนเมื่อไปยังส่วนต่างๆ แต่ควรส่งข้อมูลที่จำเป็นขั้นต่ำแทน เช่น ตัวระบุที่ไม่ซ้ำกัน หรือรหัสรูปแบบอื่นๆ เป็นอาร์กิวเมนต์เมื่อดำเนินการนำทาง
// 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 ที่เฉพาะเจาะจงกับ
Composable ได้ โดยค่าเริ่มต้น 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 ไปยัง Composable นั้นโดยอัตโนมัติเมื่อแอปอื่นทริกเกอร์ Deep Link
นอกจากนี้ยังใช้ Deep Link เดียวกันนี้เพื่อสร้าง PendingIntent ด้วย
Deep Link ที่เหมาะสมจาก Composable ได้ด้วย
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.9.0" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.9.0") } 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)
)
ใน Composable ของ BottomNavigation ให้รับ NavBackStackEntry ปัจจุบัน
โดยใช้ฟังก์ชัน currentBackStackEntryAsState() รายการนี้จะให้สิทธิ์เข้าถึง
NavDestination ปัจจุบันแก่คุณ จากนั้นจะกำหนดสถานะที่เลือกของแต่ละ BottomNavigationItem ได้โดยการเปรียบเทียบเส้นทางของรายการกับเส้นทางของปลายทางปัจจุบันและปลายทางระดับบนเพื่อจัดการกรณีที่คุณใช้การนำทางที่ซ้อนกันโดยใช้ลำดับชั้น NavDestination
นอกจากนี้ เส้นทางของรายการยังใช้เพื่อเชื่อมต่อ Lambda onClick กับการเรียก navigate เพื่อให้การแตะรายการนำผู้ใช้ไปยังรายการนั้น การใช้แฟล็ก 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 ตัวเลือกดังนี้
- กำหนดกราฟการนำทางด้วยคอมโพเนนต์การนำทางสำหรับ Fragment
- กำหนดกราฟการนำทางด้วย
NavHostใน Compose โดยใช้ปลายทาง Compose ซึ่งจะทำได้ก็ต่อเมื่อหน้าจอทั้งหมดในกราฟการนำทางเป็น Composable
ดังนั้น คำแนะนำสำหรับแอปที่ใช้ทั้ง Compose และ View คือการใช้คอมโพเนนต์การนำทางที่อิงตาม Fragment จากนั้น Fragment จะมีหน้าจอที่อิงตาม View หน้าจอ Compose และหน้าจอที่ใช้ทั้ง View และ Compose เมื่อเนื้อหาของแต่ละ Fragment อยู่ใน Compose แล้ว ขั้นตอนถัดไปคือการเชื่อมหน้าจอเหล่านั้นทั้งหมด เข้าด้วยกันด้วย Navigation Compose และนำ Fragment ทั้งหมดออก
ไปยังส่วนต่างๆ จาก Compose ด้วย Navigation สำหรับ Fragment
หากต้องการเปลี่ยนปลายทางภายในโค้ด Compose คุณต้องเปิดเผยเหตุการณ์ที่ส่งไปยังและทริกเกอร์โดย Composable ใดก็ได้ในลำดับชั้น
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
ใน Fragment คุณจะสร้างการเชื่อมต่อระหว่าง Compose กับคอมโพเนนต์การนำทางที่อิงตาม Fragment
โดยการค้นหา NavController และไปยัง
ปลายทาง
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
หรือจะส่ง NavController ลงในลำดับชั้นของ Compose ก็ได้
อย่างไรก็ตาม การเปิดเผยฟังก์ชันที่เรียบง่ายจะนำมาใช้ซ้ำและทดสอบได้ง่ายกว่ามาก
การทดสอบ
แยกโค้ดการนำทางออกจากปลายทางที่ใช้ Composable เพื่อให้ทดสอบ
แต่ละ Composable แยกกันได้ โดยแยกจาก Composable ของ NavHost
ซึ่งหมายความว่าคุณไม่ควรส่ง navController ไปยัง
Composable โดยตรง แต่ควรส่งการเรียกกลับการนำทางเป็นพารามิเตอร์แทน ซึ่งช่วยให้ทดสอบ Composable ทั้งหมดแยกกันได้ เนื่องจากไม่จำเป็นต้องมีอินสแตนซ์ของ navController ในการทดสอบ
ระดับการเปลี่ยนเส้นทางที่ composable Lambda มอบให้คือสิ่งที่ช่วยให้คุณ
แยกโค้ดการนำทางออกจาก Composable เอง ซึ่งจะทำงานได้ 2 ทิศทางดังนี้
- ส่งเฉพาะอาร์กิวเมนต์ที่แยกวิเคราะห์แล้วไปยัง Composable
- ส่งผ่าน Lambda ที่ควรทริกเกอร์โดย Composable เพื่อไปยังส่วนต่างๆ แทนที่จะเป็น
NavControllerเอง
เช่น ProfileScreen Composable ที่รับ userId เป็นอินพุตและ
อนุญาตให้ผู้ใช้ไปยังหน้าโปรไฟล์ของเพื่อนอาจมีลายเซ็นดังนี้
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
ด้วยวิธีนี้ ProfileScreen Composable จะทำงานแยกต่างหากจากการนำทาง
ทำให้ทดสอบแยกกันได้ composable Lambda จะ
แคปซูลตรรกะขั้นต่ำที่จำเป็นในการเชื่อมช่องว่างระหว่าง Navigation
API กับ Composable ของคุณ
@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, การดำเนินการนำทางที่ส่งไปยัง Composable รวมถึง
Composable ของหน้าจอแต่ละหน้า
การทดสอบ NavHost
หากต้องการเริ่มทดสอบ NavHost ให้เพิ่มการทดสอบการนำทาง
ต่อไปนี้
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
ห่อหุ้ม NavHost ของแอปใน Composable ซึ่งยอมรับ 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 ของ
currentBackStackEntry ดังนี้
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
ดูคําแนะนําเพิ่มเติมเกี่ยวกับพื้นฐานการทดสอบ Compose ได้ที่ การทดสอบเลย์เอาต์ Compose และ Codelab การทดสอบใน Jetpack Compose ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบโค้ดการนำทางขั้นสูงได้ที่คู่มือทดสอบการนำทาง
ดูข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับการนำทางของ Jetpack ได้ที่เริ่มต้นใช้งานคอมโพเนนต์การนำทางหรือทำตามโค้ดแล็บการนำทางของ Jetpack Compose
ดูวิธีออกแบบการนำทางของแอปให้ปรับเปลี่ยนตามขนาดหน้าจอ การวางแนว และรูปแบบต่างๆ ได้ที่การนำทางสำหรับ UI ที่ปรับเปลี่ยนตามอุปกรณ์
หากต้องการดูข้อมูลเกี่ยวกับการติดตั้งใช้งานการนำทาง Compose ขั้นสูงเพิ่มเติมในแอปแบบแยกส่วน รวมถึงแนวคิดต่างๆ เช่น กราฟที่ซ้อนกันและการผสานรวมแถบนำทางด้านล่าง ให้ดูแอป Now in Android ใน GitHub
ตัวอย่าง
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- Material Design 2 ใน Compose
- ย้ายข้อมูล Jetpack Navigation ไปยัง Navigation Compose
- ตำแหน่งที่จะยก สถานะ