إنشاء تنسيقات مخصّصة باستخدام "المشاهد"

تقدّم مكتبة 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.

فهم استراتيجيات المشاهد

SceneStrategy هي الآلية التي تحدّد كيفية ترتيب قائمة معيّنة من NavEntrys من الأنشطة السابقة وكيفية نقلها إلى Scene. بشكل أساسي، عند عرض إدخالات الأنشطة السابقة الحالية، يطرح SceneStrategy على نفسه سؤالين رئيسيين:

  1. هل يمكنني إنشاء Scene من هذه الإدخالات؟ إذا حدّد SceneStrategy أنّه يمكنه التعامل مع NavEntry المحدّدة وتكوين Scene مفيد (مثل مربّع حوار أو تخطيط متعدد اللوحات)، سيتم تنفيذ ذلك. بخلاف ذلك، تعرض null، ما يمنح الاستراتيجيات الأخرى فرصة لإنشاء Scene.
  2. إذا كان الأمر كذلك، كيف يمكنني ترتيب هذه الإدخالات في 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 قائمة 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)
        )
}

مثال: تخطيط أساسي لعرض قائمة وتفاصيلها (مشهد واستراتيجية مخصّصان)

يوضّح هذا المثال كيفية إنشاء تصميم بسيط لعرض على شكل قائمة مع تفاصيل يتم تفعيله استنادًا إلى شرطَين:

  1. عرض النافذة كبير بما يكفي لعرض لوحتَين (أي WIDTH_DP_MEDIUM_LOWER_BOUND على الأقل).
  2. يحتوي سجلّ الأنشطة السابقة على إدخالات أعلنت عن إمكانية عرضها في عرض على شكل قائمة مع تفاصيل باستخدام بيانات وصفية محدّدة.

المقتطف التالي هو رمز المصدر الخاص بـ 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.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تلقائيًا مع الترتيبات المعقّدة المتعددة اللوحات (القائمة والتفاصيل واللوحات الإضافية) ويعدّلها استنادًا إلى حجم النافذة وحالة الجهاز.

لإنشاء عرض على شكل قائمة مع تفاصيل SceneMaterial، اتّبِع الخطوات التالية:

  1. إضافة التبعية: أدرِج androidx.compose.material3.adaptive:adaptive-navigation3 في ملف build.gradle.kts الخاص بمشروعك.
  2. تحديد إدخالاتك باستخدام بيانات ListDetailSceneStrategy الوصفية: استخدِم listPane(), detailPane() وextraPane() لوضع علامة NavEntrys على الإدخالات من أجل عرضها في اللوحة المناسبة. تتيح لك الأداة المساعدة listPane() أيضًا تحديد detailPlaceholder عندما لا يتم اختيار أي عنصر.
  3. استخدام 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")
                        }
                    }
                )
            }
        }
    }
}

الشكل 1. مثال على المحتوى الذي يتم تشغيله في مشهد عرض على شكل قائمة مع تفاصيل Material.