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

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

ตั้งค่า

หากต้องการรองรับการเขียน ให้ใช้ทรัพยากร 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 ใน Compose โปรดดู ของส่วนสร้างตัวควบคุมการนำทาง

สร้าง NavHost

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

สำหรับข้อมูลเกี่ยวกับการไปยัง Composable โปรดดู ไปยัง ปลายทางในสถาปัตยกรรม เอกสารประกอบ

การเขียนในการนำทางยังรองรับการส่งอาร์กิวเมนต์ระหว่าง Composable ด้วย ปลายทาง ในการดำเนินการดังกล่าว คุณต้องเพิ่มตัวยึดตําแหน่งอาร์กิวเมนต์ลงใน คล้ายกับวิธีที่คุณเพิ่มอาร์กิวเมนต์ลงใน เมื่อใช้ Base ไลบรารีการนำทาง:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

โดยค่าเริ่มต้น อาร์กิวเมนต์ทั้งหมดจะได้รับการแยกวิเคราะห์เป็นสตริง พารามิเตอร์ arguments ของ composable() ยอมรับรายการออบเจ็กต์ NamedNavArgument คุณสามารถ สร้าง NamedNavArgument อย่างรวดเร็วโดยใช้เมธอด navArgument() และ จากนั้นให้ระบุ type ที่ถูกต้อง:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

คุณควรแยกอาร์กิวเมนต์จาก NavBackStackEntry ที่ ที่มีอยู่ใน lambda ของฟังก์ชัน composable()

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

หากต้องการส่งอาร์กิวเมนต์ไปยังปลายทาง คุณจะต้องเพิ่มอาร์กิวเมนต์ต่อท้ายเส้นทาง เมื่อคุณโทรออกผ่าน navigate:

navController.navigate("profile/user1234")

โปรดดูรายการประเภทที่รองรับในหัวข้อส่งข้อมูลระหว่าง ปลายทาง

เรียกข้อมูลที่ซับซ้อนขณะนำทาง

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

// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")

ควรจัดเก็บออบเจ็กต์ที่ซับซ้อนเป็นข้อมูลในแหล่งข้อมูลที่ถูกต้องแห่งเดียว เช่น ชั้นข้อมูล เมื่อไปถึงจุดหมายหลังจากนำทางแล้ว ก็ทำสิ่งต่อไปนี้ได้ โหลดข้อมูลที่จำเป็นจากแหล่งข้อมูลที่ถูกต้องแห่งเดียวโดยใช้ บัตรประจำตัวที่ผ่าน เพื่อดึงข้อมูลอาร์กิวเมนต์ใน ViewModel ที่ทําให้ ในการเข้าถึงชั้นข้อมูล ให้ใช้ SavedStateHandle ของ ViewModel:

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

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

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

เพิ่มอาร์กิวเมนต์

การเขียนในการไปยังส่วนต่างๆ ยังรองรับอาร์กิวเมนต์การนำทางที่ไม่บังคับด้วย ไม่บังคับ อาร์กิวเมนต์แตกต่างจากอาร์กิวเมนต์ที่จำเป็นใน 2 รูปแบบ ดังนี้

  • ต้องรวมค่าโดยใช้ไวยากรณ์พารามิเตอร์การค้นหา ("?argName={argName}")
  • ต้องมีชุด defaultValue หรือมี nullable = true (ซึ่งตั้งค่าเริ่มต้นเป็น null โดยปริยาย)

ซึ่งหมายความว่าต้องเพิ่มอาร์กิวเมนต์ที่ไม่บังคับทั้งหมดลงในฟังก์ชัน composable() ทำหน้าที่เป็นรายการ:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

ในตอนนี้ แม้ว่าจะไม่มีการส่งอาร์กิวเมนต์ไปยังปลายทาง แต่ defaultValue ดังนั้นจะใช้ "user1234" แทน

โครงสร้างของการจัดการอาร์กิวเมนต์ผ่านเส้นทางหมายความว่า Composables ยังคงเป็นอิสระจากการนำทาง และมีประโยชน์มากกว่า สามารถทดสอบได้

การเขียนในการไปยังส่วนต่างๆ รองรับ Deep Link โดยนัยที่กําหนดให้เป็นส่วนหนึ่งของ ฟังก์ชัน composable() ได้ด้วย พารามิเตอร์ deepLinks ของ URL ยอมรับรายการ ออบเจ็กต์ NavDeepLink ที่สามารถสร้างได้อย่างรวดเร็วโดยใช้ เมธอด navDeepLink():

val uri = "https://www.example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

Deep Link เหล่านี้ช่วยให้คุณเชื่อมโยง URL, การทำงาน หรือประเภท MIME กับ Composable โดยค่าเริ่มต้น แอปภายนอกจะไม่เห็น Deep Link เหล่านี้ ถึง ทำให้ Deep Link เหล่านี้ใช้งานได้จากภายนอก คุณต้องเพิ่มลิงก์ที่เหมาะสม <intent-filter> ลงในไฟล์ manifest.xml ของแอป เพื่อเปิดใช้แท็ก ในตัวอย่างก่อนหน้านี้ คุณควรเพิ่มข้อมูลต่อไปนี้ภายใน องค์ประกอบ <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/$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 เพิ่มทรัพยากร Dependency ของ 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"
    }
}

