إنشاء تطبيقات تكيُّفية باستخدام Jetpack Compose

1. مقدمة

في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية إنشاء تطبيقات قابلة للتكيّف مع الهواتف والأجهزة اللوحية والأجهزة القابلة للطي، وكيفية تحسين إمكانية الوصول إليها باستخدام Jetpack Compose. ستتعرّف أيضًا على أفضل الممارسات لاستخدام مكونات Material 3 وتنسيقها.

قبل البدء، من المهمّ فهم ما نقصد به المرونة.

القدرة على التكيّف

يجب أن تكون واجهة المستخدم لتطبيقك سريعة الاستجابة لتناسب مختلف أحجام النوافذ والاتجاهات وعوامل الشكل. يتغيّر التنسيق التكييفي استنادًا إلى مساحة الشاشة المتاحة له. تتراوح هذه التغييرات بين تعديلات بسيطة على التنسيق لملء المساحة واختيار أنماط التنقّل ذات الصلة وتغيير التنسيقات بالكامل للاستفادة من مساحة إضافية.

لمزيد من المعلومات، يمكنك الاطّلاع على التصميم التكيُّفي.

في هذا الدرس التطبيقي حول الترميز، يمكنك استكشاف كيفية استخدام ميزة التكيف والتفكير فيها عند استخدام Jetpack Compose. أنت بصدد إنشاء تطبيق باسم "ردّ" يعرض لك كيفية تنفيذ ميزة التكيف مع جميع أنواع الشاشات، وكيفية عمل ميزة التكيف وميزة إمكانية الوصول معًا لتقديم تجربة مثالية للمستخدمين.

المُعطيات

  • كيفية تصميم تطبيقك لاستهداف جميع أحجام النوافذ باستخدام Jetpack Compose
  • كيفية استهداف تطبيقك لأجهزة Android اللوحية القابلة للطي المختلفة
  • كيفية استخدام أنواع مختلفة من التنقل لتحسين إمكانية الوصول وسهولة الاستخدام.
  • كيفية استخدام مكوّنات Material 3 لتقديم أفضل تجربة لكل حجم نافذة

المتطلبات

  • أحدث إصدار ثابت من استوديو Android
  • جهاز Android 13 افتراضي يمكن تغيير حجمه
  • معرفة بلغة Kotlin.
  • التعرّف الأساسي على ميزة "الكتابة" (مثل تعليق @Composable التوضيحي)
  • الإلمام بشكل أساسي بتنسيقات Compose (مثل Row وColumn)
  • الإلمام الأساسي بمفاتيح التعديل (مثل Modifier.padding())

ستستخدم المحاكي القابل للتغيير في هذا الدليل التعليمي، والذي يتيح لك التبديل بين أنواع مختلفة من الأجهزة وأحجام النوافذ.

محاكي قابل للتغيير مع خيارات الهاتف والشاشة المفتوحة والجهاز اللوحي والكمبيوتر المكتبي

إذا لم تكن معتادًا على استخدام Compose، ننصحك بأخذ الدرس التطبيقي حول أساسيات Jetpack Compose قبل إكمال هذا الدرس التطبيقي.

ما ستُنشئه

  • تطبيق تفاعلي لإدارة البريد الإلكتروني يُسمى "ردّ"، يستخدم أفضل الممارسات للتصاميم القابلة للتكيّف وعناصر التنقّل المختلفة في واجهة Material Design واستخدام مساحة الشاشة على النحو الأمثل

عرض توافق التطبيق مع أجهزة متعددة يمكنك تحقيقه في هذا الدليل التعليمي

2. الإعداد

للحصول على الرمز البرمجي لهذا الدرس التطبيقي حول الترميز، استنسِخ مستودع GitHub من سطر الأوامر:

git clone https://github.com/android/codelab-android-compose.git
cd codelab-android-compose/AdaptiveUiCodelab

بدلاً من ذلك، يمكنك تنزيل المستودع كملف ZIP:

