يوفّر نظام التنقّل 3 نظامًا قويًا ومرنًا لإدارة تدفق واجهة المستخدم في تطبيقك من خلال المشاهد. تتيح لك "المشاهد" إنشاء تخطيطات مخصّصة للغاية والتأقلم مع أحجام الشاشات المختلفة وإدارة التجارب المعقدة المتعدّدة الأقسام بسلاسة.
فهم "المَشاهد"
في Navigation 3، Scene
هي الوحدة الأساسية التي تعرِض مثيلًا واحدًا أو أكثر من
NavEntry
. يمكنك اعتبار Scene
حالة مرئية أو قسمًا محددًا
في واجهة المستخدم يمكن أن يحتوي على عرض المحتوى من ملفاتك في
الخلفية ويدير هذا العرض.
يتم تحديد كل مثيل منScene
بشكل فريد من خلالkey
وفئةScene
نفسها. هذا المعرّف الفريد مهمّ جدًا لأنّه يشغّل
الرسوم المتحركة على المستوى الأعلى عند تغيُّر Scene
.
تتضمّن واجهة Scene
السمات التالية:
key: Any
: معرّف فريد لهذه النسخة المحدّدة منScene
. يضمن هذا المفتاح، إلى جانب فئةScene
، التميز، وذلك لأغراض الرسوم المتحركة في المقام الأول.entries: List<NavEntry<T>>
: هذه قائمة بعناصرNavEntry
التي يكونScene
مسؤولاً عن عرضها. من المهمّ الإشارة إلى أنّه إذا تم عرضNavEntry
نفسه في عدّةScenes
أثناء انتقال (مثلاً، في انتقالScene
عنصر مشترَك)، لن يتم عرض محتواه إلا من خلالScene
المستهدَف الأخير الذي يعرضه.previousEntries: List<NavEntry<T>>
: تحدِّد هذه السمةNavEntry
التي ستظهر في حال تنفيذ إجراء "رجوع" منScene
الحالي. من الضروري احتساب الحالة التوقّعية المناسبة للرجوع، مما يسمح لـNavDisplay
بالتوقّع والانتقال إلى الحالة السابقة الصحيحة، والتي قد تكون مشهدًا يتضمّن فئة و/أو مفتاحًا مختلفَين.content: @Composable () -> Unit
: هذه هي الدالة القابلة للتجميع التي يتم فيها تحديد كيفية عرضScene
لعنصرentries
وأي عناصر واجهة مستخدم مجاورة خاصة بهذاScene
.
فهم استراتيجيات المشهد
SceneStrategy
هي الآلية التي تحدّد كيفية ترتيب قائمة معيّنة من
NavEntry
من الحزمة الخلفية ونقلها إلى
Scene
. بشكل أساسي، عند عرض إدخالات الحزمة الخلفية الحالية، يطرح
SceneStrategy
سؤالَين رئيسيَّين على نفسه:
- هل يمكنني إنشاء
Scene
من هذه الإدخالات؟ إذا اتّضح لـSceneStrategy
أنّه يمكنه التعامل معNavEntry
المحدّدة وإنشاءScene
ذي معنى (مثل مربّع حوار أو تنسيق متعدد الأقسام)، يتابع المعالجة. بخلاف ذلك، يتم عرضnull
، ما يمنح الاستراتيجيات الأخرى فرصة لإنشاءScene
. - إذا كان الأمر كذلك، كيف يمكنني ترتيب هذه الإدخالات في
Scene?
؟ بعد أن يقرّSceneStrategy
بمعالجة الإدخالات، يتحمّل مسؤولية إنشاءScene
وتحديد كيفية عرضNavEntry
s المحدّدة ضمن هذاScene
.
يتمثل جوهر SceneStrategy
في طريقة calculateScene
:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
تأخذ هذه الطريقة List<NavEntry<T>>
الحالية من الحزمة الخلفية وonBack
للرجوع إليه. من المفترض أن يعرض الرمز Scene<T>
إذا كان بإمكانه إنشاء Scene<T>
بنجاح من الإدخالات المقدَّمة، أو null
إذا لم يكن بإمكانه ذلك.
توفّر SceneStrategy
أيضًا دالة then
داخلية ملائمة، ما يتيح
لك ربط استراتيجيات متعددة معًا. يؤدّي ذلك إلى إنشاء مسار flexible
لاتخاذ القرار يمكن فيه لكل استراتيجية محاولة احتساب Scene
،
وإذا لم تتمكّن من ذلك، يتم تفويضها إلى الاستراتيجية التالية في السلسلة.
كيفية عمل "المشاهد" واستراتيجيات المشاهد معًا
NavDisplay
هو العنصر القابل للتجميع المركزي الذي يراقب الحزمة الخلفية وي
يستخدِم SceneStrategy
لتحديد Scene
المناسب وعرضه.
تتوقّع المَعلمة NavDisplay's sceneStrategy
مَعلمة SceneStrategy
التي تُمثّل
المَعلمة المسؤولة عن احتساب Scene
المراد عرضها. إذا لم يتم احتساب أيّ Scene
بواسطة الاستراتيجية المقدّمة (أو سلسلة الاستراتيجيات)، يعودNavDisplay
تلقائيًا
إلى استخدام SinglePaneSceneStrategy
تلقائيًا.
في ما يلي تفاصيل التفاعل:
- عند إضافة مفاتيح أو إزالتها من الحزمة الخلفية (على سبيل المثال، باستخدام
backStack.add()
أوbackStack.removeLastOrNull()
)، يلاحظNavDisplay
هذه التغييرات. - تُرسِل دالة
NavDisplay
القائمة الحالية منNavEntrys
(المشتقة من مفاتيح السلسلة الخلفية) إلى طريقةSceneStrategy's calculateScene
التي تم ضبطها. - إذا أرجعت
SceneStrategy
Scene
بنجاح،NavDisplay
ثم يعرضcontent
هذاScene
. يديرNavDisplay
أيضًا الرسوم المتحرّكة والرجوع التوقّعي استنادًا إلى سماتScene
.
مثال: تنسيق لوحة واحدة (السلوك التلقائي)
أبسط تنسيق مخصّص يمكنك الحصول عليه هو عرض لوحة واحدة، وهو السلوك
التلقائي إذا لم تكن هناك SceneStrategy
أخرى لها الأولوية.
data class SinglePaneScene<T : Any>( override val key: T, 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.invoke(entry.key) } } /** * 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().key, 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.invoke(firstEntry.key) } Column(modifier = Modifier.weight(0.5f)) { secondEntry.content.invoke(secondEntry.key) } } } 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.key, secondEntry.key) 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() } } } ) }
عرض محتوى تفاصيل القائمة في "مشهد متوافق مع واجهة المستخدم المادية"
بالنسبة إلى حالة استخدام تفاصيل القائمة، يقدّم العنصر
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<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") } } ) } } } }