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