Navigation 3 giới thiệu một hệ thống mạnh mẽ và linh hoạt để quản lý luồng giao diện người dùng của ứng dụng thông qua Cảnh. Cảnh cho phép bạn tạo bố cục được tuỳ chỉnh cao, thích ứng với nhiều kích thước màn hình và quản lý liền mạch các trải nghiệm phức tạp trên nhiều ngăn.
Tìm hiểu về Cảnh
Trong Navigation 3, Scene
là đơn vị cơ bản hiển thị một hoặc nhiều thực thể NavEntry
. Hãy coi Scene
là một trạng thái hoặc phần hình ảnh riêng biệt của giao diện người dùng có thể chứa và quản lý việc hiển thị nội dung từ ngăn xếp lui.
Mỗi thực thể Scene
được xác định duy nhất bằng key
và lớp của chính Scene
. Giá trị nhận dạng duy nhất này rất quan trọng vì nó điều khiển ảnh động cấp cao nhất khi Scene
thay đổi.
Giao diện Scene
có các thuộc tính sau:
key: Any
: Giá trị nhận dạng duy nhất cho thực thểScene
cụ thể này. Khoá này, kết hợp với lớp củaScene
, đảm bảo sự khác biệt, chủ yếu là cho mục đích ảnh động.entries: List<NavEntry<T>>
: Đây là danh sách các đối tượngNavEntry
màScene
chịu trách nhiệm hiển thị. Quan trọng là nếu cùng mộtNavEntry
xuất hiện trong nhiềuScenes
trong quá trình chuyển đổi (ví dụ: trong quá trình chuyển đổi phần tử dùng chung), nội dung củaNavEntry
đó sẽ chỉ được hiển thị bởiScene
mục tiêu gần đây nhất đang hiển thị nội dung đó.previousEntries: List<NavEntry<T>>
: Thuộc tính này xác định cácNavEntry
sẽ xảy ra nếu một thao tác "quay lại" xảy ra từScene
hiện tại. Điều này rất cần thiết để tính toán trạng thái quay lại dự đoán thích hợp, cho phépNavDisplay
dự đoán và chuyển đổi sang trạng thái trước đó chính xác, có thể là một Cảnh có lớp và/hoặc khoá khác.content: @Composable () -> Unit
: Đây là hàm có khả năng kết hợp mà bạn xác định cáchScene
hiển thịentries
và mọi phần tử giao diện người dùng xung quanh dành riêng choScene
đó.
Tìm hiểu các chiến lược cảnh
SceneStrategy
là cơ chế xác định cách sắp xếp và chuyển đổi một danh sách NavEntry
nhất định từ ngăn xếp lui thành Scene
. Về cơ bản, khi được trình bày với các mục ngăn xếp lui hiện tại, SceneStrategy
sẽ tự hỏi hai câu hỏi chính:
- Tôi có thể tạo
Scene
từ các mục nhập này không? NếuSceneStrategy
xác định rằng nó có thể xử lý cácNavEntry
đã cho và tạo thành mộtScene
có ý nghĩa (ví dụ: hộp thoại hoặc bố cục nhiều ngăn), thì quá trình này sẽ tiếp tục. Nếu không, hàm này sẽ trả vềnull
, cho phép các chiến lược khác có cơ hội tạoScene
. - Nếu có, tôi nên sắp xếp các mục đó vào
Scene?
như thế nào? Sau khiSceneStrategy
cam kết xử lý các mục, lớp này sẽ chịu trách nhiệm tạoScene
và xác định cách hiển thị cácNavEntry
đã chỉ định trongScene
đó.
Phần cốt lõi của SceneStrategy
là phương thức calculateScene
:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Phương thức này lấy List<NavEntry<T>>
hiện tại từ ngăn xếp lui và lệnh gọi lại onBack
. Phương thức này sẽ trả về Scene<T>
nếu có thể tạo thành công một mảng từ các mục nhập được cung cấp hoặc null
nếu không thể.
SceneStrategy
cũng cung cấp một hàm infix then
thuận tiện, cho phép bạn liên kết nhiều chiến lược với nhau. Điều này tạo ra một quy trình ra quyết định linh hoạt, trong đó mỗi chiến lược có thể cố gắng tính toán Scene
và nếu không thể, chiến lược đó sẽ uỷ quyền cho chiến lược tiếp theo trong chuỗi.
Cách Cảnh và chiến lược cảnh hoạt động cùng nhau
NavDisplay
là thành phần kết hợp trung tâm quan sát ngăn xếp lui và sử dụng SceneStrategy
để xác định và hiển thị Scene
thích hợp.
Tham số NavDisplay's sceneStrategy
dự kiến một SceneStrategy
chịu trách nhiệm tính toán Scene
để hiển thị. Nếu chiến lược được cung cấp (hoặc chuỗi chiến lược) không tính toán được Scene
nào, thì theo mặc định, NavDisplay
sẽ tự động quay lại sử dụng SinglePaneSceneStrategy
.
Dưới đây là thông tin chi tiết về lượt tương tác:
- Khi bạn thêm hoặc xoá các khoá khỏi ngăn xếp lui (ví dụ: sử dụng
backStack.add()
hoặcbackStack.removeLastOrNull()
),NavDisplay
sẽ quan sát những thay đổi này. NavDisplay
truyền danh sáchNavEntrys
hiện tại (có nguồn gốc từ các khoá ngăn xếp lui) đến phương thứcSceneStrategy's calculateScene
đã định cấu hình.- Nếu
SceneStrategy
trả về thành công mộtScene
, thìNavDisplay
sẽ hiển thịcontent
củaScene
đó.NavDisplay
cũng quản lý ảnh động và tính năng xem trước thao tác quay lại dựa trên các thuộc tính củaScene
.
Ví dụ: Bố cục một ngăn (hành vi mặc định)
Bố cục tuỳ chỉnh đơn giản nhất mà bạn có thể có là màn hình một ngăn, đây là hành vi mặc định nếu không có SceneStrategy
nào khác được ưu tiên.
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) ) }
Ví dụ: Bố cục hai ngăn cơ bản (Cảnh và chiến lược tuỳ chỉnh)
Ví dụ này minh hoạ cách tạo một bố cục hai ngăn đơn giản được kích hoạt dựa trên hai điều kiện:
- Chiều rộng cửa sổ đủ rộng để hỗ trợ hai ngăn (tức là ít nhất là
WIDTH_DP_MEDIUM_LOWER_BOUND
). - Hai mục đầu tiên trên ngăn xếp lui khai báo rõ ràng khả năng hỗ trợ hiển thị trong bố cục hai ngăn bằng siêu dữ liệu cụ thể.
Đoạn mã sau đây là mã nguồn kết hợp cho TwoPaneScene.kt
và 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 } } }
Để sử dụng TwoPaneSceneStrategy
này trong NavDisplay
, hãy sửa đổi lệnh gọi entryProvider
để thêm siêu dữ liệu TwoPaneScene.twoPane()
cho các mục mà bạn dự định hiển thị trong bố cục hai ngăn. Sau đó, hãy cung cấp TwoPaneSceneStrategy()
làm sceneStrategy
, dựa vào phương án dự phòng mặc định cho các trường hợp có một ngăn:
// 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() } } } ) }
Hiển thị nội dung danh sách-chi tiết trong Cảnh thích ứng Material
Đối với trường hợp sử dụng danh sách-chi tiết, cấu phần phần mềm androidx.compose.material3.adaptive:adaptive-navigation3
cung cấp một ListDetailSceneStrategy
tạo danh sách-chi tiết Scene
. Scene
này tự động xử lý các bố cục nhiều ngăn phức tạp (danh sách, chi tiết và các ngăn bổ sung) và điều chỉnh các bố cục đó dựa trên kích thước cửa sổ và trạng thái thiết bị.
Để tạo Scene
chi tiết danh sách Material, hãy làm theo các bước sau:
- Thêm phần phụ thuộc: Thêm
androidx.compose.material3.adaptive:adaptive-navigation3
vào tệpbuild.gradle.kts
của dự án. - Xác định mục nhập bằng siêu dữ liệu
ListDetailSceneStrategy
: Sử dụnglistPane(), detailPane()
vàextraPane()
để đánh dấuNavEntrys
cho việc hiển thị ngăn thích hợp. Trình trợ giúplistPane()
cũng cho phép bạn chỉ định mộtdetailPlaceholder
khi không có mục nào được chọn. - Sử dụng
rememberListDetailSceneStrategy
(): Hàm có khả năng kết hợp này cung cấp mộtListDetailSceneStrategy
được định cấu hình trước màNavDisplay
có thể sử dụng.
Đoạn mã sau đây là một Activity
mẫu minh hoạ cách sử dụng 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") } } ) } } } }