تغيير حجم تطبيق 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"
  • اختَر استيراد مشروع أو ملف->جديد->استيراد مشروع.
  • الانتقال إلى المكان الذي استنسخت فيه المشروع أو استخرجته
  • افتح مجلد تغيير الحجم.
  • افتح المشروع في مجلد start (البدء). وهي تحتوي على رمز إجراء التفعيل.

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

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

ما رأيك؟

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

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

إذا نظرت في ملف 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 للاحتفاظ بالعناصر القابلة للإنشاء. حالة واجهة المستخدم الداخلية من خلال تغييرات الإعدادات التي قد تحدث بشكل متكرر نتيجة لتغيير حجم النافذة الحرة. ومع ذلك، يجب أن يرفع التطبيق غالبًا حالة واجهة المستخدم ومنطقها بعيدًا عن العناصر القابلة للإنشاء. ويُعد نقل ملكية الحالة إلى نموذج View أحد أفضل الطرق للحفاظ على الحالة أثناء تغيير الحجم. عند رفع حالتك إلى 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 للمستودع واستخراجه