طرح بندی های سفارشی را با استفاده از صحنه ها ایجاد کنید

ناوبری ۳ یک سیستم قدرتمند و انعطاف‌پذیر برای مدیریت جریان رابط کاربری برنامه شما از طریق صحنه‌ها (Scenes) معرفی می‌کند. صحنه‌ها به شما امکان می‌دهند طرح‌بندی‌های بسیار سفارشی ایجاد کنید، با اندازه‌های مختلف صفحه نمایش سازگار شوید و تجربیات پیچیده چندصفحه‌ای را به طور یکپارچه مدیریت کنید.

صحنه‌ها را درک کنید

در ناوبری ۳، یک Scene واحد اساسی است که یک یا چند نمونه NavEntry رندر می‌کند. Scene به عنوان یک حالت بصری مجزا یا بخش از رابط کاربری خود در نظر بگیرید که می‌تواند نمایش محتوا را از پشته پشتی شما در بر بگیرد و مدیریت کند.

هر نمونه Scene به طور منحصر به فرد توسط key و کلاس Scene شناسایی می‌شود. این شناسه منحصر به فرد بسیار مهم است زیرا انیمیشن سطح بالا را هنگام تغییر Scene هدایت می‌کند.

رابط Scene دارای ویژگی‌های زیر است:

  • key: Any : یک شناسه منحصر به فرد برای این نمونه Scene خاص. این کلید، همراه با کلاس Scene ، تمایز را تضمین می‌کند، در درجه اول برای اهداف انیمیشن.
  • entries: List<NavEntry<T>> : این لیستی از اشیاء NavEntry است که Scene مسئول نمایش آنها است. نکته مهم این است که اگر NavEntry یکسانی در چندین Scenes در طول یک انتقال نمایش داده شود (مثلاً در یک انتقال عنصر مشترک)، محتوای آن فقط توسط جدیدترین Scene هدف که آن را نمایش می‌دهد، رندر می‌شود.
  • previousEntries: List<NavEntry<T>> : این ویژگی NavEntry هایی را تعریف می‌کند که در صورت وقوع یک عمل "برگشت" از Scene فعلی، حاصل می‌شوند. این ویژگی برای محاسبه حالت بازگشت پیش‌بینی‌کننده مناسب ضروری است و به NavDisplay اجازه می‌دهد حالت قبلی صحیح را پیش‌بینی کرده و به آن منتقل شود، که می‌تواند یک صحنه با کلاس و/یا کلید متفاوت باشد.
  • content: @Composable () -> Unit : این تابع composable است که در آن نحوه رندر entries Scene و هر عنصر رابط کاربری اطراف آن که مختص Scene است را تعریف می‌کنید.

استراتژی‌های صحنه را درک کنید

یک SceneStrategy مکانیزمی است که تعیین می‌کند چگونه یک لیست مشخص از NavEntry ها از back stack باید مرتب شده و به یک Scene منتقل شوند. اساساً، وقتی با ورودی‌های back stack فعلی مواجه می‌شویم، SceneStrategy از خود دو سوال کلیدی می‌پرسد:

  1. آیا می‌توانم از این ورودی‌ها یک Scene ایجاد کنم؟ اگر SceneStrategy تشخیص دهد که می‌تواند NavEntry های داده شده را مدیریت کند و یک Scene معنادار (مثلاً یک کادر محاوره‌ای یا یک طرح چند قسمتی) ایجاد کند، ادامه می‌دهد. در غیر این صورت، null برمی‌گرداند و به سایر استراتژی‌ها فرصت ایجاد Scene می‌دهد.
  2. اگر چنین است، چگونه باید آن ورودی‌ها را در Scene? هنگامی که یک SceneStrategy متعهد به مدیریت ورودی‌ها می‌شود، مسئولیت ساخت یک Scene و تعریف نحوه نمایش NavEntry های مشخص شده در آن Scene را بر عهده می‌گیرد.

هسته اصلی یک SceneStrategy متد calculateScene آن است:

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

این متد یک تابع افزونه روی SceneStrategyScope است که List<NavEntry<T>> فعلی را از پشته می‌گیرد. اگر بتواند با موفقیت Scene<T> را از ورودی‌های ارائه شده تشکیل دهد، باید آن را برگرداند، و در غیر این صورت null برمی‌گرداند.

SceneStrategyScope مسئول نگهداری هرگونه آرگومان اختیاری است که SceneStrategy ممکن است به آن نیاز داشته باشد، مانند فراخوانی onBack .

SceneStrategy همچنین یک تابع مناسب then infix کردن ارائه می‌دهد که به شما امکان می‌دهد چندین استراتژی را به هم زنجیر کنید. این یک خط لوله تصمیم‌گیری انعطاف‌پذیر ایجاد می‌کند که در آن هر استراتژی می‌تواند سعی کند یک Scene محاسبه کند و اگر نتواند، آن را به استراتژی بعدی در زنجیره محول می‌کند.

