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 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 hình ảnh riêng biệt hoặc một phần 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 riêng biệt bằng key và lớp của chính Scene đó. Giá trị nhận dạng riêng biệt này rất quan trọng vì nó thúc đẩy ả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 riêng biệt cho thực thểScenecụ thể này. Khoá này, kết hợp với lớp củaScene, đảm bảo tính riêng biệt, chủ yếu cho mục đích ảnh động.entries: List<NavEntry<T>>: Đây là danh sách các đối tượngNavEntrymàScenechịu trách nhiệm hiển thị. Quan trọng là nếu cùng mộtNavEntryđược hiển thị trong nhiềuScenestrong quá trình chuyển đổi (ví dụ: trong quá trình chuyển đổi phần tử dùng chung), thì nội dung của phần tử đó sẽ chỉ được hiển thị bằngScenemục tiêu gần đây nhất đang hiển thị phần tử đó.previousEntries: List<NavEntry<T>>: Thuộc tính này xác địnhNavEntrysẽ xuất hiện nếu hành động "quay lại" xảy ra từScenehiện tại. Việc này rất cần thiết để tính toán trạng thái xem trước thao tác quay lại thích hợp, cho phépNavDisplaydự đ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, khoá khác hoặc cả hai.content: @Composable () -> Unit: Đây là hàm composable, nơi bạn xác định cáchScenehiển thịentriesvà mọi phần tử trên giao diện người dùng xung quanh dành riêng choSceneđó.metadata: Map<String, Any>: Cung cấp thông tin theo cảnh cụ thể cho các thành phần thư viện khác, chẳng hạn nhưNavDisplay. Theo mặc định, hàm này sẽ trả vềmetadatacủaNavEntrycuối cùng trongentries.
Triển khai equals và hashCode trong Cảnh tuỳ chỉnh
Các phương thức triển khai Scene tuỳ chỉnh phải triển khai chính xác equals và hashCode để hỗ trợ những việc sau:
- Chuyển đổi trạng thái cảnh: Mỗi khi một cảnh được tính toán theo danh sách chiến lược cảnh,
NavDisplaysẽ kiểm tra xem cảnh mới và cảnh trước đó có giống nhau hay không. Nếu phát hiện thấy thay đổi, hệ thống sẽ cập nhậtSeekableTransitiondùng để chuyển đổi giữa các cảnh, từ đó ảnh hưởng đến trạng thái vòng đời của các cảnh. - Quản lý lớp phủ: Đối với
OverlayScene(như hộp thoại),NavDisplaysử dụng chính đối tượng cảnh làmkeyđể theo dõi vòng đời của lớp phủ và ảnh động thoát.
Xác minh rằng tất cả thuộc tính xác định nội dung của cảnh, chẳng hạn như key, entries và previousEntries, đều có trong các phương thức triển khai equals và hashCode. Nhìn chung, hãy tránh đưa các lệnh gọi lại (như onBack) vào quá trình triển khai các phương thức này vì chúng có thể thay đổi các thực thể mà không thay đổi trạng thái cảnh logic.
Tìm hiểu các chiến lược về 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 cung cấp các mục nhập ngăn xếp lui hiện tại, SceneStrategy sẽ tự đặt ra hai câu hỏi chính:
- Tôi có thể tạo
Scenetừ những mục này không? NếuSceneStrategyxác định rằng nó có thể xử lýNavEntryđã cho và tạo thành mộtScenecó ý nghĩa (ví dụ: hộp thoại hoặc bố cục nhiều ngăn), thì nó sẽ tiếp tục. Nếu không, hàm này sẽ trả vềnull, tạo cơ hội cho các chiến lược khác 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 khiSceneStrategycam kết xử lý các mục, thì sẽ chịu trách nhiệm xây dựngScenevà xác định cách hiển thịNavEntryđược chỉ định trongSceneđó.
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à một hàm mở rộng trên SceneStrategyScope lấy List<NavEntry<T>> hiện tại từ ngăn xếp lui. Hàm này sẽ trả về một Scene<T> nếu có thể tạo thành công một
từ các mục được cung cấp, hoặc null nếu
không thể.
SceneStrategyScope chịu trách nhiệm duy trì mọi đối số không bắt buộc mà SceneStrategy có thể cần, chẳng hạn như lệnh gọi lại onBack.
Cách Cảnh và chiến lược cảnh phối hợp với nhau
NavDisplay là composable trung tâm quan sát ngăn xếp lui và sử dụng một hoặc nhiều SceneStrategy để xác định và hiển thị Scene thích hợp.
Tham số sceneStrategies của NavDisplay cần có danh sách các thực thể SceneStrategy chịu trách nhiệm tính toán Scene để hiển thị. Nếu không có Scene nào được tính toán theo các chiến lược đã cung cấp, thì NavDisplay sẽ tự động quay lại sử dụng SinglePaneSceneStrategy theo mặc định.
Dưới đây là thông tin chi tiết về hoạt động tương tác:
- Khi bạn thêm hoặc xoá khoá khỏi ngăn xếp lui (ví dụ: sử dụng
backStack.add()hoặcbackStack.removeLastOrNull()),NavDisplaysẽ quan sát những thay đổi này. NavDisplaytruyền danh sáchNavEntryhiện tại (bắt nguồn từ các khoá ngăn xếp lui) đếnsceneStrategiesđã định cấu hình theo thứ tự, gọicalculateScenetrên mỗi khoá cho đến khiSceneđược trả về.- Khi
SceneStrategytrả về thành côngScene,NavDisplaysẽ kết xuấtcontentcủaSceneđó.NavDisplaycũ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: 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) ) }
Ví dụ: Bố cục danh sách-chi tiết cơ bản (Cảnh và chiến lược tuỳ chỉnh)
Ví dụ này minh hoạ cách tạo bố cục danh sách-chi tiết đượ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
WIDTH_DP_MEDIUM_LOWER_BOUND). - Ngăn xếp lui chứa các mục đã khai báo khả năng hỗ trợ hiển thị trong bố cục danh sách-chi tiết bằng siêu dữ liệu cụ thể.
Đoạn mã sau đây là mã nguồn của ListDetailScene.kt và chứa cả ListDetailScene và 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) } } }
Để sử dụng ListDetailSceneStrategy này trong NavDisplay, hãy sửa đổi các lệnh gọi entryProvider để thêm siêu dữ liệu ListDetailScene.listPane() cho mục bạn định hiển thị dưới dạng bố cục danh sách và ListDetailScene.detailPane() cho mục bạn muốn hiển thị dưới dạng bố cục chi tiết. Sau đó, hãy cung cấp ListDetailSceneStrategy() làm sceneStrategy, dựa vào phương án dự phòng mặc định cho các trường hợp một ngăn:
// 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) }
Nếu không muốn tạo cảnh danh sách-chi tiết của riêng mình, bạn có thể sử dụng cảnh danh sách-chi tiết Material, đi kèm với các chi tiết hợp lý và hỗ trợ phần giữ chỗ, như được trình bày trong phần tiếp theo.
Hiển thị nội dung danh sách-chi tiết trong Material Adaptive Scene
Đố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 Scene danh sách-chi tiết. Scene này tự động xử lý các cách sắp xếp nhiều ngăn phức tạp (ngăn danh sách, ngăn chi tiết và ngăn bổ sung) và điều chỉnh các ngăn đó dựa trên kích thước cửa sổ và trạng thái thiết bị.
Để tạo Scene danh sách-chi tiết 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-navigation3vào tệpbuild.gradle.ktscủa dự án. - Xác định các mục nhập bằng siêu dữ liệu
ListDetailSceneStrategy: Sử dụnglistPane(), detailPane()vàextraPane()để đánh dấuNavEntryscủa bạn cho phù hợp với màn hình ngăn. Trình trợ giúplistPane()cũng cho phép bạn chỉ định mộtdetailPlaceholderkhi không có mục nào được chọn. - Sử dụng
rememberListDetailSceneStrategy(): Hàm composable này cung cấp mộtListDetailSceneStrategyđược định cấu hình sẵn màNavDisplaycó 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<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") } } ) } } } }