ننصحك بالبدء بالرمز البرمجي في الفرع main واتّباع خطوات ورشة رموز البرامج خطوة بخطوة بالوتيرة التي تناسبك.

فتح المشروع في "استوديو Android"

  1. في نافذة مرحبًا بك في "استوديو Android"، انقر على c01826594f360d94.pngفتح مشروع حالي.
  2. اختَر المجلد <Download Location>/AdaptiveUiCodelab (تأكَّد من اختيار الدليل AdaptiveUiCodelab الذي يحتوي على build.gradle).
  3. بعد استيراد "استوديو Android" المشروع، اختبِر إمكانية تنفيذ فرع main.

استكشاف رمز البدء

يحتوي رمز الفرع main على حزمة ui. ستعمل على الملفات التالية في هذه الحزمة:

  • MainActivity.kt - نشاط نقطة الدخول الذي تبدأ فيه تطبيقك.
  • ReplyApp.kt: يحتوي على عناصر قابلة للإنشاء على الشاشة الرئيسية.
  • ReplyHomeViewModel.kt: يوفّر البيانات وحالة واجهة المستخدم لمحتوى التطبيق.
  • ReplyListContent.kt: يحتوي على عناصر قابلة للتجميع لتوفير القوائم والشاشات التفصيلية.

إذا كنت تشغّل هذا التطبيق على محاكي قابل للتغيير واختبرت أنواع أجهزة مختلفة، مثل هاتف أو جهاز لوحي، ستتم توسيع واجهة المستخدم فقط لتشغل المساحة المحدّدة بدلاً من الاستفادة من مساحة الشاشة أو توفير سهولة الوصول إليها.

الشاشة الأولية على الهاتف

العرض الأولي الموسّع على الجهاز اللوحي

ستقوم بتحديثه للاستفادة من مساحة الشاشة وزيادة سهولة الاستخدام وتحسين تجربة المستخدم الإجمالية.

3- إتاحة إمكانية تكييف التطبيقات

يقدّم هذا القسم معنى جعل التطبيقات قابلة للتكيّف، والمكوّنات التي يوفّرها Material 3 لتسهيل ذلك. ويتناول أيضًا أنواع الشاشات والمناطق التي ستستهدفها، بما في ذلك الهواتف والأجهزة اللوحية والأجهزة اللوحية الكبيرة والأجهزة القابلة للطي.

ستبدأ بالاطّلاع على أساسيات أحجام النوافذ ووضعيات الطي والأنواع المختلفة لخيارات التنقّل. وبعد ذلك، يمكنك استخدام واجهات برمجة التطبيقات هذه في تطبيقك لجعله أكثر تكيّفًا.

أحجام النوافذ

تتوفر أجهزة Android بجميع الأشكال والأحجام، بدءًا من الهواتف والأجهزة القابلة للطي والأجهزة اللوحية وأجهزة ChromeOS. لكي يكون تطبيقك متوافقًا مع أكبر عدد ممكن من أحجام النوافذ، يجب أن يكون واجهة مستخدمه سريعة الاستجابة ومتكيفة. لمساعدتك في العثور على الحدّ الأدنى المناسب لتغيير واجهة مستخدم تطبيقك، حدّدنا قيم نقاط التوقف التي تساعد في تصنيف الأجهزة إلى فئات أحجام محدّدة مسبقًا (صغيرة ومتوسطة ومكبّرة)، وتُعرف هذه الفئات باسم فئات أحجام النوافذ. هذه هي مجموعة من نقاط التوقف لإطار العرض التي تستند إلى آراء تساعدك في تصميم تنسيقات التطبيقات المتجاوبة والمتوافقة وتطويرها واختبارها.

تم اختيار الفئات خصيصًا لموازنة بساطة التنسيق مع المرونة في تحسين تطبيقك للحالات الفريدة. يتم تحديد فئة حجم النافذة دائمًا حسب مساحة الشاشة المتاحة للتطبيق، والتي قد لا تكون الشاشة الفعلية بأكملها في حال استخدام ميزة "تعدد المهام" أو تقسيمات أخرى.

