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