การนำทาง 3 นำเสนอระบบที่มีประสิทธิภาพและยืดหยุ่นสำหรับจัดการการไหลเวียน UI ของแอปผ่านฉาก ฉากช่วยให้คุณสร้างเลย์เอาต์ที่ปรับแต่งได้สูง ปรับให้เข้ากับขนาดหน้าจอต่างๆ และจัดการประสบการณ์การใช้งานแบบหลายแผงที่ซับซ้อนได้อย่างราบรื่น
ทําความเข้าใจฉาก
ในการนำทาง 3 Scene
คือหน่วยพื้นฐานที่แสดงผลอินสแตนซ์ NavEntry
อย่างน้อย 1 รายการ ให้มองว่า Scene
เป็นสถานะหรือส่วนภาพที่ชัดเจนของ UI ที่สามารถบรรจุและจัดการการแสดงเนื้อหาจากแบ็กสแต็ก
อินสแตนซ์ Scene
แต่ละรายการจะระบุได้โดยไม่ซ้ำกันด้วย key
และคลาสของ Scene
นั้นๆ ตัวระบุที่ไม่ซ้ำกันนี้มีความสําคัญเนื่องจากเป็นตัวขับเคลื่อนภาพเคลื่อนไหวระดับบนสุดเมื่อ Scene
มีการเปลี่ยนแปลง
อินเทอร์เฟซ Scene
มีพร็อพเพอร์ตี้ต่อไปนี้
key: Any
: ตัวระบุที่ไม่ซ้ำกันสำหรับอินสแตนซ์Scene
ที่เฉพาะเจาะจงนี้ การใช้คีย์นี้ร่วมกับคลาสของScene
จะช่วยให้มั่นใจได้ว่าจะมีความแตกต่างกัน โดยเฉพาะเพื่อวัตถุประสงค์ในการใส่ภาพเคลื่อนไหวentries: List<NavEntry<T>>
: นี่คือรายการออบเจ็กต์NavEntry
ที่Scene
มีหน้าที่แสดง สิ่งที่สําคัญคือ หากNavEntry
เดียวกันแสดงในScenes
หลายรายการระหว่างการเปลี่ยน (เช่น ในการเปลี่ยนองค์ประกอบที่แชร์)Scene
เป้าหมายล่าสุดที่แสดงNavEntry
ดังกล่าวจะเป็นผู้แสดงผลเนื้อหาเท่านั้นpreviousEntries: List<NavEntry<T>>
: พร็อพเพอร์ตี้นี้กําหนดNavEntry
ที่จะเป็นผลลัพธ์หากเกิดการดำเนินการ "กลับ" จากScene
ปัจจุบัน ซึ่งจำเป็นต่อการคำนวณสถานะย้อนกลับตามการคาดการณ์ที่เหมาะสม ซึ่งจะช่วยให้NavDisplay
คาดการณ์และเปลี่ยนไปยังสถานะก่อนหน้าที่ถูกต้องได้ ซึ่งอาจเป็นฉากที่มีคลาสและ/หรือคีย์อื่นcontent: @Composable () -> Unit
: นี่คือฟังก์ชันคอมโพสิเบิลที่คุณกำหนดวิธีให้Scene
แสดงผลentries
และองค์ประกอบ UI รอบๆ ของScene
นั้นโดยเฉพาะ
ทําความเข้าใจกลยุทธ์ฉาก
SceneStrategy
คือกลไกที่กำหนดวิธีจัดเรียงรายการ NavEntry
จากกองซ้อนด้านหลังและเปลี่ยนเป็น Scene
โดยพื้นฐานแล้ว เมื่อแสดงรายการสแต็กย้อนกลับปัจจุบัน 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>?
วิธีนี้จะนํา List<NavEntry<T>>
ปัจจุบันจากกองซ้อนด้านหลังและ onBack
การเรียกกลับ โดยควรแสดงผลเป็น Scene<T>
หากสร้างรายการได้สําเร็จจากรายการที่ระบุ หรือ null
หากสร้างไม่ได้
SceneStrategy
ยังมีฟังก์ชัน then
อินฟิกซ์ที่สะดวก ซึ่งช่วยให้คุณต่อเชื่อมกลยุทธ์หลายรายการเข้าด้วยกันได้ วิธีนี้สร้างไปป์ไลน์การทําการตัดสินใจที่ยืดหยุ่น ซึ่งกลยุทธ์แต่ละรายการจะพยายามคํานวณ Scene
ได้ และหากคํานวณไม่ได้ ก็จะมอบหมายให้กลยุทธ์ถัดไปในเชน
ฉากและกลยุทธ์ฉากทํางานร่วมกันอย่างไร
NavDisplay
คือคอมโพสิชันส่วนกลางที่ตรวจสอบกองซ้อนด้านหลังและใช้ 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: T, 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.invoke(entry.key) } } /** * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the * list. */ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { @Composable override fun calculateScene(entries: List<NavEntry<T>>, onBack: (Int) -> Unit): Scene<T> = SinglePaneScene( key = entries.last().key, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
ตัวอย่าง: เลย์เอาต์แบบ 2 แผงพื้นฐาน (ฉากและกลยุทธ์ที่กําหนดเอง)
ตัวอย่างนี้แสดงวิธีสร้างเลย์เอาต์แบบ 2 แผงง่ายๆ ที่เปิดใช้งานตามเงื่อนไข 2 ข้อต่อไปนี้
- ความกว้างของหน้าต่างกว้างพอที่จะรองรับแผง 2 แผง (นั่นคือ
WIDTH_DP_MEDIUM_LOWER_BOUND
ขึ้นไป) - รายการ 2 รายการแรกในกองซ้อนด้านหลังประกาศอย่างชัดเจนว่ารองรับการแสดงผลในเลย์เอาต์แบบ 2 แผงโดยใช้ข้อมูลเมตาที่เฉพาะเจาะจง
ข้อมูลโค้ดต่อไปนี้คือซอร์สโค้ดที่รวมกันของ TwoPaneScene.kt
และ
TwoPaneSceneStrategy.kt
// --- TwoPaneScene --- /** * A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split. */ class TwoPaneScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val firstEntry: NavEntry<T>, val secondEntry: NavEntry<T> ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(firstEntry, secondEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.5f)) { firstEntry.content.invoke(firstEntry.key) } Column(modifier = Modifier.weight(0.5f)) { secondEntry.content.invoke(secondEntry.key) } } } companion object { internal const val TWO_PANE_KEY = "TwoPane" /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * in a two-pane layout. */ fun twoPane() = mapOf(TWO_PANE_KEY to true) } } // --- TwoPaneSceneStrategy --- /** * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough * and the top two back stack entries declare support for two-pane display. */ class TwoPaneSceneStrategy<T : Any> : SceneStrategy<T> { @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class) @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val lastTwoEntries = entries.takeLast(2) // Condition 2: Only return a Scene if there are two entries, and both have declared // they can be displayed in a two pane scene. return if (lastTwoEntries.size == 2 && lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) } ) { val firstEntry = lastTwoEntries.first() val secondEntry = lastTwoEntries.last() // The scene key must uniquely represent the state of the scene. val sceneKey = Pair(firstEntry.key, secondEntry.key) TwoPaneScene( key = sceneKey, // Where we go back to is a UX decision. In this case, we only remove the top // entry from the back stack, despite displaying two entries in this scene. // This is because in this app we only ever add one entry to the // back stack at a time. It would therefore be confusing to the user to add one // when navigating forward, but remove two when navigating back. previousEntries = entries.dropLast(1), firstEntry = firstEntry, secondEntry = secondEntry ) } else { null } } }
หากต้องการใช้ TwoPaneSceneStrategy
นี้ใน NavDisplay
ให้แก้ไขการเรียกใช้ entryProvider
ให้รวมข้อมูลเมตา TwoPaneScene.twoPane()
ของรายการที่คุณต้องการแสดงในเลย์เอาต์แบบ 2 แผง จากนั้นให้ป้อน TwoPaneSceneStrategy()
เป็น sceneStrategy
โดยอาศัยค่าเริ่มต้นสำหรับการสำรองสำหรับสถานการณ์แบบหน้าจอเดียว
// Define your navigation keys @Serializable data object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ProductList) NavDisplay( backStack = backStack, entryProvider = entryProvider { entry<ProductList>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Column { Text("Product List") Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View Details for ABC (Two-Pane Eligible)") } } } entry<ProductDetail>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Text("Product Detail: ${key.id} (Two-Pane Eligible)") } // ... other entries ... }, // Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically. sceneStrategy = TwoPaneSceneStrategy<Any>(), onBack = { count -> repeat(count) { if (backStack.isNotEmpty()) { backStack.removeLastOrNull() } } } ) }
แสดงเนื้อหารายละเอียดรายการในฉากแบบปรับเปลี่ยนได้ของ Material
สําหรับกรณีการใช้งานรายการแบบละเอียด อาร์ติแฟกต์ androidx.compose.material3.adaptive:adaptive-navigation3
จะมี ListDetailSceneStrategy
ที่สร้าง Scene
แบบละเอียด Scene
นี้จะจัดการการจัดเรียงแบบหลายแผงที่ซับซ้อน (รายการ รายละเอียด และแผงเพิ่มเติม) โดยอัตโนมัติ และปรับการจัดเรียงตามขนาดหน้าต่างและสถานะของอุปกรณ์
หากต้องการสร้างรายละเอียดรายการวัสดุ 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<Any>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, sceneStrategy = 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") } } ) } } } }