Navigation 3 ขอแนะนำระบบที่มีประสิทธิภาพและยืดหยุ่นสำหรับการจัดการโฟลว์ UI ของแอปผ่านฉาก ฉากช่วยให้คุณสร้างเลย์เอาต์ที่ปรับแต่งได้สูง ปรับให้เหมาะกับขนาดหน้าจอต่างๆ และจัดการประสบการณ์การใช้งานหลายบานหน้าต่างที่ซับซ้อนได้อย่างราบรื่น
ทำความเข้าใจฉาก
ใน Navigation 3 Scene คือหน่วยพื้นฐานที่แสดงผลอินสแตนซ์
NavEntry อย่างน้อย 1 รายการ ให้คิดว่า Scene เป็นสถานะหรือส่วนภาพที่แตกต่างกันของ UI ซึ่งสามารถมีและจัดการการแสดงเนื้อหาจาก Back Stack ได้
ระบบจะระบุอินสแตนซ์ Scene แต่ละรายการด้วย key และคลาสของ
Scene เอง ตัวระบุที่ไม่ซ้ำกันนี้มีความสำคัญเนื่องจากเป็นตัวขับเคลื่อน
แอนิเมชันระดับบนสุดเมื่อ Scene เปลี่ยนไป.
อินเทอร์เฟซ Scene มีพร็อพเพอร์ตี้ต่อไปนี้
key: Any: ตัวระบุที่ไม่ซ้ำกันสำหรับอินสแตนซ์Sceneนี้ คีย์นี้เมื่อรวมกับคลาสของSceneจะช่วยให้มั่นใจได้ถึงความแตกต่าง โดยมีวัตถุประสงค์หลักเพื่อใช้กับแอนิเมชันentries: List<NavEntry<T>>: นี่คือรายการออบเจ็กต์NavEntryที่Sceneมีหน้าที่รับผิดชอบในการแสดง สิ่งสำคัญคือ หากNavEntryเดียวกันแสดงในScenesหลายรายการระหว่างการเปลี่ยนผ่าน (เช่น ในการเปลี่ยนผ่านขององค์ประกอบที่แชร์) ระบบจะแสดงผลเนื้อหาของNavEntryนั้นโดยSceneเป้าหมายล่าสุดที่แสดงNavEntryนั้นเท่านั้นpreviousEntries: List<NavEntry<T>>: พร็อพเพอร์ตี้นี้กำหนดNavEntryที่จะเกิดขึ้นหากมีการดำเนินการ "ย้อนกลับ" จากSceneปัจจุบัน พร็อพเพอร์ตี้นี้มีความสำคัญต่อการคำนวณสถานะย้อนกลับเชิงคาดการณ์ที่เหมาะสม ซึ่งช่วยให้NavDisplayคาดการณ์และเปลี่ยนผ่านไปยังสถานะก่อนหน้าได้อย่างถูกต้อง ซึ่งอาจเป็น Scene ที่มีคลาส คีย์ หรือทั้ง 2 อย่างที่แตกต่างกันcontent: @Composable () -> Unit: นี่คือฟังก์ชันที่ใช้ร่วมกันได้ซึ่ง คุณกำหนดวิธีที่Sceneแสดงผลentriesและองค์ประกอบ UI โดยรอบ ที่เฉพาะเจาะจงกับSceneนั้นmetadata: Map<String, Any>: ให้ข้อมูลเฉพาะของฉากแก่คอมโพเนนต์อื่นๆ ของไลบรารี เช่นNavDisplayโดยค่าเริ่มต้น จะแสดงผลmetadataของNavEntryสุดท้ายในentries
ใช้ equals และ hashCode ในฉากที่กำหนดเอง
การติดตั้งใช้งาน Scene ที่กำหนดเองต้องใช้ equals และ hashCode อย่างถูกต้องเพื่อรองรับสิ่งต่อไปนี้
- การเปลี่ยนสถานะของฉาก: ทุกครั้งที่รายการกลยุทธ์ของ
ฉากคำนวณฉาก
NavDisplayจะตรวจสอบความเท่ากันของฉากใหม่และฉาก ก่อนหน้า หากตรวจพบการเปลี่ยนแปลง ระบบจะอัปเดตSeekableTransitionที่ใช้ในการเปลี่ยนผ่านระหว่างฉาก ซึ่งจะส่งผลต่อสถานะวงจรการทำงานของฉาก - การจัดการการวางซ้อน: สำหรับ
OverlayScene(เช่น กล่องโต้ตอบ)NavDisplayจะใช้ออบเจ็กต์ฉากเองเป็นkeyเพื่อติดตามวงจรการทำงานของการวางซ้อนและแอนิเมชันการออก
ตรวจสอบว่าพร็อพเพอร์ตี้ทั้งหมดที่กำหนดเนื้อหาของฉาก เช่น key, entries และ previousEntries รวมอยู่ในการติดตั้งใช้งาน equals และ hashCode โดยทั่วไป ให้หลีกเลี่ยงการรวมการเรียกกลับ (เช่น onBack) ในการติดตั้งใช้งานเมธอดเหล่านี้ เนื่องจากเมธอดเหล่านี้สามารถเปลี่ยนอินสแตนซ์ได้โดยไม่เปลี่ยนสถานะฉากเชิงตรรกะ
ทำความเข้าใจกลยุทธ์ของฉาก
A SceneStrategy เป็นกลไกที่กำหนดวิธีจัดเรียงและเปลี่ยนผ่านรายการ
NavEntry จาก Back Stack ไปยัง
Scene โดยพื้นฐานแล้ว เมื่อได้รับรายการ Back Stack ปัจจุบัน SceneStrategy จะถามคำถามสำคัญ 2 ข้อกับตัวเอง
- ฉันสร้าง
Sceneจากรายการเหล่านี้ได้ไหม หากSceneStrategyกำหนดว่าสามารถจัดการNavEntrys ที่กำหนดและสร้างSceneที่มีความหมาย (เช่น กล่องโต้ตอบหรือเลย์เอาต์หลายบานหน้าต่าง) ได้ ระบบจะดำเนินการต่อ มิฉะนั้น ระบบจะแสดงผลnullเพื่อให้กลยุทธ์อื่นๆ มีโอกาสสร้างScene - หากทำได้ ฉันควรจัดเรียงรายการเหล่านั้นเป็น
Scene?อย่างไร เมื่อSceneStrategyตัดสินใจที่จะจัดการรายการแล้ว กลยุทธ์นั้นจะรับผิดชอบในการสร้างSceneและกำหนดวิธีแสดงNavEntrys ที่ระบุภายในSceneนั้น
หัวใจสำคัญของ SceneStrategy คือเมธอด calculateScene
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
เมธอดนี้เป็นฟังก์ชันส่วนขยายใน SceneStrategyScope ซึ่งใช้
List<NavEntry<T>> ปัจจุบันจาก Back Stack เมธอดนี้ควรแสดงผล Scene<T>
หากสร้างได้สำเร็จจากรายการที่ให้ไว้ หรือ null หาก
สร้างไม่ได้
SceneStrategyScope มีหน้าที่รับผิดชอบในการดูแลอาร์กิวเมนต์ที่ไม่บังคับ
ซึ่ง SceneStrategy อาจต้องใช้ เช่น การเรียกกลับ onBack
ฉากและกลยุทธ์ของฉากทำงานร่วมกันอย่างไร
NavDisplay เป็นคอมโพสเซเบิลส่วนกลางที่สังเกต Back Stack และ
ใช้ SceneStrategy อย่างน้อย 1 รายการเพื่อกำหนดและแสดงผล
Scene ที่เหมาะสม
พารามิเตอร์ sceneStrategies ของ NavDisplay คาดหวังรายการอินสแตนซ์ SceneStrategy
ที่รับผิดชอบในการคำนวณ Scene ที่จะแสดง หากกลยุทธ์ที่ให้ไว้ไม่คำนวณ
Scene NavDisplay จะกลับไปใช้ SinglePaneSceneStrategy โดยอัตโนมัติ
โดยค่าเริ่มต้น
รายละเอียดการโต้ตอบมีดังนี้
- เมื่อคุณเพิ่มหรือนำคีย์ออกจาก Back Stack (เช่น ใช้
backStack.add()หรือbackStack.removeLastOrNull())NavDisplayจะสังเกตการเปลี่ยนแปลงเหล่านี้ NavDisplayจะส่งรายการNavEntryปัจจุบัน (ได้มาจากคีย์ Back Stack) ไปยังsceneStrategiesที่กำหนดค่าไว้ตามลำดับ โดยเรียกใช้calculateSceneในแต่ละรายการจนกว่าจะมีการแสดงผลScene- เมื่อ
SceneStrategyแสดงผลSceneได้สำเร็จNavDisplayจะ แสดงผลcontentของSceneนั้นNavDisplayยังจัดการแอนิเมชันและการย้อนกลับเชิงคาดการณ์ตามพร็อพเพอร์ตี้ของSceneด้วย
ตัวอย่าง: เลย์เอาต์บานหน้าต่างเดียว (ลักษณะการทำงานเริ่มต้น)
เลย์เอาต์ที่กำหนดเองที่ง่ายที่สุดที่คุณสามารถมีได้คือการแสดงผลบานหน้าต่างเดียว ซึ่งเป็นลักษณะการทำงานเริ่มต้นหากไม่มี SceneStrategy อื่นมีลำดับความสำคัญสูงกว่า
data class SinglePaneScene<T : Any>( override val key: Any, val entry: NavEntry<T>, override val previousEntries: List<NavEntry<T>>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(entry) override val content: @Composable () -> Unit = { entry.Content() } } /** * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the * list. */ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
ตัวอย่าง: เลย์เอาต์รายการ-รายละเอียดพื้นฐาน (ฉากและกลยุทธ์ที่กำหนดเอง)
ตัวอย่างนี้แสดงวิธีสร้างเลย์เอาต์รายการ-รายละเอียดที่จะเปิดใช้งานตามเงื่อนไข 2 ข้อต่อไปนี้
- ความกว้างของหน้าต่าง กว้างพอที่จะรองรับ 2 บานหน้าต่าง (เช่น อย่างน้อย
WIDTH_DP_MEDIUM_LOWER_BOUND) - Back Stack มีรายการที่ประกาศการรองรับการแสดงในเลย์เอาต์รายการ-รายละเอียดโดยใช้ข้อมูลเมตาที่เฉพาะเจาะจง
ข้อมูลโค้ดต่อไปนี้เป็นซอร์สโค้ดสำหรับ ListDetailScene.kt และมีทั้ง ListDetailScene และ ListDetailSceneStrategy:
// --- ListDetailScene --- /** * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. * */ data class ListDetailScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val listEntry: NavEntry<T>, val detailEntry: NavEntry<T>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.4f)) { listEntry.Content() } Column(modifier = Modifier.weight(0.6f)) { detailEntry.Content() } } } } @Composable fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass return remember(windowSizeClass) { ListDetailSceneStrategy(windowSizeClass) } } // --- ListDetailSceneStrategy --- /** * A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item * is the backstack is a detail, and before it, at any point in the backstack is a list. */ class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? { if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val detailEntry = entries.lastOrNull()?.takeIf { it.metadata.contains(DetailKey) } ?: return null val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: return null // We use the list's contentKey to uniquely identify the scene. // This allows the detail panes to be displayed instantly through recomposition, rather than // having NavDisplay animate the whole scene out when the selected detail item changes. val sceneKey = listEntry.contentKey return ListDetailScene( key = sceneKey, previousEntries = entries.dropLast(1), listEntry = listEntry, detailEntry = detailEntry ) } object ListKey : NavMetadataKey<Boolean> object DetailKey : NavMetadataKey<Boolean> companion object { /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun listPane() = metadata { put(ListKey, true) } /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun detailPane() = metadata { put(DetailKey, true) } } }
หากต้องการใช้ ListDetailSceneStrategy นี้ใน NavDisplay ให้แก้ไขการเรียก entryProvider เพื่อรวมข้อมูลเมตา ListDetailScene.listPane() สำหรับรายการที่คุณต้องการแสดงเป็นเลย์เอาต์รายการ และ ListDetailScene.detailPane() สำหรับรายการที่คุณต้องการแสดงเป็นเลย์เอาต์รายละเอียด จากนั้นระบุ ListDetailSceneStrategy() เป็น sceneStrategy โดยอาศัยการกลับไปใช้ค่าเริ่มต้นสำหรับสถานการณ์บานหน้าต่างเดียว
// Define your navigation keys @Serializable data object ConversationList : NavKey @Serializable data class ConversationDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ConversationList) val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() NavDisplay( backStack = backStack, onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(listDetailStrategy), entryProvider = entryProvider { entry<ConversationList>( metadata = ListDetailSceneStrategy.listPane() ) { Column(modifier = Modifier.fillMaxSize()) { Text(text = "I'm a Conversation List") Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) { Text(text = "Open detail") } } } entry<ConversationDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { Text(text = "I'm a Conversation Detail") } } ) } private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) { // Remove any existing detail routes, then add the new detail route removeIf { it is ConversationDetail } add(detailRoute) }
หากไม่ต้องการสร้างฉากรายการ-รายละเอียดของคุณเอง คุณสามารถใช้ฉากรายการ-รายละเอียดของ Material ซึ่งมาพร้อมกับรายละเอียดที่สมเหตุสมผลและการรองรับตัวยึดตำแหน่ง ดังที่แสดงในส่วนถัดไป
แสดงเนื้อหารายการ-รายละเอียดในฉากแบบปรับได้ของ Material
สำหรับกรณีการใช้งานรายการ-รายละเอียด อาร์ติแฟกต์
androidx.compose.material3.adaptive:adaptive-navigation3 มี
ListDetailSceneStrategy ที่สร้างScene รายการ-รายละเอียด Scene นี้จะจัดการการจัดเรียงหลายบานหน้าต่างที่ซับซ้อน (รายการ รายละเอียด และบานหน้าต่างเพิ่มเติม) โดยอัตโนมัติและปรับให้เหมาะกับขนาดหน้าต่างและสถานะอุปกรณ์
หากต้องการสร้าง Scene รายการ-รายละเอียดของ Material ให้ทำตามขั้นตอนต่อไปนี้
- เพิ่มทรัพยากร Dependency: รวม
androidx.compose.material3.adaptive:adaptive-navigation3ในไฟล์build.gradle.ktsของโปรเจ็กต์ - กำหนดรายการด้วยข้อมูลเมตา
ListDetailSceneStrategy: ใช้listPane(), detailPane()และextraPane()เพื่อทำเครื่องหมายNavEntrysสำหรับ การแสดงบานหน้าต่างที่เหมาะสม ตัวช่วยlistPane()ยังช่วยให้คุณระบุdetailPlaceholderได้เมื่อไม่มีการเลือกรายการ - ใช้
rememberListDetailSceneStrategy(): ฟังก์ชันที่ใช้ร่วมกันได้นี้ มีListDetailSceneStrategyที่กำหนดค่าไว้ล่วงหน้าซึ่งNavDisplayสามารถใช้ได้
ข้อมูลโค้ดต่อไปนี้เป็นตัวอย่าง Activity ที่แสดงการใช้งาน ListDetailSceneStrategy
@Serializable object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Serializable data object Profile : NavKey class MaterialListDetailActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3AdaptiveApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Scaffold { paddingValues -> val backStack = rememberNavBackStack(ProductList) val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(listDetailStrategy), entryProvider = entryProvider { entry<ProductList>( metadata = ListDetailSceneStrategy.listPane( detailPlaceholder = { ContentYellow("Choose a product from the list") } ) ) { ContentRed("Welcome to Nav3") { Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View product") } } } entry<ProductDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { product -> ContentBlue("Product ${product.id} ", Modifier.background(PastelBlue)) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Button(onClick = { backStack.add(Profile) }) { Text("View profile") } } } } entry<Profile>( metadata = ListDetailSceneStrategy.extraPane() ) { ContentGreen("Profile") } } ) } } } }