يقدّم 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: هي الدالة المركّبة التي تحدّد فيها كيفية عرض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. - إذا كان الأمر كذلك، كيف يمكنني ترتيب هذه الإدخالات في
Scene?بمجرد أن يلتزمSceneStrategyبمعالجة الإدخالات، فإنه يتحمل مسؤولية إنشاءSceneوتحديد كيفية عرضNavEntrys المحددة داخل هذا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.
كيفية عمل "المشاهد" واستراتيجيات المشاهد معًا
NavDisplay هي الدالة المركّبة المركزية التي تراقب الأنشطة السابقة
وتستخدم دالة SceneStrategy واحدة أو أكثر لتحديد Scene المناسبة وعرضها.
تتوقّع مَعلمة sceneStrategies في NavDisplay قائمة من نُسخ SceneStrategy المسؤولة عن احتساب Scene المطلوب عرضه. إذا لم يتم احتساب أي Scene من خلال الاستراتيجيات المقدَّمة، يعود NavDisplay تلقائيًا إلى استخدام SinglePaneSceneStrategy بشكل تلقائي.
في ما يلي تفاصيل التفاعل:
- عند إضافة مفاتيح إلى الأنشطة السابقة أو إزالتها منها (على سبيل المثال، باستخدام
backStack.add()أوbackStack.removeLastOrNull())، يلاحظNavDisplayهذه التغييرات. - يمرّر
NavDisplayالقائمة الحالية منNavEntry(المشتقة من مفاتيح الأنشطة السابقة) إلىsceneStrategiesالذي تم ضبطه بالترتيب، ويستدعيcalculateSceneفي كل منها إلى أن يتم عرضScene. - عندما يعرض
SceneStrategySceneبنجاح، يعرض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) ) }
مثال: تنسيق أساسي للقائمة والتفاصيل (مشهد واستراتيجية مخصّصان)
يوضّح هذا المثال كيفية إنشاء عرض على شكل قائمة مع تفاصيل يتم تفعيله بناءً على شرطين:
- عرض النافذة كبير بما يكفي لإتاحة لوحتَين (أي لا يقل عن
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) }
إذا كنت لا تريد إنشاء عرض على شكل قائمة مع تفاصيل خاص بك، يمكنك استخدام عرض على شكل قائمة مع تفاصيل في التصميم المتعدد الأبعاد، والذي يأتي مع تفاصيل معقولة ودعم العناصر النائبة، كما هو موضح في القسم التالي.
عرض محتوى القائمة مع التفاصيل في مشهد تكيُّفي متعدد الأبعاد
بالنسبة إلى حالة استخدام عرض على شكل قائمة مع تفاصيل، يوفّر العنصر androidx.compose.material3.adaptive:adaptive-navigation3 واجهة برمجة تطبيقات ListDetailSceneStrategy تنشئ عرضًا على شكل قائمة مع تفاصيل Scene. يتولّى هذا 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() }, 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") } } ) } } } }