1. مقدمة
في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية إنشاء تطبيقات متكيّفة للهواتف والأجهزة اللوحية والهواتف القابلة للطي، وكيفية تحسين إمكانية الوصول إليها باستخدام Jetpack Compose. ستتعرّف أيضًا على أفضل الممارسات لاستخدام عناصر Material 3 وسماتها.
قبل أن نبدأ، من المهم أن نفهم ما نعنيه بالقدرة على التكيّف.
القدرة على التكيّف
يجب أن تكون واجهة المستخدم لتطبيقك سريعة الاستجابة لتناسب أحجام النوافذ المختلفة والاتجاهات وعوامل الشكل. يتغيّر التنسيق المتجاوب استنادًا إلى مساحة الشاشة المتاحة له. وتتراوح هذه التغييرات بين تعديلات بسيطة على التنسيق لملء المساحة، واختيار أنماط التنقّل المناسبة، وتغيير التنسيقات بالكامل للاستفادة من المساحة الإضافية.
لمزيد من المعلومات، اطّلِع على التصميم التكيّفي.
في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية استخدام ميزة التكيّف وكيفية الاستفادة منها عند استخدام Jetpack Compose. يمكنك إنشاء تطبيق باسم Reply يوضّح لك كيفية تنفيذ ميزة التكيّف مع جميع أنواع الشاشات، وكيف تعمل ميزتا التكيّف وإمكانية الوصول معًا لمنح المستخدمين تجربة مثالية.
أهداف الدورة التعليمية
- كيفية تصميم تطبيقك ليناسب جميع أحجام النوافذ باستخدام Jetpack Compose
- كيفية استهداف تطبيقك لأجهزة قابلة للطي مختلفة
- كيفية استخدام أنواع مختلفة من أدوات التنقّل لتحسين إمكانية الوصول إلى المحتوى
- كيفية استخدام مكوّنات Material 3 لتقديم أفضل تجربة لكل حجم نافذة
المتطلبات
- أحدث إصدار ثابت من استوديو Android
- جهاز افتراضي قابل لتغيير الحجم يعمل بالإصدار 13 من نظام التشغيل Android
- معرفة بلغة Kotlin
- فهم أساسي لـ Compose (مثل التعليق التوضيحي
@Composable) - معرفة أساسية بتنسيقات Compose (مثل
RowوColumn) - معرفة أساسية بالمعدّلات (مثل
Modifier.padding()).
ستستخدِم المحاكي القابل لتغيير الحجم في هذا الدرس التطبيقي حول الترميز، ما يتيح لك التبديل بين أنواع مختلفة من الأجهزة وأحجام النوافذ.

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

2. طريقة الإعداد
للحصول على الرمز البرمجي لهذا الدرس التطبيقي، استنسِخ مستودع GitHub من سطر الأوامر:
git clone https://github.com/android/codelab-android-compose.git cd codelab-android-compose/AdaptiveUiCodelab
يمكنك بدلاً من ذلك تنزيل المستودع كملف ZIP باتّباع الخطوات التالية:
ننصحك بالبدء بالرمز البرمجي في فرع main واتّباع الخطوات الواردة في الدرس التطبيقي حول الترميز خطوة بخطوة بالسرعة التي تناسبك.
فتح المشروع في "استوديو Android"
- في نافذة مرحبًا بك في "استوديو Android"، انقر على
فتح مشروع حالي. - اختَر المجلد
<Download Location>/AdaptiveUiCodelab(تأكَّد من اختيار الدليلAdaptiveUiCodelabالذي يحتوي علىbuild.gradle). - بعد أن يستورد "استوديو Android" المشروع، اختبِر إمكانية تشغيل فرع
main.
استكشاف رمز البدء
يحتوي رمز فرع main على حزمة ui. ستعمل على الملفات التالية في تلك الحزمة:
-
MainActivity.kt: نشاط نقطة الدخول الذي تبدأ منه تطبيقك. -
ReplyApp.kt: يحتوي على عناصر واجهة المستخدم القابلة للإنشاء في الشاشة الرئيسية. ReplyHomeViewModel.kt: يوفّر بيانات وحالة واجهة المستخدم لمحتوى التطبيق.-
ReplyListContent.kt: تحتوي على عناصر قابلة للإنشاء لتوفير القوائم وشاشات التفاصيل.
إذا شغّلت هذا التطبيق على محاكي قابل لتغيير الحجم وجرّبت أنواعًا مختلفة من الأجهزة، مثل هاتف أو جهاز لوحي، تتوسّع واجهة المستخدم لتشغل المساحة المحدّدة بدلاً من الاستفادة من مساحة الشاشة أو توفير بيئة عمل مريحة.


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


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

