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

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

ตั้งค่า

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

ดึงดูด

dependencies {
    def nav_version = "2.8.0"

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

Kotlin

dependencies {
    val nav_version = "2.8.0"

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

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

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

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

// …

}

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

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

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 ที่เหมาะสมจาก 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

ดึงดูด

dependencies {
    implementation "androidx.compose.material:material:1.7.1"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.7.1")
}

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 เพื่อให้การแตะรายการนําไปยังรายการนั้น การใช้แฟล็ก 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 จะมีสถานะล่าสุดโดยอัตโนมัติ

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

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

  • กำหนดกราฟการนำทางด้วยคอมโพเนนต์การนำทางสำหรับข้อมูลโค้ด
  • กำหนดกราฟการนำทางด้วย NavHost ใน Compose โดยใช้ปลายทาง Compose ซึ่งจะทำได้ในกรณีที่หน้าจอทั้งหมดในกราฟการนําทางเป็นแบบคอมโพสิเบิลเท่านั้น

ดังนั้น คำแนะนำสำหรับแอป "เขียน" และ "ดู" แบบผสมคือการใช้คอมโพเนนต์การนำทางตามเศษส่วน จากนั้น ข้อมูลโค้ดจะเก็บหน้าจอแบบ View, หน้าจอเขียน และหน้าจอที่ใช้ทั้ง View และเขียน เมื่อเนื้อหาของ Fragment แต่ละรายการอยู่ใน Compose แล้ว ขั้นตอนถัดไปคือการเชื่อมโยงหน้าจอทั้งหมดเหล่านั้นเข้าด้วยกันด้วย Navigation Compose และนํา Fragment ทั้งหมดออก

หากต้องการเปลี่ยนปลายทางภายในโค้ด 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 ไปยังคอมโพสิเบิลโดยตรง แต่ให้ส่งการเรียกกลับการนำทางเป็นพารามิเตอร์แทน วิธีนี้ช่วยให้ Composable ทั้งหมดทดสอบได้ทีละรายการ เนื่องจากไม่ต้องมีอินสแตนซ์ 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 การดำเนินการไปยังส่วนต่างๆ ที่ส่งไปยัง Composable รวมถึง Composable บนหน้าจอแต่ละรายการ

การทดสอบ 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 เพื่อตรวจสอบการยืนยันสิทธิ์โดยการเปรียบเทียบเส้นทางปัจจุบันกับเส้นทางที่คาดไว้ โดยใช้ currentBackStackEntry ของ navController ดังนี้

@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

ตัวอย่าง