چگونه صحنه‌ها و استراتژی‌های صحنه با هم کار می‌کنند

NavDisplay یک کامپوننت مرکزی است که back stack شما را مشاهده می‌کند و از SceneStrategy برای تعیین و رندر Scene مناسب استفاده می‌کند.

پارامتر NavDisplay's sceneStrategy انتظار یک SceneStrategy را دارد که مسئول محاسبه‌ی Scene نمایش داده شده است. اگر هیچ Scene توسط استراتژی ارائه شده (یا زنجیره‌ای از استراتژی‌ها) محاسبه نشود، NavDisplay به طور خودکار و پیش‌فرض از SinglePaneSceneStrategy استفاده می‌کند.

در اینجا خلاصه‌ای از این تعامل آمده است:

  • وقتی کلیدهایی را به پشته پشتی خود اضافه یا حذف می‌کنید (مثلاً با استفاده از backStack.add() یا backStack.removeLastOrNull()NavDisplay این تغییرات را مشاهده می‌کند.
  • NavDisplay لیست فعلی NavEntrys (که از کلیدهای back stack مشتق شده‌اند) را به متد SceneStrategy's calculateScene پیکربندی‌شده ارسال می‌کند.
  • اگر SceneStrategy با موفقیت یک Scene برگرداند، NavDisplay content آن Scene را رندر می‌کند. NavDisplay همچنین انیمیشن‌ها و پیش‌بینی‌های بازگشتی را بر اساس ویژگی‌های Scene مدیریت می‌کند.

مثال: طرح‌بندی تک‌صفحه‌ای (رفتار پیش‌فرض)

ساده‌ترین طرح‌بندی سفارشی که می‌توانید داشته باشید، یک نمایشگر تک‌صفحه‌ای است که اگر هیچ SceneStrategy دیگری اولویت نداشته باشد، رفتار پیش‌فرض است.

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

مثال: طرح‌بندی دو قسمتی پایه (صحنه و استراتژی سفارشی)

این مثال نحوه ایجاد یک طرح‌بندی ساده دو قسمتی را نشان می‌دهد که بر اساس دو شرط فعال می‌شود:

  1. عرض پنجره به اندازه کافی عریض است که از دو پنل پشتیبانی کند (یعنی حداقل WIDTH_DP_MEDIUM_LOWER_BOUND ).
  2. دو ورودی برتر در پشته پشتی به صراحت پشتیبانی خود را از نمایش در یک طرح دو صفحه‌ای با استفاده از فراداده‌های خاص اعلام می‌کنند.

قطعه کد زیر، کد منبع ترکیبی برای TwoPaneScene.kt و 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()
            }
            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
        }
    }
}

برای استفاده از این TwoPaneSceneStrategy در NavDisplay خود، فراخوانی‌های entryProvider خود را تغییر دهید تا شامل فراداده TwoPaneScene.twoPane() برای ورودی‌هایی باشد که قصد دارید در یک طرح دو صفحه‌ای نمایش دهید. سپس، TwoPaneSceneStrategy() به عنوان sceneStrategy خود ارائه دهید، با تکیه بر جایگزین پیش‌فرض برای سناریوهای تک صفحه‌ای:

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

نمایش محتوای لیست-جزئیات در یک صحنه تطبیقی ​​متریال

برای مورد استفاده از list-detail ، آرتیفکت androidx.compose.material3.adaptive:adaptive-navigation3 یک ListDetailSceneStrategy ارائه می‌دهد که یک Scene list-detail ایجاد می‌کند. این Scene به طور خودکار چیدمان‌های پیچیده چند پنجره‌ای (لیست، جزئیات و پنجره‌های اضافی) را مدیریت می‌کند و آنها را بر اساس اندازه پنجره و وضعیت دستگاه تطبیق می‌دهد.

برای ایجاد یک Scene با جزئیات لیست مواد، این مراحل را دنبال کنید:

  1. وابستگی را اضافه کنید : androidx.compose.material3.adaptive:adaptive-navigation3 در فایل build.gradle.kts پروژه خود وارد کنید.
  2. ورودی‌های خود را با استفاده از فراداده ListDetailSceneStrategy تعریف کنید : از listPane(), detailPane() و extraPane() برای علامت‌گذاری NavEntrys های خود برای نمایش مناسب پنجره استفاده کنید. تابع کمکی listPane() همچنین به شما امکان می‌دهد وقتی هیچ آیتمی انتخاب نشده است، یک detailPlaceholder مشخص کنید.
  3. استفاده از rememberListDetailSceneStrategy (): این تابع ترکیبی، یک ListDetailSceneStrategy از پیش پیکربندی‌شده ارائه می‌دهد که می‌تواند توسط NavDisplay مورد استفاده قرار گیرد.

قطعه کد زیر یک نمونه Activity است که نحوه‌ی استفاده از 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")
                        }
                    }
                )
            }
        }
    }
}

شکل ۱. نمونه محتوایی که در صحنه لیست-جزئیات متریال اجرا می‌شود.