بالإضافة إلى ذلك، يمكن أن ينظر المستخدم إلى الشاشة الداخلية بينما يكون المفصل مفتوحًا جزئيًا، ما يؤدي إلى أوضاع جسدية مختلفة استنادًا إلى اتجاه الطي: وضع سطح الطاولة (طي أفقي، يظهر على اليسار في الصورة أعلاه) ووضع الكتاب (طي عمودي).
مزيد من المعلومات حول أوضاع الطي والمفصلات
كل هذه الأمور يجب مراعاتها عند تنفيذ التصاميم التكيُّفية التي تتوافق مع الأجهزة القابلة للطي.
الحصول على معلومات تكيّفية
توفّر مكتبة Material3 adaptive إمكانية الوصول بسهولة إلى معلومات حول النافذة التي يتم تشغيل تطبيقك فيها.
- أضِف إدخالات لهذا العنصر وإصداره إلى ملف كتالوج الإصدارات:
gradle/libs.versions.toml
[versions]
material3Adaptive = "1.0.0"
[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
- في ملف التصميم الخاص بوحدة التطبيق، أضِف اعتمادية المكتبة الجديدة، ثم نفِّذ عملية مزامنة Gradle:
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive)
}
الآن، في أي نطاق قابل للإنشاء، يمكنك استخدام currentWindowAdaptiveInfo() للحصول على عنصر WindowAdaptiveInfo يحتوي على معلومات، مثل فئة حجم النافذة الحالية وما إذا كان الجهاز في وضع قابل للطي، مثل وضع سطح الطاولة.
يمكنك تجربة هذه الميزة الآن في MainActivity.
- في
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 في Material للتبديل تلقائيًا بين مكوّنات التنقّل المختلفة استنادًا إلى معلومات مثل فئة حجم النافذة الحالية.
- أضِف اعتمادية 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)
}
- ابحث عن الدالة المركّبة
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 الحالية. ومع ذلك، يمكنك إجراء تغيير بسيط لجعله يفعل ذلك.
- أضِف الرمز التالي قبل استدعاء
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()
}
}
يحصل هذا الرمز أولاً على حجم النافذة ويحوّله إلى وحدات بكسل مستقلة عن الكثافة باستخدام currentWindowSize() وLocalDensity.current، ثم يقارن عرض النافذة لتحديد نوع تصميم واجهة مستخدم التنقّل. إذا كان عرض النافذة 1200.dp على الأقل، سيتم استخدام NavigationSuiteType.NavigationDrawer. وفي حال عدم توفّرها، يتم استخدام عملية الحساب التلقائية.
عند تشغيل التطبيق مرة أخرى على المحاكي القابل لتغيير الحجم وتجربة أنواع مختلفة، ستلاحظ أنّه كلما تغيّر إعداد الشاشة أو فتحت جهازًا قابلاً للطي، يتغيّر شريط التنقّل إلى النوع المناسب لهذا الحجم.

