การไปยังส่วนต่างๆ ด้วยการเขียน

คอมโพเนนต์การนำทางรองรับแอปพลิเคชัน Jetpack Compose คุณสามารถไปยังมาใน Composable ได้ในขณะที่ใช้ประโยชน์จากโครงสร้างพื้นฐานและฟีเจอร์ของคอมโพเนนต์ Navigation

ดูไลบรารีการนำทางเวอร์ชันอัลฟ่าล่าสุดที่สร้างขึ้นสำหรับ Compose โดยเฉพาะได้ที่เอกสารประกอบเกี่ยวกับการนำทาง 3

ตั้งค่า

หากต้องการรองรับ Compose ให้ใช้ทรัพยากร Dependency ต่อไปนี้ในไฟล์ build.gradle ของโมดูลแอป

Groovy

dependencies {
    def nav_version = "2.9.5"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.9.5"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

เริ่มต้นใช้งาน

เมื่อใช้การนำทางในแอป ให้ใช้โฮสต์การนำทาง กราฟ และ ตัวควบคุม ดูข้อมูลเพิ่มเติมได้ที่ภาพรวมการนำทาง

ดูข้อมูลเกี่ยวกับวิธีสร้าง NavController ใน Compose ได้ที่ส่วน Compose ของสร้างตัวควบคุมการนำทาง

สร้าง NavHost

ดูข้อมูลเกี่ยวกับวิธีสร้าง NavHost ใน Compose ได้ที่ส่วน Compose ของออกแบบกราฟการนำทาง

ดูข้อมูลเกี่ยวกับการไปยัง 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)

// …

}

วิธีนี้จะช่วยป้องกันไม่ให้ข้อมูลสูญหายระหว่างการเปลี่ยนแปลงการกำหนดค่าและความไม่สอดคล้องกันเมื่อมีการอัปเดตหรือเปลี่ยนแปลงออบเจ็กต์ที่เป็นปัญหา

ดูคำอธิบายเชิงลึกเพิ่มเติมเกี่ยวกับเหตุผลที่คุณควรหลีกเลี่ยงการส่งข้อมูลที่ซับซ้อนเป็นอาร์กิวเมนต์ รวมถึงรายการประเภทอาร์กิวเมนต์ที่รองรับได้ที่ส่งข้อมูลระหว่างปลายทาง

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

การนำทางแบบซ้อน

ดูข้อมูลเกี่ยวกับวิธีสร้างกราฟการนำทางที่ซ้อนกันได้ที่ กราฟที่ซ้อนกัน

สร้างแถบนำทางด้านล่างและแถบนำทางแบบปรับอัตโนมัติ

NavigationSuiteScaffold จะแสดง UI การนำทางที่เหมาะสม โดยอิงตาม WindowSizeClass ที่แอปของคุณแสดง ในหน้าจอขนาดกะทัดรัด NavigationSuiteScaffold จะแสดงแถบ Bottom Navigation ส่วนในหน้าจอที่ขยายแล้ว จะแสดง Navigation Rail แทน

ดูข้อมูลเพิ่มเติมได้ที่สร้างการนำทางแบบปรับเปลี่ยนได้

ความสามารถในการทำงานร่วมกัน

หากต้องการใช้คอมโพเนนต์การนำทางกับ Compose คุณมี 2 ตัวเลือกดังนี้

  • กำหนดกราฟการนำทางด้วยคอมโพเนนต์การนำทางสำหรับ Fragment
  • กำหนดกราฟการนำทางด้วย NavHost ใน Compose โดยใช้ปลายทาง Compose ซึ่งจะทำได้ก็ต่อเมื่อหน้าจอทั้งหมดในกราฟการนำทางเป็น Composable

ดังนั้น เราจึงแนะนำให้แอปที่ใช้ทั้ง Compose และ View ใช้คอมโพเนนต์การนำทางที่อิงตาม Fragment จากนั้น Fragment จะมีหน้าจอที่อิงตาม View หน้าจอ Compose และหน้าจอที่ใช้ทั้ง View และ Compose เมื่อเนื้อหาของแต่ละ Fragment อยู่ใน Compose แล้ว ขั้นตอนถัดไปคือการเชื่อมหน้าจอทั้งหมดเข้าด้วยกันด้วย Navigation Compose และนำ Fragment ทั้งหมดออก

หากต้องการเปลี่ยนปลายทางภายในโค้ด Compose คุณต้องเปิดเผยเหตุการณ์ที่ส่งไปยังและทริกเกอร์โดยฟังก์ชันที่ประกอบกันได้ใดก็ได้ในลำดับชั้น

@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 แลมบ์ดาให้ไว้จะช่วยให้คุณ แยกโค้ดการนำทางออกจาก Composable เองได้ ซึ่งจะทำงานได้ 2 ทิศทางดังนี้

  • ส่งเฉพาะอาร์กิวเมนต์ที่แยกวิเคราะห์แล้วไปยัง Composable
  • ส่งผ่าน Lambda ที่ควรทริกเกอร์โดย Composable เพื่อไปยังส่วนต่างๆ แทนที่จะเป็น NavController เอง

เช่น ProfileScreen Composable ที่รับ userId เป็นอินพุตและ อนุญาตให้ผู้ใช้ไปยังหน้าโปรไฟล์ของเพื่อนอาจมีลายเซ็นดังนี้

@Composable
fun ProfileScreen(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 
}

ด้วยวิธีนี้ ProfileScreen Composable จะทำงานแยกต่างหากจากการนำทาง ทำให้ทดสอบแยกกันได้ Lambda composable จะ แคปซูลตรรกะขั้นต่ำที่จำเป็นในการเชื่อมช่องว่างระหว่าง 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 ได้ที่เริ่มต้นใช้งานคอมโพเนนต์การนำทางหรือทำตามCodelab การนำทางของ Jetpack Compose

ดูวิธีออกแบบการนำทางของแอปให้ปรับตามขนาดหน้าจอ การวางแนว และรูปแบบต่างๆ ได้ที่การนำทางสำหรับ UI ที่ปรับเปลี่ยนตามอุปกรณ์

หากต้องการดูข้อมูลเกี่ยวกับการติดตั้งใช้งานการนำทาง Compose ขั้นสูงเพิ่มเติมในแอปแบบแยกส่วน รวมถึงแนวคิดต่างๆ เช่น กราฟที่ซ้อนกันและการผสานรวมแถบนำทางด้านล่าง ให้ดูแอป Now in Android ใน GitHub

ตัวอย่าง