Sahneleri kullanarak özel düzenler oluşturma

Gezinme 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

Navigation 3'te Scene, bir veya daha fazla NavEntry örneğini oluşturan temel birimdir. Scene'ü, kullanıcı arayüzünüzün arka yığınınızdaki içeriğin görüntülenmesini içerebilen ve yönetebilen ayrı bir görsel durum veya bölüm olarak düşünebilirsiniz.

Her Scene örneği, key ve Scene sınıfıyla 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 Scene örneği için benzersiz bir tanımlayıcıdır. Bu anahtar, Scene sınıfıyla birlikte, özellikle animasyon amacıyla ayrımlılık sağlar.
  • entries: List<NavEntry<T>>: Bu, Scene'nin görüntülemekten sorumlu olduğu NavEntry nesnelerinin bir listesidir. Önemli bir nokta, aynı NavEntry bir geçiş sırasında birden fazla Scenes'te görüntülenirse (ör. paylaşılan bir öğe geçişinde) içeriği yalnızca görüntüleyen en son hedef Scene tarafından oluşturulur.
  • previousEntries: List<NavEntry<T>>: Bu özellik, geçerli Scene'den "geri" işleminin yapılması durumunda ortaya çıkacak NavEntry'leri tanımlar. Doğru tahmini geri durumu hesaplamak için gereklidir. Bu sayede NavDisplay, farklı bir sınıfa ve/veya anahtara sahip bir sahne olabilecek doğru önceki duruma geçiş yapabilir.
  • content: @Composable () -> Unit: Bu, Scene'ın entries öğesini ve bu Scene'a özgü çevreleyen kullanıcı arayüzü öğelerini nasıl oluşturacağını tanımladığınız birleştirilebilir işlevdir.

Sahne stratejilerini anlama

SceneStrategy, arka yığıntaki belirli bir NavEntry listesinin nasıl düzenleneceğini ve bir Scene'ye nasıl geçileceğini belirleyen mekanizmadır. Temel olarak, mevcut geri yığın girişleri sunulduğunda SceneStrategy kendisine iki önemli soru sorar:

  1. Bu girişlerden Scene oluşturabilir miyim? SceneStrategy, verilen NavEntry'leri işleyebileceğini ve anlamlı bir Scene (ör. iletişim kutusu veya çok bölmeli düzen) oluşturabileceğini belirlerse devam eder. Aksi takdirde null döndürülür ve diğer stratejilere Scene oluşturma şansı verilir.
  2. Eğer öyleyse bu girişleri Scene? içinde nasıl düzenleyeceğim? Bir SceneStrategy, girişleri işlemeye söz verdiğinde bir Scene oluşturma ve belirtilen NavEntry'lerin bu Scene içinde nasıl gösterileceğini tanımlama sorumluluğunu üstlenir.

SceneStrategy'lerin temelinde calculateScene yöntemi bulunur:

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

Bu yöntem, arka yığıntan geçerli List<NavEntry<T>>'yi ve bir onBack geri çağırma işlevini alır. Sağlanan girişlerden başarılı bir şekilde oluşturabilirse Scene<T>, oluşturamazsa null döndürmelidir.

SceneStrategy, birden fazla stratejiyi birbirine bağlamanıza olanak tanıyan kullanışlı bir then iç ekleme işlevi de sağlar. Bu, her stratejinin bir Scene hesaplamaya çalışabileceği ve hesaplayamazsa zincirdeki bir sonraki stratejiye devredebileceği esnek bir karar verme ardışık düzeni oluşturur.

Sahneler ve sahne stratejilerinin birlikte çalışma şekli

NavDisplay, arka yığınınızı gözlemleyen ve uygun Scene'yi belirlemek ve oluşturmak için bir SceneStrategy kullanan merkezi bir derlenebilir öğ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 hesaplanmıyorsa NavDisplay varsayılan olarak otomatik olarak SinglePaneSceneStrategy kullanmaya geri döner.

Etkileşimin dökümü aşağıda verilmiştir:

  • Arka yığınınıza anahtar eklediğinizde veya arka yığınınızdan anahtar kaldırdığınızda (ör. backStack.add() veya backStack.removeLastOrNull() kullanarak) NavDisplay bu değişiklikleri gözlemler.
  • NavDisplay, mevcut NavEntrys listesini (arka uç anahtarlarından türetilir) yapılandırılmış SceneStrategy's calculateScene yöntemine iletir.
  • SceneStrategy başarılı bir şekilde bir Scene döndürürse NavDisplay, bu Scene'un content değerini oluşturur. NavDisplay, Scene'un özelliklerine göre animasyonlar ve tahmini geri oynatma işlemlerini de yönetir.

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

Kullanabileceğiniz 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: 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)
        )
}

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

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

  1. Pencere genişliği, iki bölmeyi destekleyecek kadar geniş olmalıdır (ör. en az WIDTH_DP_MEDIUM_LOWER_BOUND).
  2. Arka yığıntaki ilk iki giriş, belirli meta veriler kullanılarak iki bölmeli bir düzende gösterilme desteğini açıkça belirtir.

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

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

Bu TwoPaneSceneStrategy özelliğini NavDisplay'unuzda kullanmak için entryProvider çağrılarınızı, iki bölmeli düzende göstermek istediğiniz girişler için TwoPaneScene.twoPane() meta verilerini içerecek şekilde değiştirin. Ardından, tek bölmeli senaryolar için varsayılan yedek yöntemi kullanarak sceneStrategy olarak TwoPaneSceneStrategy() değerini 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()
                }
            }
        }
    )
}

Uyarlanabilir materyal sahnesinde liste ayrıntısı içeriğini görüntüleme

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

Malzeme listesi ayrıntısı Scene oluşturmak için aşağıdaki adımları uygulayın:

  1. Bağımlılık ekleyin: androidx.compose.material3.adaptive:adaptive-navigation3 dosyasını projenizin build.gradle.kts dosyasına ekleyin.
  2. Girişlerinizi ListDetailSceneStrategy meta verileriyle tanımlayın: NavEntrys öğenizi uygun bölme görüntüleme için listPane(), detailPane() ve extraPane() ile işaretleyin. listPane() yardımcısı, hiçbir öğe seçilmediğinde bir detailPlaceholder belirtmenize de olanak tanır.
  3. rememberListDetailSceneStrategy işlevini kullanın: Bu birleştirilebilir işlev, NavDisplay tarafından kullanılabilecek önceden yapılandırılmış bir ListDetailSceneStrategy sağlar.

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

@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. Malzeme listesi-ayrıntı sahnesinde çalışan örnek içerik.