تقدّم مكتبة 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: هذه هي الدالة القابلة للإنشاء التي تحدّد كيف يعرضSceneentriesوأي عناصر واجهة مستخدم محيطة خاصة بهذا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وتحديد كيفية عرض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.
كيفية عمل المشاهد واستراتيجيات المشهد معًا
NavDisplay هي الدالة القابلة للإنشاء المركزية التي تراقب ذاكرة التصفّح السابقة و
تستخدم واحدة أو أكثر من SceneStrategy لتحديد
Scene المناسب وعرضه.
تتوقّع المَعلمة sceneStrategies في NavDisplay's قائمة بمثيلات 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) ) }
مثال: تنسيق أساسي يتضمّن قائمة وتفاصيل (مشهد واستراتيجية مخصّصان)
يوضّح هذا المثال كيفية إنشاء تنسيق يتضمّن قائمة وتفاصيل يتم تفعيله استنادًا إلى شرطَين:
- عرض النافذة واسع بما يكفي لدعم جزءَين (أي
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) }
إذا كنت لا تريد إنشاء مشهد يتضمّن قائمة وتفاصيل خاص بك، يمكنك استخدام مشهد القائمة والتفاصيل في Material، الذي يتضمّن تفاصيل منطقية وإمكانية استخدام العناصر النائبة، كما هو موضّح في القسم التالي.
عرض محتوى يتضمّن قائمة وتفاصيل في مشهد تكيّفي في 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") } } ) } } } }