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اجازه میدهد تا وضعیت قبلی صحیح را پیشبینی کند و به آن انتقال یابد، که ممکن است یک Scene با کلاس، کلید، یا هر دو متفاوت باشد.-
content: @Composable () -> Unit: این تابع ترکیبپذیری است که در آن تعریف میکنیدSceneچگونهentriesخود و هر عنصر میانای کاربر اطراف را که مختص آنSceneاست رندر کند. -
metadata: Map<String, Any>: اطلاعات خاص صحنه را به سایر مؤلفههای کتابخانه مانندNavDisplayارائه میدهد. بهطور پیشفرض،metadataآخرینNavEntryرا درentriesبرمیگرداند.
پیادهسازی equals و hashCode در «صحنههای» سفارشی
پیادهسازیهای سفارشی Scene باید equals و hashCode را بهدرستی پیادهسازی کنند تا از موارد زیر پشتیبانی کنند:
- انتقالهای حالت صحنه: هربار که صحنهای توسط فهرست راهبردهای صحنه محاسبه میشود،
NavDisplayبرابری صحنه جدید و صحنه قبلی را بررسی میکند. اگر تغییری را شناسایی کند،SeekableTransitionمورداستفاده برای انتقال بین صحنهها را بهروز میکند که بهنوبه خود بر وضعیت چرخه حیات صحنهها تأثیر میگذارد. - مدیریت همپوشانی: برای
OverlayScene(مثل کادرهای گفتگو)،NavDisplayاز خود شیء صحنه بهعنوانkeyبرای ردیابی چرخههای حیات همپوشانی و پویانماییهای خروج استفاده میکند.
تأیید کنید که همه ویژگیهایی که محتوای صحنه را تعریف میکنند، مانند key،
entries، و previousEntries، در پیادهسازیهای equals و hashCode
گنجانده شدهاند. بهطور کلی، از گنجاندن فراخوانیهای برگشتی (مانند onBack) در پیادهسازی این روشها خودداری کنید، زیرا میتوانند نمونهها را بدون تغییر دادن وضعیت منطقی صحنه تغییر دهند.
آشنایی با استراتژیهای صحنه
SceneStrategy مکانیزمی است که تعیین میکند فهرست مشخصی از
NavEntryها از پشته قبلیها چگونه باید مرتب شوند و به
Scene منتقل شوند. اساساً، وقتی ورودیهای پشته قبلیها ارائه میشود،
SceneStrategy دو سؤال کلیدی از خود میپرسد:
- میتوانم از این ورودیها
Sceneایجاد کنم؟ اگرSceneStrategyتشخیص دهد که میتواندNavEntryهای ارائهشده را مدیریت کند وSceneمعناداری (مثلاً یک کادر گفتوگو یا چیدمان چندبخشی) تشکیل دهد، ادامه میدهد. درغیراینصورت،nullرا برمیگرداند و به سایر استراتژیها فرصت میدهد تاSceneایجاد کنند. - If so, how should I arrange those entries into the
Scene?Once aSceneStrategycommits to handling the entries, it takes on the responsibility of constructing aSceneand defining how the specifiedNavEntrys will be displayed within thatScene.
هسته 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.
نحوه عملکرد «صحنهها» و استراتژیهای صحنه با یکدیگر
NavDisplay ترکیبپذیر مرکزی است که پشته قبلیها را مشاهده میکند و
از یک یا چند SceneStrategy برای تعیین و ارائه Scene مناسب استفاده میکند.
پارامتر sceneStrategies مربوط به NavDisplay منتظر فهرستی از نمونههای SceneStrategy است که مسئول محاسبه Scene برای نمایش هستند. اگر هیچ
Scene توسط استراتژیهای ارائهشده محاسبه نشود، NavDisplay بهطور خودکار
به استفاده از SinglePaneSceneStrategy بهطور پیشفرض برمیگردد.
در اینجا جزئیات تعامل آمده است:
- وقتی کلیدها را از پشته قبلیها اضافه یا حذف میکنید (مثلاً بااستفاده از
backStack.add()یاbackStack.removeLastOrNull())،NavDisplayاین تغییرات را مشاهده میکند. -
NavDisplayفهرست فعلیNavEntry(مشتقشده از کلیدهای پشته قبلیها) را بهترتیب بهsceneStrategiesپیکربندیشده ارسال میکند وcalculateSceneرا برای هرکدام فراخوانی میکند تا زمانی کهSceneبرگردانده شود. - وقتی
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) ) }
مثال: چیدمان تفصیلی-فهرستی پایه (Scene و استراتژی سفارشی)
این مثال نشان میدهد که چگونه میتوان یک طرحبندی فهرست-جزئیات ایجاد کرد که براساس دو شرط فعال میشود:
- عرض پنجره بهاندازه کافی عریض است تا از دو قاب پشتیبانی کند (یعنی حداقل
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. * */ data 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.contains(DetailKey) } ?: return null val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: 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 ) } object ListKey : NavMetadataKey<Boolean> object DetailKey : NavMetadataKey<Boolean> companion object { /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun listPane() = metadata { put(ListKey, true) } /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun detailPane() = metadata { put(DetailKey, 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() }, sceneStrategies = listOf(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) }
If you don't want to create your own list-detail scene, you can use the Material list-detail scene, which comes with sensible details and the support for placeholders, as showcased in the next section.
نمایش محتوای تفصیلی-فهرستی در «صحنه تطبیقی Material»
برای مورد استفاده فهرست-جزئیات،
مصنوع androidx.compose.material3.adaptive:adaptive-navigation3 یک
ListDetailSceneStrategy ارائه میدهد که Scene فهرست-جزئیات ایجاد میکند. این Scene
بهطور خودکار چیدمانهای چندبخشی پیچیده (فهرست، جزئیات، و بخشهای اضافی) را مدیریت میکند و آنها را براساس اندازه پنجره و وضعیت دستگاه تطبیق میدهد.
برای ایجاد کردن Scene جزئیات فهرست Material، این مراحل را دنبال کنید:
- افزودن وابستگی:
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() }, sceneStrategies = listOf(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") } } ) } } } }