Sahneleri kullanarak özel düzenler oluşturma

Navigation 3, uygulamanızın kullanıcı arayüzü akışını Sahneler aracılığıyla yönetmek için güçlü ve esnek bir sistem sunar. Sahneler, son derece özelleştirilmiş düzenler oluşturmanıza, farklı ekran boyutlarına uyum sağlamanıza ve karmaşık çok panelli deneyimleri sorunsuz bir şekilde yönetmenize olanak tanır.

Sahneleri Anlama

3. sürüm gezinmede Scene, bir veya daha fazla NavEntry örneğini oluşturmak için kullanılan temel birimdir. Scene, kullanıcı arayüzünüzün arka yığınınızdaki içeriklerin gösterimini içerebilen ve yönetebilen ayrı bir görsel durumu veya bölümü olarak düşünülebilir.

Her Scene örneği, key ve Scene sınıfı ile benzersiz şekilde tanımlanır. Bu benzersiz tanımlayıcı, Scene değiştiğinde üst düzey animasyonu yönlendirdiği için çok önemlidir.

Scene arayüzü aşağıdaki özelliklere sahiptir:

  • key: Any: Bu belirli Scene örneği için benzersiz bir tanımlayıcı. Bu anahtar, Scene sınıfıyla birlikte özellikle animasyon amacıyla ayırt ediciliği sağlar.
  • entries: List<NavEntry<T>>: Bu, Scene öğesinin görüntülemekten sorumlu olduğu NavEntry nesnelerinin listesidir. Önemli olarak, aynı NavEntry bir geçiş sırasında birden fazla Scenes içinde gösteriliyorsa (ör. paylaşılan bir öğe geçişinde) içeriği yalnızca bunu görüntüleyen en son hedef Scene tarafından oluşturulur.
  • previousEntries: List<NavEntry<T>>: Bu özellik, mevcut Scene konumundan "geri" işlemi yapılması durumunda ortaya çıkacak NavEntry'leri tanımlar. Bu, doğru tahmini geri durumunu hesaplamak için gereklidir. NavDisplay, farklı bir sınıfa ve/veya anahtara sahip bir Sahne olabilecek doğru önceki durumu tahmin edip bu duruma geçiş yapabilir.
  • content: @Composable () -> Unit: Bu, Scene öğesinin entries öğesini ve bu Scene öğesine özgü tüm çevreleyen kullanıcı arayüzü öğelerini nasıl oluşturduğunu tanımladığınız birleştirilebilir işlevdir.

Sahne stratejilerini anlama

SceneStrategy, geri yığındaki belirli bir NavEntry listesinin nasıl düzenlenmesi ve Scene'ye nasıl geçirilmesi gerektiğini belirleyen mekanizmadır. Temel olarak, mevcut geri yığın girişleri sunulduğunda bir SceneStrategy kendisiyle ilgili iki temel soru sorar:

  1. Bu girişlerden Scene oluşturabilir miyim? SceneStrategy, NavEntry'leri işleyebileceğini ve anlamlı bir Scene oluşturabileceğini (ör. iletişim kutusu veya çok panelli düzen) belirlerse işleme devam eder. Aksi takdirde, diğer stratejilerin null oluşturmasına olanak tanımak için null değerini döndürür.Scene
  2. Bu durumda, bu girişleri Scene? Bir SceneStrategy, girişleri işlemeyi kabul ettiğinde Scene oluşturma ve belirtilen NavEntry'ların bu Scene içinde nasıl görüntüleneceğini tanımlama sorumluluğunu üstlenir.

SceneStrategy'nın temelinde calculateScene yöntemi bulunur:

@Composable
public fun calculateScene(
    entries: List<NavEntry<T>>,
    onBack: (count: Int) -> Unit,
): Scene<T>?

Bu yöntem, arka yığında geçerli List<NavEntry<T>> öğesini alan bir SceneStrategyScope üzerindeki uzantı işlevidir. Sağlanan girişlerden başarılı bir şekilde oluşturabiliyorsa Scene<T>, oluşturamıyorsa null döndürmelidir.

SceneStrategyScope, SceneStrategy'nin ihtiyaç duyabileceği isteğe bağlı bağımsız değişkenleri (ör. onBack geri çağırma) korumakla sorumludur.

SceneStrategy ayrıca, birden fazla stratejiyi birbirine bağlamanıza olanak tanıyan kullanışlı bir then infix işlevi de sunar. Bu, her stratejinin Scene hesaplamaya çalışabileceği ve hesaplayamadığı takdirde zincirdeki bir sonraki stratejiye devredeceği esnek bir karar verme hattı oluşturur.

Sahneler ve sahne stratejileri birlikte nasıl çalışır?