تهانينا، لقد تعرّفت على أنواع مختلفة من التنقّل لتوفير الدعم لأنواع مختلفة من أحجام النوافذ وحالاتها.
في القسم التالي، ستتعرّف على كيفية الاستفادة من أي مساحة متبقية على الشاشة بدلاً من توسيع عنصر القائمة نفسه من الحافة إلى الحافة.
5- استخدام مساحة الشاشة
سواء كنت تشغّل التطبيق على جهاز لوحي صغير أو جهاز مطوي أو جهاز لوحي كبير، يتم توسيع الشاشة لملء المساحة المتبقية. عليك التأكّد من إمكانية الاستفادة من مساحة الشاشة هذه لعرض المزيد من المعلومات، كما هو الحال في هذا التطبيق الذي يعرض البريد الإلكتروني وسلاسل المحادثات للمستخدمين على الصفحة نفسها.
تحدّد Material 3 ثلاثة تخطيطات أساسية يتضمّن كل منها إعدادات لفئات أحجام النوافذ المضغوطة والمتوسطة والموسّعة. يُعدّ التصميم الأساسي قائمة مع تفاصيل مثاليًا لحالة الاستخدام هذه، وهو متاح في Compose كـ ListDetailPaneScaffold.
- يمكنك الحصول على هذا المكوّن من خلال إضافة التبعيات التالية وتنفيذ عملية مزامنة 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)
}
- ابحث عن الدالة المركّبة
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 لوحتَين عند توسيع فئة حجم عرض النافذة. بخلاف ذلك، سيتم عرض إحدى اللوحتين استنادًا إلى القيم المقدَّمة لمعلَمتين: توجيه الإنشاء، وقيمة الإنشاء. للحصول على السلوك التلقائي، يستخدم هذا الرمز توجيه scaffold وقيمة scaffold التي يوفّرها Navigator.
المَعلمات المطلوبة المتبقية هي دوال lambda قابلة للإنشاء للوحات. يتم استخدام ReplyListPane() وReplyDetailPane() (الموجودَين في ReplyListContent.kt) لملء أدوار لوحَي القائمة والتفاصيل على التوالي. تتوقّع الدالة ReplyDetailPane() وسيطًا للبريد الإلكتروني، لذا يستخدم هذا الرمز حاليًا عنوان البريد الإلكتروني الأول من قائمة عناوين البريد الإلكتروني في ReplyHomeUIState.
شغِّل التطبيق وبدِّل طريقة عرض المحاكي إلى وضع الجهاز القابل للطي أو الجهاز اللوحي (قد تحتاج أيضًا إلى تغيير الاتجاه) للاطّلاع على التنسيق ذي اللوحتَين. يبدو هذا أفضل بكثير من السابق.
لننتقل الآن إلى بعض السلوكيات المطلوبة لهذه الشاشة. عندما ينقر المستخدم على رسالة إلكترونية في لوحة القائمة، يجب أن تظهر في لوحة التفاصيل مع جميع الردود. في الوقت الحالي، لا يتتبّع التطبيق عنوان البريد الإلكتروني الذي تم اختياره، ولا يؤدي النقر على عنصر إلى أي إجراء. أفضل مكان للاحتفاظ بهذه المعلومات هو مع بقية حالة واجهة المستخدم في ReplyHomeUIState.
- افتح
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
)
- في الملف نفسه، يحتوي
ReplyHomeViewModelعلى الدالةsetSelectedEmail()التي يتم استدعاؤها عندما ينقر المستخدم على عنصر في القائمة. عدِّل هذه الدالة لنسخ حالة واجهة المستخدم وتسجيل البريد الإلكتروني المحدّد:
ReplyHomeViewModel.kt
fun setSelectedEmail(email: Email) {
_uiState.update {
it.copy(selectedEmail = email)
}
}
يجب مراعاة ما يحدث قبل أن ينقر المستخدم على أي عنصر ويكون عنوان البريد الإلكتروني المحدّد هو null. ما الذي يجب عرضه في لوحة التفاصيل؟ هناك طرق متعددة للتعامل مع هذه الحالة، مثل عرض العنصر الأول في القائمة تلقائيًا.
- في الملف نفسه، عدِّل الدالة
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()
)
}
}
}
- ارجع إلى
ReplyApp.ktواستخدِم عنوان البريد الإلكتروني المحدّد، إذا كان متاحًا، لملء محتوى جزء التفاصيل:
ReplyApp.kt
ListDetailPaneScaffold(
// ...
detailPane = {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
)
شغِّل التطبيق مرة أخرى وبدِّل المحاكي إلى حجم الجهاز اللوحي، ولاحظ أنّ النقر على أحد عناصر القائمة يؤدي إلى تعديل محتوى جزء التفاصيل.
تعمل هذه الميزة بشكل رائع عندما يكون كلا اللوحين مرئيين، ولكن عندما لا تتسع النافذة إلا لعرض لوحة واحدة، يبدو وكأنّه لا يحدث شيء عند النقر على عنصر. جرِّب تبديل طريقة عرض المحاكي إلى هاتف أو جهاز قابل للطي في الوضع العمودي، ولاحظ أنّه لا يظهر سوى جزء القائمة حتى بعد النقر على أحد العناصر. ويرجع ذلك إلى أنّ ListDetailPaneScaffold يركّز على لوحة القائمة في هذه الإعدادات، حتى بعد تعديل عنوان البريد الإلكتروني المحدّد.
- لحلّ هذه المشكلة، أدرِج الرمز التالي كدالة 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. في النوافذ الأصغر، سيبدو أنّ التطبيق قد انتقل إلى الأمام.
يحتاج التطبيق أيضًا إلى التعامل مع ما يحدث عندما ينقر المستخدم على زر الرجوع من لوحة التفاصيل، وسيختلف هذا السلوك استنادًا إلى ما إذا كانت هناك لوحة واحدة أو لوحتان ظاهرتان.
- يمكنك إتاحة الرجوع إلى الخلف من خلال إضافة الرمز التالي.
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 يتم تفعيله عندما يكون بإمكان المتصفّح الرجوع إلى الصفحة السابقة، ويتم استدعاء navigateBack() داخل دالة lambda. بالإضافة إلى ذلك، لجعل الانتقال بين اللوحات أكثر سلاسة، يتم تضمين كل لوحة في عنصر AnimatedPane() قابل للإنشاء.
أعِد تشغيل التطبيق على محاكي قابل لتغيير الحجم لجميع أنواع الأجهزة المختلفة، ولاحظ أنّه كلما تغيّر إعداد الشاشة أو فتحت جهازًا قابلاً للطي، يتغيّر التنقّل ومحتوى الشاشة بشكلٍ ديناميكي استجابةً لتغييرات حالة الجهاز. جرِّب أيضًا النقر على الرسائل الإلكترونية في جزء القائمة واطّلِع على طريقة عمل التنسيق على الشاشات المختلفة، سواء بعرض كلا الجزأين جنبًا إلى جنب أو التبديل بينهما بسلاسة.

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