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

Navigation 3'te Scene, bir veya daha fazla NavEntry örneğini oluşturmak için kullanılan temel birimdir. Scene, eski yığınınızdaki içeriklerin gösterimini içerebilen ve yönetebilen, kullanıcı arayüzünüzün 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ıldığında ortaya çıkacak NavEntry konumlarını tanımlar. Bu, doğru tahmin edilen geri gitme 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 bir composable işlevdir.
  • metadata: Map<String, Any>: NavDisplay gibi diğer kitaplık bileşenlerine sahneye özgü bilgiler sağlar. Varsayılan olarak, entries içindeki son NavEntry öğesinin metadata değerini döndürür.

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 kendine iki temel soru sorar:

  1. Bu girişlerden Scene oluşturabilir miyim? SceneStrategy, verilen 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 null değerini döndürerek diğer stratejilerin Scene oluşturmasına olanak tanır.
  2. Bu durumda, bu girişleri Scene? içinde nasıl düzenlemeliyim?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, eski 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.

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

NavDisplay, geri yığınınızı gözlemleyen ve uygun Scene'ı belirleyip oluşturmak için bir veya daha fazla SceneStrategy kullanan merkezi bir composable'dır.

NavDisplay's sceneStrategies parametresi, görüntülenecek Scene değerini hesaplamaktan sorumlu SceneStrategy örneklerinin bir listesini bekler. Sağlanan stratejiler tarafından Scene hesaplanmazsa NavDisplay otomatik olarak varsayılan SinglePaneSceneStrategy kullanmaya geri döner.

Etkileşimin dökümü:

  • Geri yığında anahtar eklediğinizde veya kaldırdığınızda (ör. backStack.add() veya backStack.removeLastOrNull() kullanarak) NavDisplay bu değişiklikleri gözlemler.
  • NavDisplay, NavEntry'lerin mevcut listesini (arka yığın anahtarlarından türetilir) yapılandırılmış sceneStrategies'ye sırayla iletir ve bir Scene döndürülene kadar her birinde calculateScene'yi çağırır.
  • Bir SceneStrategy, Scene öğesini başarıyla döndürdüğünde NavDisplay, söz konusu Scene öğesinin content öğesini oluşturur. NavDisplay, Scene özelliklerine göre animasyonları ve tahmin edilen geri gitme ö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> {
    override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? =
        SinglePaneScene(
            key = entries.last().contentKey,
            entry = entries.last(),
            previousEntries = entries.dropLast(1)
        )
}

Örnek: Temel liste-ayrıntı düzeni (özel sahne ve strateji)

Bu örnekte, iki koşula göre etkinleştirilen basit bir liste-ayrıntı düzeninin 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. Geri yığın, belirli meta veriler kullanılarak liste-ayrıntı düzeninde gösterilme desteği beyan eden girişler içerir.

Aşağıdaki snippet, ListDetailScene.kt için kaynak kodudur ve hem ListDetailScene hem de ListDetailSceneStrategy içerir:

// --- ListDetailScene ---
/**
 * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split.
 *
 */
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)
        }
    }
}

Bu ListDetailSceneStrategy öğesini NavDisplay içinde kullanmak için entryProvider çağrılarınızı, liste düzeninde göstermek istediğiniz giriş için ListDetailScene.listPane() meta verilerini ve ayrıntı düzeninde göstermek istediğiniz giriş için ListDetailScene.detailPane() öğesini içerecek şekilde değiştirin. Ardından, tek pencereli senaryolarda varsayılan geri dönüşü kullanarak ListDetailSceneStrategy() değerini sceneStrategy olarak sağlayı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)
}

Kendi liste-ayrıntı sahnenizi oluşturmak istemiyorsanız sonraki bölümde gösterildiği gibi, anlamlı ayrıntılar ve yer tutucular için destek içeren Material liste-ayrıntı sahnesini kullanabilirsiniz.

Materyal uyarlanabilir sahnesinde liste-ayrıntı içeriğini görüntüleme

liste-ayrıntı düzeni kullanım alanında, androidx.compose.material3.adaptive:adaptive-navigation3 yapı, liste-ayrıntı düzeni 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 pencere boyutuna ve cihaz durumuna göre uyarlar.

Bir Material liste-ayrıntı düzeni 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 detailPlaceholder belirtmenize de olanak tanır.
  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<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")
                        }
                    }
                )
            }
        }
    }
}

Şekil 1. Materyal liste-ayrıntı düzeninde çalışan örnek içerik.