نافذة العرضSizeClass للحصول على عرض مدمج ومتوسط الحجم وموسّع.

‫WindowHeightSizeClass للارتفاع المكثّف والمتوسط والموسّع

يتم تصنيف كل من العرض والارتفاع بشكل منفصل، لذا في أي وقت، يكون لتطبيقك فئتَان لحجم النافذة، إحداهما للعرض والأخرى للارتفاع. عادةً ما يكون العرض المتاح أكثر أهمية من الارتفاع المتاح نظرًا للانتشار الرأسي للتمرير العمودي، لذا ستستخدم أيضًا في هذه الحالة فئات حجم العرض.

حالات الطي

توفّر الأجهزة القابلة للطي المزيد من الحالات التي يمكن لتطبيقك التكيّف معها بسبب أحجامها المختلفة ووجود المفصلات. يمكن أن تحجب التفاصيل جزءًا من الشاشة، ما يجعل هذه المنطقة غير مناسبة لعرض المحتوى، ويمكن أن تكون أيضًا مفصولة، ما يعني ظهور شاشتَي عرض ماديتين منفصلتين عندما يكون الجهاز غير مطوي.

وضعيات قابلة للطي، مسطّحة ونصف مفتوحة

بالإضافة إلى ذلك، يمكن أن ينظر المستخدم إلى الشاشة الداخلية عندما تكون المفصلة مفتوحة جزئيًا، ما يؤدي إلى أوضاع مختلفة استنادًا إلى اتجاه الطي: وضع الطاولة (طي أفقي، يظهر على يسار الصورة أعلاه) ووضع الكتاب (طي عمودي).

مزيد من المعلومات حول أوضاع الطي والمفصلات

يجب مراعاة كل هذه العوامل عند تنفيذ تصاميم قابلة للتكيّف تتوافق مع الأجهزة القابلة للطي.

الحصول على معلومات تكيّفية

توفّر مكتبة Material3 adaptive إمكانية الوصول بسهولة إلى معلومات عن النافذة التي يعمل فيها تطبيقك.

  1. أضِف إدخالات عن هذا العنصر وإصداره إلى ملف قائمة الإصدارات:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. في ملف الإنشاء الخاص بوحدة التطبيق، أضِف الاعتمادية الجديدة للمكتبة، ثم نفِّذ عملية مزامنة Gradle:

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

الآن، في أي نطاق قابل للتركيب، يمكنك استخدام currentWindowAdaptiveInfo() للحصول على عنصر WindowAdaptiveInfo يحتوي على معلومات مثل فئة حجم النافذة الحالية وما إذا كان الجهاز في وضع قابل للطي مثل وضع الطاولة.

يمكنك تجربة ذلك الآن في MainActivity.

  1. في onCreate() داخل المجموعة ReplyTheme، يمكنك الحصول على المعلومات التكيُّفية للنافذة وعرض فئات المقاسات في سمة Text قابلة للإنشاء. يمكنك إضافة هذا العنصر بعد العنصر ReplyApp():

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )

            val adaptiveInfo = currentWindowAdaptiveInfo()
            val sizeClassText =
                "${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
                "${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
            Text(
                text = sizeClassText,
                color = Color.Magenta,
                modifier = Modifier.padding(
                    WindowInsets.safeDrawing.asPaddingValues()
                )
            )
        }
    }
}

سيؤدي تشغيل التطبيق الآن إلى عرض فئات حجم النوافذ مطبوعًا فوق محتوى التطبيق. يمكنك استكشاف المعلومات الأخرى المقدَّمة في معلومات التكيف مع النافذة. وبعد ذلك، يمكنك إزالة هذا Text لأنّه يغطي محتوى التطبيق ولن يكون ضروريًا للخطوات التالية.

