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

คอมโพเนนต์การนำทางรองรับแอปพลิเคชัน 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 ใน 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

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

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

การผสานรวมกับแถบนำทางด้านล่าง

การกำหนด 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 คุณต้องเปิดเผยเหตุการณ์ที่ส่งไปยังและทริกเกอร์โดยฟังก์ชันที่ประกอบกันได้ใดก็ได้ในลำดับชั้น

@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 ทั้งหมดแยกกันได้ เนื่องจากไม่จำเป็นต้องมีอินสแตนซ์ของ navController ในการทดสอบ

ระดับการเปลี่ยนเส้นทางที่ composable แลมบ์ดาให้ไว้จะช่วยให้คุณ แยกโค้ดการนำทางออกจาก Composable เองได้ ซึ่งจะทำงานได้ 2 รูปแบบดังนี้

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

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

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

ด้วยวิธีนี้ ProfileScreencomposable จะทำงานแยกต่างหากจากการนำทาง ทำให้ทดสอบแยกกันได้ 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

ตัวอย่าง