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

Navigation 3 یک سیستم قدرتمند و منعطف را برای مدیریت جریان رابط کاربری برنامه شما از طریق صحنه ها معرفی می کند. صحنه‌ها به شما امکان می‌دهند طرح‌بندی‌های بسیار سفارشی ایجاد کنید، با اندازه‌های مختلف صفحه تطبیق دهید، و تجربیات پیچیده چند صفحه‌ای را به‌طور یکپارچه مدیریت کنید.

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

در Navigation 3، یک 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 : این تابع ترکیب‌پذیر است که در آن نحوه نمایش entries Scene و هر عنصر UI اطراف خاص آن Scene را تعریف می‌کنید.

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

SceneStrategy مکانیزمی است که تعیین می کند چگونه یک لیست معین از NavEntry از پشته پشته باید مرتب شده و به یک Scene تبدیل شود. اساساً، هنگامی که با ورودی های پشته فعلی ارائه می شود، یک 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>?

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

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

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

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

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

در اینجا یک تفکیک از تعامل است:

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

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

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

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

مثال: چیدمان اصلی دو جداره (صحنه و استراتژی سفارشی)

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

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

برای استفاده از این 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()
                }
            }
        }
    )
}

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

برای مورد استفاده لیست جزئیات ، مصنوع androidx.compose.material3.adaptive:adaptive-navigation3 یک ListDetailSceneStrategy را ارائه می دهد که یک Scene با جزئیات لیست ایجاد می کند. این 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")
                        }
                    }
                )
            }
        }
    }
}

شکل 1 . نمونه محتوایی که در Material list-detail Scene اجرا می شود.