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

1- مقدمة

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

قبل أن نتعمق في، من المهم أن نفهم ما نعنيه القدرة على التكيّف.

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

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

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

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

ما ستتعرَّف عليه

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

المتطلبات

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

محاكي يمكن تغيير حجمه ويتضمّن خيارات مثل الهاتف والجهاز غير المطوي والجهاز اللوحي والكمبيوتر المكتبي

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

ما الذي ستقوم ببنائه

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

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

‫2. الإعداد

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

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

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

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

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

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

التعرّف على رمز البدء

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

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

ستركّز أولاً على MainActivity.kt.

MainActivity.kt

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

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

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

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

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

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

3- تخصيص التطبيقات

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

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

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

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

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

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

WindowHeightSizeClass للارتفاع الصغير والمتوسط والموسَّع.

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

حالات الطي

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

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

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

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

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

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

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

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

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[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(20.dp)
            )
        }
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

درج التنقّل المشروط

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

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

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

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

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

تنفيذ الانتقال التفاعلي

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

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

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

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0-beta01"

[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

[versions]
material3Adaptive = "1.0.0-beta01"

[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 جزأين عند توسيع فئة حجم عرض النافذة. بخلاف ذلك، سيعرض جزءًا واحدًا أو الجزء الآخر بناءً على القيم المقدمة لمعلمتين: توجيه السقالة وقيمة السقالة. للحصول على السلوك الافتراضي، تستخدم هذه التعليمة البرمجية توجيه السقالة وقيمة التخزين التي يوفرها المستكشف.

المعلمات المطلوبة المتبقية هي lambdas قابلة للإنشاء للأجزاء. يتم استخدام 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.

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

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

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

نماذج التطبيقات

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

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