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