หากต้องการลิงก์รายการในแถบนำทางด้านล่างกับเส้นทางในกราฟการนำทาง ขอแนะนำให้กำหนดคลาสที่ปิดผนึกไว้ เช่น Screen ที่เห็นที่นี่ มีเส้นทางและรหัสทรัพยากรสตริงสำหรับปลายทาง

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

จากนั้นวางรายการเหล่านั้นในรายการที่ BottomNavigationItem:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

ใน 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
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.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 = Screen.Profile.route, Modifier.padding(innerPadding)) {
    composable(Screen.Profile.route) { Profile(navController) }
    composable(Screen.FriendsList.route) { FriendsList(navController) }
  }
}

คุณใช้ประโยชน์จาก NavController.currentBackStackEntryAsState() ได้ที่นี่ เมธอดเพื่อยกสถานะ navController ออกจากฟังก์ชัน NavHost และ แชร์กับคอมโพเนนต์ BottomNavigation ซึ่งหมายความว่า BottomNavigation จะมีสถานะล่าสุดโดยอัตโนมัติ

พิมพ์คำว่า safety ในฟีเจอร์ช่วยเขียนในการไปยังส่วนต่างๆ

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

  • อาร์กิวเมนต์ที่คุณระบุเมื่อไปยังปลายทางหรือกราฟการนำทาง เป็นประเภทที่ถูกต้องและมีอาร์กิวเมนต์ที่จำเป็นทั้งหมด
  • อาร์กิวเมนต์ที่ดึงมาจาก SavedStateHandle เป็นประเภทที่ถูกต้อง

ดูข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ที่ความปลอดภัยประเภทใน Kotlin DSL และการนำทาง เขียน

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

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

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

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

ในการเปลี่ยนปลายทางภายในโค้ดเขียน คุณจะแสดงเหตุการณ์ที่ จะส่งผ่านและทริกเกอร์โดย 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 แต่ละรายการแยกกันโดยแยกออกจาก NavHost Composable

ซึ่งหมายความว่า คุณไม่ควรส่ง navController ไปยัง Composable และส่ง Callback ของการนำทางเป็นพารามิเตอร์แทน วิธีนี้ช่วยให้ Composable ทั้งหมดสามารถทดสอบได้ทีละรายการ เนื่องจากไม่ต้องมี navController ในการทดสอบ

ระดับของทางอ้อมที่ได้จาก composable แลมบ์ดาคือสิ่งที่ช่วยให้คุณ แยกโค้ดการนำทางออกจาก Composable วิธีนี้ใช้งานได้ใน เส้นทาง:

  • ส่งผ่านเฉพาะอาร์กิวเมนต์ที่แยกวิเคราะห์ไปยัง Composable ของคุณ
  • ส่งผ่าน lambda ที่ควรทริกเกอร์โดย Composable เพื่อนำทาง แทนที่จะเป็นตัว NavController

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

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

วิธีนี้ทำให้ Composable ของ Profile ทำงานแยกจากการไปยังส่วนต่างๆ ทำให้สามารถทดสอบได้อย่างอิสระ แลมบ์ดา composable จะ สรุปตรรกะขั้นต่ำที่จำเป็นในการเชื่อมช่องว่างระหว่างการนำทาง API และ Composable:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

เราขอแนะนำให้เขียนการทดสอบที่ครอบคลุมข้อกำหนดในการไปยังส่วนต่างๆ ของแอป โดยการทดสอบ NavHost, การไปยังส่วนต่างๆ ที่ผ่าน Composable รวมถึง Composable บนหน้าจอแต่ละหน้าจอ

กำลังทดสอบ NavHost

หากต้องการเริ่มทดสอบ NavHost ให้เพิ่มการทดสอบการนำทางต่อไปนี้ การพึ่งพา:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

คุณสามารถตั้งค่าวิชาทดสอบ NavHost และสอบผ่าน ของอินสแตนซ์ navController สำหรับกรณีนี้ การนำทาง อาร์ติแฟกต์ทดสอบจะแสดง 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 เพื่อดูวิธีทดสอบฟีเจอร์นี้ควบคู่กับ Composable แต่ละรายการ แยกกัน อย่าลืมดู การทดสอบใน Jetpack Compose Codelab

นอกจากนี้ คุณยังสามารถใช้ navController เพื่อตรวจสอบการยืนยันความถูกต้องได้โดย เปรียบเทียบเส้นทางสตริงปัจจุบันกับเส้นทางที่ต้องการ โดยใช้ currentBackStackEntry ของ navController:

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

ดูคำแนะนำเพิ่มเติมเกี่ยวกับพื้นฐานการทดสอบ Compose ได้ที่ การทดสอบเลย์เอาต์ Compose และการทดสอบใน Jetpack Compose Codelab หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการทดสอบโค้ดการนำทางขั้นสูง โปรดไปที่ คู่มือการนำทางทดสอบ

ดูข้อมูลเพิ่มเติม

หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการนำทางของ Jetpack ให้ดูที่เริ่มต้นใช้งานการนำทาง คอมโพเนนต์ หรือนำ Jetpack เขียน Codelab การนำทาง

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

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

ตัวอย่าง