Navigation 3 ขอแนะนำระบบที่ทรงประสิทธิภาพและยืดหยุ่นสำหรับการจัดการโฟลว์ UI ของแอปผ่าน Scene โดยฉากจะช่วยให้คุณสร้างเลย์เอาต์ที่ปรับแต่งได้สูง ปรับให้เข้ากับขนาดหน้าจอต่างๆ และจัดการประสบการณ์แบบหลายบานหน้าต่างที่ซับซ้อน ได้อย่างราบรื่น
ทำความเข้าใจฉาก
ใน 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เป้าหมายล่าสุด ที่แสดงNavEntrypreviousEntries: List<NavEntry<T>>: พร็อพเพอร์ตี้นี้กำหนดNavEntryที่จะเกิดขึ้นหากมีการดำเนินการ "ย้อนกลับ" จากSceneปัจจุบัน ซึ่งจำเป็นต่อการคำนวณสถานะย้อนกลับเชิงคาดการณ์ที่เหมาะสม เพื่อให้NavDisplayคาดการณ์และเปลี่ยนไปยังสถานะก่อนหน้าที่ถูกต้องได้ ซึ่งอาจเป็น Scene ที่มีคลาสและ/หรือคีย์ต่างกันcontent: @Composable () -> Unit: นี่คือฟังก์ชันที่ประกอบกันได้ซึ่งคุณกำหนดวิธีที่Sceneแสดงผลentriesและองค์ประกอบ UI โดยรอบที่เฉพาะเจาะจงสำหรับSceneนั้นmetadata: Map<String, Any>: ให้ข้อมูลเฉพาะฉากแก่คอมโพเนนต์อื่นๆ ของไลบรารี เช่นNavDisplayโดยค่าเริ่มต้น จะแสดงmetadataของNavEntryสุดท้ายในentries
ทำความเข้าใจกลยุทธ์ฉาก
SceneStrategy คือกลไกที่กำหนดวิธีจัดเรียงและเปลี่ยนรายการ NavEntry จาก Back Stack ไปยัง Scene โดยพื้นฐานแล้ว เมื่อได้รับรายการใน Back Stack ปัจจุบัน a
SceneStrategy จะถามตัวเองด้วยคำถามสำคัญ 2 ข้อต่อไปนี้
- ฉันจะสร้าง
Sceneจากรายการเหล่านี้ได้ไหม หากSceneStrategyพิจารณาแล้วว่าสามารถจัดการNavEntryที่ระบุและสร้างSceneที่มีความหมายได้ (เช่น กล่องโต้ตอบหรือเลย์เอาต์หลายบานหน้าต่าง) ก็จะดำเนินการต่อ ไม่เช่นนั้น ระบบจะแสดงผลnullเพื่อให้กลยุทธ์อื่นๆ มีโอกาสสร้างScene - หากเป็นเช่นนั้น ฉันควรจัดเรียงรายการเหล่านั้นใน
Scene?อย่างไร เมื่อSceneStrategyตกลงที่จะจัดการรายการแล้ว ก็จะรับผิดชอบในการสร้างSceneและกำหนดวิธีแสดงNavEntryที่ระบุภายในSceneนั้น
หัวใจสำคัญของ SceneStrategy คือวิธีการ calculateScene ดังนี้
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
เมธอดนี้เป็นฟังก์ชันส่วนขยายใน SceneStrategyScope ที่ใช้ List<NavEntry<T>> ปัจจุบันจากสแต็กย้อนกลับ โดยควรแสดง Scene<T> หากสร้างได้สำเร็จจากรายการที่ระบุ หรือ null หากสร้างไม่ได้
SceneStrategyScope มีหน้าที่รับผิดชอบในการดูแลอาร์กิวเมนต์ที่ไม่บังคับ
ที่ SceneStrategy อาจต้องใช้ เช่น การเรียกกลับ onBack
ฉากและกลยุทธ์ฉากทำงานร่วมกันอย่างไร
NavDisplay คือ Composable ส่วนกลางที่สังเกต Back Stack และใช้ SceneStrategy เพื่อกำหนดและแสดงผล Scene ที่เหมาะสม
พารามิเตอร์ NavDisplay's sceneStrategy คาดหวัง SceneStrategy ที่
รับผิดชอบในการคำนวณ Scene ที่จะแสดง หากกลยุทธ์ (หรือกลยุทธ์ที่เชื่อมโยงกัน) ที่ระบุไม่สามารถคำนวณ Scene ได้
NavDisplay จะเปลี่ยนไปใช้ SinglePaneSceneStrategy โดยอัตโนมัติ
โดยค่าเริ่มต้น
รายละเอียดการโต้ตอบมีดังนี้
- เมื่อคุณเพิ่มหรือนำคีย์ออกจากสแต็กย้อนกลับ (เช่น ใช้
backStack.add()หรือbackStack.removeLastOrNull())NavDisplayจะสังเกตการเปลี่ยนแปลงเหล่านี้ NavDisplayจะส่งรายการNavEntrysปัจจุบัน (ได้มาจากคีย์ของแบ็กเอนด์) ไปยังเมธอดSceneStrategy's calculateSceneที่กำหนดค่าไว้- หาก
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. * */ 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
จัดการการจัดเรียงแบบหลายบานหน้าต่างที่ซับซ้อน (รายการ รายละเอียด และบานหน้าต่างเพิ่มเติม) โดยอัตโนมัติ และปรับให้เหมาะกับขนาดหน้าต่างและสถานะของอุปกรณ์
หากต้องการสร้างรายการ-รายละเอียดของ Material Scene ให้ทำตามขั้นตอนต่อไปนี้
- เพิ่มทรัพยากร 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") } } ) } } } }