تغيير حجم تطبيق Android

1. مقدمة

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

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

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

ما ستنشئه

ستستكشف آثار تغيير الحجم الحر وتحسّن تطبيق Android لتوضيح أفضل الممارسات لتغيير الحجم. سيتم إجراء ما يلي في تطبيقك:

أن يكون لديك بيان متوافق

  • إزالة القيود التي تمنع تغيير حجم التطبيق بحرية

الحفاظ على الحالة عند تغيير الحجم

  • الحفاظ على حالة واجهة المستخدم عند تغيير حجمها باستخدام rememberSaveable
  • تجنُّب تكرار العمل في الخلفية بدون داعٍ لتهيئة واجهة المستخدم

المتطلبات

  1. معرفة كيفية إنشاء تطبيقات Android أساسية
  2. معرفة ViewModel وState في Compose
  3. جهاز اختبار يتيح تغيير حجم النوافذ الحرة، مثل أحد الأجهزة التالية:

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

2. البدء

استنسِخ المستودع من GitHub.

git clone https://github.com/android/large-screen-codelabs/

...أو نزِّل ملف zip الخاص بالمستودع واستخرِجه

استيراد مشروع

  • فتح "استوديو Android"
  • اختَر استيراد مشروع (Import Project) أو ملف (File) > جديد (New) > استيراد مشروع (Import Project).
  • انتقِل إلى المكان الذي استنسخت فيه المشروع أو استخرجته
  • افتح مجلد تغيير الحجم.
  • افتح المشروع في مجلد البدء. يحتوي هذا الملف على الرمز الأولي.

تجربة التطبيق

  • إنشاء التطبيق وتشغيله
  • محاولة تغيير حجم التطبيق

ما رأيكم بذلك؟

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

القيود المفروضة على ملف البيان

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

ملف AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

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

3- تغييرات الإعدادات المتعلقة بتغيير الحجم

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

مراقبة تغييرات الإعدادات

لمشاهدة هذه التغييرات بنفسك في تطبيق تم إنشاؤه باستخدام نظام العرض في Android، يمكنك إلغاء View.onConfigurationChanged. في Jetpack Compose، يمكننا الوصول إلى LocalConfiguration.current، ويتم تعديله تلقائيًا كلما تم استدعاء View.onConfigurationChanged.

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

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

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

عند تغيير حجم التطبيق، يتم عرض معلومات الإعدادات المتغيرة في واجهة التطبيق في الوقت الفعلي

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

4. تسجيل أحداث مراحل النشاط

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

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

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

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

تعرض logcat طرق مراحل نشاط التطبيق التي يتم استدعاؤها عند تغيير الحجم

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

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

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

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

5- الاستمرارية: الحفاظ على الحالة الداخلية للعناصر القابلة للإنشاء عند تغيير حجمها

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

ابدأ بجعل الدالة القابلة للإنشاء NavigationDrawerHeader (الموجودة في ReplyHomeScreen.kt) تتوسّع لعرض عنوان البريد الإلكتروني عند النقر عليها.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

بعد إضافة العنوان القابل للتوسيع إلى تطبيقك،

  1. تشغيل التطبيق على جهاز الاختبار
  2. انقر على العنوان لتوسيعه
  3. جرِّب تغيير حجم النافذة

ستلاحظ أنّ العنوان يفقد حالته عند تغيير حجمه بشكلٍ كبير.

يتم النقر على العنوان في لوحة التنقّل الخاصة بالتطبيق وتوسيعه، ولكن يتم تصغيره بعد تغيير حجم التطبيق

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

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

6. تجنُّب التكرار غير الضروري للعمل في الخلفية

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

للاطّلاع على مثال على أنواع المشاكل التي قد تواجهها، أضِف بيان تسجيل إلى طريقة initializeUIState في ReplyViewModel.

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

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

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

لتجنُّب العمل غير الضروري في الخلفية، أزِل طلب initializeUIState() من طريقة onCreate() في نشاطك. بدلاً من ذلك، ابدأ البيانات في طريقة init الخاصة بـ ViewModel. يضمن ذلك تشغيل طريقة الإعداد مرة واحدة فقط، عند إنشاء مثيل ReplyViewModel لأول مرة:

init {
    initializeUIState()
}

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

7. تهانينا!

أحسنت! أحسنت. لقد طبّقت الآن بعض أفضل الممارسات التي تتيح تغيير حجم تطبيقات Android بشكل جيد على أجهزة ChromeOS وغيرها من بيئات النوافذ المتعددة والشاشات المتعددة.

نموذج الرمز المصدر

استنساخ المستودع من GitHub

git clone https://github.com/android/large-screen-codelabs/

...أو نزِّل ملف zip الخاص بالمستودع واستخرِجه