4. انتقال تفاعلي

يمكنك الآن تعديل تنقّل التطبيق عند تغيير حالة الجهاز وحجمه لتسهيل استخدام تطبيقك.

عندما يمسك المستخدمون الهاتف، تكون أصابعهم عادةً في أسفل الشاشة. عندما يمسك المستخدمون جهازًا قابلاً للطي أو جهازًا لوحيًا مفتوحًا، تكون أصابعهم عادةً قريبة من الجوانب. يجب أن يتمكّن المستخدمون من التنقّل في التطبيق أو بدء التفاعل معه بدون الحاجة إلى وضع أيديهم في مواضع غير مألوفة أو تغيير مواضع أيديهم.

أثناء تصميم تطبيقك وتحديد مكان وضع عناصر واجهة المستخدم التفاعلية في التنسيق، يجب مراعاة التأثيرات المريحة لمناطق الشاشة المختلفة.

  • ما هي المناطق التي يمكنك الوصول إليها أثناء حمل الجهاز؟
  • ما هي المناطق التي لا يمكن الوصول إليها إلا بتمديد الأصابع، وهو أمر غير مريح؟
  • ما هي المناطق التي يصعب الوصول إليها أو التي تبعد عن مكان إمساك المستخدم بالجهاز؟

إنّ شريط التنقّل هو أول عنصر يتفاعل معه المستخدمون، وهو يحتوي على إجراءات بالغة الأهمية ذات صلة بتجارب المستخدمين المهمة، لذا يجب وضعه في المناطق التي يسهل الوصول إليها. توفر مكتبة Material التكيُّفية العديد من المكونات التي تساعدك في تنفيذ التنقل، اعتمادًا على فئة حجم نافذة الجهاز.

شريط التنقّل السفلي

إنّ شريط التنقّل في أسفل الشاشة مثالي للتصاميم المدمجة، لأنّنا نحمل الجهاز بشكل طبيعي بحيث يمكن لسنّ الإبهام الوصول بسهولة إلى جميع نقاط اللمس في شريط التنقّل في أسفل الشاشة. استخدِم هذا الوضع عندما يكون لديك جهاز صغير الحجم أو جهاز قابل للطي في وضع مضغوط.

شريط تنقّل سفلي يتضمّن عناصر

بالنسبة إلى حجم النافذة المتوسط العرض، تكون شريط التنقّل مثاليًا للوصول إليه لأنّ الإبهام يقع بشكل طبيعي على جانب الجهاز. يمكنك أيضًا دمج شريط التنقّل مع درج التنقّل لعرض المزيد من المعلومات.

شريط التنقّل مع العناصر

يوفّر "درج التنقّل" طريقة سهلة للاطّلاع على معلومات مفصّلة حول علامات التبويب الخاصة بالتنقّل، ويمكن الوصول إليه بسهولة عند استخدام الأجهزة اللوحية أو الأجهزة الأكبر حجمًا. يتوفّر نوعان من أدراج التنقّل: درج تنقّل مشروط ودرج تنقّل دائم.

لائحة التنقل المشروطة

يمكنك استخدام درج تنقل مشروط للهواتف والأجهزة اللوحية الصغيرة والمتوسطة الحجم حيث يمكن توسيعه أو إخفاؤه كتراكب على المحتوى. ويمكن أحيانًا دمج هذا مع شريط تنقّل.

لائحة التنقّل المشروطة التي تتضمّن العناصر

لائحة التنقّل الدائم

يمكنك استخدام درج تنقّل دائم للتنقّل الثابت على الأجهزة اللوحية الكبيرة وأجهزة Chromebook وأجهزة الكمبيوتر المكتبي.

لائحة تنقّل دائمة تحتوي على عناصر

تنفيذ التنقّل الديناميكي

يمكنك الآن التبديل بين أنواع مختلفة من التنقّل عند تغيُّر حالة الجهاز وحجمه.

