ناوبری ۳ یک سیستم قدرتمند و انعطافپذیر برای مدیریت جریان رابط کاربری برنامه شما از طریق صحنهها (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 است که در آن نحوه رندرentriesSceneو هر عنصر رابط کاربری اطراف آن که مختصSceneاست را تعریف میکنید.
استراتژیهای صحنه را درک کنید
یک SceneStrategy مکانیزمی است که تعیین میکند چگونه یک لیست مشخص از NavEntry ها از back stack باید مرتب شده و به یک Scene منتقل شوند. اساساً، وقتی با ورودیهای back stack فعلی مواجه میشویم، SceneStrategy از خود دو سوال کلیدی میپرسد:
- آیا میتوانم از این ورودیها یک
Sceneایجاد کنم؟ اگرSceneStrategyتشخیص دهد که میتواندNavEntryهای داده شده را مدیریت کند و یکSceneمعنادار (مثلاً یک کادر محاورهای یا یک طرح چند قسمتی) ایجاد کند، ادامه میدهد. در غیر این صورت،nullبرمیگرداند و به سایر استراتژیها فرصت ایجادSceneمیدهد. - اگر چنین است، چگونه باید آن ورودیها را در
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برگرداند،NavDisplaycontentآن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) ) }
مثال: طرحبندی دو قسمتی پایه (صحنه و استراتژی سفارشی)
این مثال نحوه ایجاد یک طرحبندی ساده دو قسمتی را نشان میدهد که بر اساس دو شرط فعال میشود:
- عرض پنجره به اندازه کافی عریض است که از دو پنل پشتیبانی کند (یعنی حداقل
WIDTH_DP_MEDIUM_LOWER_BOUND). - دو ورودی برتر در پشته پشتی به صراحت پشتیبانی خود را از نمایش در یک طرح دو صفحهای با استفاده از فرادادههای خاص اعلام میکنند.
قطعه کد زیر، کد منبع ترکیبی برای 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 با جزئیات لیست مواد، این مراحل را دنبال کنید:
- وابستگی را اضافه کنید :
androidx.compose.material3.adaptive:adaptive-navigation3در فایلbuild.gradle.ktsپروژه خود وارد کنید. - ورودیهای خود را با استفاده از فراداده
ListDetailSceneStrategyتعریف کنید : ازlistPane(), detailPane()وextraPane()برای علامتگذاریNavEntrysهای خود برای نمایش مناسب پنجره استفاده کنید. تابع کمکیlistPane()همچنین به شما امکان میدهد وقتی هیچ آیتمی انتخاب نشده است، یکdetailPlaceholderمشخص کنید. - استفاده از
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") } } ) } } } }