NavDisplay, arka yığını gözlemleyen ve uygun Scene'ı belirleyip oluşturmak için SceneStrategy kullanan merkezi birleştirilebilir öğedir.

NavDisplay's sceneStrategy parametresi, görüntülenecek Scene değerini hesaplamaktan sorumlu bir SceneStrategy bekler. Sağlanan strateji (veya strateji zinciri) tarafından Scene hesaplanmazsa NavDisplay varsayılan olarak otomatik olarak SinglePaneSceneStrategy kullanmaya geri döner.

Etkileşimin dökümü:

  • Geriye gitme yığınıza anahtar eklediğinizde veya anahtar kaldırdığınızda (ör. backStack.add() veya backStack.removeLastOrNull() kullanarak) NavDisplay bu değişiklikleri gözlemler.
  • NavDisplay, yapılandırılmış SceneStrategy's calculateScene yöntemine NavEntrys öğelerinin mevcut listesini (arka yığın anahtarlarından türetilmiştir) iletir.
  • SceneStrategy, Scene öğesini başarıyla döndürürse NavDisplay, Scene öğesinin content öğesini oluşturur. NavDisplay ayrıca Scene özelliklerine göre animasyonları ve tahmini geri özelliğini de yönetir.

Örnek: Tek bölmeli düzen (varsayılan davranış)

En basit özel düzen, tek bölmeli bir ekrandır. Başka bir SceneStrategy öncelikli değilse varsayılan davranış budur.

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> {
    @Composable
    override fun calculateScene(entries: List<NavEntry<T>>, onBack: (Int) -> Unit): Scene<T> =
        SinglePaneScene(
            key = entries.last().contentKey,
            entry = entries.last(),
            previousEntries = entries.dropLast(1)
        )
}

Örnek: Temel iki panelli düzen (özel sahne ve strateji)

Bu örnekte, iki koşula göre etkinleştirilen basit bir iki panelli düzenin nasıl oluşturulacağı gösterilmektedir:

  1. Pencere genişliği, iki bölmeyi destekleyecek kadar geniştir (ör. en az WIDTH_DP_MEDIUM_LOWER_BOUND).
  2. Geri yığındaki ilk iki giriş, belirli meta veriler kullanılarak iki panelli düzende gösterilmeyi desteklediğini açıkça belirtir.

Aşağıdaki snippet, TwoPaneScene.kt ve TwoPaneSceneStrategy.kt için birleştirilmiş kaynak kodudur:

// --- 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()
            }
            Column(modifier = Modifier.weight(0.5f)) {
                secondEntry.Content()
            }
        }
    }

    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.contentKey, secondEntry.contentKey)

            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
        }
    }
}

Bu TwoPaneSceneStrategy özelliğini NavDisplay içinde kullanmak için entryProvider çağrılarınızı, iki panelli düzende göstermeyi planladığınız girişler için TwoPaneScene.twoPane() meta verilerini içerecek şekilde değiştirin. Ardından, tek pencereli senaryolarda varsayılan geri dönüşe güvenerek TwoPaneSceneStrategy() değerini sceneStrategy olarak sağlayı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()
                }
            }
        }
    )
}

Liste-ayrıntı içeriğini Material Adaptive Scene'de görüntüleme

Liste-ayrıntı kullanım alanında, androidx.compose.material3.adaptive:adaptive-navigation3 yapısı, liste-ayrıntı Scene oluşturan bir ListDetailSceneStrategy sağlar. Bu Scene karmaşık çok panelli düzenlemeleri (liste, ayrıntı ve ek paneller) otomatik olarak yönetir ve bunları pencere boyutuna ve cihaz durumuna göre uyarlar.

Bir Material liste-ayrıntı Scene oluşturmak için aşağıdaki adımları uygulayın:

  1. Bağımlılığı ekleyin: Projenizin build.gradle.kts dosyasına androidx.compose.material3.adaptive:adaptive-navigation3 öğesini ekleyin.
  2. Girişlerinizi ListDetailSceneStrategy meta verileriyle tanımlayın: NavEntrys öğenizi uygun bölme görünümü için işaretlemek üzere listPane(), detailPane() ve extraPane() kullanın. listPane() yardımcı aracı, öğe seçilmediğinde de listPane() belirtmenize olanak tanır.detailPlaceholder
  3. rememberListDetailSceneStrategy() işlevini kullanın: Bu composable işlev, NavDisplay tarafından kullanılabilen önceden yapılandırılmış bir ListDetailSceneStrategy sağlar.

Aşağıdaki snippet, Activity kullanımını gösteren örnek bir ListDetailSceneStrategy'dir:

@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")
                        }
                    }
                )
            }
        }
    }
}

Şekil 1. Material list-detail Scene'de çalışan örnek içerik.