يعرض التطبيق حاليًا رمز NavigationBar أسفل محتوى الشاشة دائمًا بغض النظر عن حالة الجهاز. بدلاً من ذلك، يمكنك استخدام مكوّن المادة NavigationSuiteScaffold للتبديل تلقائيًا بين مكوّنات التنقّل المختلفة استنادًا إلى معلومات مثل فئة حجم النافذة الحالية.

  1. أضِف التبعية في Gradle للحصول على هذا المكوّن من خلال تعديل قائمة الإصدارات ونص إنشاء التطبيق، ثمّ أجرِ مزامنة Gradle:

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0"

[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.navigation.suite)
}
  1. ابحث عن الدالة القابلة للتجميع ReplyNavigationWrapper() في ReplyApp.kt واستبدِل Column ومحتوياته بـ NavigationSuiteScaffold:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    NavigationSuiteScaffold(
        navigationSuiteItems = {
            ReplyDestination.entries.forEach {
                item(
                    selected = it == selectedDestination,
                    onClick = { /*TODO update selection*/ },
                    icon = {
                        Icon(
                            imageVector = it.icon,
                            contentDescription = stringResource(it.labelRes)
                        )
                    },
                    label = {
                        Text(text = stringResource(it.labelRes))
                    },
                )
            }
        }
    ) {
        content()
    }
}

وسيطة navigationSuiteItems هي مجموعة تتيح لك إضافة عناصر باستخدام الدالة item()، تمامًا مثل إضافة عناصر في LazyColumn. داخل دالة lambda اللاحقة، تستدعي هذه التعليمات البرمجية دالة content() التي تم تمريرها كوسيطة لدالة ReplyNavigationWrapperUI().

ويمكنك تشغيل التطبيق على المحاكي ومحاولة تغيير الأحجام بين الهاتف والجهاز القابل للطي والكمبيوتر اللوحي، وسيظهر شريط التنقّل بين شريط التنقُّل وشريط التنقّل.

في النوافذ الواسعة جدًا، مثل الجهاز اللوحي في الوضع الأفقي، قد تحتاج إلى عرض درج التنقّل الدائم. لا يتيح NavigationSuiteScaffold عرض درج دائم، مع أنّه لا يظهر في أي من قيم WindowWidthSizeClass الحالية. ومع ذلك، يمكنك إجراء تغيير بسيط لإجراء ذلك.

  1. أضِف الرمز التالي قبل استدعاء NavigationSuiteScaffold مباشرةً:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    val windowSize = with(LocalDensity.current) {
        currentWindowSize().toSize().toDpSize()
    }
    val layoutType = if (windowSize.width >= 1200.dp) {
        NavigationSuiteType.NavigationDrawer
    } else {
        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
            currentWindowAdaptiveInfo()
        )
    }

    NavigationSuiteScaffold(
        layoutType = layoutType,
        ...
    ) {
        content()
    }
}

تحصل هذه التعليمة البرمجية أولاً على حجم النافذة وتحوّله إلى وحدات DP باستخدام currentWindowSize() وLocalDensity.current، ثم تقارن عرض النافذة لتحديد نوع تنسيق واجهة مستخدم التنقّل. إذا كان عرض النافذة 1200.dp على الأقل، يتم استخدام NavigationSuiteType.NavigationDrawer. وإلا، يتم الرجوع إلى الحساب التلقائي.

عند تشغيل التطبيق مرة أخرى على المحاكي القابل لتغيير الحجم وتجربة أنواع مختلفة، لاحظ أنّه عند تغيير إعدادات الشاشة أو فتح جهاز قابل للطي، يتغيّر التنقّل إلى النوع المناسب لذلك الحجم.

عرض تغييرات التوافق مع أحجام الأجهزة المختلفة

نشكرك على الاطّلاع على أنواع مختلفة من التنقّل لتتوافق مع أنواع مختلفة من أحجام النوافذ وحالاتها.

