تقدّم 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> { @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() } } } ) }
عرض محتوى قائمة وتفاصيل في Material Adaptive Scene
بالنسبة إلى حالة استخدام قائمة-تفاصيل، يوفّر العنصر androidx.compose.material3.adaptive:adaptive-navigation3 ListDetailSceneStrategy ينشئ Scene قائمة-تفاصيل. يتعامل هذا المكوّن Sceneتلقائيًا مع الترتيبات المعقّدة المتعددة اللوحات (قائمة وتفاصيل ولوحات إضافية) ويعدّلها استنادًا إلى حجم النافذة وحالة الجهاز.
لإنشاء SceneMaterial list-detailScene، اتّبِع الخطوات التالية:
- إضافة التبعية: أدرِج
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") } } ) } } } }