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

ناوبری ۳ یک سیستم قدرتمند و انعطاف‌پذیر برای مدیریت جریان رابط کاربری برنامه شما از طریق صحنه‌ها (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> {
    override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? =
        SinglePaneScene(
            key = entries.last().contentKey,
            entry = entries.last(),
            previousEntries = entries.dropLast(1)
        )
}

مثال: طرح‌بندی اولیه لیست-جزئیات (صحنه و استراتژی سفارشی)

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

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

قطعه کد زیر کد منبع ListDetailScene.kt است و شامل ListDetailScene و ListDetailSceneStrategy می‌شود:

// --- 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.containsKey(DETAIL_KEY) } ?: return null
        val listEntry = entries.findLast { it.metadata.containsKey(LIST_KEY) } ?: 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
        )
    }

    companion object {
        internal const val LIST_KEY = "ListDetailScene-List"
        internal const val DETAIL_KEY = "ListDetailScene-Detail"

        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * as a list in the [ListDetailScene].
         */
        fun listPane() = mapOf(LIST_KEY to true)

        /**
         * Helper function to add metadata to a [NavEntry] indicating it can be displayed
         * as a list in the [ListDetailScene].
         */
        fun detailPane() = mapOf(DETAIL_KEY to true)
    }
}

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

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

اگر نمی‌خواهید صحنه جزئیات لیست خودتان را ایجاد کنید، می‌توانید از صحنه جزئیات لیست متریال استفاده کنید که دارای جزئیات معقول و پشتیبانی از متغیرهایی است که در بخش بعدی نشان داده شده است.

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

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

                NavDisplay(
                    backStack = backStack,
                    modifier = Modifier.padding(paddingValues),
                    onBack = { 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")
                        }
                    }
                )
            }
        }
    }
}

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