في القسم التالي، ستتعرّف على كيفية الاستفادة من أي مساحة متبقية على الشاشة بدلاً من تمديد عنصر القائمة نفسه من الحافة إلى الحافة.

5- استخدام مساحة الشاشة

سواء كنت تستخدم التطبيق على جهاز لوحي صغير أو جهاز مفتوح أو جهاز لوحي كبير، يتم تمديد الشاشة لملء المساحة المتبقية. يجب التأكّد من أنّه يمكنك الاستفادة من مساحة الشاشة هذه لعرض المزيد من المعلومات، مثل عرض الرسائل الإلكترونية والمحادثات للمستخدمين على الصفحة نفسها في هذا التطبيق.

تحدد المادة 3 ثلاثة تنسيقات أساسية يحتوي كل منها على تكوينات لفئات أحجام النوافذ الصغيرة والمتوسطة والموسعة. إنّ التنسيق الأساسي تفاصيل القائمة مثالي لحالة الاستخدام هذه، وهو متوفّر في ميزة "الإنشاء" بالرمز ListDetailPaneScaffold.

  1. احصل على هذا المكون عن طريق إضافة التبعيات التالية وإجراء مزامنة Gradle:

gradle/libs.versions.toml

[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.layout)
    implementation(libs.androidx.material3.adaptive.navigation)
}
  1. يمكنك البحث عن دالة ReplyAppContent() القابلة للإنشاء في ReplyApp.kt، والتي تعرض حاليًا جزء القائمة فقط من خلال طلب ReplyListPane(). استبدِل هذا التنفيذ بـ ListDetailPaneScaffold عن طريق إدراج الرمز التالي. بما أنّ هذه واجهة برمجة تطبيقات تجريبية، عليك أيضًا إضافة التعليق التوضيحي @OptIn في الدالة ReplyAppContent():

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            ReplyListPane(replyHomeUIState, onEmailClick)
        },
        detailPane = {
            ReplyDetailPane(replyHomeUIState.emails.first())
        }
    )
}

ينشئ هذا الرمز أولاً أداة تنقّل باستخدام rememberListDetailPaneNavigator(). يوفر المستكشف بعض إمكانية التحكم في الجزء الذي يتم عرضه والمحتوى الذي يجب تمثيله في ذلك الجزء، والذي سيتم عرضه لاحقًا.

سيعرض ListDetailPaneScaffold لوحتَين عند توسيع فئة حجم عرض النافذة. بخلاف ذلك، سيعرض أحد اللوحة أو اللوحة الأخرى استنادًا إلى القيم المقدَّمة لمعاملتَين: توجيه الإطار العملي وقيمة الإطار العملي. للحصول على السلوك التلقائي، يستخدم هذا الرمز البرمجي توجيه الإطار العمل وقيمة الإطار العمل المقدَّمة من المتصفّح.

أما المَعلمات المطلوبة المتبقية، فهي وظائف lambda قابلة للتجميع للوحات. يتم استخدام ReplyListPane() وReplyDetailPane() (المتوفّرَين في ReplyListContent.kt) لملء أدوار لوحتَي القائمة والتفاصيل، على التوالي. يتوقع ReplyDetailPane() مَعلمة بريد إلكتروني، لذا يستخدم هذا الرمز في الوقت الحالي الرسالة الإلكترونية الأولى من قائمة الرسائل الإلكترونية في ReplyHomeUIState.

شغِّل التطبيق وبدِّل طريقة عرض المحاكي إلى جهاز قابل للطي أو جهاز لوحي (قد تحتاج أيضًا إلى تغيير الاتجاه) للاطّلاع على تنسيق اللوحةَين. يبدو هذا بالفعل أفضل بكثير من ذي قبل!

