Navigation 3 memperkenalkan sistem yang canggih dan fleksibel untuk mengelola alur UI aplikasi Anda melalui Scene. Scene memungkinkan Anda membuat tata letak yang sangat disesuaikan, beradaptasi dengan berbagai ukuran layar, dan mengelola pengalaman multipanel yang kompleks dengan lancar.
Memahami Tampilan
Di Navigasi 3, Scene
adalah unit dasar yang merender satu atau beberapa
instance NavEntry
. Anggap Scene
sebagai status visual atau bagian
UI yang berbeda yang dapat berisi dan mengelola tampilan konten dari
data sebelumnya.
Setiap instance Scene
diidentifikasi secara unik oleh key
dan class
Scene
itu sendiri. ID unik ini sangat penting karena mendorong
animasi tingkat atas saat Scene
berubah.
Antarmuka Scene
memiliki properti berikut:
key: Any
: ID unik untuk instanceScene
tertentu ini. Kunci ini, yang dikombinasikan dengan classScene
, memastikan perbedaan, terutama untuk tujuan animasi.entries: List<NavEntry<T>>
: Ini adalah daftar objekNavEntry
yang diperlihatkan olehScene
. Yang penting, jikaNavEntry
yang sama ditampilkan di beberapaScenes
selama transisi (misalnya, dalam transisi elemen bersama), kontennya hanya akan dirender olehScene
target terbaru yang menampilkannya.previousEntries: List<NavEntry<T>>
: Properti ini menentukanNavEntry
yang akan dihasilkan jika tindakan "kembali" terjadi dariScene
saat ini. Hal ini penting untuk menghitung status kembali prediktif yang tepat, sehinggaNavDisplay
dapat mengantisipasi dan bertransisi ke status sebelumnya yang benar, yang mungkin berupa Scene dengan class dan/atau kunci yang berbeda.content: @Composable () -> Unit
: Ini adalah fungsi composable tempat Anda menentukan caraScene
merenderentries
dan elemen UI di sekitarnya yang khusus untukScene
tersebut.
Memahami strategi tampilan
SceneStrategy
adalah mekanisme yang menentukan cara daftar
NavEntry
tertentu dari data sebelumnya harus diatur dan ditransisikan ke
Scene
. Pada dasarnya, saat ditampilkan dengan entri data sebelumnya saat ini, SceneStrategy
akan mengajukan dua pertanyaan utama kepada dirinya sendiri:
- Dapatkah saya membuat
Scene
dari entri ini? JikaSceneStrategy
menentukan bahwaNavEntry
yang diberikan dapat ditangani dan membentukScene
yang bermakna (misalnya, dialog atau tata letak multi-panel),SceneStrategy
akan dilanjutkan. Jika tidak, sistem akan menampilkannull
, sehingga strategi lain memiliki kesempatan untuk membuatScene
. - Jika ya, bagaimana cara mengatur entri tersebut ke dalam
Scene?
SetelahSceneStrategy
berkomitmen untuk menangani entri,SceneStrategy
akan mengambil tanggung jawab untuk membuatScene
dan menentukan caraNavEntry
yang ditentukan akan ditampilkan dalamScene
tersebut.
Inti dari SceneStrategy
adalah metode calculateScene
-nya:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Metode ini mengambil List<NavEntry<T>>
saat ini dari data sebelumnya dan
callback onBack
. Metode ini akan menampilkan Scene<T>
jika berhasil membentuknya
dari entri yang diberikan, atau null
jika tidak dapat.
SceneStrategy
juga menyediakan fungsi infix then
yang praktis, yang memungkinkan
Anda merantai beberapa strategi secara bersamaan. Hal ini akan membuat pipeline pengambilan keputusan yang fleksibel, dengan setiap strategi dapat mencoba menghitung Scene
, dan jika tidak dapat, strategi tersebut akan didelegasikan ke strategi berikutnya dalam rantai.
Cara kerja Scene dan strategi scene
NavDisplay
adalah composable pusat yang mengamati data sebelumnya dan
menggunakan SceneStrategy
untuk menentukan dan merender Scene
yang sesuai.
Parameter NavDisplay's sceneStrategy
mengharapkan SceneStrategy
yang
bertanggung jawab untuk menghitung Scene
yang akan ditampilkan. Jika tidak ada Scene
yang dihitung
oleh strategi yang disediakan (atau rantai strategi), NavDisplay
akan otomatis
kembali menggunakan SinglePaneSceneStrategy
secara default.
Berikut perincian interaksinya:
- Saat Anda menambahkan atau menghapus kunci dari data sebelumnya (misalnya, menggunakan
backStack.add()
ataubackStack.removeLastOrNull()
),NavDisplay
akan mengamati perubahan ini. NavDisplay
meneruskan daftarNavEntrys
saat ini (berasal dari kunci stack balik) ke metodeSceneStrategy's calculateScene
yang dikonfigurasi.- Jika
SceneStrategy
berhasil menampilkanScene
,NavDisplay
kemudian merendercontent
dariScene
tersebut.NavDisplay
juga mengelola animasi dan kembali prediktif berdasarkan propertiScene
.
Contoh: Tata letak panel tunggal (perilaku default)
Tata letak kustom paling sederhana yang dapat Anda miliki adalah tampilan panel tunggal, yang merupakan
perilaku default jika tidak ada SceneStrategy
lain yang lebih diutamakan.
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) ) }
Contoh: Tata letak dua panel dasar (Scene dan strategi kustom)
Contoh ini menunjukkan cara membuat tata letak dua panel sederhana yang diaktifkan berdasarkan dua kondisi:
- Lebar jendela cukup lebar untuk mendukung dua panel (yaitu, setidaknya
WIDTH_DP_MEDIUM_LOWER_BOUND
). - Dua entri teratas di data sebelumnya secara eksplisit mendeklarasikan dukungannya untuk ditampilkan dalam tata letak dua panel menggunakan metadata tertentu.
Cuplikan berikut adalah kode sumber gabungan untuk TwoPaneScene.kt
dan
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 } } }
Untuk menggunakan TwoPaneSceneStrategy
ini di NavDisplay
, ubah
panggilan entryProvider
untuk menyertakan metadata TwoPaneScene.twoPane()
untuk
entri yang ingin Anda tampilkan dalam tata letak dua panel. Kemudian, berikan
TwoPaneSceneStrategy()
sebagai sceneStrategy
, dengan mengandalkan penggantian
default untuk skenario panel tunggal:
// 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() } } } ) }
Menampilkan konten daftar-detail di Scene Adaptif Material
Untuk kasus penggunaan daftar-detail, artefak androidx.compose.material3.adaptive:adaptive-navigation3
menyediakan
ListDetailSceneStrategy
yang membuat Scene
daftar-detail. Scene
ini
otomatis menangani pengaturan multipanel yang kompleks (panel daftar, detail, dan
tambahan) serta menyesuaikannya berdasarkan ukuran jendela dan status perangkat.
Untuk membuat Scene
detail daftar Material, ikuti langkah-langkah berikut:
- Tambahkan dependensi: Sertakan
androidx.compose.material3.adaptive:adaptive-navigation3
dalam filebuild.gradle.kts
project Anda. - Tentukan entri dengan metadata
ListDetailSceneStrategy
: GunakanlistPane(), detailPane()
, danextraPane()
untuk menandaiNavEntrys
untuk tampilan panel yang sesuai. HelperlistPane()
juga memungkinkan Anda menentukandetailPlaceholder
saat tidak ada item yang dipilih. - Gunakan
rememberListDetailSceneStrategy
(): Fungsi composable ini menyediakanListDetailSceneStrategy
yang telah dikonfigurasi sebelumnya yang dapat digunakan olehNavDisplay
.
Cuplikan berikut adalah contoh Activity
yang menunjukkan penggunaan
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") } } ) } } } }