والآن، لنتناول بعض السلوك المطلوب لهذه الشاشة. عندما ينقر المستخدم على رسالة إلكترونية في لوحة القائمة، من المفترض أن تظهر في لوحة التفاصيل مع جميع الردود. لا يتتبّع التطبيق حاليًا عنوان البريد الإلكتروني الذي تم اختياره، ولا يؤدي النقر على عنصر إلى أي إجراء. أفضل مكان للاحتفاظ بهذه المعلومات هو في بقية حالة واجهة المستخدم في ReplyHomeUIState.

  1. افتح ReplyHomeViewModel.kt وابحث عن فئة البيانات ReplyHomeUIState. أضِف خاصية للبريد الإلكتروني المحدّد، مع قيمة تلقائية هي null:

ReplyHomeViewModel.kt

data class ReplyHomeUIState(
    val emails : List<Email> = emptyList(),
    val selectedEmail: Email? = null,
    val loading: Boolean = false,
    val error: String? = null
)
  1. في الملف نفسه، تحتوي ReplyHomeViewModel على دالة setSelectedEmail() يتمّ استدعاؤها عندما ينقر المستخدم على عنصر في القائمة. عدِّل هذه الدالة لنسخ حالة واجهة المستخدم وتسجيل البريد الإلكتروني المحدّد:

ReplyHomeViewModel.kt

fun setSelectedEmail(email: Email) {
    _uiState.update {
        it.copy(selectedEmail = email)
    }
}

يجب مراعاة ما يحدث قبل أن ينقر المستخدم على أي عنصر ويكون عنوان البريد الإلكتروني المحدّد هو null. ما الذي يجب عرضه في لوحة التفاصيل؟ هناك طرق متعدّدة للتعامل مع هذه الحالة، مثل عرض العنصر الأول في القائمة تلقائيًا.

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

ReplyHomeViewModel.kt

private fun observeEmails() {
    viewModelScope.launch {
        emailsRepository.getAllEmails()
            .catch { ex ->
                _uiState.value = ReplyHomeUIState(error = ex.message)
            }
            .collect { emails ->
                val currentSelection = _uiState.value.selectedEmail
                _uiState.value = ReplyHomeUIState(
                    emails = emails,
                    selectedEmail = currentSelection ?: emails.first()
                )
            }
    }
}
  1. ارجع إلى ReplyApp.kt واستخدِم الرسالة الإلكترونية المحدّدة، إذا كانت متاحة، لملء محتوى اللوحة التفصيلية:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    detailPane = {
        if (replyHomeUIState.selectedEmail != null) {
            ReplyDetailPane(replyHomeUIState.selectedEmail)
        }
    }
)

شغِّل التطبيق مرة أخرى وبدِّل المحاكي إلى حجم الجهاز اللوحي، وتحقّق من أنّ النقر على عنصر في القائمة يؤدي إلى تعديل محتوى لوحة التفاصيل.

يعمل هذا بشكل رائع عندما يكون كلا الجزأين مرئيين، ولكن عندما لا تحتوي النافذة على مساحة إلا لعرض جزء واحد، يبدو أنه لا يحدث شيء عند النقر على أحد العناصر. حاوِل تبديل عرض المحاكي إلى هاتف أو جهاز قابل للطي في الوضع العمودي، ولاحظ أنّ لوحة القائمة فقط هي الظاهرة حتى بعد النقر على عنصر. يرجع ذلك إلى أنّه على الرغم من تعديل عنوان البريد الإلكتروني المحدَّد، يواصل ListDetailPaneScaffold التركيز على لوحة القائمة في هذه الإعدادات.

  1. لحلّ هذه المشكلة، أدخِل الرمز التالي كدالّة lambda تم تمريرها إلى ReplyListPane:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    listPane = {
        ReplyListPane(
            replyHomeUIState = replyHomeUIState,
            onEmailClick = { email ->
                onEmailClick(email)
                navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
            }
        )
    },
    // ...
)

يستخدم هذا الرمز البرمجي lambda المخطِّط الذي تم إنشاؤه سابقًا لإضافة سلوك إضافي عند النقر على عنصر. سيتم استدعاء دالة LAMBDA الأصلية التي تم تمريرها إلى هذه الدالة، ثم يتم أيضًا استدعاء navigator.navigateTo() لتحديد اللوحة التي يجب عرضها. ولكل جزء في سقالة دور مرتبط به، وبالنسبة إلى جزء التفاصيل هو ListDetailPaneScaffoldRole.Detail. في النوافذ الأصغر حجمًا، سيبدو أنّ التطبيق قد تنقّل إلى الأمام.

يجب أن يتعامل التطبيق أيضًا مع ما يحدث عندما يضغط المستخدم على زر الرجوع من لوحة التفاصيل، وسيختلف هذا السلوك حسب ما إذا كانت هناك لوحة واحدة أو لوحتان مرئيتان.

  1. يمكنك إتاحة الانتقال للخلف من خلال إضافة الرمز التالي.

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    BackHandler(navigator.canNavigateBack()) {
        navigator.navigateBack()
    }

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            AnimatedPane {
                ReplyListPane(
                    replyHomeUIState = replyHomeUIState,
                    onEmailClick = { email ->
                        onEmailClick(email)
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
                    }
                )
            }
        },
        detailPane = {
            AnimatedPane {
                if (replyHomeUIState.selectedEmail != null) {
                    ReplyDetailPane(replyHomeUIState.selectedEmail)
                }
            }
        }
    )
}

يعرف الملخّص الحالة الكاملة لـ ListDetailPaneScaffold، وما إذا كان من الممكن التنقّل للخلف، وما يجب فعله في جميع هذه الحالات. ينشئ هذا الرمز BackHandler يتم تفعيله عندما يتمكّن المستكشف من الرجوع إلى الصفحة السابقة، وداخل lambda يستدعي navigateBack(). بالإضافة إلى ذلك، لتسهيل الانتقال بين الأقسام، يتم لف كل قسم في عنصر AnimatedPane() قابل للتركيب.

شغِّل التطبيق مرة أخرى على محاكي قابل للتغيير في الحجم لجميع الأنواع المختلفة من الأجهزة، ولاحظ أنّه عند تغيير إعدادات الشاشة أو فتح جهاز قابل للطي، يتغيّر التنقّل ومحتوى الشاشة بشكل ديناميكي استجابةً لتغييرات حالة الجهاز. جرِّب أيضًا النقر على الرسائل الإلكترونية في لوحة القائمة ومعرفة كيفية تصرف التنسيق على شاشات مختلفة، مع عرض كلتا اللوحتَين جنبًا إلى جنب أو الانتقال بسلاسة بينهما.

عرض التغييرات في مدى التوافق مع أحجام الأجهزة المختلفة

تهانينا، لقد نجحت في جعل تطبيقك قابلاً للتكيّف مع جميع أنواع حالات الأجهزة وأحجامها. يمكنك تجربة تشغيل التطبيق على الأجهزة القابلة للطي أو الأجهزة اللوحية أو الأجهزة الجوّالة الأخرى.

6- تهانينا

تهانينا! لقد أكملت هذا الدرس التطبيقي حول الترميز بنجاح وتعرّفت على كيفية جعل التطبيقات قابلة للتكيّف باستخدام Jetpack Compose.

لقد تعرّفت على كيفية التحقّق من حجم الجهاز وحالة الطي، وتعديل واجهة المستخدم والتنقّل والوظائف الأخرى في تطبيقك وفقًا لذلك. لقد تعلمت أيضًا كيف تعمل القدرة على التكيف على تحسين إمكانية الوصول وتحسين تجربة المستخدم.

ما هي الخطوات التالية؟

اطّلِع على الدروس التطبيقية الأخرى حول الترميز في مسار إنشاء المحتوى.

أمثلة على التطبيقات

  • عيّنات التركيب هي مجموعة من التطبيقات الكثيرة التي تتضمّن أفضل الممارسات الموضّحة في الدروس التطبيقية حول الترميز.

المستندات المرجعية