1. تعلم برمجة Android
ملف PDF الخاص بالوثيقة متاح |HERE|.
أمثلة المستند متاحة |HERE|.
1.1. Introduction
1.1.1. المحتوى
هذا المستند هو نسخة معدلة من عدة مستندات موجودة:
- Android للمطورين J2EE؛
- مقدمة إلى برمجة أجهزة Android اللوحية من خلال الأمثلة؛
- التحكم في Arduino باستخدام جهاز لوحي يعمل بنظام Android؛
- مقدمة إلى برمجة أجهزة Android اللوحية من خلال الأمثلة - الإصدار 2
ويقدم المستند الجديد الميزات التالية:
- عرض المستند 1 بنية تسمى AVAT (النشاطات-العروض-الإجراءات-المهام) لتسهيل البرمجة غير المتزامنة في تطبيق أندرويد. في هذا المستند، تُستخدم المكتبة القياسية RxJava لإدارة الإجراءات غير المتزامنة؛
- استخدم المستند 2 Eclipse IDE مع مكون إضافي Android. يستخدم هذا المستند Android Studio؛
- تم استنساخ الوثيقة 3 كما هي؛
- كان المستند 4 يستخدم المكتبة [Android Annotations] (AA) مع IDE Intellij IDEA Community Edition. يستند هذا المستند إلى المستند 4 بالكامل مع الاختلافات التالية:
- أصبح IDE الآن Android Studio؛
- نظام البناء هو Gradle لجميع مشاريع العميل أو الخادم (في الوثيقة 4، كان يتم استخدام Maven أحيانًا)
- يتم تنفيذ البرمجة غير المتزامنة باستخدام مكتبة RxJava (في الوثيقة 4، كانت تُستخدم مكتبة AA)؛
- يستكشف هذا المستند مجالات لم يتم تناولها أو تم تناولها بشكل محدود في المستندات السابقة:
- مفهوم تجاور الأجزاء؛
- حفظ/استعادة النشاط وشظاياه؛
- دورة حياة الأجزاء؛
وأخيرًا، يعرض هذا المستند الهيكل الأساسي لعميل Android يتواصل مع خدمة ويب / jSON، حيث يتم تجميع عدد كبير من العناصر التي ترد بانتظام في هذا النوع من العملاء. يتم استخدام هذا الهيكل الأساسي في جميع الأمثلة بدءًا من الفصل 2. وهذا هو الجزء المبتكر حقًا في المستند.
فيما يلي الأمثلة المعروضة:
الطبيعة | |
استيراد مشروع Android موجود | |
مشروع Android أساسي | |
مشروع [Android Annotations] أساسي | |
العروض والأحداث | |
التنقل بين العروض | |
التنقل عبر علامات التبويب | |
استخدام المكتبة [Android Annotations] مع Gradle | |
إدارة أجزاء تطبيق Android | |
التنقل بين العروض المعاد النظر فيه | |
بنية ذات طبقتين | |
بنية العميل/الخادم | |
إدارة عدم التزامن باستخدام RxJava | |
مكونات إدخال البيانات | |
استخدام نموذج العرض | |
المكون ListView | |
استخدام قائمة | |
استخدام فئة أصلية للأجزاء | |
حفظ واستعادة حالة النشاط والأجزاء | |
عميل الطقس | |
هيكل عميل Android يتصل بخدمة ويب / jSON. يتم فيه تحليل عدد كبير من العناصر التي توجد بشكل منتظم في هذا النوع من عملاء Android. | |
إدارة المواعيد في عيادة طبية | |
تمرين تطبيقي - إدارة الرواتب الأساسية | |
تمرين تطبيقي - التحكم في بطاقات Arduino |
تم استخدام هذا المستند في السنة الأخيرة من كلية الهندسة IstiA بجامعة أنجيه [istia.univ-angers.fr]. وهذا ما يفسر النبرة الغريبة أحيانًا للنص. التمرينان التطبيقيان هما نصان من TP لا نقدم سوى الخطوط العريضة لحلّهما. وعلى القارئ أن يبني الحل بنفسه.
كود المصدر للأمثلة متاح |ICI|. لتنفيذ هذه الأمثلة، يجب اتباع الإجراءات الواردة في الفقرة 6.12.
هذا المستند هو مستند للتدريب الأولي على برمجة Android. ولا يهدف إلى أن يكون شاملاً. وهو يستهدف بشكل أساسي المبتدئين.
الموقع المرجعي للبرمجة على نظام Android هو URL [http://developer.android.com/guide/components/index.html]. يجب عليك زيارة هذا الموقع للحصول على نظرة عامة على البرمجة على نظام Android.
1.1.2. المتطلبات الأساسية
المتطلب الأساسي للاستفادة المثلى من هذا المستند هو إتقان لغة Java.
1.1.3. الأدوات المستخدمة
تم اختبار الأمثلة التالية في البيئة التالية:
- جهاز يعمل بنظام Windows 10 Pro 64 بت؛
- JDK 1.8؛
- Android SDK API 23؛
- Android Studio، الإصدار 2.1؛
- محاكي Genymotion، الإصدار 2.6.0؛
لمتابعة هذا المستند، يجب عليك تثبيت:
- JDK (انظر الفقرة 6.8)؛
- مدير محاكيات Android Genymotion (انظر الفقرة 6.9)؛
- مدير التبعيات Maven (انظر الفقرة 6.10)؛
- IDE [Android Studio] (انظر الفقرة 6.11)؛
1.2. مثال-01: استيراد مثال Android
1.2.1. إنشاء المشروع
لنقم بإنشاء أول مشروع Android باستخدام Android Studio. أولاً، لنقم بإنشاء مجلد فارغ [exemples] حيث سيتم وضع جميع مشاريعنا:
![]() |
ثم ننشئ مشروعًا باستخدام Android Studio. سنقوم أولاً باستيراد أحد الأمثلة المرفقة مع IDE [1-5]:
![]() |

![]() | ![]() |
قد يؤدي استيراد المشروع إلى حدوث أخطاء بسبب عدم التوافق بين البيئة المستخدمة عند إنشاء المشروع وتلك المستخدمة هنا لتنفيذه. هذه فرصة لمعرفة كيفية حل هذا النوع من الأخطاء. هنا، لدينا الخطأ التالي:
![]() | ![]() |
يتم تكوين المشروع المستورد بواسطة الملف [build.gradle] [2] التالي:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
}
dependencies {
compile "com.android.support:support-v4:23.3.0"
compile "com.android.support:support-v13:23.3.0"
compile "com.android.support:cardview-v7:23.3.0"
}
// يستخدم نموذج البناء دلائل متعددة لـ
// لفصل النصوص النمطية والرموز الشائعة عن
// كود العينة الرئيسي.
List<String> dirs = [
'main', // كود العينة الرئيسي؛ انظر هنا للاطلاع على الأمور المثيرة للاهتمام.
'common', // المكونات التي يتم إعادة استخدامها في نماذج متعددة
'template'] // الكود النمطي الذي يتم إنشاؤه بواسطة عملية قالب العينة
android {
compileSdkVersion 21
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 21
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
main {
dirs.each { dir ->
java.srcDirs "src/${dir}/java"
res.srcDirs "src/${dir}/res"
}
}
androidTest.setRoot('tests')
androidTest.java.srcDirs = ['tests/src']
}
aaptOptions {
noCompress "pdf"
}
}
- الخطأ المبلغ عنه ناتج عن الأسطر 31 و34-35: ليس لدينا SDK 21. نستبدل هذه النسخة بالنسخة 23 المتوفرة لدينا.
في الملف [build.gradle]، يقدم Android Studio اقتراحات كما يلي:
![]() |
لقبول الاقتراحات، نقوم بـ [alt-entrée] على الاقتراح:
![]() |
قد تظهر أيضًا رسالة خطأ بشأن إصدار Gradle:
![]() |
يأتي هذا الخطأ من عدم توافق بين إصدار Gradle المطلوب بواسطة ملف [build.gradle] للمشروع (2.10 السطر 6 أدناه):
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
والإصدار المسجل في الملف [<projet>/gradle/wrapper/gradle-wrapper.properties]:
#الأربعاء 10 أبريل 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
في السطر 6 أعلاه، يجب استبدال الرقم 2.8 بالرقم 2.10.
للوصول إلى الملف [<projet>/gradle/wrapper/gradle-wrapper.properties]، يجب استخدام منظور المشروع الخاص بالمشروع:
![]() | ![]() | ![]() |
بعد تصحيح ذلك، يمكننا تجميع التطبيق [1]، وتشغيل محاكي Genymotion [2] ثم تنفيذ المشروع [3]:
![]() | ![]() | ![]() |
![]() |

دعونا نوقف التطبيق:
![]() |
يمكننا الآن إغلاق المشروع. سنقوم بإنشاء مشروع جديد.
![]() |
1.2.2. بعض النقاط حول IDE
1.2.2.1. المنظورات
يوفر Android Studio (AS) وجهات نظر مختلفة للعمل على مشروع. سنستخدم اثنتين منها بشكل أساسي:
- منظور [Android] [1]:
- منظور [Project] [4]؛
![]() | ![]() |
![]() |
في معظم الأحيان، سنعمل باستخدام المنظور [Android]. وعندما ننسخ مشروعًا إلى مشروع آخر، سنحتاج إلى المنظور [Project].
1.2.2.2. إدارة التنفيذ
هناك عدة طرق لتنفيذ / إيقاف / إعادة تنفيذ مشروع AS. أولاً، هناك أزرار شريط الأدوات:
![]() | ![]() | ![]() |
الزر [Rerun] [3]، يوقف تنفيذ المشروع [2] ثم يعيد تشغيله [1].
1.2.2.3. إدارة ذاكرة التخزين المؤقت
يحتفظ Android Studio بذاكرة تخزين مؤقتة للمشاريع التي يديرها بهدف جعل IDE أكثر استجابة قدر الإمكان. مع إصدار Android 2.1 (مايو 2016)، غالبًا ما كانت هذه الذاكرة المؤقتة لا تأخذ في الاعتبار التعديلات التي تم إجراؤها للتو على الكود. في هذه الحالة، يجب إبطال صلاحية هذه الذاكرة المؤقتة:
![]() | ![]() |
مع Android 2.1 (مايو 2016)، كان يجب إجراء العملية السابقة عدة مرات، وأحيانًا لم يكن ذلك كافيًا لحل المشكلة المكتشفة. كان الحل هو تعطيل تقنية [Instant Run]:
![]() | ![]() |
- في [3-4]، تم تعطيل كل شيء؛
في كل ما يلي، تم العمل باستخدام تكوين ذاكرة التخزين المؤقت هذه ولم نواجه أي مشاكل.
1.2.2.4. إدارة السجلات
أثناء تنفيذ المشروع، تظهر السجلات في شاشة Android:
![]() | ![]() |
في علامة التبويب [Android Monitor] [1]، تظهر السجلات في علامة التبويب [logcat] [2]. يتيح الزر [3] مسح السجلات. هذا الزر مفيد عندما تريد عرض سجلات إجراء معين:
- نقوم بمسح السجلات؛
- على جهاز Android، نقوم بالإجراء الذي نريد سجلاته؛
- السجلات التي تظهر بعد ذلك هي تلك المرتبطة بالإجراء الذي تم تنفيذه؛
هناك عدة مستويات لسجلات [4]. بشكل افتراضي، يتم تحديد الوضع [Verbose]. وهذا يعني أنه يتم عرض سجلات جميع المستويات. يمكننا باستخدام [4] تحديد مستوى معين.
تعد السجلات مفيدة جدًا لمعرفة الأوقات التي يتم فيها عرض طرق معينة أثناء تنفيذ المشروع. وسنلجأ إليها كثيرًا. لنأخذ كود الفئة [MainActivity] من مشروع [Exemple-01]:
![]() |
package com.example.android.pdfrendererbasic;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_real);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.container, new PdfRendererBasicFragment(),
FRAGMENT_PDF_RENDERER_BASIC)
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_info:
new AlertDialog.Builder(this)
.setMessage(R.string.intro_message)
.setPositiveButton(android.R.string.ok, null)
.show();
return true;
}
return super.onOptionsItemSelected(item);
}
}
فيما سبق، تعد الطرق [onCreate, ligne 14] و [onCreateOptionsMenu, ligne 26] طرقًا للفئة الأم [Activity] (السطر 9). يتم استدعاؤها في أوقات مختلفة من دورة حياة التطبيق. وأحيانًا يتم تنفيذها عدة مرات. حتى عند قراءة الوثائق، يصعب أحيانًا تحديد ما إذا كانت إحدى طرق دورة الحياة هذه ستُنفَّذ قبل أو بعد طريقة كتبناها بأنفسنا. ومع ذلك، غالبًا ما يكون من المهم معرفة هذه المعلومة. يمكننا إذن وضع سجلات كما يلي:
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
...
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("MainActivity","onCreateOptionsMenu");
getMenuInflater().inflate(R.menu.main, menu);
...
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("MainActivity","onOptionsItemSelected");
switch (item.getItemId()) {
...
}
}
- في الأسطر 7 و14 و21، نستخدم الفئة [Log]. تسمح هذه الفئة بكتابة سجلات على وحدة التحكم Android [logcat]. يتم تصنيف السجلات إلى مستويات مختلفة (معلومات، تحذير، تصحيح أخطاء، تفصيلي، خطأ). تعرض [Log.d] سجلات من المستوى [debug]. الحجة الأولى هي مصدر رسالة السجل. في الواقع، يمكن لمصادر مختلفة إرسال رسائل إلى وحدة التحكم في السجلات. من أجل التمييز بينها، يتم استخدام هذه الحجة الأولى. الحجة الثانية هي الرسالة المراد كتابتها على وحدة التحكم في السجلات؛
إذا قمنا بتنفيذ المشروع [Exemple-01] مرة أخرى، فسنحصل على السجلات التالية:
05-28 08:37:12.709 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreate
05-28 08:37:12.778 23881-23923/com.example.android.pdfrendererbasic D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
[ 05-28 08:37:12.781 23881:23881 D/ ]
HostConnection::get() New Host ...
05-28 08:37:12.967 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreateOptionsMenu
ونكتشف بذلك أن الطريقة [onCreate] التي تنشئ نشاط Android يتم تنفيذها قبل الطريقة [onCreateOptionsMenu] التي تنشئ قائمة التطبيق.
الآن، إذا نقرنا على خيار القائمة في محاكي Android [1]:
![]() |
يتم إضافة السجل التالي إلى وحدة التحكم في السجلات:
05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected
في ما يلي، سنقوم في كثير من الأحيان بإضافة تعليمات السجلات إلى كود Android. في معظم الأحيان، لن نعلق عليها. فهي موجودة فقط لدعوة القارئ إلى الاطلاع على وحدة التحكم في السجلات من أجل فهم دورة حياة تطبيق Android تدريجيًا.
1.2.2.5. إدارة المحاكي [Genymotion]
في بعض الأحيان، يتعطل محاكي Genymotion ولا يمكن إعادة تشغيله. ويرجع ذلك إلى بقاء عمليات Virtualbox قيد التشغيل في مدير المهام. افتح مدير المهام [Ctrl-Alt-Supp] وقم بإزالة جميع مهام Virtualbox الموجودة:
![]() | ![]() |
بعد القيام بذلك، أعد تشغيل محاكي Genymotion من Android Studio.
1.2.2.6. تم إنشاء الملف الثنائي APK
يؤدي تجميع المشروع إلى إنتاج ملف ثنائي بامتداد .apk:
![]() | ![]() | ![]() |
هناك نسختان: الأولى تسمى [debug] والأخرى تسمى [debug-unaligned]. يجب استخدام النسخة الأولى، أما الأخرى فهي نسخة وسيطة. يمكن نقل الملف الثنائي .pak الذي تم إنتاجه في [4] مباشرة إلى محاكي أو جهاز Android. لنقله إلى محاكي، يكفي سحبه وإفلاته على المحاكي باستخدام الماوس.
1.3. مثال-02: مشروع Android أساسي
لنقم بإنشاء مشروع Android جديد باستخدام Android Studio [1-12]:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() | ![]() | ![]() |
في [13]، يتم تشغيل التطبيق. ثم يتم عرض [14] على محاكي Genymotion.
1.3.1. تكوين Gradle
يتم تكوين المشروع الذي تم إنشاؤه بواسطة الملف [build.gradle] التالي:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
}
تم إنشاء هذا الملف بواسطة IDE مع عناصر تكوينه. إنه ملف بسيط سنقوم بتحسينه تدريجيًا.
- الأسطر 3-12: خصائص تطبيق Android؛
- الأسطر 22-25: تبعياته. هذا هو المكان الذي سنقوم فيه بإجراء التعديلات وفقًا للأمثلة التي درسناها؛
1.3.2. بيان التطبيق
![]() |
يحدد الملف [AndroidManifest.xml] [1] خصائص الملف الثنائي لتطبيق Android. ومحتواه هنا هو كما يلي:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- السطر 3: حزمة مشروع Android؛
- السطر 10: اسم النشاط؛
هذه المعلومات تأتي من الإدخالات التي تمت عند إنشاء المشروع:
![]() |
- السطر 3 من البيان (package) يأتي من الإدخال [4] أعلاه. يتم إنشاء عدد من الفئات تلقائيًا في هذا الحزمة؛
![]() |
- السطر 10 من البيان (اسم النشاط) يأتي من الإدخال [1] أعلاه؛
لنعد إلى البيان:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
![]() | ![]() | ![]() |
- السطر 10: النشاط الرئيسي للتطبيق. يشير إلى الفئة [1] أعلاه؛
- السطر 6: رمز التطبيق [2]. يمكن تغييره؛
- السطر 7: اسم التطبيق. يوجد في الملف [strings.xml] [3]:
<resources>
<string name="app_name">Exemple-02</string>
</resources>
يحتوي الملف [strings.xml] على سلاسل الأحرف المستخدمة من قبل التطبيق. السطر 2، اسم التطبيق مأخوذ من الإدخال الذي تم عند إنشاء المشروع [4]:
![]() |
- السطر 10: علامة نشاط. يمكن أن يحتوي تطبيق Android على عدة أنشطة؛
- السطر 12: تم تعيين النشاط باعتباره النشاط الرئيسي؛
- السطر 13: ويجب أن تظهر في قائمة التطبيقات التي يمكن تشغيلها على جهاز Android.
1.3.3. النشاط الرئيسي
![]() | ![]() |
يعتمد تطبيق Android على نشاط واحد أو أكثر. هنا تم إنشاء نشاط [1]: [MainActivity]. يمكن للنشاط عرض عرض واحد أو أكثر حسب نوعه. الفئة [MainActivity] التي تم إنشاؤها هي التالية:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- السطر 6: الفئة [MyActivity] تمتد من فئة Android [AppCompatActivity]. وسيكون هذا هو الحال بالنسبة لجميع الأنشطة المستقبلية؛
- السطر 9: يتم تنفيذ الطريقة [onCreate] عند إنشاء النشاط. ويحدث ذلك قبل عرض العرض المرتبط بالنشاط؛
- السطر 10: يتم استدعاء الطريقة [onCreate] من الفئة الأم. يجب القيام بذلك دائمًا؛
- السطر 11: الملف [activity_main.xml] [2] هو العرض المرتبط بالنشاط. تعريف XML لهذا العرض هو كما يلي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity">
<TextView
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- السطور b-k: مدير التنسيق. النوع الذي تم اختياره افتراضيًا هو [RelativeLayout]. في هذا النوع من الحاويات، يتم وضع المكونات بالنسبة لبعضها البعض (على اليمين، على اليسار، أسفل، أعلى)؛
- السطور m-p: مكون من النوع [TextView] يستخدم لعرض النص؛
- السطر n: النص المعروض. لا يُنصح بوضع نص ثابت في العروض. من الأفضل نقل هذه النصوص إلى الملف [res/values/strings.xml] [3]:
سيكون النص المعروض إذن هو [Hello World!]. أين سيتم عرضه؟ سيملأ الحاوية [RelativeLayout] الشاشة. سيتم عرض [TextView]، وهو العنصر الوحيد فيها، في أعلى يسار هذه الحاوية، وبالتالي في أعلى يسار الشاشة؛
ماذا يعني [R.layout.activity_main] في السطر 11؟ يتم تعيين معرف لكل مورد Android (طرق عرض، أجزاء، مكونات، ...). وبالتالي، فإن العرض [V.xml] الموجود في المجلد [res / layout] سيتم تعريفه بـ [R.layout.V]. R هي فئة تم إنشاؤها في المجلد [app / build / generated] [1-3]:
![]() |
الفئة [R] هي التالية:
...............
public static final class string {
public static final int abc_action_bar_home_description=0x7f060000;
public static final int abc_action_bar_home_description_format=0x7f060001;
public static final int abc_action_bar_home_subtitle_description_format=0x7f060002;
...
public static final int app_name=0x7f060014;
}
public static final class layout {
public static final int abc_action_bar_title_item=0x7f040000;
public static final int abc_action_bar_up_container=0x7f040001;
...
public static final int activity_main=0x7f040019;
...
}
public static final class mipmap {
public static final int ic_launcher=0x7f030000;
}
- السطر 14: السمة [R.layout.activity_main] هي معرف العرض [res / layout / activity_main.xml]؛
- السطر 7: السمة [R.string.app_name] هي معرف السلسلة [app_name] في الملف [res / values / string.xml]:
- السطر 19: السمة [R.mipmap.ic_launcher] هي معرف الصورة [res / mipmap / ic_launcher]؛
لذلك يجب أن نتذكر أنه عند الإشارة إلى [R.layout.activity_main] في الكود، فإننا نشير إلى سمة من فئة [R]. يساعدنا IDE في معرفة العناصر المختلفة لهذه الفئة:
![]() | ![]() |
1.3.4. تشغيل التطبيق
لتشغيل تطبيق Android، يتعين علينا إنشاء تكوين للتشغيل:
![]() | ![]() | ![]() |
- في [1]، اختر [Edit Configurations]؛
- تم إنشاء المشروع بتكوين [app] الذي سنقوم بحذفه [2] لإعادة إنشائه؛
- في [3]، قم بإنشاء تكوين تشغيل جديد؛
![]() |
- في [4]، اختر [Android Application]؛

- في [5]، من القائمة المنسدلة اختر الوحدة النمطية [app]؛
- في [6-8]، احتفظ بالقيم المقترحة افتراضيًا؛
- في [7]، النشاط الافتراضي هو الذي تم تعريفه في الملف [AndroidManifest.xml] (السطر 1 أدناه):
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- في [8]، حدد [Show Chooser Dialog] الذي يسمح باختيار جهاز تشغيل التطبيق (محاكي، جهاز لوحي)؛
- في [9]، يُشار إلى أنه يجب حفظ هذا الاختيار؛
- تأكيد التكوين؛
![]() |
- في [11]، قم بتشغيل مدير المحاكيات [Genymotion] (انظر الفقرة 6.9)؛
![]() |
- في [12]، حدد محاكي جهاز لوحي وقم بتشغيله [13]؛
![]() | ![]() |
- في [14]، قم بتنفيذ تكوين التشغيل [app]؛
- في [15]، يظهر نموذج اختيار جهاز التنفيذ. يتوفر جهاز واحد فقط هنا: المحاكي [Genymotion] الذي تم تشغيله مسبقًا؛
يعرض المحاكي البرمجي بعد لحظات الشاشة التالية:

1.3.5. دورة حياة النشاط
لنعد إلى كود النشاط [MainActivity]:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
تعد الطريقة [onCreate] في الأسطر 8-12 جزءًا من الطرق التي يمكن استدعاؤها خلال دورة حياة النشاط. توفر وثائق Android قائمة بهذه الطرق:
![]() |
- [1]: يتم استدعاء الطريقة [onCreate] عند بدء النشاط. في هذه الطريقة يتم ربط النشاط بعرض واسترداد مراجع مكوناته؛
- [2-3]: يتم بعد ذلك استدعاء الطرق [onStart, onResume]. نلاحظ أن الطريقة [onResume] هي آخر طريقة يتم تنفيذها قبل الوصول إلى الحالة [4] للنشاط قيد التنفيذ؛
1.4. مثال-03: إعادة كتابة المشروع [Exemple-02] باستخدام المكتبة [Android Annotations]
سنقوم الآن بإدخال المكتبة [Android Annotations] التي تسهل كتابة تطبيقات Android. للقيام بذلك، نقوم بنسخ المثال [Exemple-02] في [Exemple-03] باتباع الإجراء [1-16].
![]() | ![]() |
- في [1]، استخدم منظور [Project] لعرض المشروع Android بالكامل؛
![]() | ![]() |
![]() | ![]() |
![]() | ![]() | ![]() | ![]() |
ملاحظة: بين [14] و [15]، انتقلنا من منظور [Android] إلى منظور [Project] (انظر الفقرة 1.2.2.1).
ثم نقوم بتعديل الملف [res / values / strings.xml] [17]:
![]() |
يتم تعديل الملف [strings.xml] على النحو التالي:
<resources>
<string name="app_name">Exemple-03</string>
</resources>
الآن، نقوم بتشغيل التطبيق الجديد الذي استلم كل إعدادات [Exemple-02]:
![]() | ![]() |
في [19]، نحصل على نفس النتيجة التي حصلنا عليها مع [Exemple-02] ولكن باسم جديد.
سنقوم الآن بتقديم المكتبة [Android Annotations] التي سنسميها AA لتسهيل الأمر. تقدم هذه المكتبة فئات جديدة لتعليق مصادر Android. سيتم استخدام هذه التعليقات من قبل معالج سيقوم بإنشاء فئات Java جديدة في الوحدة النمطية، وهي فئات ستشارك في تجميعها بنفس الطريقة التي تشارك بها الفئات التي كتبها المطور. وبذلك نحصل على سلسلة التجميع التالية:
![]() |
سنقوم أولاً بوضع التبعيات على مُجمِّع التعليقات التوضيحية AA (المعالج المذكور أعلاه) في الملف [build.gradle]:
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
}
- تضيف السطور 4-5 التبعيتين اللتين تشكلان المكتبة AA؛
يتم تعديل الملف [build.gradle] مرة أخرى لاستخدام مكون إضافي يسمى [android-apt] الذي يعدل عملية التحويل البرمجي إلى خطوتين:
- معالجة تعليقات Android، مما يؤدي إلى إنشاء فئات جديدة؛
- ترجمة جميع فئات المشروع؛
buildscript {
repositories {
mavenCentral()
}
dependencies {
// منذ إصدار المكون الإضافي Gradle لنظام Android 0.11، يجب عليك استخدام android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
- السطر 8: إصدار المكون الإضافي [android-apt] الذي سيتم البحث عنه في مستودع Maven المركزي (السطر 3)؛
- السطر 13: تنشيط هذا المكون الإضافي؛
في هذه المرحلة، تحقق من أن تكوين التشغيل [app] لا يزال يعمل.
سنقوم الآن بإدخال أول تعليق توضيحي لمكتبة AA في الفئة [MainActivity]:
![]() |
تبدو الفئة [MainActivity] في الوقت الحالي كما يلي:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
لقد سبق أن شرحنا هذا الرمز في الفقرة 1.3.3. ونقوم بتعديله على النحو التالي:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- السطر 7: التعليق التوضيحي [@EActivity] هو تعليق توضيحي AA (السطر 3). معلمته هي العرض المرتبط بالنشاط؛
ستنتج هذه التعليقات التوضيحية فئة [MainActivity_] مشتقة من الفئة [MainActivity]، وستكون هذه الفئة هي النشاط الفعلي. لذلك، يجب تعديل بيان المشروع [AndroidManifest.xml] على النحو التالي:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity_">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- السطر 11: النشاط الجديد؛
وبعد ذلك، يمكننا ترجمة مشروع [1]:
![]() | ![]() |
- في [2]، نرى الفئة [MainActivity_] التي تم إنشاؤها في المجلد [app / build / generated / source / apt / debug]؛
الفئة [MainActivity_] التي تم إنشاؤها هي التالية:
//
// DO NOT EDIT THIS FILE.
// تم إنشاؤه باستخدام AndroidAnnotations 4.0.0.
//
// يمكنك إنشاء عمل أكبر يحتوي على هذا الملف وتوزيع هذا العمل وفقًا للشروط التي تختارها.
//
package exemples.android;
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
public final class MainActivity_
extends MainActivity
implements HasViews
{
private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
@Override
public void onCreate(Bundle savedInstanceState) {
OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
init_(savedInstanceState);
super.onCreate(savedInstanceState);
OnViewChangedNotifier.replaceNotifier(previousNotifier);
setContentView(R.layout.activity_main);
}
...
- السطران 24-25: الفئة [MainActivity_] توسع الفئة [MainActivity]؛
لن نحاول شرح كود الفئات التي تم إنشاؤها بواسطة AA. فهي تدير التعقيد الذي تسعى التعليقات التوضيحية إلى إخفائه. ولكن قد يكون من الجيد أحيانًا فحصه عندما نريد فهم كيفية "ترجمة" التعليقات التوضيحية التي نستخدمها.
يمكننا الآن تنفيذ التكوين [app] مرة أخرى. نحصل على نفس النتيجة السابقة. سنبدأ الآن من هذا المشروع الذي سنقوم بنسخه لتقديم المفاهيم المهمة في برمجة Android.
1.5. مثال-04: العروض والأحداث
1.5.1. إنشاء المشروع
سنتبع الإجراء الموضح لنسخ [Exemple-02] في [Exemple-03] في الفقرة 1.4:
نقوم بما يلي:
- نسخ المشروع [Exemple-03] في [Exemple-04] (بعد حذف المجلد [app / build] من [Exemple-03])؛
- نقوم بتحميل المشروع [Exemple-04]؛
- تغيير اسم المشروع في الملف [app / res / values / strings.xml] (منظور Android)؛
- نحذف الملف [Exemple-04 / Exemple-04.iml] (منظور Project)؛
- نقوم بتجميع المشروع ثم تنفيذه؛
![]() | ![]() |
1.5.2. إنشاء عرض
سنقوم الآن بتعديل العرض الذي يعرضه المشروع [Exemple-04] باستخدام المحرر الرسومي:
![]() | ![]() |
- في [1-4]، قم بإنشاء عرض جديد XML؛
- في [5]، قم بتسمية العرض؛
- في [6]، حدد العلامة الجذرية للعرض. هنا، نختار حاوية [RelativeLayout]. في حاوية المكونات هذه، يتم وضع المكونات بالنسبة لبعضها البعض: "على يمين"، "على يسار"، "أسفل"، "فوق"؛
![]() |
الملف [vue1.xml] الذي تم إنشاؤه [7] هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</RelativeLayout>
- السطر 2: حاوية فارغة [RelativeLayout] ستشغل كامل عرض الجهاز اللوحي (السطر 3) وكامل ارتفاعه (السطر 4)؛
![]() | ![]() |
- في [1]، حدد علامة التبويب [Design] في العرض [vue1.xml] المعروض؛
- في [2-4]، انتقل إلى وضع الجهاز اللوحي؛
![]() | ![]() | ![]() |
- في [5]، اضبط المقياس على 1 للكمبيوتر اللوحي؛
- في [6]، اختر وضع "الأفق" للكمبيوتر اللوحي؛
- تلخص لقطة الشاشة [7] الخيارات التي تم تحديدها.
![]() | ![]() | ![]() |
- في [1]، خذ [Large Text] واسحبه إلى العرض [2]؛
- في [3]، انقر نقرًا مزدوجًا على المكون؛
- في [4]، قم بتعديل النص المعروض. بدلاً من وضعه بشكل "ثابت" في العرض XML، سنقوم بتحويله إلى ملف خارجي في الملف [res / values / string.xml]
![]() | ![]() | ![]() |
- في [5]، نضيف قيمة جديدة في الملف [strings.xml]؛
- في [8]، يتم تعيين معرف للسلسلة؛
- في [9]، يتم تحديد قيمة السلسلة؛
- في [10]، العرض الجديد بعد التحقق من صحة الخطوة السابقة؛
![]() | ![]() | ![]() |
- بعد النقر المزدوج على المكون، يتم تغيير معرفه [11]؛
- إلى [12]، في خصائص المكون، يتم تغيير حجم الأحرف [50sp]؛
- إلى [13]، العرض الجديد؛
تطور الملف [vue1.xml] على النحو التالي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
- التعديلات التي تم إجراؤها في واجهة المستخدم الرسومية موجودة في الأسطر 10 و 11 و 14. أما السمات الأخرى لـ [TextView] فهي قيم افتراضية أو ناتجة عن موضع المكون في العرض؛
- السطران 7-8: حجم المكون هو حجم النص الذي يحتوي عليه (wrap_content) من حيث الارتفاع والعرض؛
- السطر 13: يتم محاذاة الجزء العلوي من المكون مع الجزء العلوي من العرض (السطر 13)، 50 بكسل أسفل (السطر 13)؛
- السطر 12: يتم محاذاة الجانب الأيسر للمكون مع الجانب الأيسر للعرض (السطر 13)، على بعد 213 بكسل إلى اليمين (السطر 12)؛
بشكل عام، سيتم تحديد الأحجام الدقيقة للهوامش اليسرى واليمنى والعلوية والسفلية مباشرة في XML.
وبنفس الطريقة، قم بإنشاء العرض التالي [1]:
![]() |
المكونات هي كما يلي:
قد يكون وضع المكونات بالنسبة لبعضها البعض عملية محبطة، حيث أن ردود فعل المحرر الرسومي قد تكون مفاجئة في بعض الأحيان. قد يكون من الأفضل استخدام خصائص المكونات:
يجب وضع المكون [textView1] على بعد 50 بكسل أسفل العنوان و50 بكسل من الحافة اليسرى للحاوية:
![]() | ![]() | ![]() |
- في [1]، يتم محاذاة الحافة العلوية (top) للمكون مع الحافة السفلية (bottom) للمكون [textViewTitreVue1] على مسافة 50 بكسل [3] (top)؛
- في [2]، يتم محاذاة الحافة اليسرى (left) للمكون مع الحافة اليسرى للحاوية بمسافة 50 بكسل [3] (left)؛
يجب وضع المكون [editTextNom] على بعد 60 بكسل إلى يمين المكون [textView1] ومحاذاته من الأسفل مع هذا المكون نفسه؛
![]() | ![]() |
- في [1]، يتم محاذاة الحافة اليسرى (left) للمكون مع الحافة اليمنى (right) للمكون [textView1] على مسافة 60 بكسل من [2] (left). ويتم محاذاة الحافة السفلية (bottom:bottom) للمكون [textView1] مع المكون [1]؛
يجب وضع المكون [buttonValider] على بعد 60 بكسل إلى اليمين من المكون [editTextNom] ومحاذاته من الأسفل مع هذا المكون نفسه؛
![]() | ![]() |
- في [1]، يتم محاذاة الحافة اليسرى (left) للمكون مع الحافة اليمنى (right) للمكون [editTextNom] على مسافة 60 بكسل [2] (left). ويتم محاذاة الحافة السفلية للمكون (bottom:bottom) [editTextNom] [1] ؛
يجب وضع المكون [buttonVue2] على بعد 50 بكسل أسفل المكون [textView1] ومحاذاة يساره مع هذا المكون نفسه؛
![]() | ![]() |
- في [1]، يتم محاذاة الحافة اليسرى (left) للمكون مع الحافة اليسرى (left) للمكون [textView1] ويتم وضعه أسفل (top:bottom) على مسافة 50 بكسل من [2] (top)؛
الملف XML الذي تم إنشاؤه هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="49dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_nom"
android:id="@+id/textView1"
android:layout_below="@+id/textViewTitreVue1"
android:layout_alignParentLeft="true"
android:layout_marginLeft="50dp"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
</RelativeLayout>
نجد فيه كل ما تم تنفيذه من الناحية الرسومية. وهناك طريقة أخرى لإنشاء عرض وهي كتابة هذا الملف مباشرة. وعندما يعتاد المرء على ذلك، قد يكون هذا أسرع من استخدام المحرر الرسومي.
- في السطر 38، نجد معلومة لم نعرضها. يتم تقديمها عبر خصائص المكون [editTextNom] [1]:
![]() | ![]() |
جميع النصوص مأخوذة من الملف [strings.xml] [2] التالي:
<resources>
<string name="app_name">Exemple-04</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
الآن، لنقوم بتعديل النشاط [MainActivity] بحيث يتم عرض هذه الطريقة عند بدء تشغيل التطبيق:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- السطر 7: يتم الآن عرض طريقة العرض [vue1.xml] بواسطة النشاط؛
قم بتعديل الملف [AndroidManifest.xml] على النحو التالي:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- السطر 12: يمنع سطر التكوين هذا ظهور لوحة المفاتيح فور عرض العرض [vue1]. في الواقع، يحتوي هذا العرض على حقل إدخال يتم التركيز عليه عند عرض العرض. يؤدي هذا التركيز إلى ظهور لوحة المفاتيح الافتراضية بشكل افتراضي؛
قم بتشغيل التطبيق وتأكد من أن العرض [vue1.xml] هو الذي يتم عرضه بالفعل:

1.5.3. إدارة الأحداث
لنقم الآن بإدارة النقر على الزر [Valider] في العرض [Vue1]:

يتطور كود [MainActivity] على النحو التالي:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
// عناصر الواجهة المرئية
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews(){
Log.d("MainActivity","afterViews");
}
// مدير الأحداث
@Click(R.id.buttonValider)
protected void doValider() {
// يتم عرض الاسم الذي تم إدخاله
Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- السطران 17-18: يتم ربط الحقل [protected EditText editTextNom] بمكون المعرف [R.id.editTextNom] في الواجهة المرئية. يجب أن يكون الحقل المرتبط بالمكون متاحًا في الفئة المشتقة [MainActivity_]، ولهذا السبب لا يمكن أن يكون نطاقه [private]. الحقل المحدد بـ [R.id.editTextNom] يأتي من العرض [vue1.xml]:
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
ملاحظة: لا تضع أحرفًا مشددة في المعرفات [id]. AA لا يديرها بشكل صحيح.
- السطر 32: يشير التعليق التوضيحي [@Click(R.id.buttonValider)] إلى الطريقة التي تدير حدث "النقر" على زر المعرف [R.id.buttonValider]. يأتي هذا المعرف أيضًا من العرض [vue1.xml]:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
- السطر 35: يعرض الاسم الذي تم إدخاله:
- Toast.makeText(...).show() : يعرض نصًا على الشاشة،
- المعلمة الأولى لـ makeText هي النشاط،
- المعلمة الثانية هي النص المراد عرضه في المربع الذي سيتم عرضه بواسطة makeText،
- المعلمة الثالثة هي مدة عرض المربع: Toast.LENGTH_LONG أو Toast.LENGTH_SHORT؛
- السطر 26، التعليق التوضيحي [@AfterViews] يوضح الطريقة التي يجب تنفيذها عندما يتم تهيئة جميع الحقول الموضحة بواسطة [@ViewById]. من المهم معرفة متى يتم تهيئة هذه الحقول. على سبيل المثال، هل يمكن استخدام المرجع الموجود في السطر 18 في الطريقة [onCreate]؟ للإجابة على هذا السؤال، قمنا بوضع سجلات؛
قم بتنفيذ المشروع [Exemple-04] وتحقق من حدوث شيء ما عند النقر على الزر [Valider]. نحصل على السجلات التالية:
نستنتج من ذلك أنه عند تنفيذ الأسلوب [onCreate]، فإن الحقول المُعلَّمة بواسطة [@ViewById] لم يتم تهيئتها بعد. مرة أخرى، يُنصح القارئ المبتدئ بوضع هذا النوع من السجلات في الأساليب التي تدير دورة حياة التطبيق.
1.6. مثال-05: التنقل بين العروض
في المشروع السابق، لم يتم استخدام الزر [Vue n° 2]. نقترح استخدامه من خلال إنشاء عرض ثانٍ وإظهار كيفية التنقل من عرض إلى آخر. هناك عدة طرق لحل هذه المشكلة. الطريقة المقترحة هنا هي ربط كل عرض بنشاط. طريقة أخرى هي وجود نشاط واحد من النوع [AppCompatActivity] يعرض طرق عرض من النوع [Fragment]. ستكون هذه هي الطريقة المستخدمة في التطبيقات القادمة.
1.6.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-04] إلى [Exemple-05]. للقيام بذلك، نتبع الإجراء الموصوف لنسخ [Exemple-02] إلى [Exemple-03] في الفقرة 1.4 والذي تم تكراره في الفقرة 1.5.
![]() | ![]() |
1.6.2. إضافة نشاط ثانٍ
لإدارة عرض ثانٍ، سنقوم بإنشاء نشاط ثانٍ. وهو الذي سيدير العرض رقم 2. نحن هنا في نموذج عرض واحد = نشاط واحد. هناك نماذج أخرى ممكنة.
123

- في [1-4]، نقوم بإنشاء نشاط جديد؛

- في [5]، اسم الفئة التي سيتم إنشاؤها؛
- في [6]، اسم العرض (vue2.xml) المرتبط بالنشاط الجديد؛
![]() |
- في [7-8]، الملفات المتأثرة بالتكوين السابق؛
النشاط [SecondActivity] هو التالي:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.vue2);
}
}
- السطر 11: النشاط مرتبط بالعرض [vue2.xml]؛
العرض [vue2.xml] هو كما يلي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
</RelativeLayout>
هذه طريقة عرض فارغة في الوقت الحالي مع مدير تخطيط من النوع [RelativeLayout] (السطر 2). في السطر 11، نرى أنها تم ربطها بالنشاط الجديد.
وقد تطورت قائمة الوحدة النمطية Android [AndroidManifest.xml] على النحو التالي:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity">
</activity>
</application>
</manifest>
السطر 20، تم تسجيل نشاط ثانٍ.
1.6.3. التنقل من العرض رقم 1 إلى العرض رقم 2
لنعد إلى كود الفئة [MainActivity] التي تعرض العرض رقم 1. لا يتم حالياً إدارة الانتقال إلى العرض رقم 2:
![]() |
نقوم بإدارته بالطريقة التالية:
// الانتقال إلى العرض رقم 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// الانتقال إلى العرض رقم 2 مع تمرير الاسم الذي تم إدخاله في العرض رقم 1
// يتم إنشاء Intent
Intent intent = new Intent();
// ربط هذا Intent بنشاط
intent.setClass(this, SecondActivity.class);
// نربط المعلومات بهذا النية
intent.putExtra("NOM", editTextNom.getText().toString().trim());
// يتم تشغيل النشاط من النوع [SecondActivity] مع تمرير Intent إليه
startActivity(intent);
}
- السطران 2-3: تدير الطريقة [navigateToView2] "النقر" على الزر المحدد بواسطة [R.id.buttonVue2] المحدد في العرض [vue1.xml]:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
تصف التعليقات الخطوات التي يجب اتباعها لتغيير العرض:
- السطر 6: إنشاء كائن من النوع [Intent]. سيسمح هذا الكائن بتحديد النشاط المطلوب تشغيله والمعلومات التي سيتم تمريرها إليه؛
- السطر 8: ربط النية (Intent) بنشاط، وهو هنا نشاط من النوع [SecondActivity] الذي سيتولى عرض العرض رقم 2. يجب تذكر أن النشاط [MainActivity] يعرض هو العرض رقم 1. وبالتالي، لدينا عرض = نشاط. سيتعين علينا تعريف النوع [SecondActivity]؛
- السطر 10: بشكل اختياري، أدخل معلومات في الكائن [Intent]. هذه المعلومات مخصصة للنشاط [SecondActivity] الذي سيتم تشغيله. معلمات [Intent.putExtra] هي (مفتاح الكائن، قيمة الكائن). تجدر الإشارة إلى أن الطريقة [EditText.getText()] التي تعرض النص الذي تم إدخاله في منطقة الإدخال لا تعرض النوع [String] بل النوع [Editable]. يجب استخدام الطريقة [toString] للحصول على النص الذي تم إدخاله؛
- السطر 12: قم بتشغيل النشاط المحدد بواسطة الكائن [Intent].
قم بتنفيذ المشروع [Exemple-05] وتأكد من حصولك على العرض رقم 2 (فارغ في الوقت الحالي):
![]() | ![]() |
1.6.4. إنشاء العرض رقم 2
![]() | ![]() |
- في [1-2]، نحذف العرض [main.xml] الذي لم يعد مفيدًا لنا، ثم نعدل العرض [vue2.xml] بالطريقة التالية:
![]() |
المكونات هي كما يلي:
الملف XML [vue2.xml] هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue2"
android:id="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textViewBonjour"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_below="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:layout_marginLeft="50dp"
android:textSize="30sp"
android:text="Bonjour !"
android:textColor="#ffffb91b"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue1"
android:id="@+id/buttonVue1"
android:layout_marginTop="50dp"
android:textSize="30sp"
android:layout_alignLeft="@+id/textViewBonjour"
android:layout_below="@+id/textViewBonjour"/>
</RelativeLayout>
قم بتشغيل المشروع [Exemple-05] وتأكد من حصولك على العرض الجديد بالنقر فوق الزر [Vue n° 2].
1.6.5. النشاط [SecondActivity]
في [MainActivity]، كتبنا الكود التالي:
// الانتقال إلى العرض رقم 2
protected void navigateToView2() {
// ننتقل إلى العرض رقم 2 ونمرر إليه الاسم الذي تم إدخاله في العرض رقم 1
// يتم إنشاء Intent
Intent intent = new Intent();
// نربط هذا Intent بنشاط
intent.setClass(this, SecondActivity.class);
// نربط المعلومات بهذا النية
intent.putExtra("NOM", edtNom.getText().toString().trim());
// يتم تشغيل النشاط من النوع [SecondActivity] عن طريق تمرير Intent إليه
startActivity(intent);
}
في السطر 9، أدخلنا معلومات لم يتم استخدامها في [SecondActivity]. سنستخدمها الآن، ويحدث ذلك في كود [SecondActivity]:
![]() |
يتطور كود [SecondActivity] على النحو التالي:
package exemples.android;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
// مكونات الواجهة المرئية
@ViewById
protected TextView textViewBonjour;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews() {
// يتم استرداد النية إن وجدت
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// نسترد الاسم
String nom = extras.getString("NOM");
if (nom != null) {
// نقوم بعرضه
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
}
}
}
- السطر 11: نستخدم التعليق التوضيحي [@EActivity] للإشارة إلى أن الفئة [SecondActivity] هي نشاط مرتبط بالعرض [vue2.xml]؛
- السطران 15-16: يتم استرداد مرجع للمكون [TextView] المحدد بواسطة [R.id.textViewBonjour]. هنا، لم يتم كتابة [@ViewById(R.id.textViewBonjour)]. في هذه الحالة، يفترض AA أن معرف المكون مطابق للحقل المُعلَّم، وهو هنا الحقل [textViewBonjour]؛
- السطر 23: التعليق التوضيحي [@AfterViews] يعلق على طريقة يجب تنفيذها بعد تهيئة الحقول المُعلَّمة بـ [@ViewById]. في الطريقة [OnCreate] (السطر 19)، لا يمكن استخدام هذه الحقول لأنها لم يتم تهيئتها بعد. في المشروع [Exemple-05]، يتم التبديل من نشاط إلى آخر ولم يكن واضحًا مسبقًا ما إذا كانت الطريقة المُعلَّمة [@AfterViews] ستُنفَّذ مرة واحدة عند التهيئة الأولية للنشاط أم في كل مرة يتم فيها بدء النشاط. أظهرت الاختبارات أن الفرضية الثانية هي الصحيحة؛
- السطر 26: تحتوي الفئة [AppCompatActivity] على طريقة [getIntent] التي تُرجع الكائن [Intent] المرتبط بالنشاط؛
- السطر 28: تُرجع الطريقة [Intent.getExtras] نوعًا [Bundle] وهو نوع من القاموس يحتوي على المعلومات المرتبطة بالكائن [Intent] الخاص بالنشاط؛
- السطر 31: يتم استرداد الاسم الموجود في الكائن [Intent] الخاص بالنشاط؛
- السطر 34: يتم عرضه.
تذكير: يجب ألا تحتوي الحقول المُعلَّمة بالتعليق التوضيحي [@ViewById] على أحرف مُشَدَّدة.
لنعد إلى الفئة [SecondActivity]. لأننا كتبنا:
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
فإن AA ستولد فئة [SecondActivity_] مشتقة من [SecondActivity]، وهذه الفئة هي التي ستكون النشاط الفعلي. وهذا يقودنا إلى إجراء تعديلات في:
[MainActivity]
// الانتقال إلى العرض رقم 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// نربط هذا النية بنشاط
intent.setClass(this, SecondActivity_.class);
...
}
- السطر 6، يجب استبدال [SecondActivity] بـ [SecondActivity_] ؛
[AndroidManifest.xml]
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity_">
</activity>
</application>
</manifest>
- في السطر 20، يجب استبدال [SecondActivity] بـ [SecondActivity_]؛
اختبر هذه النسخة الجديدة. اكتب اسمًا في العرض رقم 1 وتأكد من أن العرض رقم 2 يعرضه بشكل صحيح.
![]() | ![]() |
1.6.6. التنقل من العرض رقم 2 إلى العرض رقم 1
للتنقل من العرض رقم 2 إلى العرض رقم 1، سنتبع الإجراء الذي رأيناه سابقًا:
- وضع كود التنقل في النشاط [SecondActivity] الذي يعرض العرض رقم 2؛
- كتابة الطريقة [@AfterViews] في النشاط [MainActivity] الذي يعرض العرض رقم 1؛
يتطور كود [SecondActivity] على النحو التالي:
@Click(R.id.buttonVue1)
protected void navigateToView1() {
// يتم إنشاء نية للنشاط [MainActivity]
Intent intent1 = new Intent();
intent1.setClass(this, MainActivity_.class);
// نسترد النية (Intent) للنشاط الحالي [SecondActivity]
Intent intent2 = getIntent();
if (intent2 != null) {
Bundle extras2 = intent2.getExtras();
if (extras2 != null) {
// يتم وضع الاسم في Intent الخاص بـ [MainActivity]
intent1.putExtra("NOM", extras2.getString("NOM"));
}
// يتم تشغيل [MainActivity]
startActivity(intent1);
}
}
- السطران 1-2: نربط الطريقة [navigateToView1] بالنقر على الزر [btn_vue1]؛
- السطر 4: يتم إنشاء [Intent] جديد؛
- السطر 5: مرتبط بالنشاط [MainActivity_]؛
- السطر 7: يتم استرداد النية المرتبطة بـ [SecondActivity]؛
- السطر 9: يتم استرداد معلومات هذا النية؛
- السطر 12: يتم استرداد المفتاح [NOM] من [intent2] لوضعه في [intent1] بنفس القيمة المرتبطة؛
- السطر 15: يتم تشغيل النشاط [MainActivity_].
في كود [MainActivity]، تتم إضافة الطريقة [@AfterViews] التالية:
@AfterViews
protected void afterViews() {
// نسترد النية إن وجدت
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// نسترد الاسم
String nom = extras.getString("NOM");
if (nom != null) {
// نقوم بعرضه
editTextNom.setText(nom);
}
}
}
}
قم بإجراء هذه التعديلات واختبر تطبيقك. الآن عند العودة من العرض رقم 2 إلى العرض رقم 1، يجب أن نجد الاسم الذي تم إدخاله في البداية، وهو ما لم يكن الحال عليه حتى الآن.
![]() | ![]() |
1.6.7. دورة حياة الأنشطة
لقد عرضنا في الفقرة 1.3.5 دورة حياة النشاط. لدينا هنا نشاطان وننتقل من أحدهما إلى الآخر أثناء التنفيذ. تحتوي هذه الأنشطة على طريقتين لا نعرف بالضبط متى يتم استدعاء إحداهما بالنسبة للأخرى: [onCreate] و [afterViews]. من المهم معرفة ذلك. ولهذا الغرض، نضيف سجلات في كلتا النشاطين:
وهكذا، في الفئة [MainActivity]، نكتب:
// الشركة المصنعة
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
...
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
}
}
- الأسطر 2-4: نريد معرفة ما إذا كانت الفئة [MainActivity] قد تم إنشاء مثيل لها مرة واحدة أو عدة مرات؛
- السطر 8: نريد معرفة ما إذا كانت الطريقة [onCreate] قد تم استدعاؤها مرة واحدة أو عدة مرات؛
- السطر 14: نريد معرفة ما إذا كانت الطريقة [afterViews] قد تم استدعاؤها مرة واحدة أو عدة مرات؛
نقوم بنفس الشيء تمامًا في الفئة [SecondActivity].
عند بدء تشغيل التطبيق، نحصل على السجلات التالية:
تم تنفيذ الطرق [onCreate, afterViews] من النشاط الأول بهذا الترتيب. عند النقر على الزر [Vue n° 2]، تكون السجلات الجديدة كما يلي:
تم تنفيذ طرق [onCreate, afterViews] الخاصة بالنشاط الثاني بهذا الترتيب. عند النقر على الزر [Vue n° 1]، تكون السجلات الجديدة كما يلي:
وبالتالي، يتم إنشاء مثيل جديد للفئة [MainActivity]. عند النقر على الزر [Vue n° 2]، تكون السجلات الجديدة كما يلي:
وبالتالي، يتم إنشاء مثيل جديد للفئة [SecondActivity].
وبالتالي، يتم إعادة إنشاء النشاطين بشكل منهجي عند تغيير النشاط.
سنكتشف الآن بنية تحتوي على نشاط واحد يمكنه إدارة عدة طرق عرض تسمى الأجزاء. لن يتم إنشاء النشاط وطرق العرض إلا مرة واحدة على عكس الطريقة السابقة حيث كان من الممكن إنشاء النشاط عدة مرات.
1.7. مثال-06: التنقل عبر علامات التبويب
سنستكشف هنا واجهات علامات التبويب. المثال معقد ولكنه يقدم جميع العناصر التي سنستخدمها لاحقًا: نشاط واحد، مدير الأجزاء (طرق العرض)، حاوية الأجزاء، التنقل بين الأجزاء. يختلف مفهوم علامات التبويب عن مفهوم الأجزاء وهو ثانوي فيما نريد إظهاره في هذا المثال.
1.7.1. إنشاء المشروع
نقوم بإنشاء مشروع جديد:
![]() | ![]() |
![]() |
![]() |
- في [7]، نختار نشاطًا مع علامات تبويب (Tabbed Activity)؛
![]() |
- في [10-14]، نحتفظ بالقيم المقترحة افتراضيًا؛
- في [15]، نختار علامات تبويب مع شريط عناوين؛
يصبح المشروع الذي تم إنشاؤه كما يلي:
![]() | ![]() |
- في [1]، النشاط؛
- في [2]، العروض؛
تم إنشاء تكوين تشغيل [app]، باسم الوحدة النمطية، تلقائيًا [2b]:
![]() |
يمكن تنفيذه. ثم تظهر نافذة بها ثلاث علامات تبويب [3-6]:

1.7.2. تكوين Gradle
تم إنشاء المشروع [Exemple-06] باستخدام الملف [build.gradle] التالي:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
}
هناك جديد مقارنة بما سبق: السطر 25. هذه المكتبة ضرورية للمكونات الجديدة التي يستخدمها التطبيق الذي تم إنشاؤه.
1.7.3. الرؤية [activity_main]
![]() |
الطريقة [activity_main] هي الطريقة المرتبطة بالنشاط [MainActivity] للمشروع. في الوضع [design]، تكون الطريقة كما يلي:

وهي تحتوي على المكونات التالية:
![]() |
- [main_content] هي العرض بالكامل؛
- [appbar] (المربع الأحمر، 1) هو شريط التطبيق. وهو يحتوي على مكونين:
- [toolbar] (المربع الأصفر 4) هو شريط الأدوات؛
- [tabs] (المربع البرتقالي 5) هو شريط عنوان علامات التبويب؛
- [container] (المربع الأخضر، 2) يمكنه استيعاب أجزاء متنوعة. الجزء هو عرض. وبالتالي، سيتمكن النشاط نفسه من عرض عدة عروض (أجزاء) في هذا الحاوية؛
- [fab] (المكون 3) يُسمى المكون العائم؛
في الوضع [text]، يكون الكود كما يلي:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="exemples.android.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
نجد العناصر الموصوفة سابقًا:
- السطور 2-49: تعريف المكون [main_content] (السطر 5) الذي يمثل العرض بأكمله. نرى أنه عبارة عن layout (مدير تخطيط المكونات) من النوع [CoordinatorLayout] (السطر 2)؛
- الأسطر 11-33: الحاوية [appbar] (السطر 12). وهي عبارة عن layout من النوع [AppBarLayout] (السطر 11)؛
- الأسطر 18-24: المكون [toolbar] (السطر 19) من النوع [Toolbar] (السطر 18)؛
- الأسطر 28-31: الحاوية [tabs] (السطر 29). وهي عبارة عن layout من النوع [TabLayout] (السطر 28). ستعرض عناوين علامات التبويب؛
- السطور 35-39: المكون [container] (السطر 36). هذا هو الحاوية التي تعرض طرق العرض المختلفة للنشاط؛
- الأسطر 41-47: المكون [fab] (السطر 42) من النوع [FloatingActionButton] (السطر 41). وهو زر يمكن النقر عليه. يتم وضعه افتراضيًا في أسفل يمين العرض الكلي؛
لن نحاول فهم معنى جميع سمات هذه المكونات. سنستخدمها كما هي. من خلال الخبرة، وغالبًا في الوضع [design]، نكتشف دورها. في هذا الوضع، نكتشف أن المكونات تحتوي على عشرات السمات. بشكل عام، يتم تهيئة بعضها فقط، بينما تحتفظ البقية بقيمتها الافتراضية.
ومع ذلك، دعونا نوضح بعض النقاط. يتم تجميع معظم القيم التي تكوّن العروض المختلفة في المجلد [res / values]:
![]() |
يتم الإشارة إلى هذه القيم في الأسطر 15-16 و23 و39 و46 من الملف [activity_main.xml]. لنأخذ مثالاً:
- السطر 15:
android:paddingTop="@dimen/appbar_padding_top"
تشير التعليقات التوضيحية [@dimen] إلى الملف [res / values / dimens.xml]:
<resources>
<!-- هوامش الشاشة الافتراضية، وفقًا لإرشادات تصميم Android. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen>
</resources>
تشير السطر 15 من الملف [activity_main.xml] إلى السطر (f) أعلاه؛
وبالمثل، يشير التعليق:
- [@string] يشير إلى ملف الموارد [res / values / strings.xml]؛
- يشير [@color] إلى ملف الموارد [res / values / colors.xml]؛
- يشير [@style] إلى ملف الموارد [res / values / styles.xml]؛
1.7.4. النشاط
![]() |
الرمز الذي تم إنشاؤه للنشاط يرقى إلى مستوى العرض الموصوف سابقًا: فهو معقد. سنقوم بتحليله على عدة مراحل.
1.7.4.1. إدارة الأجزاء وعلامات التبويب
الرمز الخاص بـ [MainActivity] المتعلق بالأجزاء وعلامات التبويب هو التالي:
package exemples.android;
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// مدير الأجزاء
private SectionsPagerAdapter mSectionsPagerAdapter;
// حاوية الأجزاء
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// الأصل
super.onCreate(savedInstanceState);
// عرض
setContentView(R.layout.activity_main);
// شريط الأدوات
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// مدير الأجزاء
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// حاوية الأجزاء مرتبطة بمدير الأجزاء
// أي أن الجزء رقم i من حاوية الأجزاء هو الجزء رقم i الذي يقدمه مدير الأجزاء
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// شريط علامات التبويب مرتبط أيضًا بحاوية الأجزاء
// أي أن علامة التبويب رقم i تعرض الجزء رقم i من الحاوية
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
// جزء
public static class PlaceholderFragment extends Fragment {
...
}
// مدير الأجزاء
// وهو الذي يُطلب منه عرض الأجزاء في العرض الرئيسي
//الطريقتين [getItem] و [getCount] - أما الطرق الأخرى فهي اختيارية
public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
}
- السطر 28: يوفر Android حاوية عرض من النوع [android.support.v4.view.ViewPager] (السطر 12). يجب تزويد هذه الحاوية بمدير للعروض أو الأجزاء. ويقوم المطور بتوفيره؛
- السطر 25: مدير الأجزاء المستخدم في هذا المثال. يوجد تنفيذه في الأسطر 61-63؛
- السطر 31: الطريقة التي يتم تنفيذها عند إنشاء النشاط؛
- السطر 35: يتم ربط العرض [activity_main.xml] بالنشاط؛
- السطر 37: يتم استرداد مرجع المكون [toolbar] من العرض عبر معرّفه؛
- السطر 38: تصبح شريط الأدوات هذا شريط الإجراءات (مفهوم في Android) للنشاط؛
- السطر 40: يتم إنشاء مثيل لمدير الأجزاء. معلمة المنشئ هي فئة Android [android.support.v4.app.FragmentManager] (السطر 10)؛
- السطر 44: يتم استرداد مرجع حاوية الأجزاء في العرض [activity_main.xml] عبر معرفها؛
- السطر 45: يتم ربط مدير الأجزاء بحاوية الأجزاء. وهذا يعني أنه عندما يُطلب من حاوية الأجزاء عرض الجزء رقم i، سيتم طلبه من مدير الأجزاء؛
- السطر 48: يتم استرداد مرجع على شريط علامات التبويب عبر معرّفه؛
- السطر 49: مدير علامات التبويب مرتبط بحاوية الأجزاء. وهذا يعني أنه عند النقر على علامة التبويب رقم i، ستعرض الحاوية الجزء رقم i. يتيح لنا الربط بين مدير علامات التبويب وحاوية الأجزاء تجنب أي إدارة لعلامات التبويب. وبالتالي، لا يتعين علينا تعريف مدير أحداث للنقر على علامة تبويب. يتم توفير ذلك بشكل افتراضي من خلال الارتباط بحاوية الأجزاء. سنرى مثالاً حيث سيكون عدد الأجزاء أكبر من عدد علامات التبويب. في هذه الحالة، لا يتم إجراء هذا الارتباط.
مدير الأجزاء [SectionsPagerAdapter] هو التالي:
// مدير الأجزاء
// يُطلب منه الأجزاء المراد عرضها في العرض الرئيسي
// يجب أن يحدد الطرق [getItem] و [getCount] - البقية اختيارية
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// رقم الجزء والموضع
@Override
public Fragment getItem(int position) {
// يتم إنشاء مثيل لجزء [PlaceHolder] وعرضه
return PlaceholderFragment.newInstance(position + 1);
}
// يعرض عدد الأجزاء المدارة
@Override
public int getCount() {
return 3;
}
// اختياري - يعطي عنوانًا للأجزاء المدارة
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "SECTION 1";
case 1:
return "SECTION 2";
case 2:
return "SECTION 3";
}
return null;
}
}
}
- تعتمد الأجزاء التي تعرضها التطبيق على التطبيق نفسه. يتم تعريف مدير الأجزاء من قبل المطور؛
- السطر 5: يوسع مدير الأجزاء فئة Android [android.support.v4.app.FragmentPagerAdapter]. يتم فرض المنشئ علينا. يجب علينا تعريف الطريقتين التاليتين على الأقل:
- int getCount(): ترجع عدد الأجزاء المطلوب إدارتها؛
- Fragment getItem(i): تعرض الجزء رقم i؛
الطريقة CharSequence getPageTitle(i) التي تعرض عنوان المقطع رقم i هي طريقة اختيارية. نظرًا لأن مدير علامات التبويب قد تم ربطه بمدير الأجزاء، فإن عنوان علامة التبويب رقم i سيكون عنوان الجزء رقم i. وبالتالي، ستكون عناوين الأسطر 27-33 هي عناوين علامات التبويب؛
- الأسطر 18-21: getCount تعرض عدد الأجزاء المدارة، وهي ثلاثة في هذه الحالة؛
- الأسطر 11-15: getItem(i) تعرض المقطع رقم i. هنا ستكون جميع المقاطع متطابقة من النوع [PlaceholderFragment]؛
- الأسطر 24-35: getPageTitle(int i) تعرض عنوان الجزء رقم i؛
1.7.4.2. الأجزاء المعروضة
![]() |
جميع أجزاء النشاط هنا من نفس النوع وجميعها مرتبطة بالعرض التالي XML [fragment_main]:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- الأسطر 1-16: layout من النوع [RelativeLayout]؛
- الأسطر 11-14: المكون الوحيد للعرض (الجزء): [TextView] المحدد بـ [section_label]؛
في [MainActivity]، الأجزاء المدارة هي من النوع [PlaceholderFragment] التالي:
// جزء
public static class PlaceholderFragment extends Fragment {
// نص معروض في الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
// يعرض جزءًا مع معلومة: رقم الجزء الذي تم تمريره كمعلمة
public static PlaceholderFragment newInstance(int sectionNumber) {
// جزء
PlaceholderFragment fragment = new PlaceholderFragment();
// معلومات مضمنة
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// النتيجة
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// تم إنشاء مثيل لـ [fragment_main]
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// تم العثور على [TextView]
TextView textView = (TextView) rootView.findViewById(R.id.section_label);
// تم تعديل محتواه
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
// يتم إرجاع العرض
return rootView;
}
}
- السطر 2: الفئة [PlaceholderFragment] تمتد من فئة Android [Fragment]. وهذا هو الحال دائمًا بشكل عام؛
- السطر 2: الفئة [PlaceholderFragment] ثابتة. تسمح طريقتها [newInstance] (السطر 10) بالحصول على مثيلات من النوع [PlaceholderFragment]؛
- الأسطر 10-19: الطريقة [newInstance] تنشئ وتُرجع كائنًا من النوع [PlaceholderFragment]؛
- الأسطر 14-16: يتم إنشاء الجزء مع وسيطة؛
يجب أن يحدد الجزء الطريقة [onCreateView] في السطر 22. يجب أن تعرض هذه الطريقة العرض المرتبط بالجزء.
- السطر 25: العرض [fragment_main.xml] مرتبط بالجزء؛
- السطر 27: تحتوي هذه العرض على مكون [TextView] يتم استرداد مرجعها عبر معرفها؛
- السطر 29: يتم عرض نص في [TextView]؛
- [getString] هي طريقة للفئة الأصلية [AppCompatActivity]؛
- الحجة الأولى هي رقم المكون. يشير [R.string.section_format] إلى رقم المكون المحدد بواسطة [section_format] في الملف [res / values / strings.xml] (السطر 4 أدناه):
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
- (تابع)
- السطر (د) أعلاه %1$d يشير إلى أن الحجة رقم 1 (%1) يجب أن يتم تنسيقها كرقم صحيح ($d)؛
- الحجة الثانية لـ [getString] هي القيمة التي يجب إعطاؤها للحجة $1 في السطر (د) أعلاه؛
- يقدم [getArguments] مرجع حزمة حجج الجزء. يجب أن نتذكر هنا أن كل حجة تم إنشاؤها باستخدام الحزمة التالية (السطور f-h):
// يعرض جزءًا يحتوي على معلومة: رقم الجزء الذي تم تمريره كمعلمة
public static PlaceholderFragment newInstance(int sectionNumber) {
// جزء
PlaceholderFragment fragment = new PlaceholderFragment();
// معلومات مضمنة
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// النتيجة
return fragment;
}
- (تابع)
- getArguments().getInt(ARG_SECTION_NUMBER) ستُرجع بالتالي القيمة [sectionNumber] للسطرين (g) و(b) أعلاه؛
- السطر 31: يتم عرض المنظر الذي تم إنشاؤه بهذه الطريقة؛
1.7.4.3. إدارة القائمة
في التطبيق الذي تم إنشاؤه، توجد قائمة:
![]() |
محتوى الملف [menu_main.xml] هو كما يلي:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- الأسطر 1-9: القائمة؛
- الأسطر 5-8: عنصر من القائمة محدد بـ [action_settings] (السطر 5)؛
- السطر 6: تسمية خيار القائمة. توجد في الملف [res / values / strings.xml] (السطر (ج) أدناه:
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
الرمز السابق يتوافق مع العنصر المرئي التالي (توجد القائمة في أعلى يمين نافذة تشغيل Android):
![]() | ![]() |
يتم إدارة هذه القائمة بالطريقة التالية في النشاط [MainActivity]:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// تضخيم القائمة؛ يؤدي هذا إلى إضافة عناصر إلى شريط الإجراءات إن كان موجودًا.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// معالجة النقرات على عناصر شريط الإجراءات هنا. سيقوم شريط الإجراءات
// معالجة النقرات على زر الصفحة الرئيسية/للأعلى تلقائيًا، طالما
// ما دمت تحدد نشاطًا رئيسيًا في AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
- الأسطر 1-6: يتم استدعاء هذه الطريقة عندما يكون النظام جاهزًا لإنشاء قائمة التطبيق. المعلمة المدخلة [Menu menu] هي قائمة فارغة لا تحتوي على خيارات بعد؛
- السطر 4: يتم استخدام الملف [res / menu / menu_main.xml]. يتم تعيين خيارات القائمة المحددة في هذا الملف إلى الكائن [Menu menu] الذي تم تمريره كمعلمة؛
- السطر 5: يُشار إلى أن إنشاء القائمة قد تم؛
- الأسطر 8-21: يتم تنفيذ الطريقة [onOptionsItemSelected] بمجرد النقر على أحد خيارات القائمة؛
- السطر 13: مرجع خيار القائمة الذي تم النقر عليه؛
- الأسطر 16-18: إذا كان الخيار الذي تم النقر عليه هو الخيار ذو المعرف [action_settings]، فلا يتم القيام بأي شيء ويتم الإشارة إلى أن الحدث قد تمت معالجته (السطر 17)؛
- السطر 20: يتم تمرير الحدث إلى الفئة الأصلية؛
لفهم ما يحدث في هذا القائمة بشكل أفضل، نضيف سجلات في الكود السابق:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("menu", "création menu en cours");
// قم بتوسيع القائمة؛ يؤدي ذلك إلى إضافة عناصر إلى شريط الإجراءات إن كان موجودًا.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// تعامل مع نقرات عناصر شريط الإجراءات هنا. سيقوم شريط الإجراءات
// معالجة النقرات على زر "الصفحة الرئيسية/لأعلى" تلقائيًا، طالما
// ما دمت تحدد نشاطًا رئيسيًا في AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
// الأم
return super.onOptionsItemSelected(item);
}
1.7.4.4. الزر العائم
تحتوي الواجهة التي تم إنشاؤها على زر عائم:
![]() |
يتم تعريف هذا المكون في العرض الرئيسي [activity-main.xml]:
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
تشير السطر 7 إلى صورة مقدمة من دعم Android، وهي صورة مظروف.
يتم إدارة هذا المكون في الفئة [MainActivity] بالطريقة التالية:
// زر عائم
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
- السطر 2: يتم استرداد مرجع الزر العائم في العرض المرتبط بالنشاط (activity_main)؛
- الأسطر 3-9: نربطه بمدير لإدارة النقر عليه؛
- السطر 6: تتيح الفئة [Snackbar] عرض رسائل مؤقتة على العرض باستخدام طريقتها [Snackbar.make]. الحجة الأولى هي عرض يبحث فيه [Snackbar] عن عرض أبوي لعرض الرسالة فيه. هنا، [view] هو عرض المغلف الذي تم النقر عليه (السطر 5). سيكون العرض الأصلي الذي سيتم العثور عليه هو العرض [activity_main]. الحجة الثانية هي الرسالة المراد عرضها. الحجة الثالثة هي مدة العرض (SHORT أو LONG)؛
- السطر 7: يمكن النقر على الرسالة المعروضة وبالتالي تشغيل إجراء. هنا لا يوجد أي إجراء مرتبط بالنقر على الرسالة. أخيرًا، تعرض الطريقة [show] الرسالة؛
يؤدي النقر على الزر العائم إلى النتيجة المرئية التالية:
![]() |
1.7.5. تنفيذ المشروع
الآن بعد أن شرحنا تفاصيل الكود الذي تم إنشاؤه، يمكننا فهم تنفيذه بشكل أفضل:

عند النقر على علامة التبويب رقم i، يتم عرض المقطع رقم i في حاوية العروض. ويمكن ملاحظة ذلك من خلال النص المعروض في [4]. يمكن ملاحظة أيضًا أنه يمكن الانتقال من علامة تبويب إلى أخرى عن طريق سحب العرض إلى اليمين أو اليسار بالماوس (التمرير). سنرى أنه يمكن التحكم في هذا السلوك.
عند النقر على خيار القائمة في [6]، نحصل على السجلات التالية:
![]() |
1.7.6. دورة حياة الأجزاء
![]() | ![]() |
- في [1]، نرى أن الطريقة [onCreateView] والطرق التالية يتم تنفيذها عند العرض الأول للجزء وفي كل مرة يتعين على النشاط إعادة عرضه؛
لمتابعة دورة حياة النشاط والأجزاء، نضيف السجلات التالية في كود [MainActivity]:
// منشئ
public MainActivity(){
Log.d("MainActivity","constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
// الأصل
super.onCreate(savedInstanceState);
...
}
// جزء
public static class PlaceholderFragment extends Fragment {
// نص معروض في الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
// يعرض جزءًا مع معلومة: رقم الجزء الذي تم تمريره كمعلمة
public static PlaceholderFragment newInstance(int sectionNumber) {
Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
// جزء
PlaceholderFragment fragment = new PlaceholderFragment();
...
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
...
}
}
}
نقوم بتنفيذ المشروع مرة أخرى. السجلات الأولى هي كما يلي:
- السطر 1: إنشاء النشاط؛
- السطر 2: تنفيذ طريقة [onCreate] الخاصة بها؛
- السطران 3-4: إنشاء مثيل للجزء رقم 1؛
- السطران 5-6: إنشاء مثيل للجزء رقم 2؛
- السطر 7: تهيئة الجزء رقم 2؛
- السطر 8: تهيئة الجزء رقم 1؛
- السطر 9: إنشاء قائمة النشاط؛
يجب أن نتذكر هنا الكود الذي يحكم إنشاء الأجزاء:
// مدير الأجزاء
// يُطلب منه عرض الأجزاء في العرض الرئيسي
// يجب أن يحدد الطرق [getItem] و [getCount] - أما الطرق الأخرى فهي اختيارية
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// رقم الجزء والموضع
@Override
public Fragment getItem(int position) {
// يتم إنشاء مثيل لجزء [PlaceHolder] وعرضه
return PlaceholderFragment.newInstance(position + 1);
}
...
- السطور 11-15: يتم إنشاء مثيل لجزء بواسطة [newInstance] في كل مرة يطلب فيها حاوية الأجزاء ذلك؛
تُظهر السجلات أعلاه أن المقطعين الأولين قد تم إنشاء مثيل لهما وتهيئتهما.
الآن، لنضغط على علامة التبويب رقم 2. السجلات الجديدة هي كما يلي:
- السطور 1-3: يتم إنشاء مثيل للجزء رقم 3 وتهيئته. نذكر أن الجزء رقم 2 هو الذي يتم عرضه؛
الآن، لنضغط على علامة التبويب رقم 3. لا يوجد أي سجل هنا. ربما لأن الجزء رقم 3 المراد عرضه قد تم إنشاء مثيل له بالفعل. الآن، لنعد إلى علامة التبويب رقم 1. تكون السجلات عندئذ كما يلي:
لم يتم إنشاء مثيل للجزء رقم 1 مرة أخرى، ولكن تم تنفيذ طريقة [onCreateView] الخاصة به مرة أخرى. يتكرر هذا السلوك مع الجزأين الآخرين.
من هذه السجلات، نستنتج ما يلي:
- تم إنشاء مثيل النشاط ثم تهيئته مرة واحدة؛
- تم إنشاء مثيل لكل جزء مرة واحدة؛
- أن الطريقة [onCreateView] لكل جزء تم تنفيذها عدة مرات؛
ما يجب معرفته وما تؤكده السجلات هو أنه بشكل افتراضي، عند عرض الجزء رقم i، يتم إنشاء مثيلات الأجزاء i-1 و i+1، إذا لم تكن موجودة بالفعل. وهذا ما يفسر، على سبيل المثال، أنه عند بدء التشغيل، في حين أنه يجب عرض الجزء رقم 1، فإن الأجزاء 1 و 2 هي التي تم إنشاء مثيل لها وتهيئتها. ما تظهره السجلات أيضًا هو أن الطريقة [getItem(i)] لا يتم استدعاؤها إلا مرة واحدة، حتى لو تم عرض الجزء رقم i عدة مرات. وبالتالي، يبدو أن حاوية الأجزاء [ViewPager] التي يجب أن تعرض الجزء رقم i تطلب هذا الجزء مرة واحدة من مدير الأجزاء [SectionsPagerAdapter]. بعد ذلك، لا تطلبه مرة أخرى وتستمر في استخدام الجزء الذي حصلت عليه.
وأخيرًا، توفر السجلات معلومات حول طريقة [onCreateView] الخاصة بالأجزاء:
- عند بدء التشغيل، تم إنشاء مثيلات المقتطفات 1 و 2 وتنفيذ طريقتها [onCreateView]؛
- عند الانتقال من الجزء 1 إلى الجزء 2، لا يتم إعادة تنفيذ طريقة [onCreateView] الخاصة بالجزء 2. لذلك لا يمكن استخدامها لتحديث الجزء 2. ومع ذلك، قد يكون المستخدم قد أجرى عملية باستخدام الجزء 1 يجب أن يعرض الجزء 2 نتيجتها. نرى أن الطريقة [onCreateView] لن يمكن استخدامها لتحديث الجزء 2. سيتعين إيجاد حل آخر؛
1.8. المثال-07: إعادة كتابة المثال-06 باستخدام المكتبة [AA]
1.8.1. إنشاء المشروع
سنقوم بنسخ المشروع [Exemple-06] في [Exemple-07] لإدخال تعليقات Android في هذا الأخير. للقيام بذلك، اتبع الإجراء الوارد في الفقرة 1.4. نحصل على النتيجة التالية:
![]() | ![]() |
1.8.2. تكوين Gradle
![]() |
نقوم بتحديث الملف [build.gradle] بالطريقة التالية:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// منذ إصدار المكون الإضافي Gradle 0.11 لنظام Android، يجب استخدام android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
أضفنا التكوين اللازم لاستخدام المكتبة [Android Annotations] (انظر الفقرة 1.4).
1.8.3. إضافة التعليقات التوضيحية الأولى AA
سنقوم بإنشاء تعليقات توضيحية AA في [MainActivity]:
![]() |
تتطور الفئة [MainActivity] على النحو التالي:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// مدير الأجزاء
private SectionsPagerAdapter mSectionsPagerAdapter;
// حاوية الأجزاء
@ViewById(R.id.container)
protected MyPager mViewPager;
// مدير علامات التبويب
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// الزر العائم
@ViewById(R.id.fab)
protected FloatingActionButton fab;
// المنشئ
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// شريط الأدوات
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// مدير الأجزاء
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// حاوية الأجزاء مرتبطة بمدير الأجزاء
// أي أن الجزء رقم i من حاوية الأجزاء هو الجزء رقم i الذي يقدمه مدير الأجزاء
mViewPager.setAdapter(mSectionsPagerAdapter);
// شريط علامات التبويب مرتبط أيضًا بحاوية الأجزاء
// أي أن علامة التبويب رقم i تعرض الجزء رقم i من الحاوية
tabLayout.setupWithViewPager(mViewPager);
// زر عائم
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- السطر 1: التعليق التوضيحي [@EActivity] يجعل من [MainActivity] فئة تدار بواسطة AA. معلمته [R.layout.activity_main] هي معرف العرض [activity_main.xml] المرتبط بالنشاط؛
- السطران 11-12: يتم إدخال المكون المحدد بـ [R.id.tabs] في الحقل [tabLayout]. وهو مدير علامات التبويب؛
- السطران 14-15: يتم إدخال المكون المحدد بـ [R.id.fab] في الحقل [fab]. هذا هو الزر العائم؛
- الأسطر 23-50: تم نقل الكود الذي كان موجودًا سابقًا في الأسلوب [onCreate] إلى أسلوب آخر يحمل اسمًا عشوائيًا ولكنه مُعلَّم بـ [@AfterViews] (السطر 23). في الطريقة المُعلَّمة بهذه الطريقة، نضمن أن جميع مكونات الواجهة المرئية المُعلَّمة بـ [@ViewById] قد تم تهيئتها؛
- كما تم وضع سجلات لرصد دورة حياة النشاط؛
نتذكر أن التعليق التوضيحي [@EActivity] سيولد فئة [MainActivity_] التي ستكون النشاط الفعلي للمشروع. لذلك يجب تعديل الملف [AndroidManifest.xml] بالطريقة التالية:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- السطر 12: النشاط الجديد.
في هذه المرحلة، قم بتشغيل المشروع مرة أخرى وتأكد من أنك لا تزال تحصل على الواجهة ذات علامات التبويب.
1.8.4. إعادة كتابة الأجزاء
سنراجع إدارة أجزاء المشروع. في الوقت الحالي، تعد الفئة [PlaceholderFragment] فئة داخلية ثابتة للنشاط [MainActivity]. سنعود إلى حالة استخدام أكثر شيوعًا، وهي الحالة التي يتم فيها تعريف الأجزاء في فئات خارجية. علاوة على ذلك، نقدم التعليقات التوضيحية AA للأجزاء.
يتطور المشروع [Exemple-07] على النحو التالي:
![]() |
فيما سبق، نرى ظهور الفئة [PlaceholderFragment] التي تم نقلها خارج الفئة [MainActivity]. وقد أعيدت كتابتها على النحو التالي:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// الجزء هو عرض يتم عرضه بواسطة حاوية الأجزاء
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// مكون من مكونات الواجهة المرئية
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// رقم الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
// المنشئ
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// الأصل
super.onResume();
// العرض
if (textViewInfo != null) {
Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
}
}
- السطر 15: يتم توضيح الجزء بعلامة [@EFragment] التي يكون معلمتها هو معرف العرض XML المرتبط بالجزء، وهو هنا العرض [fragment_main.xml]؛
- السطران 19-20: يتم إدخال مرجع مكون [fragment_main.xml] المحدد بواسطة [R.id.section_label]، وهو من النوع [TextView] (السطر (l) أدناه)، في الحقل [textViewInfo]:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- السطور 42-52: يتم تنفيذ الطريقة [onResume] قبل عرض العرض المرتبط بالجزء. يمكن استخدامها لتحديث الواجهة المرئية التي سيتم عرضها؛
- السطر 47: يجب استدعاء الطريقة التي تحمل الاسم نفسه من الفئة الأصلية؛
- السطر 49: هناك صعوبة في معرفة ما إذا كان يمكن تنفيذ الطريقة [onResume] أم لا قبل تهيئة الحقل في السطر 20. ستخبرنا السجلات التي تم وضعها لتتبع دورة حياة الجزء بذلك. في الوقت الحالي وكتدبير احترازي، نقوم بإجراء اختبار للصفر؛
- السطر 51: نقوم بتحديث معلومات الحقل [textViewInfo] باستخدام الحجة الصحيحة التي تم تمريرها إلى الجزء عند إنشائه؛
تفقد الفئة [MainActivity] فئتها الداخلية [PlaceholderFragment] ويشهد مدير الأجزاء الخاص بها التطور التالي:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء
private Fragment[] fragments;
// عدد الأجزاء
private static final int FRAGMENTS_COUNT = 3;
// رقم الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
// الشركة المصنعة
public SectionsPagerAdapter(FragmentManager fm) {
// الأصل
super(fm);
// تهيئة جدول الأجزاء
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length; i++) {
// يتم إنشاء جزء
fragments[i] = new PlaceholderFragment_();
// يمكن تمرير معلمات إلى الجزء
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
}
// المقطع رقم الموضع
@Override
public Fragment getItem(int position) {
Log.d("MainActivity", String.format("getItem[%s]", position));
return fragments[position];
}
// يعرض عدد الأجزاء المدارة
@Override
public int getCount() {
return fragments.length;
}
// اختياري - يعطي عنوانًا للأجزاء المدارة
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
}
- السطر 4: يتم وضع الأجزاء في مصفوفة؛
- الأسطر 16-23: تتم تهيئة مصفوفة الأجزاء في المنشئ. وهي من النوع [PlaceholderFragment_] (السطر 18) وليست [PlaceholderFragment]. فقد تم بالفعل توضيح الفئة [PlaceholderFragment] بواسطة توضيح AA وستؤدي إلى إنشاء فئة [PlaceholderFragment_] مشتقة من [PlaceholderFragment]، وهذه هي الفئة التي يجب أن تستخدمها النشاط. يتم تمرير حجة عددية لكل جزء تم إنشاؤه، وسيتم عرضها بواسطة الجزء؛
- السطور 42-45: تم تغيير عناوين الأجزاء. ونظرًا لأن هذه العناوين هي أيضًا عناوين علامات التبويب، فمن المفترض أن نرى تغييرًا في شريط علامات التبويب؛
لنقوم بتجميع [Make] [1] هذا المشروع:
![]() | ![]() |
- في [2]، نرى أن الفئات التي تم إنشاؤها بواسطة المكتبة AA موجودة في المجلد [app / build / generated / source / apt / debug] (يجب أن تكون في منظور [Project] لرؤية [2])؛
قم بتشغيل المشروع [Exemple-07] وتأكد من أنه لا يزال يعمل.
1.8.5. فحص السجلات
عند تشغيل التطبيق، تكون السجلات كما يلي:
- السطر 1: إنشاء النشاط الوحيد؛
- السطر 2: طريقة [afterViews] للنشاط: يتم تهيئة حقوله المُعلَّمة بواسطة [@ViewById]؛
- الأسطر 3-5: إنشاء الأجزاء الثلاثة؛
- السطران 6-7: حاوية الأجزاء [ViewPager] تطلب أول جزأين؛
- السطور 8-9: طرق الجزء 2؛
- السطور 10-11: طرق الجزء 1؛
- السطور 12-13: طريقة [onResume] للجزء 1؛
- السطران 14-15: طريقة [onResume] الخاصة بالجزء 2؛
- السطر 16: إنشاء قائمة النشاط؛
تجدر الإشارة إلى أننا حصلنا على إجابة لسؤال طُرح سابقًا: فالطريقة [onResume] من الجزء 1 على سبيل المثال (السطر 12) تُنفَّذ بعد الطريقة [afterViews] من الجزء (السطر 11). لذلك، عندما يتم تنفيذ الطريقة [onResume]، يمكنها استخدام الحقول التي تم تعليقها بواسطة [@ViewById]. وبالتالي، يمكننا الآن كتابة الطريقة [onResume] على النحو التالي:
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// الأصل
super.onResume();
// العرض
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
الآن لننتقل من علامة التبويب 1 إلى علامة التبويب 2. السجلات الجديدة هي كما يلي:
- السطر 1: حاوية الأجزاء [ViewPager] تطلب الجزء رقم 3؛
- السطران 2-3: طرق الجزء رقم 3. نذكر أن هذا الجزء قد تم إنشاء مثيل له منذ بدء تشغيل التطبيق؛
- السطران 4-5: يتم تنفيذ طريقة [onResume] للجزء رقم 3. نذكر أن الجزء رقم 2 هو الذي يتم عرضه؛
الآن ننتقل من علامة التبويب 2 إلى علامة التبويب 3. لا يوجد أي سجل. وبالتالي، لا يتم تنفيذ أي من طرق [onCreateView, afterViews, onResume] الخاصة بالجزء رقم 3. يتم عرض النص [Hello World from section:3] بشكل صحيح فقط لأن هذا النص تم إنشاؤه بالفعل في الخطوة السابقة عند عرض الجزء رقم 2. نتذكر بالفعل أنه في هذه الخطوة، تم تنفيذ الطريقة [onResume] من الجزء رقم 3. نلاحظ هنا أنه، تمامًا مثل الطريقة [onCreateView]، لا يمكن استخدام الطريقة [onResume] لتحديث الجزء 3. لو كان من الضروري تغيير النص المعروض بواسطة الجزء، لما استطاعت أي من هاتين الطريقتين القيام بذلك.
الآن، لنعد من علامة التبويب رقم 3 إلى علامة التبويب رقم 1. تكون السجلات عندئذٍ كما يلي:
نرى أن جميع طرق الجزء 1 قد تم تنفيذها. ونرى أن الطريقة getItem لم يتم استدعاؤها. كما ذكرنا، لا يتم استدعاء هذه الطريقة سوى مرة واحدة لكل جزء؛
الآن، لننتقل من علامة التبويب 1 إلى علامة التبويب المجاورة 2. لدينا السجلات التالية:
مثير للدهشة، أليس كذلك؟ يتم إعادة تنفيذ جميع طرق الجزء رقم 3.
لفهم هذه الظواهر، يجب أن نتذكر أنه بشكل افتراضي، عندما يعرض حاوية الأجزاء الجزء i، فإنه يقوم بتهيئة الأجزاء i-1 و i و i+1. لنقرأ السجلات مرة أخرى في ضوء هذه المعلومات.
أولاً، سجلات تشغيل التطبيق:
نظرًا لأن حاوية الأجزاء ستعرض الجزء 1، يتم تهيئة الجزأين 1 و 2 (الأسطر 8-15).
ننتقل الآن من علامة التبويب 1 إلى علامة التبويب 2:
نظرًا لأن حاوية الأجزاء ستعرض الجزء 2، يجب تهيئة الأجزاء 1 و2 و3. وقد تمت تهيئة الأجزاء 1 و2 بالفعل في الخطوة السابقة. يتم تهيئة الجزء 3 في الأسطر 2-5.
ننتقل من علامة التبويب 2 إلى علامة التبويب 3. لا توجد سجلات. نظرًا لأن حاوية الأجزاء ستعرض الجزء 3، يجب تهيئة الأجزاء 2 و3. ولكن منذ الخطوة السابقة، تم تهيئتها بالفعل. ما لا نراه هنا هو أن الجزء 1 الذي لا يجاور الجزء 3 يفقد حالته التي لا يتم الاحتفاظ بها في الذاكرة.
ننتقل من علامة التبويب 3 إلى علامة التبويب 1. السجلات هي كما يلي:
نظرًا لأن حاوية الأجزاء ستعرض الجزء 1، يجب أيضًا تهيئة الجزء 2. وقد تمت تهيئته منذ الخطوة السابقة. في هذه الخطوة نفسها، فقدت حالة الجزء 1. لذلك يتم إعادة تهيئتها في الأسطر 1-4. ما لا نراه هنا هو أن الجزء 3 غير المجاور للجزء 1 يفقد حالته التي لا يتم الاحتفاظ بها في الذاكرة.
عند الانتقال من علامة التبويب 1 إلى علامة التبويب المجاورة 2، نحصل على السجلات التالية:
نظرًا لأن حاوية الأجزاء ستعرض الجزء 2، يجب تهيئة الأجزاء 1 و2 و3. وقد تمت تهيئة الأجزاء 1 و2 بالفعل في الخطوة السابقة. يتم تهيئة الجزء 3 في الأسطر 1-4.
ماذا تعلمنا؟
- أن الإدارة الافتراضية للأجزاء خاصة جدًا ويجب معرفتها إذا كنا لا نريد أن نفقد صوابنا. يمكننا تغيير طريقة الإدارة هذه وسنفعل ذلك لاحقًا؛
- أنه مع هذه الإدارة الافتراضية، لا يمكن استخدام أي من طرق [onCreateView, onResume] لتحديث الجزء الذي سيتم عرضه لأننا لسنا متأكدين من أنها ستنفذ؛
1.8.6. onDestroyView
تعد الطريقة [onDestroyView] جزءًا من دورة حياة الأجزاء (انظر الفقرة 1.7.6):
![]() | ![]() |
نلاحظ أنه في دورة حياة الجزء:
- يمكن تنفيذ الطريقة [onCreateView] عدة مرات؛
- قبل العودة إلى الطريقة [onCreateView] لاحقًا، لا بد من المرور بالطريقة [onDestroyView] [2]؛
سنقوم بإدراج هذه الطرق في الأجزاء لمتابعة دورة حياتها بشكل أفضل. يصبح كود الجزء كما يلي:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// الجزء هو عرض يتم عرضه بواسطة حاوية الأجزاء
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
...
@Override
public void onDestroyView() {
// سجل
Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// الأصل
super.onDestroyView();
}
}
لنقم بتشغيل التطبيق. السجلات الأولى هي كما يلي:
- السطر 1: إنشاء النشاط الوحيد؛
- السطر 2: طريقة [afterViews] للنشاط: يتم تهيئة حقولها المُعلَّمة بـ [@ViewById]؛
- الأسطر 3-5: إنشاء الأجزاء الثلاثة؛
- السطران 6-7: حاوية الأجزاء [ViewPager] تطلب أول جزأين؛
- السطور 8-9: يتم إنشاء عرض الجزء 2 (ليس بالضرورة أن يكون مرئيًا)؛
- السطور 10-11: يتم إنشاء عرض الجزء 1 (ليس بالضرورة أن يكون مرئيًا)؛
- السطران 12-13: طريقة [onResume] للجزء 1؛
- السطران 14-15: طريقة [onResume] للجزء 2؛
- السطر 16: إنشاء قائمة النشاط؛
لننتقل من علامة التبويب 1 إلى علامة التبويب 3:
06-03 02:50:02.685 2346-2346/exemples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 1
- السطر 1: حاوية الأجزاء تطلب الجزء الثالث؛
- السطران 2-3: يتم إنشاء عرض الجزء 3 (ليس بالضرورة عرضه)؛
- السطران 4-5: يتم تنفيذ الطريقة [onResume] للجزء 3؛
- السطر 6: يتم تنفيذ الطريقة [onDestroyView] الخاصة بالجزء 1. وهذا يعني أنه عندما يعود المستخدم إلى الجزء 1 أو إلى جزء مجاور، سيتم إعادة تنفيذ دورة حياة هذا الجزء؛
نعود من علامة التبويب 3 إلى علامة التبويب 1:
06-03 02:53:46.255 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 3
- الأسطر 1-4: يتم إعادة تنفيذ دورة حياة الجزء 1 لأنه خضع لـ [onDestroyView]؛
- السطر 5: يتم الآن تنفيذ طريقة [onDestroyView] الخاصة بالجزء 3. ومرة أخرى، عندما يعود المستخدم إلى الجزء 3 أو إلى جزء مجاور، سيتم إعادة تنفيذ دورة حياة هذا الجزء؛
1.8.7. setUserVisibleHint
تقوم طريقة [onCreateView] الخاصة بدورة الحياة بإنشاء مثيل للعرض المرتبط بالجزء ولكنها لا تجعله مرئيًا بالضرورة. وهذا ما سنراه الآن. يتم تنفيذ طريقة [Fragment.setUserVisibleHint] في كل مرة تتغير فيها رؤية الجزء. نضيف هذه الطريقة إلى كود الجزء:
package exemples.android;
....
// الجزء هو عرض يتم عرضه بواسطة حاوية الأجزاء
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// مكون من واجهة المستخدم المرئية
@ViewById(R.id.section_label)
protected TextView textViewInfo;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// سجل
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
}
}
عند البدء، تكون السجلات كما يلي:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- تُظهر السجلات في الأسطر 7 و9-10 أن الجزء 1 هو الوحيد الذي يصبح مرئيًا. ونلاحظ أيضًا أنه يصبح مرئيًا قبل تنفيذ طريقة [onCreateView] الخاصة به؛
لننتقل من علامة التبويب 1 إلى علامة التبويب 2:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
- يتم إخفاء الجزء 1 (السطر 3)، ويتم عرض الجزء 2 (السطر 4)؛
لننتقل من علامة التبويب 2 إلى علامة التبويب 3:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
- يتم إخفاء المقطع 2 (السطر 1)، ويتم عرض المقطع 3 (السطر 2)؛
لنعد إلى علامة التبويب 1:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
- الجزء 3 مخفي (السطر 2)، والجزء 1 معروض (السطر 3)؛
ماذا تعلمنا؟
- يتم تنفيذ الطريقة [setUserVisibleHint] مرة واحدة مع الخاصية [isVisibleToUser] إلى true، بالنسبة للجزء الذي سيتم عرضه؛
- لا يمكن تحديد متى سيتم تنفيذ هذه الطريقة بالنسبة لدورة حياة الجزء. وبالتالي، بالنسبة للجزء 1، تم تنفيذ الطريقة [setUserVisibleHint, true] قبل الطريقة [onCreateView] في بداية دورة حياة هذا الجزء، بينما حدث العكس بالنسبة للجزأين 2 و3؛
1.8.8. setOffscreenPageLimit
تُظهر السجلات السابقة أنه عندما يستعد حاوية الأجزاء [ViewPager] لعرض الجزء رقم i، فإنه يقوم، إن لم يكن قد فعل ذلك بالفعل، بتنفيذ دورة حياة الأجزاء المجاورة i-1 و i+1. يمكن التحكم في هذا التشغيل بواسطة الطريقة [ViewPager].setOffscreenPageLimit:
باستخدام التعليمات المذكورة أعلاه،
- عندما يستعد حاوية الأجزاء [ViewPager] لعرض الجزء رقم i، فإنه يقوم، إذا لم يكن قد تم ذلك بالفعل، بتنفيذ دورة حياة الأجزاء المجاورة في الفاصل [i-n, i+n]؛
- إذا تم بعد ذلك عرض الجزء j:
- تتكرر نفس الظاهرة بالنسبة للأجزاء المجاورة في الفاصل [j-n, j+n]؛
- يمكن أن تخضع الأجزاء التي تم تهيئتها في الخطوة 1 والتي لم تعد في الجوار [j-n, j+n] للجزء الجديد لعملية [onDestroyView]. ومع ذلك، لاحظت في تطبيقات أخرى، لا سيما تلك الواردة في الفصل 3، أن هذا لم يكن هو الحال دائمًا؛
نقوم بتعديل الطريقة [MainActivity.afterViews] على النحو التالي:
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// شريط الأدوات
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// مدير الأجزاء
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// حاوية الأجزاء مرتبطة بمدير الأجزاء
// أي أن الجزء رقم i من حاوية الأجزاء هو الجزء رقم i الذي تم إصداره بواسطة مدير الأجزاء
mViewPager.setAdapter(mSectionsPagerAdapter);
// يتم منع التمرير بين الأجزاء
mViewPager.setSwipeEnabled(false);
// إزاحة الأجزاء
mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
// شريط علامات التبويب مرتبط أيضًا بحاوية الأجزاء
// أي أن علامة التبويب رقم i تعرض الجزء رقم i من الحاوية
tabLayout.setupWithViewPager(mViewPager);
// زر عائم
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- السطر 20: نضع عدد الأجزاء المجاورة المطلوب تهيئتها، في إجمالي عدد الأجزاء -1. وبالتالي، عند بدء التشغيل، عندما يعرض حاوية الأجزاء الجزء رقم 1، فإنه سيقوم في الوقت نفسه بتهيئة الأجزاء 2، 3، ...، n حيث n=1+mSectionsPagerAdapter.getCount() - 1=mSectionsPagerAdapter.getCount(). وبالتالي، سيتم تهيئة جميع الأجزاء. وعندما تنتقل نافذة العرض إلى جزء آخر، سيكتشف حاوية الأجزاء:
- سيكتشف أن جميع الأجزاء المجاورة للجزء الجديد قد تم تهيئتها بالفعل، وبالتالي لن يقوم بتهيئتها؛
- ونظرًا لأن المجاورة للجزء الجديد تغطي أيضًا جميع الأجزاء، فلن يتم "إلغاء تهيئة" أي منها بواسطة حاوية الأجزاء؛
بشكل إجمالي، من المفترض أن نرى جميع الأجزاء التي تم إنشاء مثيل لها وتهيئتها عند بدء تشغيل التطبيق، ثم لا نراها بعد ذلك أبدًا. وهذا ما نتحقق منه الآن من خلال فحص السجلات.
عند بدء التشغيل، لدينا السجلات التالية:
- الأسطر 4-6: إنشاء الأجزاء الثلاثة؛
- الأسطر 7 و9 و11: حاوية الأجزاء تطلب الأجزاء الثلاثة. في الإصدار السابق، كانت تطلب اثنين؛
- الأسطر 14-25: يتم تنفيذ دورة حياة الأجزاء الثلاثة؛
لننتقل الآن من علامة التبويب 1 إلى علامة التبويب 2:
لننتقل من علامة التبويب 2 إلى علامة التبويب 3:
ثم من علامة التبويب 3 إلى علامة التبويب 1:
تؤكد السجلات هذه النظرية. تم إنشاء جميع الأجزاء وتهيئتها عند بدء التشغيل. بعد ذلك، لم يتم تنفيذ أساليب دورة حياتها. لدينا هنا طريقة عمل متوقعة جدًا للأجزاء، مما يسهل استخدامها بشكل كبير.
ما نريد العثور عليه هو وسيلة لتحديث جزء سيتم عرضه، بغض النظر عن تجاور الأجزاء الذي يختاره المطور. أظهرت لنا السجلات أمرين:
- يتم دائمًا تنفيذ الطريقة [setUserVisibleHint, true] للجزء الذي سيتم عرضه وليس للجزء الآخر؛
- قد يحدث هذا الحدث قبل أو بعد دورة حياة المقطع. وهذا يعتمد على تجاور المقاطع الذي يختاره المطور. وهذا يمثل مشكلة، لأنه إذا لم تكن دورة الحياة قد حدثت بعد، فهذا يعني أنه لا يمكن تحديث المقطع باستخدام الطريقة [setUserVisibleHint, true]؛
كانت السجلات عند بدء تشغيل التطبيق عندما كانت تجاور الأجزاء 1 كما يلي:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- نرى أنه عندما يصبح الجزء 1 مرئيًا، لم يتم إنشاء عرضه بعد. لذلك لا يمكن التعامل معه. ويمكن القيام بذلك خلال دورة حياة الجزء، على سبيل المثال في الطرق [onCreateView] (السطر 11) أو [onResume] (السطران 13-14). نظرًا لأننا نستخدم التعليقات التوضيحية AA، فليس علينا عادةً كتابة الطريقة [onCreateView]. لذا، تبدو الطريقة [onResume] هي الأنسب هنا لتحديث الجزء 1؛
عندما انتقلنا من علامة التبويب 1 إلى علامة التبويب 2، كانت السجلات كما يلي:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
هذه المرة، لدينا فقط الطريقة [setUserVisibleHint, true] في السطر 4 لتحديث الجزء 2؛
عندما انتقلنا من علامة التبويب 2 إلى علامة التبويب 3، كانت السجلات كما يلي:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
هنا، لدينا فقط الطريقة [setUserVisibleHint, true] في السطر 2 لتحديث الجزء 3؛
عندما انتقلنا من علامة التبويب 3 إلى علامة التبويب 1، كانت السجلات كما يلي:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
هنا، يجب استخدام الطريقة [onResume] من الجزء 1 (السطران 6-7) لتحديث الجزء 1.
لذلك، في هذا المثال، نرى أنه لتحديث جزء سيتم عرضه، لدينا طريقتان: [setUserVisibleHint] و [onResume].
سنقوم بتنفيذ هذا الحل في مشروع جديد حيث سيتعين على كل جزء عرض عدد المرات التي تم عرضها فيها، وهو ما سنسميه زيارة. لذلك سيتعين تحديث عرضه في كل مرة يتم عرضها فيها. هذه هي المشكلة التي نسعى لحلها.
قبل ذلك، سننظر في المرحلة الأخيرة من دورة حياة النشاط أو المقطع، وهي المرحلة التي يتم فيها إتلافه. يمكن للنظام أن يبادر بإزالة نشاط ما إذا كانت هناك أنشطة أخرى ذات أولوية أعلى تطالب بموارد غير متوفرة. لتحرير هذه الموارد، سيبادر النظام بإزالة بعض الأنشطة. سيتم عندئذٍ استدعاء الطريقة [onDestroy] الخاصة بالنشاط والأجزاء.
1.8.9. OnDestroy
![]() | ![]() | ![]() |
سنسمح للمستخدم بحذف النشاط باستخدام خيار قائمة [5]. للقيام بذلك، نضيف خيار قائمة جديد في الملف [menu_main.xml] [1]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/action_terminate"
android:title="@string/action_terminate"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
نقوم بنسخ ولصق الخيار الأول من القائمة ونقوم بتعديل النتيجة (السطران 9 و 10). يتم إضافة نص هذا الخيار الجديد في الملف [strings.xml] [2]:
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="action_terminate">Terminate</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
أخيرًا، في الفئة [MainActivity]، ندير النقر على الخيار [Terminate]:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// تعامل مع نقرات عناصر شريط الإجراءات هنا. سيقوم شريط الإجراءات
// معالجة النقرات على زر الصفحة الرئيسية/للأعلى تلقائيًا، طالما
// ما دمت تحدد نشاطًا رئيسيًا في AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
if (id == R.id.action_terminate) {
Log.d("menu", "action_terminate selected");
//عند إنهاء النشاط
finish();
return true;
}
// الأم
return super.onOptionsItemSelected(item);
}
- الأسطر 14-19: نقوم بنسخ ولصق الأسطر 10-13 ونقوم بتكييف الكود مع الخيار الجديد؛
- السطر 17: يتم إنهاء النشاط بواسطة إجراء برمجي؛
الآن، لنقم بتنفيذ هذه النسخة الجديدة، ثم بمجرد عرض العرض الأول، نضغط على خيار القائمة [Terminate]. تكون السجلات عندئذٍ كما يلي:
- السطران 1-2: النقر على الخيار [Terminate]؛
- السطر 4: يتم استدعاء الطريقة [onDestroy] للنشاط؛
- السطران 4-5: يتم استدعاء الطريقة [onDestroyView] للجزء 1 ثم طريقتها [onDestroy]؛
- الأسطر 6-9: تتكرر هذه العملية مع الشريحتين الأخريين؛
يجب أن نتذكر أن الطريقة [onDestroy] الخاصة بالنشاط والأجزاء يتم استدعاؤها عندما يتم حذف النشاط بواسطة النظام أو المطور أو المستخدم. يمكن استخدام هذه الطريقة لحفظ المعلومات، على سبيل المثال محليًا على الجهاز اللوحي، من أجل استرجاعها عندما يقوم المستخدم بإعادة تشغيل التطبيق.
1.9. مثال-08: تحديث جزء مع تجاور متغير للأجزاء
1.9.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-07] إلى [Exemple-08]. للقيام بذلك، سنتبع الإجراء الموضح لنسخ [Exemple-02] إلى [Exemple-03] في الفقرة 1.4.
![]() | ![]() |
1.9.2. إعادة كتابة الجزء [PlaceholderFragment]
الرمز الجديد للجزء [PlaceholderFragment] هو التالي. وهو يعمل بغض النظر عن التجاور المحدد للأجزاء (1، جزئي، كلي):
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// الجزء هو عرض يتم عرضه بواسطة حاوية الأجزاء
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// مكون من واجهة المستخدم
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// البيانات
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
// رقم الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
// المنشئ
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// سجل
Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
if (!initDone) {
// النص الأولي
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// تمت المبادرة
initDone = true;
}
// عرض النص الحالي
textViewInfo.setText(text);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
@Override
public void onDestroyView() {
...
}
@Override
public void onResume() {
...
}
// تحديث جزء
public void update() {
// يعتمد العمل المطلوب على رقم الزيارة
if (numVisit > 1) {
// سجل
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// نص معدل
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// معلومات محلية للسجلات
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
}
}
- الأسطر 34-48: من المحتمل أن يتم تنفيذ الطريقة [@AfterViews] عدة مرات. كنا نستخدمها لتهيئة نص المقطع (السطر 42). ما زلنا نفعل ذلك، ولكن لكي لا نكرره إلا مرة واحدة، ندير متغيرًا منطقيًا [initDone] (السطر 44) للإشارة إلى أن التهيئة قد تمت ولا داعي لإعادتها؛
- السطور 56-59: نقدم الطريقة [onDestroyView] لتسجيل حقيقة أنه في المرة التالية التي سيتم فيها إعادة عرض المقتطف، سيتم إعادة تنفيذ دورة حياته؛
- أظهرت السجلات أن هناك طريقتين يمكن تنفيذهما بعد الطريقة [@AfterViews]: الطريقتان [setUserVisibleHint] و [onResume]. لا يتم تنفيذ الطريقة [onResume] إلا عند تنفيذ دورة حياة الجزء. أما الطريقة [setUserVisibleHint]، فلا يتم تنفيذها دائمًا بعد الطريقة [@AfterViews]. أظهرت السجلات أن إحدى الطريقتين على الأقل يتم تنفيذها بعد الطريقة [@AfterViews]. لم تظهر السجلات أبدًا أن كلاهما يمكن تنفيذهما معًا بعد الطريقة [@AfterViews]. فإما أن يتم تنفيذ إحداهما أو الأخرى. كإجراء احترازي، سيتم تعيين قيمة منطقية [updateDone] عند إجراء تحديث؛
الطريقتان [setUserVisibleHint] و [onResume] هما التاليتان:
// البيانات
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// الأصل
super.setUserVisibleHint(isVisibleToUser);
// الذاكرة
this.isVisibleToUser = isVisibleToUser;
// سجل
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// عدد الزيارات
if (isVisibleToUser) {
// زيادة
numVisit++;
// تحديث الجزء
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// سيتم إخفاء الجزء
updateDone = false;
}
}
@Override
public void onResume() {
// الأصل
super.onResume();
// سجل
Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// تحديث
if (isVisibleToUser && !updateDone) {
update();
updateDone = true;
}
}
- السطر 14: يتم حفظ حالة ظهور الجزء أو عدم ظهوره؛
- الأسطر 22-25: إذا كان الجزء مرئيًا وتم تنفيذ الطريقة [@AfterViews]، يتم تنفيذ الطريقة [update] ويتم تمرير القيمة المنطقية [updateDone] إلى true؛
- السطور 26-28: إذا كان المقطع سيتم إخفاؤه، يتم إعادة تعيين القيمة المنطقية [updateDone] إلى false. نحتاج في الواقع إلى حدث لإعادة تعيين المتغير المنطقي [updateDone]، الذي تم تعيينه إلى true، إلى false بمجرد استدعاء الأسلوب [update] حتى يمكن إجراء تحديثات جديدة. نستفيد من حقيقة أن الجزء لم يعد مرئيًا للقيام بذلك. عندما يصبح مرئيًا مرة أخرى، يجب إجراء تحديث الجزء من جديد؛
- الأسطر 32-42: تُظهر السجلات أنه وفقًا للمجاورة المختارة للأجزاء، يمكن تنفيذ الطريقة [onResume] في حين أن الجزء غير مرئي. إذا لم يكن مرئيًا، لا يتم إجراء التحديث (السطر 39) وكما فعلنا مع [setMenuVisibility]، ندير القيمة المنطقية [updateDone].
أخيرًا، الطريقة [onDestroyView] هي كما يلي:
@Override
public void onDestroyView() {
// الأصل
super.onDestroyView();
// تحديث المؤشر
afterViewsDone = false;
// سجل
Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}
يتم تنفيذ الطريقة [onDestroyView] عند انتهاء دورة حياة الجزء. ويمكن أن تستأنف دورة أخرى لاحقًا.
- السطر 6: الطريقة [onDestroyView] تحذف أي ارتباط مع العرض المرتبط بالجزء. وسيتم إعادة إنشاؤه في دورة حياة الجزء التالية. في الوقت الحالي، يتعين علينا تعيين القيمة المنطقية [afterViews] إلى false، للإشارة إلى أن الارتباط مع العرض لم يعد موجودًا؛
سنقوم بتشغيل التطبيق مع 5 أجزاء ذات تجاور يساوي 2. يتم إجراء التعديلات في [MainActivity]:
// عدد الأجزاء
private final int FRAGMENTS_COUNT = 5;
// تجاور الأجزاء
private final int OFF_SCREEN_PAGE_LIMIT=2;
// مدير الأجزاء
private SectionsPagerAdapter mSectionsPagerAdapter;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
....
// إزاحة الأجزاء
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
...
}
سجلات التشغيل عند البدء هي كما يلي:
05-31 06:23:07.015 32551-32551/exemples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/exemples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/exemples.android D/menu: création menu en cours
- الأسطر 8 و10 و12: يطلب حاوية الأجزاء جميع الأجزاء المجاورة للجزء 1؛
- السطور 9 و 11 و 13: يتم تنفيذ الطريقة [setUserVisibleHint] لهذه الأجزاء مع [visibleToUser] إلى false؛
- السطر 14: يتم تنفيذ الطريقة [setUserVisibleHint] للجزء 1 مع [visibleToUser] إلى true؛
- الأسطر 15-17: يتم استدعاء الطريقة [afterViews] للقطاعات الثلاثة المجاورة. نرى هنا حالة يتم فيها استدعاء هذه الطريقة بعد أن أصبح جزء ما مرئيًا (الجزء 1 في السطر 14)؛
- الأسطر 18-20: يتم استدعاء الطريقة [onResume] للقطاعات الثلاثة المجاورة؛
ننتقل من علامة التبويب 1 إلى علامة التبويب 2:
05-31 06:52:36.132 32551-32551/exemples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- نظرًا لأن تجاور الأجزاء قد تم إزاحته بمقدار موضع واحد إلى اليمين، يتم طلب الجزء 4 بواسطة حاوية الأجزاء؛
- السطر 2: يتم استدعاء الطريقة [setUserVisibleHint] للجزء 4 مع [visibleToUser] إلى false؛
- السطر 3: يتم استدعاء الطريقة [setUserVisibleHint] للجزء 1 مع [visibleToUser] إلى false. في الواقع، أصبح الجزء 1 مخفيًا الآن؛
- السطر 4: يتم استدعاء الطريقة [setUserVisibleHint] للجزء 2 مع [visibleToUser] إلى true. أصبح الجزء 2 مرئيًا الآن؛
- السطران 5-6: يستمر دورة حياة الجزء 4؛
ننتقل من علامة التبويب 2 إلى علامة التبويب 3:
05-31 06:58:16.228 32551-32551/exemples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- نظرًا لأن تجاور الأجزاء قد تم إزاحته بمقدار موضع واحد إلى اليمين، يتم طلب الجزء 5 من قبل حاوية الأجزاء؛
- السطر 2: يتم استدعاء الطريقة [setUserVisibleHint] الخاصة بالجزء 5 مع [visibleToUser] إلى false؛
- السطر 3: يتم استدعاء الطريقة [setUserVisibleHint] الخاصة بالجزء 2 مع [visibleToUser] إلى false. في الواقع، أصبح الجزء 2 مخفيًا الآن؛
- السطر 4: يتم استدعاء الأسلوب [setUserVisibleHint] الخاص بالجزء 3 من خلال [visibleToUser] إلى true. أصبح الجزء 3 مرئيًا الآن؛
- السطران 5-6: يستمر دورة حياة الجزء 5؛
ننتقل من علامة التبويب 3 إلى علامة التبويب 4:
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- السطر 1: أصبح الجزء 3 مخفيًا الآن؛
- السطر 2: أصبح المقطع 4 مرئيًا الآن. تجدر الإشارة إلى أنه لم يتم تنفيذ دورة حياة المقطع 4. فقد تم تنفيذها بالفعل قبل خطوتين؛
- السطر 3: يخرج الجزء 1 من جوار الجزء 4 المعروض. يتم تنفيذ طريقة [onDestroyView] الخاصة به. في المرة التالية التي يتم عرضها، سيتم إعادة تنفيذ دورة العرض [onCreateView, afterViews, onResume] الخاصة بها؛
ننتقل من علامة التبويب 4 إلى علامة التبويب 5:
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- السطر 1: أصبح الجزء 4 مخفيًا الآن؛
- السطر 2: أصبح الجزء 5 مرئيًا الآن. تجدر الإشارة إلى أنه لم يتم تنفيذ دورة حياة الجزء 5. فقد تم تنفيذها بالفعل قبل خطوتين؛
- السطر 3: يخرج الجزء 2 من جوار الجزء 5 المعروض. يتم تنفيذ طريقة [onDestroyView] الخاصة به؛
ننتقل من علامة التبويب 5 إلى علامة التبويب 1:
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- الأسطر 1 و4 و5 و6: يتم إعادة تنفيذ دورة حياة الجزء 1. في الواقع، فقد الاتصال مع طريقة العرض الخاصة به؛
- الأسطر 2 و5 و8 و9: لنفس السبب، يتم إعادة تنفيذ دورة حياة الجزء 2؛
- السطران 10-11: يخرج المقطعان 4 و5 من جوار المقطع المعروض؛
- السطر 7: يتم تحديث الجزء 1؛
![]() |
لم تظهر السجلات أبدًا أن الطريقتين [setUserVisibleHint] و [onResume] حاولتا معًا تحديث الجزء. إما إحداهما أو الأخرى. يُطلب من القارئ إجراء اختبارات أخرى ومتابعة السجلات لفهم مفهوم الجوار ودورة حياة الأجزاء بشكل جيد.
الآن، لنفترض وجود تجاور كامل ونجري نفس الاختبارات.
في [MainActivity]:
// عدد الأجزاء
private final int FRAGMENTS_COUNT = 5;
// تجاور الأجزاء
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;
السجلات عند بدء التشغيل هي كما يلي:
05-31 07:34:44.717 28908-28908/exemples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/exemples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/exemples.android D/menu: création menu en cours
- تُظهر السجلات أن دورة حياة الأجزاء الخمسة قد تم تنفيذها؛
- يتم عرض الجزء 1 في السطر 18؛
ننتقل من علامة التبويب 1 إلى علامة التبويب 2:
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- السطر 1: يتم إخفاء الجزء 1؛
- السطر 2: يتم عرض الجزء 2؛
ننتقل من علامة التبويب 2 إلى علامة التبويب 3:
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- السطر 1: الجزء 2 مخفي؛
- السطر 2: يتم عرض المقطع 3؛
ننتقل من علامة التبويب 3 إلى علامة التبويب 4:
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- السطر 1: الجزء 3 مخفي؛
- السطر 2: يتم عرض المقطع 4؛
ننتقل من علامة التبويب 4 إلى علامة التبويب 5:
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- السطر 1: الجزء 4 مخفي؛
- السطر 2: يتم عرض الجزء 5؛
ننتقل من علامة التبويب 5 إلى علامة التبويب 1:
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- السطر 1: الجزء 5 مخفي؛
- السطر 2: يتم عرض المقطع 1؛
- السطر 3: يتم تحديث المقطع 1؛
ننتقل من علامة التبويب 1 إلى علامة التبويب 4:
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: update 4 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- السطر 1: يتم إخفاء المقطع 1؛
- السطر 2: يتم عرض المقطع 4؛
- السطر 3: يتم تحديث الجزء 4؛
نلاحظ أنه مع التجاور الكلي، يصبح سلوك الأجزاء أكثر قابلية للتنبؤ به.
الآن، لنضع تجاورًا صفريًا ونرى ما سيحدث. تتطور الفئة [MainActivity] على النحو التالي:
// عدد الأجزاء
private final int FRAGMENTS_COUNT = 5;
// تجاور الأجزاء
private final int OFF_SCREEN_PAGE_LIMIT = 0;
سجلات التشغيل عند البدء هي كما يلي:
06-01 03:11:52.068 5679-5679/exemples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/exemples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/exemples.android D/menu: création menu en cours
- في السطرين 8 و10، نرى أن حاوية الأجزاء قد طلبت جزأين، رقم 1 ورقم 2. لذا، فإن كل شيء يحدث كما لو كان هناك تجاور يساوي 1. وبالتالي، تم تجاهل التجاور الذي يساوي 0.
1.9.3. التواصل بين الأجزاء
في البنية السابقة، لدينا نشاط و n شظايا. يتفاعل المستخدم مع الشظايا المختلفة. تؤدي هذه التفاعلات إلى تغيير حالة التطبيق. ونسمي هنا حالة التطبيق، مجموعة المعلومات التي يخزنها طوال فترة وجوده. عندئذٍ يطرح السؤال التالي:
- عندما يتفاعل المستخدم مع الجزء i، ينتقل التطبيق من الحالة E1 إلى الحالة E2؛
- تؤدي إحدى إجراءات المستخدم على الجزء i إلى عرض الجزء j؛
- كيف يمكن تحديث الجزء j مع الحالة الحالية E2 للتطبيق؛
من الأمثلة السابقة، نعرف كيفية تحديث الجزء j. ولكن أين نجد حالة التطبيق E2 لتحديثه؟
هناك حلول مختلفة لهذه المشكلة. وقد رأينا أحدها: يمكن للجزء i أن ينقل حالة التطبيق E2 إلى الجزء j عبر المعلمات. وقد صادفنا هذه الطريقة في الفئة [MainActivity] عند إنشاء الأجزاء:
for (int i = 0; i < fragments.length; i++) {
// يتم إنشاء جزء
fragments[i] = new PlaceholderFragment_();
// يمكن تمرير معلمات إلى الجزء
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
لا يمكن استخدام هذا الحل هنا على الفور. في الواقع، عندما ينقر المستخدم على علامة التبويب j التي ستظهر الجزء j، لا يتم استدعاء كودنا. يتم تنفيذ كود النظام فقط. سنرى في مشروع قادم كيفية اعتراض النقر على علامة التبويب، ولكن في الوقت الحالي سنتبع مسارًا آخر.
لقد تحدثنا عن حالة التطبيق: مجموعة البيانات التي يديرها التطبيق بمرور الوقت. هنا يتكون التطبيق من نشاط و n أجزاء يتم إنشاء مثيل لكل منها مرة واحدة عند بدء تشغيل التطبيق وتستمر مدة حياتها طوال مدة حياة التطبيق. لذلك يمكن أن يكون كل عنصر من هذه العناصر أو عدة عناصر معًا مرشحة لتخزين حالة التطبيق. يتمتع كل جزء بالوصول، من خلال الطريقة [Fragment.getActivity()]، إلى النشاط الذي أنشأه. وبما أن جميع الأجزاء لديها وصول إلى النشاط، يبدو من الطبيعي تخزين حالة التطبيق فيه.
ومع ذلك، فإن نتيجة الطريقة [Fragment.getActivity()] تعتمد على الوقت الذي يتم استدعاؤها فيه خلال دورة الحياة. نوضح هذه النقطة بإضافة بعض السجلات في الفئة [PlaceholderFragment]:
// تحديث الجزء
public void update() {
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// يعتمد العمل المطلوب على رقم الزيارة
if (numVisit > 1) {
// سجل
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// نص معدل
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// معلومات محلية للسجلات
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
- الأسطر 14-16: تعرض الطريقة [getInfos] جزءًا من حالة التطبيق؛
نقوم بتشغيل التطبيق مع تجاور شظايا يساوي 2. السجلات عند بدء تشغيل التطبيق:
06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/exemples.android D/menu: création menu en cours
- الأسطر 9 و10 و13 و14: نرى أنه في الطرق [setUserVisibleHint]، لدينا [getActivity()==null] إذا لم يكن الجزء مرئيًا بعد (isVisibleToUser==false)؛
- السطر 19: نرى أنه عندما يصل تدفق التنفيذ إلى الطريقة [update] للجزء 1، فإن الطريقة [getActivity] تعرض النشاط بشكل صحيح؛
عندما نضع تجاور الأجزاء على 4 (تجاور كامل)، تكون السجلات كما يلي:
06-01 03:35:23.553 2814-2814/exemples.android D/MainActivity: constructor
06-01 03:35:23.751 2814-2819/exemples.android I/art: Ignoring second debugger -- accepting and dropping
06-01 03:35:23.900 2814-2814/exemples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.210 2814-2814/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.211 2814-2814/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.214 2814-2814/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:26.602 2814-2814/exemples.android D/menu: création menu en cours
نحصل على نفس النتائج. ونستنتج من ذلك أنه بمجرد أن يصبح الجزء مرئيًا، تعرض الطريقة [getActivity] نشاط الجزء. ونلاحظ أيضًا أنه عندما يصل التنفيذ إلى الطريقة [update] للجزء الذي سيتم عرضه، تعرض الطريقة [getActivity] قيمة بشكل صحيح.
لتوضيح الاتصال بين الأجزاء، نقوم بإنشاء مشروع جديد.
1.10. مثال-09: التواصل بين الأجزاء، التمرير السريع والتمرير
1.10.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-07] إلى [Exemple-08]. للقيام بذلك، سنتبع الإجراء الموصوف لنسخ [Exemple-02] إلى [Exemple-03] في الفقرة 1.4.
![]() | ![]() |
1.10.2. الجلسة
في هذا المشروع الجديد، نريد أن تعرض الأجزاء العدد الإجمالي للأجزاء التي يعرضها المستخدم. يجب هنا الحفاظ على عداد يمكن لجميع الأجزاء الوصول إليه. سنسمي الكائن الذي يغلف البيانات المشتركة بين الأجزاء بـ "الجلسة". تأتي هذه المصطلحات من مجال تطوير الويب، حيث يتم وضع البيانات المراد مشاركتها بين العروض المختلفة التي يطلبها المستخدم نفسه في "جلسة". إن تغليف المعلومات المشتركة بين الأجزاء المختلفة في كائن واحد يجعل الأمور أكثر قابلية للقراءة.
ستكون الفئة [Session] كما يلي:
![]() |
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// عدد الأجزاء التي تمت زيارتها
private int numVisit;
// أدوات الاسترجاع والتعيين
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
}
- السطر 8: ستقوم الجلسة بتغليف عدد الأجزاء التي تمت زيارتها؛
- السطر 5: التعليق التوضيحي [EBean] هو تعليق توضيحي AA. يشير السمة [scope] إلى نطاق (أو مدة حياة) الفئة المُعلَّمة بهذه الطريقة. هنا، يجعل السمة [scope = EBean.Scope.Singleton] الفئة [Session] فئة فردية: سيتم إنشاء مثيل لها مرة واحدة فقط عند بدء تشغيل التطبيق. يمكن بعد ذلك إدخال مرجع فئة موصوفة بـ [EBean] في فئة أخرى. هذا هو مفهوم إدخال التبعيات؛
1.10.3. النشاط [MainActivity]
تتطور النشاط [MainActivity] على النحو التالي:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
...
// حقن الجلسة
@Bean(Session.class)
protected Session session;
// عدد الأجزاء
private final int FRAGMENTS_COUNT = 5;
// تجاور الأجزاء
private final int OFF_SCREEN_PAGE_LIMIT = 2;
@AfterInject
protected void afterInject(){
Log.d("MainActivity", "afterInject");
// تهيئة الجلسة
session.setNumVisit(0);
}
...
- السطران 7-8: حقن المرجع إلى عنصر singleton للجلسة بفضل التعليق التوضيحي [@Bean]. معلمة التعليق التوضيحي هي فئة bean المراد حقنها. لا يمكن أن يكون للحقل المُعلَّق عليه هذا النطاق [private]؛
- السطر 15: تُستخدم التعليقة التوضيحية [@AfterInject] لتعيين طريقة يتم استدعاؤها عند الانتهاء من جميع عمليات الحقن للفئة. وبالتالي، عند الدخول إلى الطريقة [afterInject] في السطر 16، تكون الإشارة في السطر 8 قد تم تهيئتها؛
- السطر 20: يتم إعادة تعيين عداد الزيارات إلى الصفر؛
1.10.4. الجزء [PlaceholderFragment]
يتطور الجزء [PlaceholderFragment] على النحو التالي:
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
....
// الجلسة
protected Session session;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// الأصل
super.setUserVisibleHint(isVisibleToUser);
// الذاكرة
this.isVisibleToUser = isVisibleToUser;
// السجل
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// عدد الزيارات
if (isVisibleToUser) {
// تحديث الجزء
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// سيتم إخفاء الجزء
updateDone = false;
}
}
// تحديث الجزء
public void update() {
// سجل
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// جلسة
if (session == null) {
session = ((MainActivity) getActivity()).getSession();
}
// زيادة رقم الزيارة
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// نص معدل
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- السطر 7: الجلسة؛
- الأسطر 35-37: نعلم أنه عند الوصول إلى الأسلوب [update]، فإن الأسلوب [getActivity] يعرض النشاط بشكل صحيح. نستغل هذه الفرصة لاسترداد الجلسة وتخزينها محليًا (السطر 36)؛
- الأسطر 39-41: لزيادة رقم الزيارة، نبحث عنه في الجلسة. كان بإمكاننا وضع هذا الرمز في الأسلوب [setUserVisibleHint] بدءًا من السطر 19 لأننا نعلم أن الأسلوب [getActivity] يعرض النشاط في هذه الحالة. قررنا هنا عدم إعطاء دور خاص لهذه الطريقة ونقل الكود الخاص بجزء ما في الطريقة [update] المخصصة لهذا الغرض؛
- السطر 43: يعرض رقم الزيارة؛
عند تشغيل هذا التطبيق مع 5 أجزاء، وتجاور جزأين، تكون السجلات الأولى كما يلي:
05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/exemples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/exemples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
...
- السطران 2-3: نرى أن الطريقة [afterInject] للنشاط يتم تنفيذها قبل الطريقة [afterViews]؛
يُطلب من القارئ اختبار هذا التطبيق الجديد.
1.10.5. تعطيل ميزة التمرير
في التطبيق السابق، عند تمرير الماوس على محاكي Android إلى اليسار أو اليمين، فإن العرض الحالي يفسح المجال للعرض الأيمن أو الأيسر حسب الحالة. هذا السلوك الافتراضي ليس مرغوبًا دائمًا. سنتعلم كيفية تعطيل التمرير (swipe) بين العروض.
لنعد إلى العرض الرئيسي XML [activity_main]:
![]() |
في كود XML للطريقة، نجد كود حاوية الأجزاء:
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
تشير السطر 1 إلى الفئة التي تدير صفحات النشاط. نجد هذه الفئة في النشاط [MainActivity]:
import android.support.v4.view.ViewPager;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// مدير الأجزاء
private SectionsPagerAdapter mSectionsPagerAdapter;
// حاوية الأجزاء
@ViewById(R.id.container)
protected ViewPager mViewPager;
...
السطر 12، حاوية الأجزاء من النوع [android.support.v4.view.ViewPager] (السطر 1). لتعطيل المسح، يتعين اشتقاق هذه الفئة بالطريقة التالية:
![]() |
package exemples.android;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// التحكم في التمرير
private boolean isSwipeEnabled;
// المنشئون
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// طرق يجب إعادة تعريفها لإدارة التمرير
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// هل التمرير مسموح به؟
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// هل التمرير مسموح به؟
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// المُعيّن
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- السطر 8: الفئة [MyPager] توسع الفئة Android [ViewPager] (السطر 4)؛
- عند تمرير اليد، يمكن استدعاء معالجات الأحداث في السطرين 24 و 34. كلاهما يعيد قيمة منطقية. يكفي أن يعيدا القيمة المنطقية [false] لمنع التمرير؛
- السطر 11: القيمة المنطقية التي تُستخدم للإشارة إلى قبول أو رفض تمرير اليد.
بعد ذلك، يجب الآن استخدام معالج الصفحات الجديد. يتم ذلك في العرض XML [activity_main.xml] وفي النشاط الرئيسي [MainActivity]. في [activity_main.xml] نكتب:
![]() |
<exemples.android.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
في السطر 1، يتم استخدام الفئة الجديدة. في [MainActivity]، يتغير الرمز على النحو التالي:
package exemples.android;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// مدير الأجزاء
private SectionsPagerAdapter mSectionsPagerAdapter;
// حاوية الأجزاء
@ViewById(R.id.container)
protected MyPager mViewPager;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
// حاوية الأجزاء مرتبطة بمدير الأجزاء
// أي أن الجزء رقم i من حاوية الأجزاء هو الجزء رقم i الذي يقدمه مدير الأجزاء
mViewPager.setAdapter(mSectionsPagerAdapter);
// يتم منع التمرير بين الأجزاء
mViewPager.setSwipeEnabled(false);
// شريط علامات التبويب مرتبط أيضًا بحاوية الأجزاء
...
- السطر 12: أصبح نوع مدير الصفحات الآن [MyPager]؛
- السطر 23: يتم تعطيل أو عدم تعطيل تمرير اليد.
اختبر هذه النسخة الجديدة. قم بتعطيل التمرير أو لا، ولاحظ الفرق في سلوك العروض عند سحبها إلى اليمين أو اليسار بالماوس. في جميع التطبيقات القادمة، سيتم تعطيل التمرير. لن نذكر ذلك مرة أخرى.
1.10.6. تعطيل التمرير بين الأجزاء
لنواصل مع تحسين مدير علامات التبويب. عند الانتقال من علامة التبويب 1 إلى علامة التبويب 4، نرى تمرير علامتي التبويب الوسيطتين 2 و 3. يُسمى هذا في مصطلحات Android بـ smoothScrolling. قد يصبح هذا السلوك مزعجًا إذا كان هناك العديد من علامات التبويب. يمكن تعطيله بإضافة الكود التالي في مدير الأجزاء [MyPager]:
// يتحكم في التمرير
private boolean isSwipeEnabled;
// التحكم في التمرير
private boolean isScrollingEnabled;
...
// التمرير
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// أدوات الضبط
...
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
نظرًا لأن مدير علامات التبويب قد تم ربطه بمدير الأجزاء [MyPager]، فعند النقر على علامة التبويب رقم i، يتم عرض الجزء رقم i بواسطة حاويات الأجزاء باستخدام الطريقة [setCurrentItem] المذكورة أعلاه (السطر 9). [position] هو رقم الجزء المراد عرضه؛
- السطر 10: يتم استدعاء الطريقة [setCurrentItem] من الفئة الأصلية. تطلب الحجة الثانية في [false] أن يكون هناك انتقال فوري بين الجزء القديم والجزء الجديد (بدون تمرير)، وفي [true] أن يكون هناك انتقال عبر scrolling. هنا، الحجة الثانية هي قيمة الحقل في السطر 4، وهو حقل يمكن للمطور تعيينه باستخدام الطريقة الواردة في الأسطر 16-18؛
إذا أردنا تعطيل التمرير، فستكون الفئة [MainActivity] كما يلي:
...
// إزاحة الأجزاء
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// يتم منع التمرير بين الأجزاء
mViewPager.setSwipeEnabled(false);
// لا تمرير
mViewPager.setScrollingEnabled(false);
...
قم بتشغيل المشروع مرة أخرى وتأكد من عدم وجود scrolling بين علامتي التبويب 1 و 4 على سبيل المثال. في ما يلي، سنقوم دائمًا بتعطيل التمرير. ولن نذكر ذلك مرة أخرى.
1.10.7. جزء جديد
في مثالنا، جميع الأجزاء من نفس النوع [PlaceHolderFragment]. سنقوم الآن بتعلم كيفية إنشاء جزء جديد وعرضه.
أولاً، لننسخ العرض [vue1.xml] من المشروع [Exemple-04] إلى المشروع [Exemple-09] [1]:
![]() | ![]() |
- في [1]، العرض [vue1.xml]؛
- في [3]، تحتوي العرض على أخطاء ناتجة عن نصوص مفقودة في الملف [res/values/strings.xml]؛
في [2]، نضيف النصوص المفقودة بأخذها من الملف [res/values/strings.xml] من المشروع [Exemple-04]
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- الطريقة 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
- أعلاه، تمت إضافة الأسطر 6-9؛
الآن، نقوم بإنشاء الفئة [Vue1Fragment] التي ستكون الجزء المسؤول عن عرض العرض [vue1.xml]:
![]() |
ستكون الفئة [Vue1Fragment] كما يلي:
package exemples.android;
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
// عناصر الواجهة المرئية
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// مدير الأحداث
@Click(R.id.buttonValider)
protected void doValider() {
// يتم عرض الاسم الذي تم إدخاله
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- السطر 10: التعليق التوضيحي [@EFragment] يجعل الجزء المستخدم من قبل النشاط هو في الواقع الفئة [Vue1Fragment_]. يجب تذكر ذلك. الجزء مرتبط بالعرض [vue1.xml]؛
- السطران 14-15: يتم إدخال المكون المحدد بـ [R.id.editTextNom] في الحقل [editTextNom] في السطر 15؛
- السطور 18-20: تدير الطريقة [doValider] حدث "النقر" على الزر المحدد بـ [R.id.buttonValider]؛
- السطر 21: المعلمة الأولى لـ [Toast.makeText] هي من النوع [Activity]. تسمح الطريقة [Fragment.getActivity()] بالحصول على النشاط الذي يوجد فيه الجزء. يتعلق الأمر بـ [MainActivity] لأننا في هذه البنية لا نملك سوى نشاط واحد يعرض طرق عرض أو أجزاء مختلفة؛
في الفئة [MainActivity]، يتطور مدير الأجزاء على النحو التالي:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء
private Fragment[] fragments;
// رقم الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
// المنشئ
public SectionsPagerAdapter(FragmentManager fm) {
// الأصل
super(fm);
// تهيئة جدول الأجزاء
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
// يتم إنشاء جزء
fragments[i] = new PlaceholderFragment_();
// يمكن تمرير معلمات إلى الجزء
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// جزء من +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
...
}
- السطر 13: يوجد [FRAGMENTS_COUNT] أجزاء: [FRAGMENTS_COUNT-1] أجزاء من النوع [PlaceholderFragment] (السطور 14-21) وجزء من النوع [Vue1Fragment_]، السطر 23 (انتبه إلى علامة التسطير)؛
قم بتجميع ثم تشغيل المشروع [Exemple-09]. يجب أن تكون علامة التبويب رقم 5 مختلفة:
![]() |
1.10.8. اجعل جميع الأجزاء مشتقة من نفس الفئة المجردة
يحتاج الجزء الجديد [Vue1Fragment] أيضًا إلى التحديث عند عرضه. للقيام بذلك، سيتعين علينا إنشاء كود مشابه للكود الذي تم إنشاؤه للجزء [PlaceholderFragment]. لتجنب التكرار، سنقوم بتجميع ما يمكن تجميعه في فئة مجردة سترثها جميع أجزاء التطبيق.
لهذا الغرض، سننشئ مشروعًا جديدًا.
1.11. مثال-10: جعل جميع الأجزاء مشتقة من فئة مجردة
1.11.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-09] إلى [Exemple-10]:
![]() | ![]() |
1.11.2. إدارة وضع التصحيح
نضيف إلى المشروع إمكانية عرض سجلات وضع التصحيح أو عدم عرضها. للقيام بذلك، نضيف ثابتًا ثابتًا إلى الفئة [MainActivity]:
// وضع التصحيح
public static final boolean IS_DEBUG_ENABLED = false;
1.11.3. الفئة المجردة الأم لجميع الأجزاء
![]() |
الفئة [AbstractFragment] هي كما يلي:
package exemples.android;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
public abstract class AbstractFragment extends Fragment {
// بيانات خاصة
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private String className;
// بيانات متاحة للفئات الفرعية
protected boolean afterViewsDone = false;
protected boolean isDebugEnabled = true;
// النشاط
protected MainActivity activity;
// جلسة
protected Session session;
// المنشئ
public AbstractFragment() {
// التشغيل
isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
className = getClass().getSimpleName();
// سجل
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// الأصل
super.setUserVisibleHint(isVisibleToUser);
...
}
@Override
public void onDestroyView() {
// الأصل
super.onDestroyView();
...
}
@Override
public void onResume() {
// الأصل
super.onResume();
...
}
// معلومات محلية
protected String getParentInfos() {
return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
}
// تحديث جزء
protected void update() {
...
// يُطلب من الفئة الفرعية التحديث
updateFragment();
}
protected abstract void updateFragment();
}
- السطر 7: الفئة [AbstractFragment] تمتد من فئة Android [Fragment]؛
- يجب أن يكون بإمكان أي جزء التحديث. ولهذا السبب تفرض الفئة الأم [AbstractFragment] على فئاتها الفرعية وجود طريقة [updateFragment] (السطر 68) التي تستدعيها (السطر 65)؛
- السطر 19: ستخزن الفئة مرجعًا إلى نشاط التطبيق؛
- السطر 22: ستخزن الفئة مرجعًا إلى الجلسة التي تجمع البيانات المشتركة بين الأجزاء والنشاط؛
- الأسطر 25-33: منشئ الفئة المجردة؛
- السطر 27: إنشاء نسخة من الثابت [MainActivity.IS_DEBUG_ENABLED] في حقل السطر 16؛
- السطر 28: يتم تخزين اسم الفئة التي تم إنشاء مثيل لها، أي اسم فئة فرعية؛
- الأسطر 15-22: تحتوي هذه الحقول على السمة [protected] حتى تتمكن الفئات الفرعية من الوصول إليها. يُلاحظ أن الفئات الفرعية تتجاهل وجود القيم المنطقية [isVisibleToUser] و [updateDone] (السطور 10-11)؛
- السطر 57: تحتوي الطريقة [getParentInfos] على السمة [protected] حتى تتمكن الفئات الفرعية من استدعائها؛
تظل الطرق [setUserVisibleHint, onDestroyView, onResume] مماثلة لما كانت عليه في الفئة [PlaceholderFragment] في المشروع السابق:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// الأصل
super.setUserVisibleHint(isVisibleToUser);
// الذاكرة
this.isVisibleToUser = isVisibleToUser;
// سجل
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
}
// الحالة التي سيصبح فيها الجزء مرئيًا
if (isVisibleToUser) {
// تحديث الجزء
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// مغادرة الجزء
updateDone = false;
}
}
@Override
public void onDestroyView() {
// الأصل
super.onDestroyView();
// تحديث المؤشر
afterViewsDone = false;
// سجل
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
}
}
@Override
public void onResume() {
// الأصل
super.onResume();
// سجل
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
}
if (isVisibleToUser) {
// تحديث
if (!updateDone) {
update();
updateDone = true;
}
}
}
الطريقة [update] هي كما يلي:
// تحديث جزء
protected void update() {
// استرداد النشاط والجلسة
if (activity == null) {
Activity activity = getActivity();
if (activity != null) {
this.activity = (MainActivity) activity;
this.session = this.activity.getSession();
}
}
// يُطلب من الفئة الفرعية التحديث
updateFragment();
}
وفقًا للرمز أعلاه، عندما يتم تنفيذ الطريقة [update] لجزء ما، يصبح هذا الجزء مرئيًا. وهذا أمر مهم لأنه يعني أن الطريقة [Fragment.getActivity] تعطي عندئذٍ مرجعًا إلى نشاط التطبيق (انظر الفقرة 1.10.8)، مما يتيح بعد ذلك الوصول إلى الجلسة.
- الأسطر 4-10: يتم تهيئة النشاط والجلسة إذا لم يتم ذلك بالفعل؛
- السطر 12: يتم استدعاء الطريقة [updateFragment] للفئة الفرعية. وعندما يتم تنفيذها، تكون الحقول [activity] و [session] التي يمكنها الوصول إليها قد تم تهيئتها؛
1.11.4. الفئة [PlaceholderFragment]
![]() |
تتطور الفئة [PlaceholderFragment] على النحو التالي:
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
// الجزء هو عرض يتم عرضه بواسطة حاوية الأجزاء
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
// مكون من مكونات الواجهة المرئية
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// البيانات
private boolean initDone;
// البيانات
private String text;
private int numVisit;
// رقم الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
// المنشئ
public PlaceholderFragment() {
super();
// سجل
if (isDebugEnabled) {
Log.d("PlaceholderFragment", "constructor");
}
}
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
...
}
// تحديث الجزء
public void updateFragment() {
...
}
}
- السطر 10: الفئة [PlaceholderFragment] توسع الفئة [AbstractFragment]. مع هذه البنية، يتكون كتابة جزء من:
- كتابة الطريقة [@AfterViews] التي تُستخدم لتهيئة المقطع خلال دورة حياته الأولى أو لإعادة تهيئته إذا كان هناك [onDestroyView] من قبل. السطر 39 إلزامي لإدارة دورة حياة المقطع بشكل صحيح؛
- كتابة الطريقة [updateFragment] التي ستقوم بتحديث الجزء قبل عرضه مباشرة. يمكن لهذه الطريقة استخدام جلسة عمل فئتها الأم؛
- كتابة معالجات أحداث الجزء. وهذا ما سنقوم به في المشاريع المستقبلية؛
تظل الطريقتان [@AfterViews] و [updateFragment] مماثلتين لما كانتا عليه في المشروع السابق:
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// سجل
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
if (!initDone) {
// النص الأولي
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// تم التهيئة
initDone = true;
}
// عرض النص الحالي
textViewInfo.setText(text);
}
// تحديث جزء
public void updateFragment() {
// سجل
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
// زيادة رقم الزيارة
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// نص معدل
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
// معلومات محلية للسجلات
protected String getLocalInfos() {
return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
numVisit, initDone, getActivity() == null);
}
- السطر 7 و 23: في السجلات، نعرض معلومات الفئة الأم باستخدام الطريقة الموروثة [getParentInfos]؛
1.11.5. الفئة [Vue1Fragment]
![]() |
تتميز الفئة [Vue1Fragment] بنفس بنية الفئة [PlaceholderFragment]:
package exemples.android;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر الواجهة المرئية
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// البيانات
private int numVisit;
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// السجل
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// مدير الأحداث
@Click(R.id.buttonValider)
protected void doValider() {
// يتم عرض الاسم الذي تم إدخاله
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// معلومات محلية للسجلات
protected String getLocalInfos() {
return String.format("numVisit=%s", numVisit);
}
// تحديث الجزء
@Override
protected void updateFragment() {
// زيادة رقم الزيارة
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// يتم عرض رقم الزيارة
Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
- السطر 9: الفئة [Vue1Fragment] توسع الفئة [AbstractFragment]؛
- الأسطر 18-26: لا توجد مهام مهمة للطريقة [@AfterViews]. ومع ذلك، يجب كتابتها لتعيين القيمة المنطقية [afterViewsDone] إلى true، لأن هذه المعلومة تستخدمها الفئة الأم؛
- الأسطر 42-49: تتمثل طريقة [updateFragment] في عرض رسالة قصيرة توضح رقم الزيارة (السطر 48) وزيادة هذا الرقم في الجلسة (الأسطر 44-46)؛
ندعو القارئ إلى تجربة هذا المشروع الجديد.
سنستخدم هذه البنية في جميع المشاريع المستقبلية:
- نشاط واحد و n أجزاء؛
- جميع الأجزاء تمتد من الفئة [AbstractFragment]؛
- يتم وضع البيانات المراد مشاركتها بين الأجزاء وبين الأجزاء والنشاط في الفئة [Session]؛
1.11.6. الربط بين علامات التبويب والأجزاء
في الفئة [MainActivity] التي تدير علامات التبويب، مكتوب:
// شريط علامات التبويب مرتبط أيضًا بحاوية الأجزاء
// أي أن علامة التبويب رقم i تعرض الجزء رقم i من الحاوية
tabLayout.setupWithViewPager(mViewPager);
السطر 3 يربط مدير علامات التبويب بحاوية الأجزاء. لقد رأينا نتيجة لهذا الربط: عندما ينقر المستخدم على علامة التبويب رقم i، تعرض حاوية الأجزاء الجزء رقم i. لم نر العكس: عندما نطلب من حاوية الأجزاء عرض الجزء رقم i، يتم تحديد علامة التبويب رقم i تلقائيًا.
لتوضيح هذا السلوك، سنضيف الخيارات [Fragment 1, Fragment 2, ...] إلى القائمة الحالية. عندما ينقر المستخدم على الخيار [Fragment i]، سيُطلب من حاوية الأجزاء عرض الجزء رقم i. سنرى عندئذٍ ما إذا تم تحديد علامة التبويب رقم i أم لا.
تبدأ هذه الخطوة بتغيير قائمة التطبيق:
![]() | ![]() |
يتغير محتوى الملف [res / menu / menu_main.xml] على النحو التالي:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment5"
android:title="@string/fragment5"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- الأسطر 9-28: الخيارات الخمسة الجديدة في القائمة؛
- يتم تعريف تسميات الخيارات (الأسطر 10 و14 و18 و22 و26) في الملف [res / values / strings.xml] [2]:
<resources>
<string name="app_name">Exemple-10</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- عرض 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
<!-- القائمة -->
<string name="fragment1">Fragment 1</string>
<string name="fragment2">Fragment 2</string>
<string name="fragment3">Fragment 3</string>
<string name="fragment4">Fragment 4</string>
<string name="fragment5">Fragment 5</string>
</resources>
والنتيجة المرئية هي كما يلي:
![]() |
تتم إدارة النقر على خيارات القائمة هذه في الفئة [MainActivity]:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// معالجة خيارات القائمة
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
case R.id.fragment5: {
showFragment(4);
break;
}
}
// العنصر المعالج
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// يتم تغيير الجزء المعروض
mViewPager.setCurrentItem(i);
}
}
- السطر 2: يتم استدعاء الطريقة [onOptionsItemSelected] عند النقر على أحد خيارات القائمة؛
- السطر 8: يتم استرداد معرف الخيار الذي تم النقر عليه؛
- الأسطر 9-36: تتم معالجة الحالات المختلفة بواسطة switch؛
- الأسطر 16-36: يؤدي النقر على الخيار [Fragment i] إلى إعادة التوجيه إلى الطريقة [showFragment(i-1)] في الأسطر 41-45؛
- السطر 43: يُطلب من حاوية الأجزاء عرض الجزء المطلوب؛
- السطر 42: يتم التحقق مسبقًا من إمكانية القيام بذلك (الشرط 1) ومن ضرورة القيام بذلك (الشرط 2)؛
ندعو القارئ إلى تجربة هذه النسخة الجديدة. نلاحظ أنه عند طلب عرض الجزء رقم i، يتم عرضه بالفعل ويتم تحديد علامة التبويب رقم i نفسها.
الآن بعد أن رأينا كيف تعمل الارتباط بين علامات التبويب والأجزاء، سننتقل إلى حالة أخرى: وهي الحالة التي تكون فيها إدارة علامات التبويب منفصلة عن إدارة الأجزاء. وهذا هو الحال على سبيل المثال عندما يكون عدد علامات التبويب أقل من عدد الأجزاء. لتوضيح حالة الاستخدام الجديدة هذه، سنقوم بإنشاء مشروع جديد.
1.12. مثال-11: علامات التبويب المنفصلة عن الأجزاء
1.12.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-10] إلى [Exemple-11]:
![]() | ![]() |
1.12.2. الأهداف
سيحتوي التطبيق الجديد على علامتي تبويب:
- ستعرض علامة التبويب الأولى دائمًا المقطع [Vue1]؛
- ستعرض علامة التبويب الثانية جزءًا مختارًا من القائمة؛

- في [1]، المقطع [Vue1]؛
- إلى [2]، الجزء من النوع [PlaceholderFragment] الذي اختاره المستخدم؛
- في [3]، يستمر حساب الزيارات؛
1.12.3. الجلسة
![]() |
ستكون الجلسة الجديدة كما يلي:
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// عدد الأجزاء التي تمت زيارتها
private int numVisit;
// رقم الجزء من النوع [PlaceholderFragment] المعروض في علامة التبويب الثانية
private int numFragment;
// مُستردات ومُعيّنات
...
}
- السطر 10: سنقوم بأنفسنا بإدارة النقر على علامات التبويب. عند النقر على علامة تبويب، يجب استعادة الجزء الذي كانت تعرضه آخر مرة تم تحديدها. سيحفظ الحقل [numFragment] رقم هذا الجزء لعلامة التبويب رقم 2، وهو رقم موجود في [0, Fragments_COUNT-2]. عند النقر على علامة التبويب رقم 2، سنبحث في الجلسة عن رقم الجزء المراد عرضه؛
1.12.4. القائمة
![]() |
تتطور القائمة [res / menu / menu_main.xml] على النحو التالي:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
ستعرض علامة التبويب رقم 2 أحد الأجزاء الأربعة من الأسطر 9-24. الجزء الخامس هو الجزء [Vue1Fragment] الذي سيظل معروضًا دائمًا في علامة التبويب رقم 1.
1.12.5. الفئة [MainActivity]
يجب أن تتولى الفئة [MainActivity] الآن إدارة علامات التبويب والتنقل بينها، وهو ما لم تكن تقوم به حتى الآن. يتطور كودها على النحو التالي:
// مدير علامات التبويب
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
...
@AfterViews
protected void afterViews() {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
...
// لا يوجد تمرير
mViewPager.setScrollingEnabled(false);
// عرض Vue1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
// في البداية لدينا علامة تبويب واحدة فقط
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// مدير الأحداث
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// تم تحديد علامة تبويب - يتم تغيير الجزء المعروض بواسطة حاوية الأجزاء
...
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
...
}
- السطر 17: سيكون الجزء الأول الذي يعرضه حاوية الأجزاء هو الجزء [Vue1Fragment]. وبحكم التصميم، سيكون هذا الجزء هو الجزء الأخير في الحاوية؛
- الأسطر 20-22: نظرًا لعدم وجود ارتباط بين علامات التبويب وحاوية الأجزاء، يتعين علينا إدارة علامات التبويب بأنفسنا. في البداية، لا تحتوي شريط علامات التبويب [tabLayout] في السطر 3 على أي علامة تبويب؛
- السطر 20: ننشئ علامة التبويب الأولى؛
- السطر 21: نمنحها عنوانًا. في الأمثلة السابقة، كان عنوان علامات التبويب هو عنوان الأجزاء. انتهى هذا الأمر الآن. وبالتالي، نزيل الطريقة [getPageTitle] من مدير الأجزاء. لم نعد بحاجة إليها:
// اختياري - إعطاء عنوان للأجزاء المدارة
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
- السطر 22: تتم إضافة علامة التبويب التي تم إنشاؤها إلى شريط علامات التبويب. أصبح شريط علامات التبويب لدينا يحتوي الآن على علامة تبويب. ما الذي تعرضه علامة التبويب هذه؟ يجب أن نفهم أن علامات التبويب والأجزاء هما مفهومان مستقلان. الجزء المعروض هو دائمًا الجزء الذي يختاره حاوية الأجزاء. إذا قمنا بتغيير علامة التبويب ولم نطلب من الحاوية تغيير الجزء المعروض، فلن يحدث شيء: سيظل الجزء نفسه معروضًا ولكن علامة التبويب المحددة هي التي تغيرت. لذا هنا، الجزء المعروض هو الذي تم اختياره السطر 17: الجزء [Vue1Fragment]؛
- السطور 26-30: الطريقة التي يجب كتابتها لإدارة تغيير علامة التبويب من قبل المستخدم؛
يتم تشغيل الطريقة [onTabSelected] في الأسطر 26-30 بمجرد حدوث تغيير في علامة التبويب (إذا نقر المستخدم على علامة تبويب محددة بالفعل، فلن يحدث شيء). ورمزها هو كما يلي:
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (IS_DEBUG_ENABLED) {
Log.d("onglets", "onTabSelected");
}
// تم تحديد علامة تبويب - يتم تغيير الجزء المعروض بواسطة حاوية الأجزاء
// موضع علامة التبويب
int position = tab.getPosition();
// رقم الجزء المراد عرضه
int numFragment;
switch (position) {
case 0:
// رقم الجزء [Vue1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// رقم الجزء [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// عرض الجزء
mViewPager.setCurrentItem(numFragment);
}
- السطر 8: يتم استرداد موضع علامة التبويب التي تم النقر عليها. سنسترد هنا الرقم 0 أو 1؛
- الأسطر 12-15: إذا كانت علامة التبويب الأولى هي التي تم النقر عليها، فإننا نستعد لعرض المقطع [Vue1Fragment]؛
- الأسطر 16-18: في الحالات الأخرى (النقر على علامة التبويب رقم 2)، نستعد لإعادة عرض الجزء الذي كان معروضًا في المرة الأخيرة التي تم فيها تحديد علامة التبويب رقم 2. وقد تم عندئذٍ إدراج رقم هذا الجزء في جلسة التطبيق؛
- السطر 21: يُطلب من حاوية الأجزاء عرض الجزء المطلوب؛
لنلقِ نظرة الآن على إدارة خيارات القائمة (دائمًا في [MainActivity]):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// معالجة خيارات القائمة
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// العنصر المعالج
return true;
}
- الأسطر 16-31: إدارة الخيارات الأربعة في القائمة. يستدعي كل مدير الطريقة [showFragment] مع رقم الجزء المراد عرضه؛
الطريقة [showFragment] هي كما يلي:
// علامة التبويب رقم 2
private TabLayout.Tab tab2 = null;
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// إذا لم تكن علامة التبويب الثانية موجودة بعد، يتم إنشاؤها
if (tab2 == null) {
tab2 = tabLayout.newTab();
tabLayout.addTab(tab2);
}
// يتم تحديد عنوان علامة التبويب الثانية
tab2.setText(String.format("Fragment n° %s", (i + 1)));
// يتم تغيير الجزء المعروض
mViewPager.setCurrentItem(i);
// يتم تسجيل رقم الجزء المعروض
session.setNumFragment(i);
// يتم تحديد علامة التبويب 2 - لا يتم القيام بأي شيء إذا كانت محددة بالفعل
tab2.select();
}
}
- نتذكر أنه عند بدء تشغيل التطبيق، لا يوجد سوى علامة تبويب واحدة؛
- السطر 2: مرجع إلى علامة التبويب رقم 2، null في البداية؛
- السطر 5: لم تتغير شروط العرض مقارنة بالإصدار السابق؛
- الأسطر 7-10: إذا لم تكن علامة التبويب رقم 2 موجودة بعد، يتم إنشاؤها (السطر 8) وإضافتها إلى شريط علامات التبويب (السطر 9)؛
- السطر 12: يتم وضع رقم المقطع الذي سيتم عرضه في عنوان علامة التبويب الثانية مع ترقيم يبدأ من 1؛
- السطر 14: يتم عرض الجزء المطلوب؛
- السطر 16: يتم وضع رقمه في الجلسة؛
- السطر 18: يتم تحديد علامة التبويب رقم 2. إذا كانت محددة بالفعل، فلن يحدث شيء: لن يتم تنفيذ الطريقة [onTabSelected]. إذا لم تكن محددة بالفعل، فسيتم تشغيل الطريقة [onTabSelected]. ثم تطلب هذه الطريقة من حاوية الأجزاء عرض الجزء المعروض بالفعل في السطر 14. اختبار بسيط في الطريقة [onTabSelected] يتجنب هذه الحالة:
// عرض الجزء فقط إذا لزم الأمر
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
ندعو القارئ إلى تجربة هذه النسخة الجديدة.
1.12.6. التحسينات
لدينا الآن فهم جيد للأجزاء ودورة حياتها ومفهوم تجاور الأجزاء وعلاقتها بشريط علامات التبويب. كما أن لدينا بنية قوية اجتازت للتو اختبار المثال 11:
- نشاط واحد و n شظايا؛
- جميع الأجزاء تمتد من الفئة [AbstractFragment]؛
- يتم وضع البيانات المراد مشاركتها بين الأجزاء وبين الأجزاء والنشاط في الفئة [Session]؛
سنقوم في مشروع جديد بتحديد العلاقات بين النشاط والأجزاء عن طريق إضافة واجهة.
1.13. المثال 12: ترميز العلاقات بين النشاط والأجزاء
في هذا المثال، نريد تحديد العلاقات الدنيا بين النشاط والأجزاء. ولذلك، سنستخدم:
- واجهة [IMainActivity] التي ستحدد ما يمكن للأجزاء أن تطلبه من النشاط؛
- فئة مجردة [AbstractFragment] ستحدد الحالة والطرق التي يجب أن تتوفر في كل جزء؛
1.13.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-11] إلى [Exemple-12] باتباع الإجراء الوارد في الفقرة 1.4. ونحصل على النتيجة التالية:
![]() | ![]() |
1.13.2. واجهة [IMainActivity]
من الأمثلة السابقة، يبدو أن الأجزاء تحتاج إلى الوصول إلى الجلسة التي تم إنشاؤها بواسطة النشاط. علاوة على ذلك، وهو أمر غير مرئي في هذه الأمثلة، ولكنه متوقع: تنتهي معالجات أحداث الأجزاء أحيانًا بتغيير في العرض. سيُطلب من النشاط إجراء هذا التغيير. قد تكون واجهة [IMainActivity] كما يلي:
![]() |
package exemples.android;
public interface IMainActivity {
// الوصول إلى الجلسة
Session getSession();
// تغيير العرض
void navigateToView(int position);
// وضع التصحيح
boolean IS_DEBUG_ENABLED = true;
}
في السطر 12، لاحظ وجود ثابت كان موجودًا سابقًا في الفئة [MainActivity]. نريد تقليل الترابط بين الأجزاء والنشاط وتقليصه إلى ترابط بين [AbstractFragment] و [IMainActivity]. وبذلك يمكن تسمية النشاط باسم آخر غير [MainActivity]. ونظرًا لاستخدام الثابت [IS_DEBUG_ENABLED] في الأجزاء، يتم نقله إلى الواجهة [IMainActivity].
1.13.3. الفئة المجردة [AbstractFragment]
لا تتغير الفئة المجردة [AbstractFragment] إلا قليلاً:
// البيانات المتاحة للفئات الفرعية
protected boolean afterViewsDone = false;
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// النشاط
protected IMainActivity mainActivity;
protected Activity activity;
...
// تحديث جزء
protected void update() {
// استرداد النشاط والجلسة
if (mainActivity == null) {
this.activity = getActivity();
if (this.activity != null) {
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
}
}
// يُطلب من الفئة الفرعية التحديث
updateFragment();
}
- السطران 6 و7: يتم الاحتفاظ بنوعين من المراجع في النشاط:
- السطر 6: مرجع على النشاط الذي ينفذ واجهة [IMainActivity]؛
- السطر 7: مرجع إلى النشاط الذي يرث من فئة Android [Activity]. وهذا هو الحال بالنسبة لجميع الأنشطة؛
تشير هاتان المرجعان بالطبع إلى نفس الكائن. ولكن يتم عرض هذا الكائن بنوعين مختلفين. وهذا سيوفر علينا عمليات التحويل بين الأنواع أثناء التنفيذ؛
- السطر 14: يتم استرداد مرجع إلى النشاط باستخدام الطريقة [getActivity]؛
- السطر 15: إذا كانت هذه المرجعية غير صفرية، فيمكننا الوصول إلى الجلسة؛
- السطران 16-17: يتم تخزين النشاط باعتباره منفذًا للواجهة [IMainActivity] والجلسة؛
1.13.4. تعديل مدير الأجزاء
يتم تعديل مدير الأجزاء [SectionsPagerAdapter] في الفئة [MainActivity] في نقطة واحدة فقط: بدلاً من إدارة الأجزاء من النوع [Fragment]، فإنه يدير الآن الأجزاء من النوع [AbstractFragment]:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء
private AbstractFragment[] fragments;
// رقم الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
// المنشئ
public SectionsPagerAdapter(FragmentManager fm) {
// الأصل
super(fm);
// تهيئة مصفوفة الأجزاء
fragments = new AbstractFragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
...
}
// جزء من +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
// رقم موضع الجزء
@Override
public AbstractFragment getItem(int position) {
...
}
// يعرض عدد الأجزاء المدارة
@Override
public int getCount() {
...
}
}
1.13.5. تعديل الفئة [MainActivity]
يجب أن تنفذ الفئة [MainActivity] واجهة [IMainActivity]:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
...
// حقن الجلسة
@Bean(Session.class)
protected Session session;
...
// استرداد الجلسة
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// عرض الموضع
if(mViewPager.getCurrentItem()!=position){
// عرض الجزء
mViewPager.setCurrentItem(position);
}
}
- الأسطر 10-12: كانت الطريقة [getSession] موجودة بالفعل؛
- الأسطر 15-22: تعرض الطريقة [navigateToView] الجزء رقم [position]؛
- السطر 17: يتم التحقق مما إذا كان هناك ما يجب القيام به؛
- السطر 19: يتم عرض الجزء رقم [position]؛
في هذه المرحلة، قم بتشغيل التطبيق. يجب أن يعمل.
1.13.6. تعديل عرض الأجزاء في [MainActivity]
حاليًا، تعرض الفئة [MainActivity] جزءًا باستخدام التعليمات التالية:
// عرض Vue1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
نظرًا لأن الطريقة [navigateToView] تقوم بنفس الشيء، يتم استبدال هذا النوع من التعليمات في كل مكان (مكانان) بـ:
ثم قم بتشغيل التطبيق. يجب أن يعمل بشكل طبيعي.
1.13.7. الخلاصة
من الآن فصاعدًا، سنستخدم دائمًا البنية السابقة:
- نشاط ينفذ واجهة [IMainActivity]؛
- أجزاء تمتد إلى الفئة [AbstractFragment]، مما يفرض عليها تنفيذ الطريقة [updateFragment]. يجب أن تحتوي هذه الأجزاء أيضًا على طريقة [@AfterViews] حيث تقوم بتعيين القيمة المنطقية [afterViewsDone] إلى true؛
- جلسة عمل تغلف البيانات المراد مشاركتها بين الأجزاء والنشاط؛
1.14. مثال-13: مثال-05 مع الأجزاء
في المشروع [Exemple-05]، أدخلنا التنقل بين العروض. كان الأمر يتعلق آنذاك بالتنقل بين الأنشطة: عرض واحد = نشاط واحد. نقترح هنا أن يكون لدينا نشاط واحد مع عدة عروض من النوع [AbstractFragment].
1.14.1. إنشاء المشروع
نقوم بنسخ المشروع السابق [Exemple-12] في [Exemple-13] باتباع الإجراء الوارد في الفقرة 1.4. ونحصل على النتيجة التالية:
![]() | ![]() |
1.14.2. تنظيم المشروع
سنبدأ في استخدام الحزم لتنظيم الكود. في الوقت الحالي، يمكننا التمييز بين مجالين منفصلين:
- إدارة النشاط؛
- إدارة الأجزاء؛
نقوم بإنشاء حزمتين لهما هما [exemples.android.activity] و [exemples.android.fragments]:
![]() |
![]() | ![]() |
ونفعل الشيء نفسه لإنشاء الحزمة [exemples.android.fragments]:
![]() | ![]() |
في [8]، نقوم بإنشاء حزمة ثالثة تسمى [architecture] سنضع فيها الكيانات [IMainActivity, AbstractFragment, Session, MyPager] التي تمثل العناصر الأساسية لهيكل تطبيقنا. وذلك لتذكيرنا بأننا اتخذنا خيارًا محددًا للهيكل. بعد ذلك، انقل العناصر الموجودة في المشروع كما هو موضح في [9]. يجب تأكيد كل عملية نقل بالنقر على الزر [Refactor].
في هذه المرحلة، قم بتجميع التطبيق. لدينا الأخطاء التالية في [MainActivity:
![]() |
أثناء نقل الفئات إلى الحزم، أجرى Android Studio التغييرات اللازمة في كود التطبيق (السطور 18-21، على سبيل المثال). لم يتم نقل الفئات المعنية بالسطور 15 و17. يتم إنشاؤها بواسطة مكتبة Android Annotations. بالنسبة لهذه الفئات، يجب تغيير imports يدويًا. وبالتالي، تصبح هذه الأسطر:
![]() |
بعد القيام بذلك، لن تكون هناك أخطاء في التحويل البرمجي. قم بتشغيل التطبيق. عندها ستظهر الخطأ التالي:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}:
ينشأ هذا الخطأ من ملف تعريف التطبيق:
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
تحدد السطران 3 و 12 أن النشاط المحدد هو [exemples.android.MainActivity_]. ولكن نظرًا لأن النشاط قد تم ترحيله إلى الحزمة [activity]، يجب أن يصبح السطر 12 كما يلي:
android:name=".activity.MainActivity_"
انتبه إلى علامة . قبل [activity]. مرة أخرى، لم يتمكن Android Studio من تحديث البيان لأن هذا الأخير يشير إلى فئة Android Annotations التي لم يتم نقلها. لذا، فإن استخدام مكتبة AA يجلب معه عددًا من المشاكل.
1.14.3. تنظيف المشروع
في المشروع الجديد:
- لم يعد هناك علامات تبويب أو أزرار عائمة أو قوائم؛
- تختفي أجزاء [PlaceholderFragment]. ستدير التطبيق جزأين هما [Vue1Fragment] الموجود بالفعل و [Vue2Fragment] الذي سيتعين إنشاؤه؛
- لم تعد الجلسة هي نفسها؛
1.14.3.1. تنظيف الأجزاء
احذف الفئة [PlaceHolderFragment] [1]:
![]() | ![]() |
وبالمثل، احذف العرض [res / layout / fragment_main.xml] المرتبط بهذا الجزء [2].
1.14.3.2. تنظيف الجلسة
الجلسة الحالية هي كما يلي:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// عدد الأجزاء التي تمت زيارتها
private int numVisit;
// رقم الجزء من النوع [PlaceholderFragment] المعروض في علامة التبويب الثانية
private int numFragment;
// أدوات الاسترجاع والتعيين
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
public int getNumFragment() {
return numFragment;
}
public void setNumFragment(int numFragment) {
this.numFragment = numFragment;
}
}
لن نحتفظ بأي شيء من هذه الجلسة.
قم بتجميع المشروع. الأسطر الخاطئة هي تلك التي كانت تستخدم محتوى الجلسة. احذفها. في الفئة [Vue1Fragment]، نقوم أيضًا بحذف المتغير [numVisit] من الكود الذي يصبح كما يلي:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر الواجهة المرئية
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// السجل
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// مدير الأحداث
@Click(R.id.buttonValider)
protected void doValider() {
// يتم عرض الاسم الذي تم إدخاله
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// تحديث الجزء
@Override
protected void updateFragment() {
}
}
1.14.3.3. حذف علامات التبويب والزر العائم والقائمة
يتم حذف علامات التبويب والزر العائم في مكانين:
- في العرض [res / layout / activity-main.xml] الذي يحدد هذه العناصر وموقعها في العرض؛
- في كود النشاط [MainActivity]؛
يتم حذف القائمة أيضًا في مكانين:
- في العرض [res / menu / menu-main.xml] الذي يحدد خيارات القائمة؛
- في كود النشاط [MainActivity]؛
رمز العرض [res / layout / activity-main.xml] هو حاليًا كما يلي:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
- يتم حذف الأسطر [28-31, 41-47]؛
- نحذف أيضًا شريط الأدوات من الأسطر 18-24؛
رمز القائمة [res / menu / menu_main.xml] هو حاليًا كما يلي:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- سنحذف الأسطر 9-24. وبذلك نترك خيارًا لن نستخدمه. وذلك فقط للحصول على مثال لإعلان خيار قائمة يمكننا تكراره عن طريق النسخ واللصق؛
في الفئة [MainActivity]، نحذف كل ما يشير إلى علامات التبويب، والزر العائم، وشريط الأدوات، والقائمة. للعثور على هذه الإشارات، أسهل طريقة هي حذف إعلاناتها:
// مدير علامات التبويب
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// الزر العائم
@ViewById(R.id.fab)
protected FloatingActionButton fab;
ثم إعادة ترجمة التطبيق. الأسطر الخاطئة هي تلك التي تشير إلى العناصر التي تم حذفها. قم بحذف كل هذه الأسطر. بالإضافة إلى ذلك، قم بتعديل مدير الأجزاء بحيث لا يشير إلى الجزء [PlaceholderFragment] الذي قمنا بحذفه:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء
private AbstractFragment[] fragments;
// المنشئ
public SectionsPagerAdapter(FragmentManager fm) {
// الأصل
super(fm);
}
// رقم موضع الجزء
@Override
public AbstractFragment getItem(int position) {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// يعرض عدد الأجزاء المدارة
@Override
public int getCount() {
return fragments.length;
}
}
- الأسطر 7-10: تم حذف كل عملية إنشاء الأجزاء؛
في هذه المرحلة، يجب ألا يكون هناك أي خطأ في التجميع. في الفئة [MainActivity]، توصلنا إلى الكود الوسيط التالي:
package exemples.android.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.IMainActivity;
import exemples.android.architecture.MyPager;
import exemples.android.architecture.Session;
import exemples.android.fragments.Vue1Fragment_;
import org.androidannotations.annotations.*;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
// حاوية الأجزاء
@ViewById(R.id.container)
protected MyPager mViewPager;
// شريط الأدوات
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
// جلسة الإدخال
@Bean(Session.class)
protected Session session;
// عدد الأجزاء
private final int FRAGMENTS_COUNT = 5;
// تجاور الأجزاء
private final int OFF_SCREEN_PAGE_LIMIT = 2;
// وضع التصحيح
public static final boolean IS_DEBUG_ENABLED = true;
// مدير الأجزاء
private SectionsPagerAdapter mSectionsPagerAdapter;
// المنشئ
public MainActivity() {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "constructor");
}
}
@AfterViews
protected void afterViews() {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
// شريط الأدوات - حيث يتم عرض اسم التطبيق
setSupportActionBar(toolbar);
// مدير الأجزاء
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// حاوية الأجزاء مرتبطة بمدير الأجزاء
// أي أن الجزء رقم i من حاوية الأجزاء هو الجزء رقم i الذي تم إصداره بواسطة مدير الأجزاء
mViewPager.setAdapter(mSectionsPagerAdapter);
// إزاحة الأجزاء
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// يتم منع التمرير بين الأجزاء
mViewPager.setSwipeEnabled(false);
// لا يوجد تمرير
mViewPager.setScrollingEnabled(false);
// عرض Vue1
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// سجل
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// جلسة عمل
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// عرض موضع العرض
if (mViewPager.getCurrentItem() != position) {
// عرض الجزء
mViewPager.setCurrentItem(position);
}
}
// مدير الأجزاء
// يُطلب منه الأجزاء المراد عرضها في العرض الرئيسي
// يجب تعريف الطرق [getItem] و [getCount] - الباقي اختياري
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء
private AbstractFragment[] fragments;
// المنشئ
public SectionsPagerAdapter(FragmentManager fm) {
// الأصل
super(fm);
}
// رقم الجزء والموضع
@Override
public AbstractFragment getItem(int position) {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// يعرض عدد الأجزاء المدارة
@Override
public int getCount() {
return fragments.length;
}
}
}
لا يزال هناك بعض التعديلات التي يجب إجراؤها:
- احذف السطر 31 الذي لم يعد له وجود؛
- السطر 33: ضع 1 كجوار للأجزاء؛
- السطر 76: انتقل إلى العرض 0. سيكون هذا هو العرض الذي سيظهر أولاً؛
- السطر 108: قم بتهيئة المصفوفة باستخدام الجزء [Vue1Fragment_]:
// الأجزاء
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
لذلك لدينا جزء واحد فقط. قم بتشغيل التطبيق. يجب أن تحصل على النتيجة التالية:

يجب أن يعمل الزر [Valider].
1.14.4. إنشاء الأجزاء والعروض المرتبطة بها
سيكون للتطبيق عرضان، وهما عرضا المشروع [Exemple-05]. لدينا بالفعل طريقة العرض [vue1.xml] في المشروع الحالي. نقوم الآن بنسخ [vue2.xml] من [Exemple-05] إلى [Exemple-12] (افتح المشروعين وقم بالنسخ واللصق) بينهما.
![]() | ![]() |
- في [1]، العرض الجديد. عند محاولة تحريره، تظهر أخطاء [2]. نحتاج إلى تعديل الملف [strings.xml] [3] لإضافة السلاسل المشار إليها في هذه العرض الجديد:
<resources>
<string name="app_name">Exemple-13</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- عرض 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<!-- عرض 2 -->
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
</resources>
نقوم بنسخ الفئة [Vue1Fragment] في [Vue2Fragment]:
![]() |
ونقوم بتعديل الكود المنسوخ على النحو التالي:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// السجل
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// تحديث الجزء
@Override
protected void updateFragment() {
}
}
- السطر 9: يتم ربط المقطع بالعرض [res / layout / vue2.xml]؛
- السطر 10: الفئة تمتد من الفئة المجردة [AbstractFragment]؛
- الأسطر 12-20: الطريقة [@AfterViews] إلزامية؛
- الأسطر 23-25: الطريقة [updateFragment] إلزامية؛
1.14.5. تنفيذ الأجزاء والتنقل بينها
ستدير النشاط الآن جزأين. تتطور فئتها [SectionsPagerAdapter] على النحو التالي:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
}
تضمن واجهة [IMainActivity] التنقل بين العروض باستخدام طريقة [navigateToView]. سنقوم بإدارة النقر على الزر [Vue n° 2] في الجزء [Vue1Fragment]:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر الواجهة المرئية
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// سجل
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// مديرو الأحداث ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// يتم عرض الاسم الذي تم إدخاله
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
mainActivity.navigateToView(1);
}
// تحديث الجزء
@Override
protected void updateFragment() {
}
}
- الأسطر 37-40: تتولى الطريقة [showVue2] إدارة حدث "النقر" على الزر [Vue n° 2]؛
- السطر 39: يتم التنقل باستخدام الطريقة [navigateToView] الخاصة بالنشاط. نذكر هنا أن النشاط تم تخزينه في الفئة الأم بالشكل التالي:
// النشاط
protected IMainActivity mainActivity;
وأن هذا النشاط قد تم تهيئته بالفعل عند الوصول إلى أي مدير أحداث.
- السطر 34: تستخدم التعليمات المتغير [activity] من الفئة الأصلية، وهو مرجع للنشاط باعتباره مثيلًا من النوع Android [Activity]؛
protected Activity activity;
نجد رمزًا مشابهًا لجزء [Vue2Fragment]:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// السجل
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// مديرو الأحداث ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// تحديث جزء
@Override
protected void updateFragment() {
}
}
- الأسطر 24-27: تدير الطريقة [showVue1] حدث "النقر" على الزر [Vue n° 1]؛
قم بتشغيل المشروع وتحقق من أن التنقل بين العروض يعمل.
1.14.6. تعريف الجلسة
تعمل التطبيق على النحو التالي:
- إدخال اسم في العرض رقم 1؛
- عرض هذا الاسم في العرض رقم 2؛
لكي تتمكن العرض رقم 1 من إرسال الاسم الذي تم إدخاله إلى العرض رقم 2، سنستخدم الجلسة التالية؛
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// الاسم
private String nom;
// مُستردات ومُعيّنات
...
}
- السطر 8: الاسم الذي تم إدخاله؛
ستقوم الفئة [MainActivity] بتهيئة الجلسة على النحو التالي:
// حقن الجلسة
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// تعيين الجلسة
session.setNom("");
}
1.14.7. الكتابة النهائية للأجزاء
في الجزء [Vue1Fragment]، نقوم بتعديل كود معالج النقر على الزر [Valider]:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر الواجهة المرئية
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
...
// مديرو الأحداث ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// يتم حفظ الاسم الذي تم إدخاله
String nom = editTextNom.getText().toString();
// عرضه
Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
// يتم إدراج الاسم الذي تم إدخاله في الجلسة
session.setNom(editTextNom.getText().toString());
// الانتقال إلى العرض رقم 2
mainActivity.navigateToView(1);
}
// تحديث الجزء
@Override
protected void updateFragment() {
}
}
- الأسطر: 31-37: ندير النقر على الزر [Vue n° 2]؛
- السطر 34: قبل الانتقال إلى العرض رقم 2، نضع الاسم الذي تم إدخاله في الجلسة حتى يتمكن العرض الجديد من الوصول إليه؛
تتطور طريقة عرض [Vue2Fragment] على النحو التالي:
package exemples.android.fragments;
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// مكونات الواجهة المرئية
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// السجل
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// مديرو الأحداث ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// تحديث جزء
@Override
protected void updateFragment() {
// استرداد الاسم الذي تم إدخاله في الجلسة
String nom = session.getNom();
// عرضه
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
عند عرض العرض رقم 2، يجب عرض الاسم الذي تم إدخاله في العرض رقم 1. نعلم أنه فور عرضه، سيتم تنفيذ طريقة [updateFragment] الخاصة به. لذلك، يمكن وضع كود عرض الاسم في هذه الطريقة (الأسطر 36-42).
- الأسطر 16-17: إعلان المكون المرئي الوحيد للطريقة؛
- السطر 39: يتم استرداد الاسم الذي تم إدخاله في العرض رقم 1 من الجلسة؛
- السطر 41: يتم تعديل التسمية [textViewBonjour]؛
قم بتشغيل المشروع وتحقق من أنه يعمل.
1.14.8. إدارة دورة حياة الأجزاء
في الجزء [Vue1Fragment]، تكون الطريقة [@AfterViews] كما يلي:
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// السجل
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
هذه الطريقة غير مكتملة. في الواقع، يجب دائمًا توقع الحالة التي يتم فيها إعادة تدوير الجزء بعد عملية [onDestroyView]. في هذه الحالة، يتم إعادة إنشاء عرض الجزء 1 وسيختفي الاسم الذي تم إدخاله مسبقًا من العرض. ونحن لا نريد ذلك. حاليًا، يظل الاسم الذي تم إدخاله معروضًا لأن تجاور أجزاء 1 يجعل دورة حياة الجزء [Vue1Fragment] تُنفذ مرة واحدة فقط. ومع ذلك، من الأفضل توقع حالة إعادة تدوير الجزء.
هناك عدة طرق لحل هذه المشكلة:
- يمكن الاستفادة من حقيقة أن الطريقة [update] يتم تنفيذها بشكل منهجي عند كل عرض للجزء لتحديث الاسم الذي تم إدخاله؛
- يمكن إجراء هذا التحديث فقط عند إعادة تنفيذ الطريقة [@AfterViews]. وهذا هو الخيار الذي نتبناه؛
نقوم بتعديل كود [Vue1Fragment] بالطريقة التالية:
// عناصر الواجهة المرئية
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// البيانات
private String nom;
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// سجل
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// إعادة تهيئة النص المعروض
editTextNom.setText(nom);
}
// مديرو الأحداث ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showVue2() {
// تدوين الاسم الذي تم إدخاله لاستعادته في حالة إعادة استخدام المقطع
nom = editTextNom.getText().toString();
// يتم وضع الاسم الذي تم إدخاله في الجلسة
session.setNom(nom);
// الانتقال إلى العرض رقم 2
activity.navigateToView(1);
}
- السطر 27: عند الاستعداد للانتقال من العرض 1 إلى العرض 2، يتم حفظ الاسم الذي تم إدخاله؛
- السطر 17: عند كل تنفيذ جديد لدورة حياة الجزء، يتم إعادة عرض آخر اسم تم إدخاله؛
بالنسبة للجزء [Vue2Fragment]، يكفي الرمز الموجود:
// مكونات الواجهة المرئية
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// السجل
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// تحديث الجزء
@Override
protected void updateFragment() {
// استرداد الاسم الذي تم إدخاله في الجلسة
String nom = session.getNom();
// عرضه
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
- يتم تحديث المكون المرئي الوحيد للطريقة (السطر 3) في كل مرة يتم فيها عرض الطريقة (السطر 21). وبالتالي، لا يوجد ما تضيفه الطريقة [@AfterViews]؛
1.14.9. الخلاصة
في هذه المرحلة، أظهرنا مرة أخرى مدى ملاءمة بنية نظامنا:
- نشاط ينفذ واجهة [IMainActivity]؛
- أجزاء تمتد إلى فئة [AbstractFragment]، مما يفرض عليها تنفيذ الطريقة [updateFragment]. يجب أن تحتوي هذه الأجزاء أيضًا على طريقة [@AfterViews] حيث تقوم بتعيين القيمة المنطقية [afterViewsDone] إلى true؛
- جلسة عمل تغلف البيانات المراد مشاركتها بين الأجزاء والأنشطة؛
1.15. مثال-14: بنية ذات طبقتين
سنقوم ببناء تطبيق ذي عرض واحد له البنية التالية:
![]() |
1.15.1. إنشاء المشروع
نقوم بنسخ المشروع السابق [Exemple-12] إلى [Exemple-13] باتباع الإجراء الوارد في الفقرة 1.4. ونحصل على النتيجة التالية:
![]() | ![]() |
1.15.2. عرض [vue1]
سيحتوي التطبيق على عرض واحد فقط هو [vue1.xml]. ولذلك، سنقوم بحذف العرض الآخر [vue2.xml] بالإضافة إلى الجزء المرتبط به:
![]() | ![]() |
قم بتجميع التطبيق. تظهر أخطاء في [MainActivity]:
![]() |
قم بتصحيح السطر 4 أدناه في مدير الأجزاء [SectionsPagerAdapter]
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
يصبح السطر 4 أعلاه كما يلي:
// الأجزاء
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
احذف الاستيرادات التي أصبحت غير ضرورية [Ctrl-Shift-O]. يجب ألا تكون هناك أخطاء في التجميع بعد الآن. قم بتنفيذ المشروع: يجب أن تظهر العرض رقم 1. سنقوم الآن بتعديله.
سنقوم بإنشاء العرض [vue1.xml] الذي سيسمح بتوليد أرقام عشوائية:
![]() |
مكوناتها هي التالية:
رمزها XML هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:orientation="vertical" >
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/txt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_Titre2"
android:layout_marginTop="20dp"
android:text="@string/txt_nbaleas" />
<EditText
android:id="@+id/edt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_nbaleas"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorNbAleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_nbaleas"
android:text="@string/txt_errorNbAleas"
android:textColor="@color/red" />
<TextView
android:id="@+id/txt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_nbaleas"
android:layout_marginTop="20dp"
android:text="@string/txt_a" />
<EditText
android:id="@+id/edt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_a"
android:inputType="number" />
<TextView
android:id="@+id/txt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_a"
android:text="@string/txt_b" />
<EditText
android:id="@+id/edt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_b"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorIntervalle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_b"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_b"
android:text="@string/txt_errorIntervalle"
android:textColor="@color/red" />
<Button
android:id="@+id/btn_Executer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_a"
android:layout_marginTop="20dp"
android:text="@string/btn_executer" />
<TextView
android:id="@+id/txt_Reponses"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btn_Executer"
android:layout_marginTop="30dp"
android:text="@string/list_reponses"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/blue" />
<ListView
android:id="@+id/lst_reponses"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_Reponses"
android:layout_marginTop="40dp"
android:background="@color/wheat"
android:clickable="true"
tools:listitem="@android:layout/simple_list_item_1" >
</ListView>
</RelativeLayout>
تستخدم العرض السابق تسميات محددة في الملف [res / values / strings.xml]:
<resources>
<string name="app_name">Exemple-14</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- عرض 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="list_reponses">Liste des réponses</string>
<string name="btn_executer">Exécuter</string>
<string name="aleas">Génération de N nombres aléatoires</string>
<string name="txt_nbaleas">Valeur de N :</string>
<string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
<string name="txt_b">"b : "</string>
<string name="txt_dummy">Dummy</string>
<string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
<string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>
الألوان المستخدمة في [vue1.xml] محددة في الملف [res / values / colors.xml]:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<!-- ألوان التطبيق -->
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
</resources>
1.15.3. الجلسة
![]() |
نظرًا لوجود جزء واحد فقط هنا، فلا داعي للتخطيط للاتصال بين الأجزاء. وبالتالي، ستكون الجلسة فارغة:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}
في هذه المرحلة، قم بتجميع التطبيق. ستظهر أخطاء في الأسطر التي كانت تستخدم عناصر من الجلسة التي أصبحت فارغة الآن. احذف هذه الأسطر وتأكد من أن التجميع لم يعد ينتج أخطاء.
1.15.4. الجزء [Vue1Fragment]
![]() |
نقوم بتعديل الجزء [Vue1Fragment] الموجود بالطريقة التالية:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر الواجهة المرئية
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// قائمة الردود على أمر
private List<String> reponses = new ArrayList<>();
// محول عرض القائمة
private ArrayAdapter<String> adapterReponses;
// المدخلات
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// الذاكرة
afterViewsDone = true;
// السجل
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// إخفاء رسائل الخطأ
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Executer)
void doExecuter() {
// يتم إخفاء أي رسائل خطأ سابقة
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// يتم اختبار صحة البيانات المدخلة
if (!isPageValid()) {
return;
}
}
// يتم التحقق من صحة البيانات المدخلة
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// سجل
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- لا يوجد هنا سوى جزء واحد لن يتم تنفيذ دورة حياته سوى مرة واحدة فقط، عند بدء تشغيل التطبيق. لهذا السبب، لن يتم تنفيذ الطريقتين [@AfterViews] (الأسطر 46-57) و [udateFragment] (الأسطر 75-81) إلا مرة واحدة عند بدء تشغيل التطبيق؛
- السطور 55-56: يتم إخفاء رسالتي الخطأ من العرض (الموضحتين أدناه) [1-2]؛
![]() |
- السطور 59-60: الطريقة التي يتم تنفيذها عند النقر على الزر [Exécuter]؛
- السطور 71-73: يتم التحقق من صحة المدخلات؛
الطريقة [isPageValid] هي كما يلي:
// البيانات المدخلة
private int nbAleas;
private int a;
private int b;
...
// يتم التحقق من صحة البيانات المدخلة
private boolean isPageValid() {
// إدخال عدد الأرقام العشوائية
nbAleas = 0;
Boolean erreur;
int nbErreurs = 0;
try {
nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
erreur = (nbAleas < 1);
} catch (Exception ex) {
erreur = true;
}
// خطأ؟
if (erreur) {
nbErreurs++;
txtErrorAleas.setVisibility(View.VISIBLE);
}
// إدخال a
a = 0;
erreur = false;
try {
a = Integer.parseInt(edtA.getText().toString());
} catch (Exception ex) {
erreur = true;
}
// خطأ؟
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// إدخال b
b = 0;
erreur = false;
try {
b = Integer.parseInt(edtB.getText().toString());
erreur = b < a;
} catch (Exception ex) {
erreur = true;
}
// خطأ؟
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// رجوع
return (nbErreurs == 0);
}
- السطور 2-4: يتم تهيئة هذه الحقول الثلاثة بواسطة الطريقة [isPageValid]. بالإضافة إلى ذلك، تُرجع هذه الطريقة true إذا كانت جميع المدخلات صالحة، وfalse في حالة عدم صلاحيتها. إذا كانت المدخلات غير صالحة، يتم عرض رسائل الخطأ المرتبطة بها؛
في هذه المرحلة، يكون التطبيق قابلاً للتنفيذ. تحقق من عمل الطريقة [isPageValid] عن طريق إدخال بيانات غير صحيحة.
1.15.5. الطبقة [métier]
![]() |
![]() |
تقدم الطبقة [métier] واجهة [IMetier] التالية:
package exemples.android.metier;
import java.util.List;
public interface IMetier {
List<Object> getAleas(int a, int b, int n);
}
تُرجع الطريقة [getAleas(a,b,n)] عادةً n أعدادًا صحيحة عشوائية في النطاق [a,b]. ومن المتوقع أيضًا أن ترجع استثناءً مرة واحدة من كل ثلاث مرات، ويتم إدراج هذا الاستثناء أيضًا في الردود التي ترجعها الطريقة. وفي النهاية، ترجع هذه الطريقة قائمة من الكائنات من النوع [Exception] أو [Integer].
فيما يلي تنفيذ [Metier] لهذه الواجهة:
package exemples.android.metier;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@EBean(scope = EBean.Scope.Singleton)
public class Metier implements IMetier {
public List<Object> getAleas(int a, int b, int n) {
// قائمة العناصر
List<Object> réponses = new ArrayList<Object>();
// بعض عمليات التحقق
if (n < 1) {
réponses.add(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));
}
if (a < 0) {
réponses.add(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));
}
if (b < 0) {
réponses.add(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));
}
if (a >= b) {
réponses.add(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));
}
// خطأ؟
if (réponses.size() != 0) {
return réponses;
}
// يتم إنشاء أرقام عشوائية
Random random = new Random();
for (int i = 0; i < n; i++) {
// يتم إنشاء استثناء عشوائي مرة واحدة من كل ثلاث مرات
int nombre = random.nextInt(3);
if (nombre == 0) {
réponses.add(new AleaException("Exception aléatoire"));
} else {
// وإلا يتم إرجاع رقم عشوائي بين حدين [a,b]
réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
}
}
// النتيجة
return réponses;
}
}
- السطر 9: يتم استخدام التعليق التوضيحي AA [@EBean] على الفئة [Metier] حتى يتسنى إدراج مراجع منها في الطبقة [Présentation]. يؤدي السمة (scope = EBean.Scope.Singleton) إلى إنشاء مثيل واحد فقط للفئة [Metier]. وبالتالي، يتم دائمًا إدخال المرجع نفسه إذا تم إدخاله عدة مرات في الطبقة [Présentation]؛
- أما باقي الكود فهو تقليدي؛
النوع [AleaException] المستخدم من قبل الفئة [Metier] هو التالي:
package exemples.android.metier;
public class AleaException extends RuntimeException {
private static final long serialVersionUID = 1L;
public AleaException() {
}
public AleaException(String detailMessage) {
super(detailMessage);
}
public AleaException(Throwable throwable) {
super(throwable);
}
public AleaException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
}
- السطر 3: الفئة [AleaException] توسع فئة النظام [RuntimeException]، مما يجعلها استثناءً غير خاضع للرقابة: لا يلزم إدارتها في try / catch، ولا وضعها في توقيع الطرق؛
1.15.6. إعادة النظر في النشاط [MainActivity]
![]() |
الطبقة
[metier]
النشاط
عرض
المستخدم
ستقوم النشاط بتنفيذ واجهة [IMetier] من الطبقة [métier]. وبالتالي، لن يكون للجزء/العرض سوى النشاط كطرف متواصل.
تقوم النشاط [MainActivity] بالفعل بتنفيذ واجهة [IMainActivity]. لكي تقوم أيضًا بتنفيذ واجهة [IMetier]، يمكن:
- إضافة واجهة [IMetier] إلى الواجهات التي تنفذها النشاط؛
- التأكد من أن الواجهة [IMainActivity] تمتد بدورها الواجهة [IMetier]. وهذا هو المسار الذي نتبعه؛
تصبح الواجهة [IMainActivity] كما يلي:
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// الوصول إلى الجلسة
Session getSession();
// تغيير العرض
void navigateToView(int position);
// وضع التصحيح
public static final boolean IS_DEBUG_ENABLED = true;
}
- السطر 5: واجهة [IMainActivity] توسع واجهة [IMetier]
تتطور الفئة [MainActivity] على النحو التالي:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
...
// حقن الجلسة
@Bean(Session.class)
protected Session session;
// حقن الأعمال
@Bean(Metier.class)
protected IMetier metier;
...
// التنفيذ IMetier --------------------------------------------------------------------
@Override
public List<Object> getAleas(int a, int b, int n) {
return metier.getAleas(a, b, n);
}
- السطران 11-12: يتم إدخال الطبقة [métier] في النشاط. يتم استخدام التعليق التوضيحي AA [@Bean] لهذا الغرض، والذي يكون معلمته هي الفئة التي تحمل التعليق التوضيحي AA [@EBean]؛
- السطر 2: تنفذ النشاط واجهة [IMainActivity] وبالتالي واجهة [IMetier] من الطبقة [métier]؛
- الأسطر 16-19: تنفيذ الطريقة الوحيدة للواجهة [IMetier]. نكتفي بتفويض الاستدعاء إلى الطبقة [métier]؛
1.15.7. إعادة النظر في الجزء [Vue1Fragment]
![]() |
يتطور كود الفئة [Vue1Fragment] على النحو التالي:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر الواجهة المرئية
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// قائمة الردود على أمر
private List<String> reponses = new ArrayList<>();
// محول عرض القائمة
private ArrayAdapter<String> adapterReponses;
// المدخلات
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
...
}
@Click(R.id.btn_Executer)
void doExecuter() {
...
}
// التحقق من صحة البيانات المدخلة
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// السجل
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// لن يتم تنفيذه إلا مرة واحدة عند بدء تشغيل التطبيق
// يتم إنشاء محول ListView - ولهذا يجب أن تكون المتغير [activity] قد تم تهيئتها
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
}
- السطران 69-70: يتم تعيين محول المكون من النوع [ListView]؛
يستخدم المكون [ListView] لعرض قائمة بالعناصر. ويقوم بذلك عن طريق محول من النوع [ListAdapter] المرتبط بدوره بمصدر البيانات الذي يجب أن يغذي [ListView]. لتعريف محول [ListView]، تتوفر الطريقة التالية [ListView.setAdapter]:
public void setAdapter (ListAdapter adapter)
[ListAdapter] هي واجهة. الفئة [ArrayAdapter] هي فئة تنفذ هذه الواجهة. المنشئ المستخدم في السطر 69 أعلاه هو التالي:
- [context] هي النشاط الذي يعرض [ListView]؛
- [resource] هو العدد الصحيح الذي يحدد العرض المستخدم لعرض عنصر من [ListView]. يمكن أن يكون لهذا العرض أي درجة من التعقيد. يقوم المطور بإنشائه وفقًا لاحتياجاته؛
- [textViewResourceId] هو العدد الصحيح الذي يحدد مكون [TextView] في العرض [resource]. سيتم عرض السلسلة بواسطة هذا المكون؛
- [objects]: قائمة الكائنات المعروضة بواسطة [ListView]. تُستخدم طريقة [toString] الخاصة بالكائنات لعرض الكائن الموجود في [TextView] المحدد بواسطة [textViewResourceId] في العرض المحدد بواسطة [resource].
تتمثل مهمة المطور في إنشاء العرض [resource] الذي سيعرض كل عنصر من عناصر [ListView]. في الحالة البسيطة التي لا نرغب فيها سوى في عرض سلسلة أحرف بسيطة كما هو الحال هنا، يوفر Android العرض المحدد بـ [android.R.layout.simple_list_item_1]. يحتوي هذا العرض على مكون [TextView] المحدد بـ [android.R.id.text1]. هذه هي الطريقة المستخدمة في السطر 69 لإنشاء محول [ListView]. لا يحتاج هذا المحول إلى التعريف إلا مرة واحدة. للسماح بإعادة استخدامه، تم تعريفه كمتغير مثيل للفئة (السطر 39). لننظر مرة أخرى إلى السطر 69:
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
المعلمة الأولى لمُنشئ [ArrayAdapter] هي النشاط الذي تم الحصول عليه في جزء بواسطة [getActivity] والذي تم تخزينه هنا في متغير [activity] للفئة الأصلية. لا تحتوي هذه الحقل دائمًا على قيمة. وبالتالي، تظهر السجلات أنه عند الوصول إلى الطريقة [@AfterViews]، لم يتم تهيئتها بعد، وبالتالي لا يمكن وضع السطرين 69-70 في هذه الطريقة. في الطريقة [updateFragment]، يكون ذلك ممكنًا لأننا نعلم أنه عند تنفيذ هذه الطريقة، يكون لدينا بالضرورة [activity!=null]. يرتبط المحول هنا بمصدر البيانات [reponses] المحدد في السطر 37؛
تعالج الطريقة [doExecuter] النقر على الزر [Exécuter]. ورمزها هو التالي:
@Click(R.id.btn_Executer)
void doExecuter() {
// يتم إخفاء أي رسائل خطأ سابقة
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// يتم مسح الردود السابقة
reponses.clear();
adapterReponses.notifyDataSetChanged();
// يتم اختبار صحة المدخلات
if (!isPageValid()) {
return;
}
// يتم طلب الأرقام العشوائية من النشاط
List<Object> data = mainActivity.getAleas(a, b, nbAleas);
// يتم إنشاء قائمة من سلاسل الأحرف (String) بناءً على هذه البيانات
for (Object o : data) {
if (o instanceof Exception) {
reponses.add(((Exception) o).getMessage());
} else {
reponses.add(o.toString());
}
}
// تحديث عرض القائمة
adapterReponses.notifyDataSetChanged();
}
- السطران 7-8: نريد إفراغ ListView. وللقيام بذلك، نقوم بإفراغ مصدر البيانات [reponses] ونطلب من المهايئ المرتبط بـ ListView أن يقوم بالتحديث؛
- السطور 10-12: قبل تنفيذ الإجراء المطلوب، نتحقق من صحة القيم المدخلة؛
- السطر 14: يتم طلب قائمة الأرقام العشوائية من النشاط. يتم الحصول على قائمة من الكائنات حيث يكون كل كائن من النوع [Integer] أو [AleaException]؛
- الأسطر 16-22: انطلاقًا من قائمة الكائنات التي تم الحصول عليها، يتم تحديث مصدر البيانات [reponses] الذي يعرضه ListView؛
- السطر 24: يُطلب من محول ListView التحديث؛
1.15.8. التنفيذ
قم بتنفيذ المشروع وتحقق من أنه يعمل بشكل صحيح.
1.16. مثال-15: بنية العميل/الخادم
نتناول بنية شائعة لتطبيق Android، وهي تلك التي يتواصل فيها تطبيق Android مع خدمات الويب البعيدة. سيكون لدينا الآن البنية التالية:
![]() |
أضفنا إلى تطبيق Android طبقة [DAO] للتواصل مع الخادم البعيد. ستتواصل هذه الطبقة مع الخادم الذي يولد الأرقام العشوائية التي تعرضها لوحة Android. سيكون لهذا الخادم بنية من طبقتين كما يلي:
![]() |
يقوم العملاء باستعلام بعض URL من الطبقة [web / jSON] ويتلقون ردًا نصيًا بتنسيق jSON (ترميز كائنات JavaScript). هنا، ستعالج خدمة الويب الخاصة بنا URL واحد من النوع [/a/b] والذي سيعيد رقمًا عشوائيًا في النطاق [a,b]. سنصف التطبيق بالترتيب التالي:
الخادم
- طبقته [métier]؛
- خدمته [web / jSON] المُنفذة باستخدام Spring MVC؛
العميل
- طبقته [DAO]. لن تكون هناك طبقة [métier]؛
1.16.1. الخادم [web / jSON]
نريد بناء البنية التالية:
![]() |
1.16.1.1. إنشاء المشروع
سنقوم ببناء خدمة الويب باستخدام نظام Spring [http://spring.io/]. ننتقل إلى الموقع [http://start.spring.io/] (يونيو 2016) الذي سيسمح لنا بإنشاء مشروع Gradle مع التبعيات اللازمة لمشروعنا، وهو ليس مشروع Android ولا يقدم Android Studio أي مساعدة لإنشائه:
![]() |
- في [1]: اختر مشروع Gradle؛
- في [2-3]: خصائص التبعية jar التي أنشأها المشروع (انظر أدناه)؛
- في [4]: اختر التبعية الويب [5] حتى تتوفر الملفات الثنائية اللازمة لخدمتنا الويب؛
- في [6]: قم بإنشاء المشروع. يتم بعد ذلك إنشاء ملف مضغوط لمشروع Gradle نموذجي ويتم عرضه للتنزيل؛
ماذا نضع في [2-3]؟ لقد استخدمنا بالفعل تبعيات Gradle. على سبيل المثال، كانت التبعية في المشروع السابق كما يلي:
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// منذ إصدار المكون الإضافي Gradle 0.11 لنظام Android، يجب استخدام android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
...
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
- السطر 22: تظهر التبعية في شكل [groupId:artifactId:version]. ما هو مطلوب في نموذج موقع [http://start.spring.io/]:
- في [2] هو [groupId]؛
- في [3] هو [artifactId]؛
قم بفك ضغط الملف المضغوط أعلاه في مجلد المشاريع الأخرى:
![]() | ![]() ![]() | ![]() |
باستخدام Android Studio، افتح مشروع Gradle [server-01] [1-2]. المشروع المفتوح هو [3] (منظور المشروع).
1.16.1.2. تكوين Gradle
![]() |
ملف Gradle الذي تم إنشاؤه (يونيو 2016) هو التالي:
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
eclipse {
classpath {
containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
}
}
- السطور 14 و34-38 مخصصة لـ IDE Eclipse. نقوم بحذفها؛
- الأسطر 1-11 و15 تُستخدم لإضافة مكون إضافي يُسمى [spring-boot] إلى مشروع Gradle الخاص بنا. Spring Boot هو مشروع من نظام Spring [http://projects.spring.io/spring-boot/]. يحدد هذا المكون الإضافي إصدارات التبعيات الأكثر استخدامًا مع Spring. وهذا يتيح عدم تحديد إصداراتها (السطران 30 و31). ويكون الإصدار عندئذ هو الإصدار المحدد بواسطة إصدار Spring Boot المستخدم (السطر 3)؛
- السطران 22-23: إصدار Java المطلوب استخدامه، وهو هنا الإصدار 1.8؛
- السطور 25-27: مستودعات الملفات الثنائية التي سيتم استخدامها لتنزيل التبعيات؛
- السطر 26: يشير إلى مستودع Maven المركزي. وهو حاليًا أكبر مستودع للملفات الثنائية مفتوحة المصدر المتاحة؛
- الأسطر 29-32: التبعيات اللازمة للمشروع:
- السطر 30: هذه التبعية تجلب معها جميع الملفات الثنائية اللازمة لبناء خدمة ويب Spring؛
- السطر 31: هذه التبعية تجلب معها جميع الملفات الثنائية اللازمة للاختبارات، ولا سيما اختبارات JUnit؛
- تشير التبعية [compile] إلى أننا بحاجة إلى التبعية لتجميع المشروع. تشير التبعية [testCompile] إلى أننا بحاجة إلى التبعية فقط لتنفيذ الاختبارات. وبالتالي، لا يتم تضمينها في الملف الثنائي للمشروع؛
نقوم بإجراء تنظيف أولي لملف Gradle:
// Spring Boot
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// المكونات الإضافية
apply plugin: 'java'
apply plugin: 'spring-boot'
// ملف ثنائي للمشروع
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
// إصدارات Java
sourceCompatibility = 1.8
targetCompatibility = 1.8
// مستودعات Maven
repositories {
mavenLocal()
mavenCentral()
}
// التبعيات
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- السطر 30: أضفنا مستودع Maven المحلي لمحطة التطوير. يتم إنشاء هذا المستودع عند تثبيت Maven (انظر الفقرة 6.10). إذا كانت التبعية المطلوبة موجودة بالفعل في مستودع Maven المحلي، فلن يتم طلبها من مستودع Maven المركزي؛
- الأسطر 19-22: مهمة Gradle تسمح بإنشاء ملف ثنائي للمشروع. سنستخدمها لنرى ما الذي يتم إنجازه؛
![]() | ![]() | ![]() |
- في [1-4]، قم بتنفيذ المهمة [jar] المحددة في الملف [build.gradle] (يوجد [1] في أعلى اليمين وبجانب IDE)؛
تؤدي العملية السابقة إلى إنشاء أرشيف jar للمشروع ووضعه في المجلد [build / libs] [5]:
![]() |
يأتي اسم الأرشيف مباشرة من المعلومات المقدمة إلى المهمة [jar] في الملف [build.gradle] (الأسطر 19-22).
يمكن عرض جميع تبعيات المشروع على النحو التالي:
![]() |
يمكن ملاحظة في [1] أن التبعية الوحيدة للمشروع [compile('org.springframework.boot:spring-boot-starter-web')] قد جلبت معها عشرات الملفات الثنائية. تضمن Spring Boot للويب التبعيات التي من المحتمل أن تحتاجها تطبيقات الويب Spring MVC. وهذا يعني أن بعضها قد يكون غير ضروري. يعد Spring Boot مثاليًا للدروس التعليمية:
- فهو يوفر التبعيات التي سنحتاجها على الأرجح؛
- يحتوي على خادم Tomcat مدمج [1] مما يوفر علينا نشر التطبيق على خادم ويب خارجي؛
يمكن العثور على العديد من الأمثلة التي تستخدم Spring Boot على موقع نظام Spring البيئي [http://spring.io/guides].
نقوم الآن باستكمال الملف [build.gradle] بالطريقة التالية:
// Spring Boot
...
// التبعيات
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
// مكون إضافي لإنشاء ملف ثنائي وفقًا لمعايير Maven في مستودع Maven المحلي
apply plugin: 'maven-publish'
publishing {
publications {
maven(MavenPublication) {
groupId 'istia.st.exemples.android'
artifactId 'server-01'
version '0.0.1-SNAPSHOT'
from components.java
}
}
repositories {
maven {
// قم بالتغيير للإشارة إلى مستودعك، e.g. http://my.org/repo
url 'file://D:\\maven'
}
}
}
- السطر 10: نستورد مكونًا إضافيًا لـ Gradle يسمى [maven-publish] والذي يسمح بنشر الملف الثنائي للمشروع في مستودع Maven مع الالتزام بمعايير Maven؛
- السطر 11: مهمة Gradle تسمى [publishing]؛
- السطران 14-15: خصائص ملف Maven الثنائي الذي سيتم إنشاؤه؛
- السطر 23: مستودع Maven الذي سيتم النشر عليه، وهو هنا مستودع Maven محلي؛
أدى إضافة المكون الإضافي [maven-publish] إلى إنشاء مهام جديدة في مشروع Gradle:
![]() | ![]() |
إذا قمنا في [2] بتنفيذ المهمة [publish]، فسيتم إنشاء ملف المشروع الثنائي وتثبيته في المجلد المحدد في السطر 23 من ملف [build.gradle]:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() |
تسمح المهمة [jar] بإنشاء ملف ثنائي للمشروع. هذا الملف الثنائي يخلو من التبعيات، وبالتالي فهو غير قابل للتنفيذ. من الممكن إنشاء ملف ثنائي مع جميع التبعيات وقابل للتنفيذ. للقيام بذلك، نضيف إلى الملف [build.gradle] الكود التالي:
// إنشاء ملف ثنائي مع جميع تبعياته
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'istia.st.exemples.android.Server01Application'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
- السطر 6: يجب إدخال الاسم الكامل للفئة القابلة للتنفيذ للمشروع:
![]() |
سيكون كود هذه الفئة كما يلي:
package istia.st.exemples.android;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Server01Application {
public static void main(String[] args) {
System.out.println("Server01Application running");
//SpringApplication.run(Server01Application.class, args);
}
}
قم بتحديث مشروع Gradle ثم قم بتنفيذ المهمة [fatJar]:
![]() | ![]() |
يتم إنشاء الملف الثنائي في المجلد [build / libs] ويمكن تشغيله [1-7]:
![]() | ![]() |
1.16.1.3. تكوين المشروع
لا يكفي تكوين Gradle. علينا أيضًا تكوين المشروع. نظرًا لأن هذا ليس مشروع Android تم إنشاؤه بواسطة IDE، فإن هذا التكوين الذي لم نقم به حتى الآن يجب إجراؤه هنا.
![]() | ![]() |
- في [3-4]: استخدم JDK 1.8؛
لتجميع المشروع، لم يعد الزر المتاح لمشاريع Android موجودًا. سنستخدم خيارًا من القائمة [1-2]:
![]() | ![]() |
فيما يلي، يُطلب من القارئ إنشاء المشروع التالي. سنعلق على الكود النهائي لمشروع [3].
1.16.1.4. الطبقة [métier]
![]() |
![]() |
تستند الطبقة [métier] إلى روح الطبقة [métier] من المثال السابق. وستكون لها الواجهة [IMetier] التالية:
package exemples.android.server.metier;
public interface IMetier {
// رقم عشوائي في [a,b]
int getAlea(int a, int b);
}
- السطر 5: الطريقة التي تولد رقمًا عشوائيًا واحدًا في [a,b]
رمز الفئة [Metier] التي تنفذ هذه الواجهة هو التالي:
package exemples.android.server.metier;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class Metier implements IMetier {
@Override
public int getAlea(int a, int b) {
// بعض عمليات التحقق
if (a < 0) {
throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0", 2);
}
if (b < 0) {
throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0", 3);
}
if (a >= b) {
throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b", 4);
}
// توليد النتيجة
Random random=new Random();
random.setSeed(new Date().getTime());
return a + random.nextInt(b - a + 1);
}
}
لن نعلق على الفئة: فهي مشابهة لتلك الموجودة في المثال السابق باستثناء أنها لا تطلق استثناءات عشوائية. نلاحظ فقط في السطر 8 تعليق Spring [@Service] الذي سيجعل Spring يقوم بإنشاء مثيل واحد للفئة (singleton) وإتاحة مرجعها لمكونات Spring الأخرى. كان من الممكن استخدام تعليقات Spring أخرى هنا لتحقيق نفس الغرض. تحتوي مكونات Spring على أسماء افتراضية يمكن تحديدها كسمة للتعليق التوضيحي المستخدم. بدون هذه السمة، كما هو الحال هنا، تحمل مكونة Spring اسم الفئة مع الحرف الأول صغيرًا. وهكذا، تحمل مكونة Spring هنا وبشكل افتراضي الاسم [metier]؛
تطلق الفئة [Metier] استثناءات من النوع [AleaException]:
package exemples.android.server.metier;
public class AleaException extends RuntimeException {
// رمز الخطأ
private int code;
// المنشئون
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// مُستردات ومُعيّنات
....
}
- السطر 3: [AleaException] توسع الفئة [RuntimeException]. لذا فهي استثناء غير خاضع للرقابة (لا يلزم معالجتها باستخدام try / catch)؛
- السطر 6: نضيف رمز خطأ إلى الفئة [RuntimeException]؛
1.16.1.5. خدمة الويب / jSON
![]() |
![]() |
يتم تنفيذ خدمة الويب / jSON بواسطة Spring MVC. يقوم Spring MVC بتنفيذ نموذج الهندسة المعمارية المسمى MVC (النموذج – العرض – وحدة التحكم) بالطريقة التالية:
![]() |
تتم معالجة طلب العميل على النحو التالي:
- الطلب - تكون URL المطلوبة على شكل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] هي فئة Spring التي تعالج URL الواردة. وهي "توجه" URL إلى الإجراء الذي يجب أن يعالجها. هذه الإجراءات هي طرق لفئات معينة تسمى [Contrôleurs]. الحرف C في MVC هو هنا السلسلة [Dispatcher Servlet, Contrôleur, Action]. إذا لم يتم تكوين أي إجراء لمعالجة URL الواردة، فسترد الخدمة [Dispatcher Servlet] بأن URL المطلوبة لم يتم العثور عليها (خطأ 404 NOT FOUND)؛
- المعالجة
- يمكن للإجراء المختار استخدام المعلمات parami التي أرسلتها إليه الخدمة [Dispatcher Servlet]. وقد تأتي هذه المعلمات من عدة مصادر:
- مسار [/param1/param2/...] لـ URL،
- من المعلمات [p1=v1&p2=v2] الخاصة بـ URL,
- من المعلمات التي أرسلها المتصفح مع طلبه؛
- أثناء معالجة طلب المستخدم، قد تحتاج العملية إلى الطبقة [metier] [2b]. وبمجرد معالجة طلب العميل، يمكن أن تستدعي هذه العملية استجابات متنوعة. ومن الأمثلة الشائعة على ذلك:
- صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
- صفحة تأكيد في الحالات الأخرى
- تطلب الإجراء عرض طريقة عرض معينة [3]. ستعرض طريقة العرض هذه البيانات التي نسميها نموذج طريقة العرض. هذا هو M في MVC. ستقوم الإجراء بإنشاء هذا النموذج M [2c] وستطلب عرض طريقة عرض V [3]؛
- الاستجابة - تستخدم طريقة العرض V المختارة النموذج M الذي أنشأته الإجراء لتهيئة الأجزاء الديناميكية من الاستجابة HTML التي يجب أن ترسلها إلى العميل ثم ترسل هذه الاستجابة.
بالنسبة لخدمة الويب / jSON، يتم تعديل البنية السابقة بشكل طفيف:
![]() |
- في [4a]، يتم تحويل النموذج الذي هو فئة Java إلى سلسلة jSON بواسطة مكتبة jSON؛
- في [4b]، يتم إرسال هذه السلسلة jSON إلى المتصفح؛
يتم عرض مثال على تسلسل كائن Java إلى سلسلة jSON وإلغاء تسلسل سلسلة jSON إلى كائن Java في الملاحق في الفقرة 6.14.
لنعد إلى الطبقة [web] في تطبيقنا:
![]() |
في تطبيقنا، يوجد عنصر تحكم واحد فقط:
![]() |
سترسل خدمة الويب / jSON إلى عملائها استجابة من النوع [Response] كما يلي:
package exemples.android.server.web;
import java.util.List;
public class Response<T> {
// ----------------- الخصائص
// حالة العملية
private int status;
// رسائل الخطأ المحتملة
private List<String> messages;
// نص الرد
private T body;
// المنشئات
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// مُستردات ومُعيّنات
...
}
- السطر 13: الحقل [T body] هو الرد الذي يتوقعه العميل. قررنا أن يكون لدينا هنا رد عام من النوع T، بدلاً من النوع Integer للرقم العشوائي المتوقع. نريد أن نتمكن من إعادة استخدام هذه الفئة في حالات أخرى. أثناء معالجة طلب العميل، قد يواجه الخادم مشكلة يتم تلخيصها في الحقلين الآخرين؛
- السطر 8: رمز الحالة (0 في حالة عدم وجود خطأ)؛
- السطر 9: إذا كان status!=0، قائمة برسائل الخطأ، عادةً تلك الموجودة في مكدس الاستثناءات في حالة وجود استثناء، null في حالة عدم وجود أخطاء؛
وحدة التحكم [WebController] هي كما يلي:
package exemples.android.server.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import exemples.android.server.metier.IMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;
@Controller
public class WebController {
// طبقة الأعمال
@Autowired
private IMetier metier;
// التعيين JSON
@Autowired
private ObjectMapper mapper;
// أرقام عشوائية
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
// الرد
Response<Integer> response = new Response<>();
// يتم استخدام طبقة الأعمال
try {
response.setBody(metier.getAlea(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// نقوم بإرجاع الإجابة
return mapper.writeValueAsString(response);
}
private List<String> getMessagesFromException(Throwable e) {
// قائمة الرسائل
List<String> messages = new ArrayList<String>();
// يتم استعراض مكدس الاستثناءات
Throwable th = e;
while (th != null) {
messages.add(e.getMessage());
th = th.getCause();
}
// نقوم بإرجاع النتيجة
return messages;
}
}
- السطر 17: تشير التعليقات التوضيحية [@Controller] إلى أن الفئة هي وحدة تحكم MVC التي تعالج طرقها الطلبات لبعض URL في تطبيق الويب؛
- السطران 21-22: يطلب التعليق التوضيحي [@Autowired] من Spring حقن مكون من النوع [IMetier] في الحقل. وستكون هذه هي الفئة [Metier] السابقة. وذلك لأننا وضعنا عليها التعليق التوضيحي [@Service]، لذا يتم التعامل معها كمكون Spring؛
- السطران 24-25: نفعل الشيء نفسه مع مخطط jSON الذي سنحدده لاحقًا. سترسل خدمة الويب الخاصة بنا ردها في شكل سلسلة jSON. وسيكون هذا المُعَيِّن هو الذي سيقوم بتسلسل الرد إلى jSON؛
- السطر 30: الطريقة التي تولد الرقم العشوائي. لا يهم اسمها. عند تنفيذها، تم تهيئة معلماتها بواسطة Spring MVC. سنرى كيف. علاوة على ذلك، إذا تم تنفيذها، فذلك لأن خادم الويب تلقى طلبًا HTTP GET لـ URL في السطر 28؛
- السطر 28: تحدد التعليقات التوضيحية [@RequestMapping] بعض خصائص الطريقة المُعلَّقة:
- [value]: URL المقبولة من قبل الطريقة؛
- [method]: الطريقة HTTP التي تقبلها الطريقة. هناك اثنتان رئيسيتان، GET و POST. تُستخدم الطريقة [POST] عندما يرغب العميل في إرفاق مستند بطلبه HTTP؛
- [produces]: تحدد أحد رؤوس الرد HTTP الذي سيتم إرساله إلى العميل. هنا، في رؤوس HTTP المرسلة مع رد العميل، سيكون هناك رأس يخبره أن الرد يتم إرساله إليه في شكل سلسلة jSON. هذه الرأسية ليست إلزامية. يتم تقديمها للعميل على سبيل الإعلام إذا كان يتوقع ردودًا قد تتخذ أشكالًا مختلفة؛
- [consumes]: غير موجود هنا. يسمح بالإشارة إلى الرؤوس HTTP التي يجب أن تصاحب طلب العميل HTTP حتى يتم قبوله؛
- السطر 29: تشير التعليقات التوضيحية [@ResponseBody] إلى أن النتيجة التي تنتجها الطريقة يجب إرسالها إلى العميل. بدون هذه التعليقات التوضيحية، يُعتبر رد الطريقة بمثابة مفتاح يسمح باختيار الصفحة HTML لإرسالها إلى العميل. في خدمة الويب / jSON، لا توجد صفحات HTML؛
- السطر 28: URL المعالجة هي على شكل /{a}/{b} حيث يمثل {x} متغيرًا. يتم تعيين المتغيرين {a} و{b} إلى معلمات الطريقة في السطر 30. يتم ذلك عبر التعليق التوضيحي @PathVariable("x"). تجدر الإشارة إلى أن {a} و{b} هما مكونان لـ URL وبالتالي فهما من النوع String. قد يفشل تحويل String إلى نوع المعلمات. عندئذٍ يطلق Spring MVC استثناءً. لنلخص: إذا طلبت باستخدام متصفح URL /100/200، فستُنفَّذ الطريقة getAlea في السطر 30 باستخدام المعلمات الصحيحة a=100، b=200؛
- السطر 36: يُطلب من الطبقة [métier] عدد عشوائي في النطاق [a,b]. نتذكر أن الطريقة [metier].getAlea يمكن أن تطلق استثناءً؛
- السطر 37: لا توجد أخطاء؛
- السطر 39: رمز الخطأ؛
- السطر 40: قائمة رسائل الاستجابة هي قائمة مكدس الاستثناءات (السطور 46-57). هنا، نعلم أن المكدس لا يحتوي إلا على استثناء واحد، لكننا أردنا عرض طريقة أكثر عمومية؛
- السطر 43: يتم عرض الاستجابة من النوع [Response<Integer>] في شكل سلسلة jSON؛
1.16.1.6. تكوين مشروع Spring
![]() |
هناك طرق مختلفة لتكوين Spring:
- مع ملفات XML؛
- مع كود Java؛
- مع مزيج من الاثنين؛
نختار تكوين تطبيق الويب الخاص بنا باستخدام كود Java. وتقوم الفئة [Config] التالية بتوفير هذا التكوين:
package exemples.android.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(basePackages = { "exemples.android.server.metier", "exemples.android.server.web" })
@EnableWebMvc
public class Config {
// تكوين الويب ------------------------------------
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// أداة التعيين jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- السطر 12: نُخبر Spring في أي حزم سيجد المكونين اللذين يجب عليه إدارتهما:
- المكون [Metier] المُعلَّم بـ [@Service] في الحزمة [exemples.android.server.metier]؛
- المكون [WebController] المُعلَّم بـ [@Controller] في الحزمة [exemples.android.server.web]؛
- السطر 13: تسمح التعليقات التوضيحية [@EnableWebMvc] لـ Spring Boot بإجراء عدد من التكوينات القياسية بنفسه لتطبيق Spring MVC. وهذا يقلل من عبء العمل على المطور؛
- الأسطر 16 و22 و27 و33: تحدد التعليقة التوضيحية [@Bean] أيضًا مكونات (beans) Spring بنفس الطريقة التي تحددها التعليقتان التوضيحيتان المذكورتان (@Service، @Controller). هنا، يعلق التعليق التوضيحي [@Bean] على طريقة وليس على فئة، ونتيجة الطريقة هي مكون Spring. في حالة عدم وجود سمة تسمية داخل التعليق التوضيحي [@Bean]، يحمل مكون Spring الذي تم إنشاؤه اسم الطريقة المعلقة؛
- الأسطر 16-20: تحدد bean [dispatcherServlet]. وهو اسم محدد مسبقًا في Spring MVC يحدد وحدة التحكم الأمامية للتطبيق MVC، وهو كائن تمر عبره جميع طلبات العملاء ويقوم بتوزيعها (ومن هنا جاء اسمه) على مختلف [@Controller] في تطبيق Spring MVC؛
- السطر 18: الكائن [dispatcherServlet] هو مثيل لفئة [DispatcherServlet] التي يوفرها Spring MVC؛
- الأسطر 22-25: يُستخدم bean [servletRegistrationBean] لتحديد أي URL تقبلها التطبيق. في السطر 24، يتم قبول جميع URL؛
- الأسطر 27-30: تُستخدم الحبة [embeddedServletContainerFactory] لتحديد الخادم المدمج في تبعيات المشروع الذي يجب أن يستضيف تطبيق الويب. تشير السطر 29 إلى أنه خادم Tomcat وأنه سيعمل على المنفذ 8080. بشكل افتراضي، يتم جلب الملفات الثنائية لهذا الخادم الويب من خلال التبعية [org.springframework.boot:spring-boot-starter-web] في ملف Gradle؛
1.16.1.7. تشغيل خدمة الويب / jSON
![]() |
يتم تشغيل المشروع من الفئة القابلة للتنفيذ [Boot] التالية:
package exemples.android.server.boot;
import exemples.android.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Boot {
public static void main(String[] args) {
// تشغيل التطبيق
SpringApplication.run(Config.class, args);
}
}
- الفئة [Boot] هي فئة قابلة للتنفيذ (الأسطر 7-10)؛
- السطر 9: الطريقة الثابتة [SpringApplication.run] هي طريقة تابعة لـ [spring Boot] (السطر 4) والتي ستقوم بتشغيل التطبيق. المعلمة الأولى لها هي فئة Java التي تقوم بتكوين المشروع. وهنا الفئة [Config] التي وصفناها للتو. المعلمة الثانية هي مصفوفة الحجج التي تم تمريرها إلى الطريقة [main] (السطر 7)؛
يمكن تشغيل تطبيق الويب بعدة طرق، منها ما يلي:
![]() |
في وحدة التحكم، يظهر عدد من السجلات:
- الأسطر 12-14: يتم تشغيل خادم Tomcat المدمج؛
- الأسطر 15-19: يتم تحميل وتكوين السيرفلت [DispatcherServlet] من Spring MVC؛
- السطر 20: تم الكشف عن URL [/{a}/{b}] لخادم الويب؛
الآن، لنستخدم متصفحًا ونختبر خدمة الويب / jSON:
![]() |
![]() |
![]() |
![]() |
نحصل في كل مرة على تمثيل jSON لكائن من النوع [Response<Integer>].
بدلاً من استخدام متصفح قياسي، لنستخدم الآن ملحق [Advanced Rest Client] لمتصفح Chrome (انظر الملاحق، الفقرة 6.13):

- إلى [1]، و URL المطلوب؛
- إلى [2]، باستخدام GET؛
- إلى [3]، يتم إرسال الطلب؛

- في [4]، رؤوس HTTP من رد الخادم. تجدر الإشارة إلى أن هذا الأخير يشير إلى أن المستند المرسل هو سلسلة jSON؛
- في [5]، السلسلة jSON المستلمة؛
1.16.1.8. إنشاء ملف jar القابل للتنفيذ للمشروع
في الفقرة 1.16.1.2، أوضحنا كيفية تكوين ملف Gradle لإنشاء ملف قابل للتنفيذ للتطبيق مع جميع تبعياته. بعد تكييفها مع التطبيق الحالي، يصبح هذا التكوين كما يلي:
// إنشاء ملف ثنائي مع جميع تبعياته
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'exemples.android.server.boot.Boot'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
لإنشاء هذا الملف القابل للتنفيذ، يمكن اتباع الخطوات التالية [1-5]:
![]() | ![]() |
لتشغيله، سنوقف خدمة الويب إذا كانت قيد التشغيل [1]، ثم سنقوم بتشغيل الأرشيف [2-4]:
![]() | ![]() |
استخدم متصفحًا واطلب URL [localhost:8080/100/200]. يجب أن تحصل على نفس النتائج التي حصلت عليها من قبل.
1.16.1.9. إدارة السجلات
عند تشغيل الأرشيف القابل للتنفيذ، نلاحظ أننا لا نحصل على نفس السجلات التي نحصل عليها عند تشغيل المشروع من IDE. لدينا سجلات في وضع [DEBUG]:
...
09:32:03.741 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletConfigInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletContextInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:32:03.810 [main] INFO org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:32:03.813 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.984 seconds (JVM running for 2.206)
يمكن إدارة مستوى السجلات عن طريق إضافة ملف [logback.xml] إلى مجلد [resources] الخاص بالمشروع:
![]() |
قد يحتوي هذا الملف على المحتوى التالي:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- يتم تعيين النوع افتراضيًا للمشفرات
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- التحكم في مستوى السجلات -->
<root level="info"> <!-- معلومات، تصحيح أخطاء، تحذير -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
يتم التحكم في مستوى السجلات في السطر 12. إذا قمنا الآن بإعادة إنشاء الأرشيف القابل للتنفيذ وقمنا بتشغيله، فلن نحصل إلا على سجلات من المستوى [info]:
...
09:36:52.433 [main] INFO o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:36:52.923 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:36:52.926 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.865 seconds (JVM running for 2.203)
1.16.2. عميل Android لخادم الويب / jSON
سيكون للعميل Android البنية التالية:
![]() |
سيتألف العميل من مكونين:
- طبقة [Présentation] (عرض + نشاط) مشابهة لتلك التي درسناها في المثال [Exemple-14]؛
- الطبقة [DAO] التي تتصل بالخدمة [web / jSON] التي درسناها سابقًا.
1.16.2.1. إنشاء المشروع
نقوم بنسخ المشروع السابق [Exemple-14] في [Exemple-15] باتباع الإجراء الوارد في الفقرة 1.4. ونحصل على النتيجة التالية:
![]() | ![]() |
فيما يلي، يُطلب من القارئ إنشاء المشروع التالي.
1.16.2.2. تكوين Gradle
![]() |
الملف [build.gradle] هو التالي:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// منذ إصدار المكون الإضافي Gradle 0.11 لنظام Android، يجب عليك استخدام android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// خيارات التعبئة والتغليف اللازمة لإنتاج APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
نكتفي بالتعليق على ما لم يتم تناوله من قبل:
- السطران 46-47: إدراج مكون إضافي AA. يسمح المكون الإضافي [rest-spring-api] بتفويض المكتبة AA بالتبادل بين العميل والخادم؛
- السطر 50: المكتبة [spring-android-rest-template] هي المكتبة التي يستخدمها AA لضمان التبادل بين العميل والخادم. الإصدار [2.0.0.M3] هو إصدار يُعرف باسم "milestone" ولا يوجد في مستودعات Maven المعتادة. لذلك يجب تحديد، في الأسطر 56-59، المستودع الذي سيتم استخدامه (السطر 58) للعثور على المكتبة؛
- السطر 51: مكتبة jSON؛
- الأسطر 33-39: بدون هذه الخاصية، تظهر أخطاء عند إنشاء الملف الثنائي APK للمشروع؛
1.16.2.3. بيان التطبيق Android
![]() |
يجب تعديل الملف [AndroidManifest.xml]. في الواقع، يتم تعطيل الوصول إلى الإنترنت بشكل افتراضي. يجب تفعيله بواسطة توجيه خاص:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity_"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- السطر 5: الوصول إلى الإنترنت مسموح به؛
1.16.2.4. الطبقة [DAO]
![]() |
![]() |
1.16.2.4.1. واجهة [IDao] للطبقة [DAO]
ستكون واجهة الطبقة [DAO] كما يلي:
package exemples.android.dao;
public interface IDao {
// رقم عشوائي
int getAlea(int a, int b);
// URL لخدمة الويب
void setUrlServiceWebJson(String url);
// الحد الأقصى لوقت انتظار استجابة الخادم (بالمللي ثانية)
void setTimeout(int timeout);
// فترة انتظار العميل بالمللي ثانية قبل الطلب
void setDelay(int delay);
}
- السطر 6: طريقة خدمة الويب / jSON للحصول على رقم عشوائي في النطاق [a,b] لهذه الخدمة؛
- السطر 9: URL لخدمة الويب / jSON لتوليد الأرقام العشوائية؛
- السطر 12: نحدد مهلة انتظار قصوى لانتظار رد الخادم؛
- السطر 15: نريد تحديد مهلة انتظار قبل تنفيذ الطلب المرسَل إلى الخادم، وذلك لإتاحة الوقت للمستخدم لإلغاء طلبه؛
1.16.2.4.2. واجهة [WebClient]
![]() |
تتولى الواجهة [WebClient] مهمة التواصل مع خدمة الويب. وفيما يلي كودها:
package exemples.android.dao;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// رقم عشوائي واحد في النطاق [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- السطر 12: [WebClient] هي واجهة ستقوم مكتبة AA بتنفيذها بنفسها بفضل التعليقات التوضيحية التي سنضعها فيها. يجب أن تنفذ هذه الواجهة استدعاءات URL التي يعرضها خدمة الويب / jSON:
// رقم عشوائي
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
- السطر 11: التعليق التوضيحي [@Rest] هو تعليق توضيحي AA. قيمة السمة [converters] هي مصفوفة من المحولات. هنا، المحول [MappingJackson2HttpMessageConverter.class] يجعل أنه عندما يرسل الخادم سلسلة jSON، يتم فك تسلسلها تلقائيًا. وهكذا نرى في السطر (d) أن URL [/{a}/{b}] يُرجع نوعًا String وهو في الواقع سلسلة jSON (السطر b). باستخدام هذه المعلومات وتلك الخاصة بالنوع المتوقع في السطر 16، ستقوم مثيل [WebClient] الخاص بالعميل بإلغاء تسلسل السلسلة التي سيتلقاها إلى نوع [Response<Integer>]؛
- السطر 15: تعليق AA يشير إلى أن URL يجب استدعاؤها باستخدام طريقة HTTP GET. معلمة التعليق التوضيحي [@Get] هي شكل URL المتوقع من قبل خدمة الويب. يكفي استعادة المعلمة [value] من التعليق التوضيحي [@RequestMapping] (السطر ب) للطريقة التي تم استدعاؤها في وحدة التحكم [WebController] للخادم. تحيط الأقواس {} بمعلمات URL التي يجب استرجاعها في معلمات الطريقة في السطر 16. تجعل صيغة [@Path("a") int a] المعلمة [a] للطريقة تُعيَّن إلى القيمة {a} لـ URL. عندما يكون معلمة URL ومعلمة الطريقة يحملان نفس الاسم كما هو الحال هنا، يمكن كتابة [@Path int a] بشكل أبسط؛
في حالة طلب HTTP POST، سيكون توقيع طريقة الاستدعاء كما يلي:
@Post("/{a}/{b}")
Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);
التعليق [@Body] هو الذي يشير إلى القيمة المرسلة. سيتم تسلسل هذه القيمة تلقائيًا إلى jSON. على جانب الخادم، سيكون لدينا التوقيع التالي:
// أرقام عشوائية
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
- السطر 2: نحدد أننا ننتظر طلبًا HTTP POST وأن نص هذا الطلب (الكائن المرسل) يجب أن يُنقل في شكل سلسلة jSON (السمة consumes)؛
- السطر 4: سيتم استرداد القيمة المرسلة في المعلمة [@RequestBody T body] الخاصة بالطريقة؛
لنعد إلى كود الفئة [WebClient]:
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
- يجب أن نتمكن من تحديد URL لخدمة الويب التي يجب الاتصال بها. يتم الحصول على ذلك من خلال توسيع واجهة [RestClientRootUrl] التي توفرها AA. تعرض هذه الواجهة طريقة [setRootUrl(urlServiceWeb] التي تسمح بتحديد URL لخدمة الويب المطلوب الاتصال بها؛
- بالإضافة إلى ذلك، نريد التحكم في استدعاء خدمة الويب لأننا نريد الحد من وقت انتظار الرد. ولهذا الغرض، نقوم بتوسيع واجهة [RestClientSupport] التي تعرض الطريقة [setRestTemplate] التي ستسمح لنا بما يلي:
- إنشاء الكائن [RestTemplate] بأنفسنا، والذي يُستخدم لإدارة التبادل بين العميل والخادم؛
- ضبط إعدادات هذا الكائن لتحديد الحد الأقصى لوقت انتظار الرد؛
1.16.2.4.3. الفئة [Response]
تُرجع الطريقة [getAlea] للواجهة [IDao] استجابة من النوع [Response] كما يلي:
package exemples.android.dao;
import java.util.List;
public class Response<T> {
// ----------------- الخصائص
// حالة العملية
private int status;
// رسائل الخطأ المحتملة
private List<String> messages;
// نص الرد
private T body;
// المنشئات
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// مُستردات ومُعيّنات
...
}
هذه هي الفئة [Response] المستخدمة بالفعل على جانب الخادم (الفقرة 1.16.1.5). في الواقع، من وجهة نظر البرمجة، يبدو الأمر كما لو أن طبقة [DAO] الخاصة بالعميل تتواصل مباشرة مع وحدة التحكم [WebController] الخاصة بخدمة الويب:
![]() |
يكون الاتصال الشبكي بين العميل والخادم وكذلك تسلسل/إلغاء تسلسل كائنات Java من جانب العميل شفافًا للمبرمج.
1.16.2.4.4. تنفيذ الطبقة [DAO]
![]() |
يتم تنفيذ واجهة [IDao] باستخدام الفئة [Dao] التالية:
package exemples.android.dao;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@EBean
public class Dao implements IDao {
// عميل الخدمة REST
@RestService
protected WebClient webClient;
// المُخطِط jSON
private ObjectMapper mapper = new ObjectMapper();
// فترة الانتظار قبل تنفيذ الطلب
private int delay;
// واجهة IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setDelay(int delay) {
this.delay = delay;
}
}
- السطر 15: نعلق على الفئة [Dao] بالتعليق [@EBean] لتحويلها إلى bean AA يمكننا حقنه في مكان آخر؛
- السطران 19-20: نقوم بإدخال التنفيذ الذي سيتم لواجهة [WebClient] التي وصفناها. التعليق التوضيحي [@RestService] هو الذي يضمن هذا الإدخال؛
- تقوم الطرق الأخرى بتنفيذ واجهة [IDao] (السطور 27-46)؛
الطريقة [setTimeout]
الطريقة [setTimeout] هي كما يلي:
@Override
public void setTimeout(int timeout) {
// يتم تعيين مهلة انتظار طلبات العميل REST
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// يتم إنشاء restTemplate
RestTemplate restTemplate = new RestTemplate(factory);
// يتم تعيين المحول jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// يتم تعيين restTemplate لعميل الويب
webClient.setRestTemplate(restTemplate);
}
- سيتم تنفيذ الواجهة [WebClient] بواسطة فئة AA باستخدام التبعية Gradle [org.springframework.android:spring-android-rest-template]. تنفذ [spring-android-rest-template] حوار العميل مع خادم الويب / jSON عن طريق فئة من النوع [RestTemplate]؛
- السطر 4: يتم توفير الفئة [SimpleClientHttpRequestFactory] بواسطة التبعية [spring-android-rest-template]. ستسمح لنا هذه الفئة بتحديد الحد الأقصى لفترة انتظار استجابة الخادم (السطران 5-6)؛
- السطر 8: نقوم بإنشاء الكائن من النوع [RestTemplate] الذي سيكون وسيلة الاتصال مع خدمة الويب. نمرر له كمعلمة الكائن [factory] الذي تم إنشاؤه للتو؛
- السطر 10: يمكن أن يتخذ الحوار بين العميل والخادم أشكالاً متنوعة. تتم المراسلات عبر أسطر نصية وعلينا أن نحدد للكائن من النوع [RestTemplate] ما يجب عليه فعله بهذه السطر النصي. ولهذا الغرض، نزوده بمحولات، وهي فئات قادرة على معالجة الأسطر النصية. يتم اختيار المحول عمومًا عبر الرؤوس HTTP التي تصاحب سطر النص. هنا، نعلم أننا نتلقى فقط سطور نص بتنسيق jSON. علاوة على ذلك، رأينا في الفقرة 1.16.1.7 أن الخادم كان يرسل الرأس HTTP:
Content-Type: application/json;charset=UTF-8
السطر 10، سيكون المحول الوحيد لـ [RestTemplate] هو محول jSON الذي تم تنفيذه باستخدام مكتبة [Jackson]. هناك شيء غريب بشأن هذه المحولات: يفرض علينا AA أن يكون موجودًا أيضًا في تعليق عميل الويب [WebClient]:
@Rest(converters = {MappingJacksonHttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
في السطر 1، نحن مضطرون إلى تحديد محول على الرغم من أننا نحدده برمجيًا.
- السطر 12: يتم إدراج الكائن [RestTemplate] الذي تم إنشاؤه بهذه الطريقة في تنفيذ واجهة [WebClient]، وهذا الكائن هو الذي سيقوم بإجراء الحوار بين العميل والخادم؛
طريقة [getAlea]
الطريقة [getAlea] هي كما يلي:
@Override
public int getAlea(int a, int b) {
// تشغيل الخدمة
Response<Integer> info;
DaoException ex;
try {
// في انتظار
waitSomeTime(delay);
// تنفيذ الخدمة
info = webClient.getAlea(a, b);
int status = info.getStatus();
if (status == 0) {
// إرجاع النتيجة
return info.getBody();
} else {
// تسجيل الاستثناء
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// تسجيل الاستثناء
ex = new DaoException(e, 100);
}
// إطلاق الاستثناء
throw ex;
}
...
// طرق خاصة -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- السطر 8: ننتظر [delay] ميلي ثانية؛
- السطر 10: نكتفي باستدعاء الطريقة التي تحمل نفس التوقيع في الفئة التي تنفذ واجهة [WebClient]؛
- السطر 11: يتم تحليل الاستجابة التي تم الحصول عليها من الخادم من خلال النظر إلى [status]؛
- الأسطر 12-14: إذا لم تكن هناك أخطاء من جانب الخادم (status=0)، فإننا نُرجع نتيجة الطريقة؛
- السطر 17: إذا كان هناك خطأ من جانب الخادم (status!=0)، يتم إعداد استثناء دون تشغيله. أرسل الخادم قائمة برسائل الخطأ. نقوم بإنشاء استثناء مع سلسلة jSON من قائمة رسائل الخادم كرسالة وحيدة؛
- الأسطر 19-22: حالات استثناء أخرى؛
- السطر 24: عند الوصول إلى هنا، لا بد أن يكون قد حدث استثناء. لذا يتم تشغيله؛
الاستثناء [DaoException] المستخدم في هذا الكود هو التالي:
package exemples.android.dao;
import java.util.ArrayList;
import java.util.List;
public class DaoException extends RuntimeException {
// رمز الخطأ
private int code;
// المنشئات
public DaoException() {
}
public DaoException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public DaoException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
// مُستردات ومُعيّنات
...
}
- السطر 6: الاستثناء [DaoException] هو استثناء غير خاضع للرقابة؛
طريقة [setUrlServiceWebJson]
الطريقة [setUrlServiceWebJson] هي التالية:
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// يتم تعيين URL لخدمة REST
webClient.setRootUrl(urlServiceWebJson);
}
- السطر 4: يتم تعيين URL لخدمة الويب عبر الطريقة [setRootUrl] للواجهة [WebClient]. توجد هذه الطريقة لأن هذه الواجهة توسع واجهة [RestClientRootUrl]؛
1.16.2.5. الحزمة [architecture]
تجمع الحزمة [architecture] العناصر التي تشكل بنية التطبيق:
![]() |
![]() |
1.16.2.5.1. الواجهة [IMainActivity]
تسرد واجهة [IMainActivity] الطرق التي يجب أن تنفذها نشاط التطبيق:
package exemples.android.architecture;
import exemples.android.dao.IDao;
public interface IMainActivity extends IDao {
// الوصول إلى الجلسة
Session getSession();
// تغيير العرض
void navigateToView(int position);
// الانتظار
void beginWaiting();
void cancelWaiting();
// وضع التصحيح
boolean IS_DEBUG_ENABLED = true;
// مهلة انتظار الرد
int TIMEOUT = 1000;
// تجاور الأجزاء
int OFF_SCREEN_PAGE_LIMIT = 1;
}
- السطر 5: واجهة [IMainActivity] توسع واجهة [IDao]؛
- الأسطر 13-16: إلى الطرق الموجودة بالفعل في الأمثلة السابقة (الأسطر 7-11)، أضفنا طريقتين لإدارة صورة انتظار التطبيق (السطران 14 و16)؛
- السطر 21: يتم تعيين مدة انتظار قصوى لاستجابة الخادم تبلغ ثانية واحدة؛
1.16.2.5.2. الفئة [Utils]
تم تجميع طرق مساعدة ثابتة في الفئة [Utils] يمكن استدعاؤها من أماكن مختلفة في بنية التطبيق:
package exemples.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// قائمة رسائل الاستثناء - الإصدار 1
static public List<String> getMessagesFromException(Throwable ex) {
// يتم إنشاء قائمة برسائل الخطأ من مكدس الاستثناءات
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// قائمة رسائل الاستثناء - الإصدار 2
static public String getMessagesForAlert(Throwable th) {
// يتم إنشاء النص المراد عرضه
StringBuilder texte = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// النتيجة
return texte.toString();
}
}
- الأسطر 9-18: تنشئ قائمة رسائل الخطأ الموجودة في ملف Throwable؛
- الأسطر 21-32: تعتمد على الطريقة السابقة لإنشاء النص المراد عرضه في رسالة تنبيه Android من قائمة الرسائل التي تم الحصول عليها؛
- السطور 27-28: يتم ترقيم الرسائل. الرقم الأصغر (1) يتوافق مع الاستثناء الأولي والرقم الأكبر مع الاستثناء الأحدث في مكدس الاستثناءات؛
1.16.2.5.3. الفئة المجردة [AbstractFragment]
تتمثل وظيفتا الفئة [AbstractFragment] في:
- ضمان استدعاء الطريقة [updateFragments] للفئات الفرعية دائمًا عند عرض الجزء، وذلك مرة واحدة فقط؛
- تحليل حالة وأساليب الفئات الفرعية التي يمكن تحليلها؛
والوظيفة الثانية هي التي تجعلنا نضع في هذه الفئة عمليات إدارة الصورة المؤقتة: فجميع أجزاء تطبيق Android غير المتزامن عليها إدارة هذا النوع من المشكلات:
// إدارة الانتظار
protected void beginWaiting() {
// وضع ساعة الرمل
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// إزالة رمز الساعة الرملية
mainActivity.cancelWaiting();
}
1.16.2.6. الطريقة
![]() |
1.16.2.6.1. العرض [vue1.xml]
![]() |
بالنسبة للمثال السابق، تتطور طريقة العرض [vue1.xml] على النحو التالي:
![]() |
![]() |
- في [1]، يجب على المستخدم تحديد URL لخدمة الويب وكذلك مهلة الانتظار [2] قبل كل استدعاء لخدمة الويب؛
- في [3]، يتم حساب الردود؛
- في [4]، يمكن للمستخدم إلغاء طلبه؛
- في [5]، يتم عرض مؤشر انتظار عند طلب الأرقام. ويختفي هذا المؤشر عند استلام جميع الأرقام أو إلغاء العملية؛

- في [6]، يتم التحقق من صحة الإدخالات؛
يُطلب من القارئ تحميل الملف [vue1.xml] من الأمثلة. وفيما يلي، نقدم معرفات المكونات الجديدة:

أزرار [10-11] موجودة فعليًا فوق بعضها البعض. في وقت ما، سيتم إظهار أحدهما فقط.
1.16.2.6.2. المقطع [Vue1Fragment]
![]() |
هيكل الجزء [Vue1Fragment] هو كما يلي:
package exemples.android.fragments;
import android.app.AlertDialog;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.*;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.*;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.api.BackgroundExecutor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر الواجهة المرئية
@ViewById(R.id.editTextUrlServiceWeb)
EditText edtUrlServiceRest;
@ViewById(R.id.textViewErreurUrl)
TextView txtMsgErreurUrlServiceWeb;
@ViewById(R.id.editTextDelay)
EditText edtDelay;
@ViewById(R.id.textViewErreurDelay)
TextView textViewErreurDelay;
@ViewById(R.id.lst_reponses)
ListView listReponses;
@ViewById(R.id.txt_Reponses)
TextView infoReponses;
@ViewById(R.id.edt_nbaleas)
EditText edtNbAleas;
@ViewById(R.id.edt_a)
EditText edtA;
@ViewById(R.id.edt_b)
EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
TextView txtErrorIntervalle;
@ViewById(R.id.btn_Executer)
Button btnExecuter;
@ViewById(R.id.btn_Annuler)
Button btnAnnuler;
...
// البيانات المحلية
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
@AfterViews
void afterViews() {
// الذاكرة
afterViewsDone=true;
// في البداية لا توجد رسائل خطأ
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// زر [Annuler] مخفي
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
// قائمة الردود
reponses = new ArrayList<>();
}
...
- الأسطر 24-49: المراجع الخاصة بمكونات العرض [vue1.xml] (السطر 20)؛
- الأسطر 55-69: الطريقة [@AfterViews] التي يتم تنفيذها عند تهيئة المراجع الواردة في الأسطر 24-49؛
- السطر 58: لا تنسى - ضروري لدورة حياة الجزء؛
- الأسطر 60-63: يتم إخفاء رسائل الخطأ؛
- الأسطر 65-66: يتم إخفاء الزر [Annuler] (السطر 65) وعرض الزر [Exécuter] (السطر 66). تجدر الإشارة إلى أنهما متراكبان فعليًا؛
- السطر 68: سيحتوي حقل السطر 52 على قائمة سلاسل الأحرف التي سيتم عرضها بواسطة ListView من الردود؛
مباشرة بعد الطريقة [@AfterViews]، سيتم تنفيذ الطريقة التالية [updateFragment]:
@Override
protected void updateFragment() {
// يتم إنشاء محول قائمة الإجابات
adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
- السطران 4-5: يتم إنشاء محول ListView للاستجابات. ويتم تخزينه في متغير مثيل ليكون متاحًا للطرق الأخرى في الفئة؛
يؤدي "النقر" على الزر [Exécuter] إلى تنفيذ الطريقة التالية:
// المدخلات
private int nbAleas;
private int a;
private int b;
private String urlServiceWebJson;
private int delay;
// البيانات المحلية
private int nbInfos;
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
private boolean hasBeenCanceled;
@Click(R.id.btn_Executer)
protected void doExecuter() {
// يتم مسح الإجابات السابقة
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// إعادة ضبط عداد الإجابات على 0
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// اختبار صحة الإدخالات
if (!isPageValid()) {
return;
}
// تهيئة النشاط
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// يتم طلب الأرقام العشوائية
for (int i = 0; i < nbAleas; i++) {
getAlea(a, b);
}
// يبدأ الانتظار
beginWaiting();
}
@Background(id = "alea")
void getAlea(int a, int b) {
// يجب القيام بأقل عدد ممكن من الإجراءات هنا
// على أي حال لا يوجد عرض - يجب أن يتم ذلك في UiThead
try {
// نعرض النتيجة في UiThread
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
// يتم عرض الاستثناء في UiThread
showAlert(e);
}
}
- السطران 17-18: يتم مسح قائمة الإجابات السابقة من الخادم. وللقيام بذلك، في السطر 17، يتم إفراغ مصدر البيانات [reponses] المرتبط بمحول ListView؛
- السطر 19: قيمة منطقية ستساعدنا في معرفة ما إذا كان المستخدم قد ألغى طلبه أم لا؛
- السطران 21-22: يتم عرض عداد صفر لعدد الردود؛
- السطور 24-26: يتم استرداد المدخلات من السطور [2-6] والتحقق من صحتها. إذا كانت إحداها غير صالحة، يتم التخلي عن الطريقة (السطر 25) ويعود المستخدم إلى الواجهة المرئية؛
- السطور 28-29: إذا كانت جميع البيانات المدخلة صالحة، يتم إرسال URL لخدمة الويب (السطر 28) إلى النشاط، بالإضافة إلى فترة الانتظار قبل كل استدعاء للخدمة (السطر 29). هذه المعلومات ضرورية للطبقة [DAO]، ويُذكر أن النشاط هو الذي يتواصل معها؛
- السطور 31-33: يتم طلب الأرقام العشوائية واحدًا تلو الآخر من الطريقة [getAlea] في السطر 39؛
- السطر 38: يتم توضيح الأسلوب [getAlea] بالتوضيح AA [@Background]، مما يعني أنه سيتم تنفيذه في مؤشر ترابط آخر (تدفق التنفيذ، عملية) غير ذلك الذي يتم فيه تنفيذ الواجهة المرئية. فمن الضروري بالفعل تنفيذ أي استدعاء عبر الإنترنت في مؤشر ترابط مختلف عن مؤشر ترابط الواجهة المرئية. وبالتالي، في وقت معين، قد يكون لدينا عدة مؤشرات ترابط:
- الخيط الذي يعرض الواجهة المرئية UI (واجهة المستخدم) ويدير أحداثها،
- خيوط [nbAleas] التي يطلب كل منها رقمًا عشوائيًا من خدمة الويب. يتم تشغيل هذه الخيوط بشكل غير متزامن: يقوم خيط UI بتشغيل خيط [getAlea] (السطر 32) الذي يطلب رقمًا عشوائيًا من خدمة الويب ولا ينتظر انتهاء العملية. سيتم إخطاره بانتهاء العملية من خلال حدث. وبالتالي، سيتم تشغيل خيوط [nbAleas] بالتوازي. من الممكن تكوين التطبيق بحيث لا يشغل سوى خيط واحد في كل مرة. وعندها يكون هناك قائمة انتظار للخيوط المطلوب تنفيذها؛
السطر 38، المعلمة [id] تعطي اسمًا للخيط الذي تم إنشاؤه. هنا، تحمل جميع خيوط [nbAleas] نفس الاسم [alea]. سيسمح لنا ذلك بإلغاء تشغيلها جميعًا في نفس الوقت. هذا المعامل اختياري إذا لم يتم إدارة إلغاء تشغيل الخيط؛
- السطر 44: يتم استدعاء الطريقة [getAlea] للنشاط. سيتم استدعاؤها إذن في مؤشر ترابط منفصل عن مؤشر ترابط UI. سيقوم هذا المؤشر باستدعاء خدمة الويب ولن ينتظر الرد. سيتم إخطاره لاحقًا بواسطة حدث بأن الرد متاح. في هذه اللحظة، في السطر 44، سيتم استدعاء الطريقة [showInfo] مع الاستجابة المستلمة كمعلمة؛
- السطور 45-47: قد يؤدي تنفيذ طلب الويب إلى حدوث استثناء. عندئذٍ يُطلب عرض رسائل الخطأ الخاصة بالاستثناء في رسالة تنبيه؛
- السطر 35: ننتظر النتائج:
- سيتم عرض مؤشر انتظار؛
- سيحل الزر [Annuler] محل الزر [Exécuter]. ونظرًا لأن الخيوط التي تم تشغيلها غير متزامنة، فإن خيط UI لا ينتظرها ويتم تنفيذ السطر 35 قبل انتهائها. بمجرد انتهاء الطريقة [beginWaiting]، يمكن لـ UI الاستجابة مرة أخرى لطلبات المستخدم مثل النقر على الزر [Annuler]. إذا كانت الخيوط التي تم تشغيلها متزامنة، فلن نصل إلى السطر 35 إلا بعد انتهاء جميع الخيوط. عندئذٍ لن يكون لإلغاء هذه الخيوط أي معنى؛
الطريقة [showInfo] هي كما يلي:
@UiThread
protected void showInfo(int alea) {
if (!hasBeenCanceled) {
// معلومة إضافية
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// هل انتهينا؟
if (nbInfos == nbAleas) {
// ننهي الانتظار
cancelWaiting();
}
// نضيف المعلومات إلى قائمة الردود
reponses.add(0, String.valueOf(alea));
// نعرض الإجابات
adapterReponses.notifyDataSetChanged();
}
}
- يتم استدعاء الطريقة [showInfo] داخل الخيط [getAlea] المُعلَّم بواسطة [@Background]. ستقوم هذه الطريقة بتحديث الواجهة المرئية UI. ولا يمكنها القيام بذلك إلا من خلال تنفيذها داخل مؤشر ترابط UI. وهذا هو معنى التعليق التوضيحي [@UiThread] في السطر 1؛
- السطر 2: تتلقى الطريقة رقمًا عشوائيًا؛
- السطر 3: لا يتم تنفيذ نص الدالة إلا إذا لم يقم المستخدم بإلغاء طلبه؛
- السطران 5-6: يتم زيادة عداد الردود وعرضه؛
- السطور 8-11: إذا تم استلام جميع الردود المتوقعة، يتم إنهاء الانتظار (نهاية إشارة الانتظار، ويحل الزر [Exécuter] محل الزر [Annuler])؛
- السطور 12-15: نضيف الرقم العشوائي المستلم إلى قائمة الإجابات التي يعرضها المكون [ListView listReponses] ونقوم بتحديثه؛
طريقة [showAlert] هي كما يلي:
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
// نلغي كل شيء
doAnnuler();
// نقوم بعرضها
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
نجد منطقًا مشابهًا لمنطق الطريقة [showInfo]:
- السطر 1: التعليق التوضيحي [@UiThread] إلزامي؛
- السطر 2: تتلقى الطريقة الاستثناء الذي حدث؛
- السطر 3: يتم تنفيذ الطريقة فقط إذا لم يقم المستخدم بإلغاء طلبه؛
- السطر 5: يتم إلغاء طلب المستخدم كما لو كان قد نقر بنفسه على الزر [Annuler]؛
- السطر 7: يتم عرض التنبيه باستخدام فئة Android [AlertDialog]:
- [activity]: هي النشاط من النوع [Activity] المخزّن في الفئة الأم [AbstractFragment]؛
- [setTitle]: يحدد عنوان نافذة التنبيه [1]؛
- [setMessage]: يحدد الرسالة المعروضة في نافذة التنبيه [2]؛
- [setNeutral]: يحدد الزر الذي سيغلق نافذة التنبيه [3]؛
- [show]: يطلب عرض نافذة التنبيه؛
![]() |
يتم التعامل مع "النقر" على الزر [Annuler] بالطريقة التالية:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// الذاكرة
hasBeenCanceled=true;
// إلغاء المهمة غير المتزامنة
BackgroundExecutor.cancelAll("alea", true);
// نهاية الانتظار
cancelWaiting();
}
- السطر 4: نلاحظ أن المستخدم قد ألغى طلبه؛
- السطر 6: يلغي جميع المهام المحددة بالسلسلة [alea]. المعلمة الثانية [true] تعني أنه يجب إلغاؤها حتى لو كانت قد بدأت بالفعل. المعرف [alea] هو المعرف المستخدم لتوصيف الطريقة [getAlea] للجزء (السطر 1 أدناه):
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
ملاحظة: تبين أن السطر 6 من كود الطريقة [doAnnuler] كان يعمل بشكل غير صحيح. ولهذا السبب تمت إضافة المتغير المنطقي [hasBeenCanceled]. في الواقع، في حالة حدوث استثناء (غياب الخادم)، كنا نسترد نافذة التنبيه n مرة إذا كنا قد طلبنا أرقامًا عشوائية n.
1.16.2.7. النشاط [MainActivity]
![]() |
1.16.2.7.1. العرض [activity-main.xml]
![]() |
بالنسبة للمثال السابق، أضفنا صورة انتظار في العرض المرتبط بالنشاط [MainActivity]:
...
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
<!-- صورة الانتظار -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- صورة الانتظار -->
</android.support.design.widget.AppBarLayout>
...
- الأسطر 17-21: صورة الانتظار؛
1.16.2.7.2. النشاط [MainActivity]
لا يختلف النشاط [MainActivity] كثيرًا عما كان عليه في [Exemple-14]. أولاً، يتم إدخال الطبقة [DAO] إليه:
// حقن داو
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// سجل
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// تعيين معلمات الطبقة [DAO]
setTimeout(TIMEOUT);
}
- السطران 2-3: إدخال الطبقة [DAO] بواسطة تعليق توضيحي AA؛
- السطور 5-13: كود يتم تنفيذه بعد هذا الإدخال؛
- السطر 12: يتم تعيين timeout للطبقة [DAO]
بالإضافة إلى ذلك، يجب أن تنفذ النشاط [MainActivity] واجهة [IMainActivity] التي تمتد بدورها واجهة [IDao]:
// التنفيذ IMainActivity --------------------------------------------------------------------
@Override
public void navigateToView(int position) {
// عرض منظر الموضع
if (mViewPager.getCurrentItem() != position) {
// عرض الجزء
mViewPager.setCurrentItem(position);
}
}
// إدارة صورة الانتظار
public void cancelWaiting() {
loadingPanel.setVisibility(View.INVISIBLE);
}
public void beginWaiting() {
loadingPanel.setVisibility(View.VISIBLE);
}
// التنفيذ IDao --------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// التنفيذ
return dao.getAlea(a, b);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
1.16.2.8. تنفيذ المشروع
قم بتشغيل خدمة الويب (الفقرة 1.16.1.7) ثم قم بتشغيل عميل Android:

لمعرفة ما يجب وضعه في [1]، اتبع الخطوات التالية. افتح نافذة الأوامر واكتب الأمر التالي:
C:\Program Files\Console2>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 3 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet VirtualBox Host-Only Network :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::e481:1583:cd2a:c47%27
Adresse IPv4. . . . . . . . . . . . . .: 192.168.82.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet VirtualBox Host-Only Network #2 :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::8191:14ad:407d:b840%54
Adresse IPv4. . . . . . . . . . . . . .: 192.168.64.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet Ethernet :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::d972:ad53:3b8a:263f%28
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . : uang ad.univ-angers.fr univ-angers.fr
إذا قمت بتثبيت [GenyMotion]، فإن الجهاز الظاهري VirtualBox قد أضاف عناوين IP إلى جهازك (السطران 10 و18). هذه العناوين مفيدة بشكل خاص لأنها لا يتم حظرها بواسطة جدار حماية Windows. يقدم السطر 30 عنوان IP لجهازك على شبكة محلية. لاستخدام هذا العنوان، يجب عادةً تعطيل جدار حماية Windows. إذا كنت متصلاً بشبكة واي فاي، فاستخدم عنوان الواي فاي وقم أيضًا بتعطيل جدار الحماية إذا كان لديك واحد.
اختبر التطبيق في الحالات التالية:
- 100 رقم عشوائي في النطاق [1000, 2000] بدون فترة انتظار؛
- 2000 رقم عشوائي في النطاق [10000, 20000] بدون فترة انتظار وقم بإلغاء الانتظار قبل انتهاء عملية التوليد؛
- 5 أرقام عشوائية في النطاق [100, 200] مع فترة انتظار تبلغ 5000 مللي ثانية وقم بإلغاء الانتظار قبل انتهاء عملية التوليد؛
1.16.2.9. إدارة الإلغاء
لمتابعة ما يحدث عندما يطلب المستخدم الإلغاء أو عندما يُطلب الإلغاء بسبب حدوث استثناء، نضيف الطريقة التالية إلى الواجهة [IDao] (انظر الفقرة 1.16.2.4.1):
package exemples.android.dao;
public interface IDao {
...
// وضع التصحيح
void setDebugMode(boolean isDebugEnabled);
}
في الفئة [Dao]، نضيف الكود التالي:
// وضع التصحيح
private boolean isDebugEnabled;
// اسم الفئة
private String className;
..
// المنشئ
public Dao() {
// اسم الفئة
className = getClass().getSimpleName();
}
...
// واجهة IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// تشغيل الخدمة
Response<Integer> info;
...
@Override
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
- السطر 9: نسجل اسم الفئة؛
- الأسطر 16-18: نكتب سجلًا في كل مرة يتم فيها استدعاء الطريقة [getAlea]؛
بالإضافة إلى ذلك، في الجزء [Vue1Fragment]، نضيف السجلات التالية:
@UiThread
protected void showInfo(int alea) {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
....
}
@UiThread
protected void showAlert(Throwable th) {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
...
}
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
...
}
في كل مرة يتلقى المقطع [Vue1Fragment] معلومة من الطبقة [DAO]، يتم إصدار سجل. علاوة على ذلك، عند استدعاء الأسلوب [doAnnuler]، يتم تسجيل الحدث.
الاختبار 1
يتم طلب 5 أرقام في حين أن الخادم لم يتم تشغيله. لدينا السجلات التالية:
- الأسطر 1-5: يتم استدعاء الطريقة [getAlea] من الفئة [Dao] خمس مرات. نذكر أن هذه استدعاءات غير متزامنة يتم إجراؤها بواسطة الجزء [VueFragment] وأن هذا الجزء لا ينتظر نتيجة استدعائه؛
- السطر 7: تم تنفيذ الطلب الأول HTTP وتلقى الجزء [VueFragment] استثناءه الأول؛
- السطر 8: ثم يطلب إلغاء جميع الطلبات؛
- الأسطر 9-12: نرى مع ذلك أنه يتلقى الاستثناءات الأربعة التالية. وبالتالي، تم تنفيذ جميع الطلبات غير المتزامنة التي كانت قيد الانتظار؛
الاختبار 2
الآن، لنقم بتشغيل الخادم ونطلب 5 أرقام بفاصل زمني مدته 5 ثوانٍ ونضغط على [Annuler] قبل انتهاء هذا الفاصل الزمني. السجلات هي كما يلي:
- الأسطر 1-5: تم استدعاء الطريقة [getAlea] من الفئة [Dao] خمس مرات؛
- السطر 7: طلب المستخدم إلغاء الطلبات؛
- السطر 8: نرى أن [Vue1_Fragment] تتلقى 5 قيم. مرة أخرى، تم تنفيذ جميع الطلبات غير المتزامنة التي كانت قيد الانتظار؛
وهذا هو السبب الذي دفعنا إلى التعامل مع متغير منطقي [hasBeenCanceled] لتجنب عرض أي شيء في حين تم طلب الإلغاء. في كود الإلغاء:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// الذاكرة
hasBeenCanceled = true;
// إلغاء المهمة غير المتزامنة
BackgroundExecutor.cancelAll("alea",true);
// نهاية الانتظار
cancelWaiting();
}
لا يعمل كود السطر 10 كما هو متوقع. من المحتمل أن يكون ذلك بسبب أن المهام غير المتزامنة تشترك في نفس الطريقة المُعلَّمة [@Background]:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. مثال-16: إدارة عدم التزامن باستخدام RxAndroid
نقترح الآن إدارة عدم التزامن اللازم لتطبيقات Android باستخدام مكتبة تسمى RxJava [http://reactivex.io/] ونسختها المشتقة لبيئة Android [RxAndroid]. ولتحقيق ذلك، سنستخدم الدورة التدريبية [Introduction à RxJava. Application aux environnements Swing et Android].
1.17.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-1] في [Exemple-16]:
![]() | ![]() |
1.17.2. تكوين Gradle
![]() |
في [build.gradle]، نضيف التبعية إلى مكتبة [RxAndroid]:
dependencies {
...
compile 'io.reactivex:rxandroid:1.2.0'
}
1.17.3. الطبقة [DAO]
![]() |
1.17.4. الواجهة [IDao]
تصبح واجهة [IDao] كما يلي:
package exemples.android.dao;
import rx.Observable;
public interface IDao {
// رقم عشوائي
Observable<Integer> getAlea(int a, int b);
// URL لخدمة الويب
void setUrlServiceWebJson(String url);
// الحد الأقصى لفترة انتظار (بالمللي ثانية) لاستجابة الخادم
void setTimeout(int timeout);
// فترة انتظار العميل بالمللي ثانية قبل الطلب
void setDelay(int delay);
// وضع التصحيح
void setDebugMode(boolean isDebugEnabled);
}
- السطر 8: أصبحت الطريقة [getAlea] تُرجع الآن نوعًا [Observable] من مكتبة RxJava (السطر 3). والمبدأ هو كما يلي:
يتم مراقبة تدفق العناصر من النوع Observable<T> بواسطة مشترك واحد أو أكثر (مشتركين، مراقبين، مستهلكين) من النوع Subscriber<T>. تسمح المكتبة RxJava بتشغيل تدفق Observable<T> في مؤشر ترابط T1 ومراقبه Subscriber<T> في مؤشر ترابط T2 دون أنيضطر إلى القلق بشأن إدارة دورة حياة هذه الخيوط والمشاكل الصعبة بطبيعة الحال، مثل مشاركة البيانات بين الخيوط ومزامنتها لتنفيذ مهمة شاملة. وبالتالي، فهي تسهل البرمجة غير المتزامنة.
1.17.5. الفئة [AbstractDao]
سنقوم بتشتيت الفئة [Dao] من الفئة [AbstractDao] التالية:
package exemples.android.dao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// المُخطِط jSON
private ObjectMapper mapper = new ObjectMapper();
// الطرق المحمية ----------------------------------------------------------
// واجهة عامة
protected interface IRequest<T> {
Response<T> getResponse();
}
// طلب عام
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// تنفيذ الخدمة
return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// تنفيذ الخدمة
try {
// يتم إجراء الطلب المتزامن وإرسال الرد إلى المشترك
Response<T> response = request.getResponse();
// خطأ؟
int status = response.getStatus();
if (status != 0) {
// تسجيل الاستثناء
ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
} else {
// إرسال الرد
subscriber.onNext(response.getBody());
// يتم الإبلاغ عن نهاية المراقبة
subscriber.onCompleted();
}
} catch (JsonProcessingException | RuntimeException e) {
// تسجيل الاستثناء
ex = new DaoException(e, 100);
}
// استثناء؟
if (ex != null) {
// يتم إرسال الاستثناء
subscriber.onError(ex);
}
}
});
}
}
- تحتوي الفئة [AbstractDao] كعنصر رئيسي على طريقة عامة [getResponse] تُستخدم للحصول من الخادم على نوع [Response<T>] حيث T هو نوع النتيجة المطلوبة من قبل العميل HTTP (هنا Integer)؛
- السطر 20: المعلمة الوحيدة للطريقة العامة [getResponse] هي مثيل للواجهة العامة [IRequest<T>] في الأسطر 15-17. لا تحتوي هذه الواجهة سوى على طريقة واحدة [getResponse]، وهذه الطريقة هي التي توفر الإجابة المطلوبة [Response<T>]؛
- بفضل العنصرين السابقين، يمكن أن تكون الفئة [AbstractDao] بمثابة فئة أصلية لأي طبقة [Dao] عميلة لخادم يرسل استجابات من النوع [Response<T>]؛
- السطر 20: تُرجع الطريقة العامة [getResponse] نوعًا [Observable<T>] يمثل النتيجة المتوقعة فعليًا من قبل العميل HTTP (وهنا نوع Observable<Integer>)؛
- الأسطر 22-51: الطريقة الثابتة [rx.Observable.create] تنشئ نوعًا [Observable]؛
- السطر 22: المعلمة الوحيدة لهذه الطريقة هي مثيل من النوع [rx.Observable.OnSubscribe<T>]، وهي واجهة تحتوي على الطرق التالية:
- [onNext(T element)]: تسمح بإرسال عنصر من النوع T إلى مراقب؛
- [onError(Throwable th)]: تسمح بإرسال استثناء إلى مراقب؛
- [onCompleted]: تسمح بإبلاغ المراقب بنهاية الإرسالات؛
يخضع النوع [Observable<T>] لبعض القيود:
- يُرسل عناصره باستخدام الطريقة [onNext(T element)]؛
- يجب استدعاء الطريقة [onCompleted] مرة واحدة فقط بمجرد انتهاء العناصر المراد إرسالها إلى المراقب؛
- لا يتم استدعاء الطريقة [onCompleted] إذا تم استدعاء الطريقة [onError(Throwable th)]؛
في مثالنا:
- سيكون المراقب هو الجزء [Vue1Fragment]. وهو الذي يستهلك العناصر التي يتم إرسالها بواسطة [Observable<T>] (عنصر أو استثناء)؛
- النوع [Observable<T>] الذي تم إنشاؤه لن يصدر سوى عنصر واحد (السطر 37)؛
- السطر 29: يقوم بإرسال طلب HTTP متزامن إلى الخادم ويحصل على النوع [Response<T>]. يتم تنفيذ هذا الطلب HTTP بواسطة النوع [IRequest] الذي تم تمريره كمعلمة إلى الطريقة العامة [getResponse]؛
- السطر 31: يتم استرداد status من الرد؛
- الأسطر 32-34: إذا كان هذا status هو رمز خطأ، يتم إعداد استثناء؛
- الأسطر 36-39: إذا لم يكن هذا status مرتبطًا بخطأ، يتم إرسال الاستجابة التي ينتظرها العميل فعليًا (السطر 37) وإبلاغ المراقب بأنه لن تكون هناك إرسالات أخرى (السطر 39)؛
- الأسطر 41-44: إذا انتهت الطلب HTTP باستثناء، يتم تسجيله؛
- الأسطر 46-49: إذا كان الاستثناء [ex] مختلفًا عن null، يتم إرساله إلى المراقب. لا داعي هنا لاستدعاء الطريقة [onCompleted] لإعلام المراقب بأنه لن يكون هناك المزيد من إرسال العناصر. فهذا أمر ضمني؛
سنستخلص من هذه التفسيرات ما يلي:
- الطريقة العامة [<T> Observable<T> getResponse(final IRequest<T> request)] تُرجع نوعًا [Observable<T>] الذي يُرسل عنصرًا واحدًا من النوع T أو استثناءً؛
- أن هذه الطريقة تقبل كمعلمة وحيدة نوع [IRequest<T>] الذي تنفذ طريقته الوحيدة [getResponse()] الوصول HTTP الذي يعيد النوع [Response<T>]؛
1.17.6. الفئة [Dao]
تتطور الفئة [Dao] على النحو التالي:
@EBean
public class Dao extends AbstractDao implements IDao {
// عميل الخدمة REST
@RestService
protected WebClient webClient;
// فترة الانتظار قبل تنفيذ الطلب
private int delay;
// وضع التصحيح
private boolean isDebugEnabled;
// اسم الفئة
private String className;
// المنشئ
public Dao() {
// اسم الفئة
className = getClass().getSimpleName();
}
// واجهة IDao -------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// تشغيل عميل الويب
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// في انتظار
waitSomeTime(delay);
// استدعاء HTTP متزامن
return webClient.getAlea(a, b);
}
});
}
...
- السطر 2: الفئة [Dao] توسع الفئة [AbstractDao]؛
- السطر 24: تعيد الطريقة [getAlea] الآن نوع [Observable<Integer>]؛
- السطر 30: استدعاء الطريقة العامة [getResponse] من الفئة الأصلية. يتم تمرير معلمة من النوع [IRequest<Integer>] إليها؛
- الأسطر 32-37: تنفيذ واجهة [IRequest<Integer>]؛
- السطر 36: يتم إجراء الاستعلام HTTP عبر الواجهة AA [webClient] كما تم سابقًا. نعلم أننا سنحصل على نوع [Response<Integer>] وهو بالفعل النوع الذي يجب أن ترجع به الطريقة [IRequest<Integer>.getReponse()]؛
- السطر 36: نستخدم هنا خاصية تسمى closure: وهي القدرة على تغليف قيم خارجية في مثيل عند إنشائه، وهنا قيم [a, b] من السطر 24. وهذا ما يسمح للطريقة [IRequest<Integer>.getReponse()] بعدم وجود معلمات. فقد تم تضمين هذه المعلمات في نص الطريقة. وحيثما يتم عادةً تغيير معلمات الطريقة (a,b) -> (x,y)، يتم هنا إنشاء مثيل جديد لـ [IRequest<Integer>] يضم قيم x و y؛
1.17.7. الفئة [MainActivity]
تتطور الفئة [MainActivity] التي تنفذ الواجهة [IDao] على النحو التالي:
// تنفيذ IDao --------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(int a, int b) {
// تنفيذ
return dao.getAlea(a, b);
}
1.17.8. الفئة [Vue1Fragment]
تتطور الفئة [Vue1Fragment] على النحو التالي:
@Click(R.id.btn_Executer)
protected void doExecuter() {
// يتم مسح الردود السابقة
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// إعادة ضبط عداد الردود إلى 0
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// اختبار صحة المدخلات
if (!isPageValid()) {
return;
}
// تهيئة النشاط
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// يتم طلب الأرقام العشوائية
getAleasInBackground(a, b);
// بدء الانتظار
beginWaiting();
}
- السطر 18: يتم طلب الأرقام العشوائية من الطريقة [getAleasInBackground] التي سميت بهذا الاسم لأن الأرقام سيتم طلبها في مؤشر ترابط مختلف عن مؤشر ترابط واجهة المستخدم؛
private int nbReponses = 0;
// الاشتراكات في العناصر القابلة للمراقبة
private List<Subscription> abonnements;
// تعليق [Background] غير ضروري
void getAleasInBackground(int a, int b) {
// في البداية لا توجد ردود ولا اشتراكات
nbReponses = 0;
abonnements.clear();
// يتم إعداد المراقبة
Observable<Integer> response = Observable.empty();
// يتم دمج نتائج المكالمات المختلفة HTTP
// يتم تنفيذها على مؤشر ترابط E/S
for (int i = 0; i < nbAleas; i++) {
response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
}
// سيتم مراقبة المراقب التراكمي على مؤشر ترابط UI
response = response.observeOn(AndroidSchedulers.mainThread());
try {
// يتم تنفيذ المراقب
abonnements.add(response.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
// تتم إضافة المعلومات إلى قائمة الردود
showInfo(alea);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// رسالة خطأ
showAlert(th);
// نهاية الانتظار
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// نهاية الانتظار
cancelWaiting();
}
}));
} catch (RuntimeException e) {
// يتم عرض الاستثناء في UiThread
showAlert(e);
}
}
- السطر 3: لكل مراقب مشتركون. يُسمى الرابط بين المشترك والعملية التي يراقبها اشتراكًا (Subscription). هنا سيكون لدينا عملية مراقبة واحدة ومشترك واحد فقط. وبالتالي سيكون لدينا اشتراك واحد فقط. من حيث المبدأ، نتصرف كما لو كان بإمكاننا الحصول على عدة عمليات مراقبة من قبل مراقبين مختلفين، مما سيؤدي إلى عدة اشتراكات؛
- الأسطر 11-18: يتم تكوين العملية المراقبة (المراقبة). يجب أن نفهم أن هذا مجرد تكوين: لا يتم تنفيذ العملية؛
- السطر 11: نبدأ بمراقب فارغ، أي مراقب لا يصدر أي شيء؛
- الأسطر 14-16: إلى هذا القابل للمراقبة الفارغ، نضيف القابلات للمراقبة [nbAleas] التي ستكون الطلبات [nbAleas] التي ستُرجع أرقامًا عشوائية HTTP؛
- السطر 15: كما سبق، يُطلب الرقم العشوائي رقم i من الفئة [MainActivity]. يجب أن نفهم جيدًا أنه هنا، لم يتم تنفيذ أي طلب HTTP بعد. يتم تنفيذ الطريقة [mainActivity.getAlea(a, b)] وتُرجع نوعًا [Observable<Integer>]. هذه عملية سيتم ملاحظتها عند تشغيلها؛
- السطر 15: تطلب الطريقة [subscribeOn(Schedulers.io())] أن يتم تنفيذ العملية (عندما يتم تنفيذها) على مؤشر ترابط E/S. توفر مكتبة RxJava أنواعًا مختلفة من الخيوط. خيط الإدخال/الإخراج مناسب لاستدعاءات HTTP؛
- السطر 15: يتم دمج المراقب رقم i مع المراقب الأولي في السطر 11: من المراقبين [nbAleas] الذين يصدر كل منهم عنصرًا واحدًا، يتم إنشاء مراقب يصدر عنه [nbAleas] عنصرًا. وهذا هو الذي سيتم مراقبته. يُصدر هذا المراقب الإشعار [onCompleted] عندما تصدر جميع المراقبين المكونين له إشعاراتهم الخاصة [onCompleted]. وهذا سيوفر علينا عناء عد الردود، كما فعلنا في الإصدار السابق، لمعرفة ما إذا كنا قد تلقينا جميع الأرقام المتوقعة؛
- السطر 18: عند الوصول إلى هنا، نكون قد قمنا بتكوين ملاحظ قابل للمراقبة يتكون من ملاحظات [nbAleas] التي يتم تنفيذ كل منها على مؤشر ترابط إدخال/إخراج؛
- السطر 18: تُستخدم الطريقة [observeOn(AndroidSchedulers.mainThread())] لتحديد الخيط الذي يجب أن تتم عليه مراقبة القيم الصادرة عن المراقب. هنا، ينتمي مؤشر الترابط [AndroidSchedulers.mainThread())] إلى المكتبة RxAndroid وليس إلى RxJava. وهي تشير إلى مؤشر ترابط واجهة المستخدم (Ui) الذي يُسمى أيضًا حلقة الأحداث (event loop). هذه النقطة مهمة: في تطبيق Android، لا يمكن تعديل مكون من مكونات واجهة المستخدم إلا في مؤشر ترابط واجهة المستخدم، وإلا فسيحدث استثناء؛
- الأسطر 19-45: الآن بعد أن تم تكوين العملية المراد مراقبةها، يتم تنفيذها؛
- السطر 21: العملية [Observable.subscribe] هي التي تطلق تنفيذ العملية المراقبة. ستطلق هذه العملية العمليات غير المتزامنة [nbAleas] التي تم تكوينها مسبقًا. وستتاح نتائج هذه العمليات تلقائيًا للمراقب على مؤشر ترابط واجهة المستخدم؛
- نتذكر أن العنصر القابل للمراقبة يصدر ثلاثة أنواع من الأحداث:
- [onNext]: عند إصدار عنصر؛
- [onError]: عندما يواجه استثناءً؛
- [onCompleted]: عندما يشير إلى أنه لن يصدر بعد الآن؛
تتضمن معلمات الطريقة [Observable.subscribe] ثلاثة كائنات [Action1<Integer>, Action1<Throwable>, Action0] تستخدم طرقها [call] لمعالجة كل من هذه الأحداث الثلاثة؛
- الأسطر 21-27: المعلمة الأولى من النوع [Action1<Integer>] تُستخدم لمعالجة الحدث [onNext]. وتستقبل طريقتها [call] العنصر الذي تم إرساله بواسطة المراقب (السطر 23)؛
- السطر 25: يتم إعادة استخدام الطريقة [showInfo] من المثال السابق؛
- الأسطر 27-35: المعلمة الثانية من النوع [Action1<Throwable>] تُستخدم لمعالجة الحدث [onError]. تستقبل طريقتها [call] الاستثناء الذي تم إصداره بواسطة المراقب (السطر 29)؛
- السطر 31: يتم إعادة استخدام الطريقة [showAlert] من المثال السابق؛
- السطر 33: يتم تشغيل إجراء إلغاء طلب المستخدم. وسيتضمن ذلك إلغاء جميع العناصر القابلة للمراقبة التي قيد التنفيذ؛
- الأسطر 35-41: يُستخدم المعامل الثالث من النوع [Action0] لمعالجة الحدث [onCompleted]. ولا تتلقى طريقتها [call] أي معامل؛
- السطر 39: يتم إلغاء الانتظار؛
تتطور الطريقة [showInfo] على النحو التالي:
// تعليق [UiThread] غير ضروري
protected void showInfo(int alea) {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
if (!hasBeenCanceled) {
// معلومة إضافية
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// يتم إضافة المعلومات إلى قائمة الإجابات
reponses.add(0, String.valueOf(alea));
// عرض الإجابات
adapterReponses.notifyDataSetChanged();
}
}
تتضمن الطريقة تغييرين:
- السطر 1: تمت إزالة التعليق التوضيحي AA [@UiThread]؛
- لم يعد يتم حساب الردود لمعرفة ما إذا كان يجب إيقاف الانتظار أم لا. أصبح الحدث [onCompleted] الخاص بالمراقب هو الذي يوفر هذه المعلومات؛
تتطور الطريقة [showAlert] على النحو التالي:
// تعليق [UiThread] غير ضروري
protected void showAlert(Throwable th) {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
if (!hasBeenCanceled) {
// إلغاء كل شيء
doAnnuler();
// مُدرج في القائمة
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
- التغيير الوحيد هو في السطر 1: تمت إزالة التعليق التوضيحي AA [@UiThread]؛
أخيرًا، تتطور الطريقة [doAnnuler] على النحو التالي:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// الذاكرة
hasBeenCanceled = true;
// إلغاء المهام غير المتزامنة
if (abonnements != null) {
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
}
// نهاية الانتظار
cancelWaiting();
}
- السطر 12: يلغي اشتراكًا وبالتالي مراقبة العملية المرتبطة به؛
1.17.9. التنفيذ
قم بتشغيل خدمة الويب (الفقرة 1.16.1.7)، وقم بتشغيل عميل Android وأعد إجراء الاختبارات التي قمت بها مع المثال السابق (الفقرة 1.16.2.8).
1.17.10. إدارة الإلغاء
نعيد إجراء نفس الاختبارات التي أجريناها في المثال السابق (الفقرة 1.16.2.9).
الاختبار 1
اطلب 5 أرقام في حين أن الخادم لم يتم تشغيله. لدينا السجلات التالية:
بعد السطر 7، لم يعد هناك سجلات، مما يدل على أن المراقب (Vue1Fragment) لم يعد يتلقى إخطارات من العملية المراقبة.
الاختبار 2
الآن، لنقم بتشغيل الخادم ونطلب 5 أرقام بفاصل زمني مدته 5 ثوانٍ ونضغط على [Annuler] قبل انتهاء هذا الفاصل الزمني. السجلات هي كما يلي:
بعد السطر 6، لم يعد هناك سجلات، مما يدل على أن المراقب (Vue1Fragment) لم يعد يتلقى إخطارات من العملية المراقبة.
هذا هو السلوك المتوقع للإلغاء. لذلك يمكننا في كود [Vue1Fragment] إزالة المتغير المنطقي [hasBeenCanceled] الذي أدخلناه في المثال السابق لأن الإلغاء لم يقم بما كان متوقعًا منه.
حقيقة أن المراقب لم يعد يتلقى إخطارات بعد إلغاء القابل للمراقبة، لا تعني أن الطلبات HTTP قد تم إلغاؤها هي نفسها. وللتأكد من ذلك، نقوم بتعديل الفئة [Dao] بالطريقة التالية:
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// تشغيل عميل الويب
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// الانتظار
waitSomeTime(delay);
// استدعاء HTTP متزامن
Response<Integer> response= webClient.getAlea(a, b);
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
} catch (JsonProcessingException e) {
Log.d(String.format("%s", className),"erreur désérialisation jSON");
}
}
return response;
}
});
}
- الأسطر 15-21: نقوم بتسجيل نتيجة الطلب HTTP من السطر 14؛
تكون سجلات الاختبار رقم 2 كما يلي:
- السطور 1-5: تم إجراء الطلبات الخمسة؛
- السطر 6: قام المستخدم بالإلغاء؛
- السطور 7-11: تم استلام الردود على الطلبات الخمسة HTTP. ولكن، بسبب إلغاء المراقب، لم يتم إرسال هذه العناصر إلى المراقب؛
1.17.11. الخلاصة
في بقية هذا المستند، سيتم تنفيذ تطبيقات العميل/الخادم باستخدام المكتبة RxAndroid بدلاً من المكتبة AA للأسباب التالية:
- يمكن استخدام RxAndroid في تطبيق Android لا يستخدم AA؛
- تقوم مكتبة RxAndroid بأكثر من مجرد تسهيل العمليات غير المتزامنة. فهي توفر العديد من الطرق لإنشاء عنصر قابل للمراقبة جديد من عنصر آخر. ولا يوجد ما يعادل هذه الطرق في مكتبة AA؛
- بمجرد أن نرغب في اشتقاق فئة مُعلَّمة بـ AA، مثل جزء، نواجه مشاكل خطيرة. عندئذ نضطر إلى التخلي عن AA واستخدام الحل 1 للبرمجة غير المتزامنة؛
يمكن للقارئ المهتم بالتعمق في إمكانيات مكتبة RxAndroid قراءة الوثيقة [Introduction à RxJava. Application aux environnements Swing et Android]. حيث يتم فيها استخدام RxAndroid بدون مكتبة AA.
1.18. مثال-17: مكونات إدخال البيانات
سنقوم بكتابة مشروع جديد لتقديم بعض المكونات الشائعة في نماذج إدخال البيانات.
1.18.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-13] إلى [Exemple-17]:
![]() | ![]() |
سيحتوي المشروع الجديد على عرض واحد فقط [vue1.xml]. لذلك، نقوم بحذف العرض [vue2.xml] والجزء المرتبط به [Vue2Fragment] [2]. نأخذ هذا التعديل في الاعتبار في مدير الأجزاء لـ [Mainactivity]:
// يجب إعادة تعريف مدير الأجزاء الخاص بنا لكل تطبيق
// يجب أن يحدد الطرق التالية: getItem، getCount، getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء
private final Fragment[] fragments = {new Vue1Fragment_()};
....
}
أعد تشغيل المشروع. يجب أن تظهر العرض رقم 1 كما في السابق. سنعمل انطلاقًا من هذا المشروع.
1.18.2. عرض XML للنموذج
![]() |
الطريقة التي ينتجها الملف [vue1.xml] هي التالية:

النص XML للعرض هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="30dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textViewFormulaireTitre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
<Button
android:id="@+id/formulaireButtonValider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/TextViewFormulaireCombo"
android:layout_below="@+id/TextViewFormulaireCombo"
android:layout_marginTop="30dp"
android:text="@string/formulaire_valider"/>
<TextView
android:id="@+id/textViewFormulaireCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTitre"
android:layout_below="@+id/textViewFormulaireTitre"
android:layout_marginTop="30dp"
android:text="@string/formulaire_checkbox"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireCheckBox"
android:layout_below="@+id/textViewFormulaireCheckBox"
android:layout_marginTop="30dp"
android:text="@string/formulaire_radioButton"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireRadioButton"
android:layout_below="@+id/textViewFormulaireRadioButton"
android:layout_marginTop="30dp"
android:text="@string/formulaire_seekBar"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireEdtText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireSeekBar"
android:layout_below="@+id/textViewFormulaireSeekBar"
android:layout_marginTop="30dp"
android:text="@string/formulaire_saisie"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireBool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireEdtText"
android:layout_below="@+id/textViewFormulaireEdtText"
android:layout_marginTop="30dp"
android:text="@string/formulaire_bool"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireDate"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_alignLeft="@+id/textViewFormulaireBool"
android:layout_below="@+id/textViewFormulaireBool"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@string/formulaire_date"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireMultilignes"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center"
android:layout_alignBaseline="@+id/textViewFormulaireTitre"
android:layout_alignParentTop="true"
android:layout_marginLeft="400dp"
android:layout_toRightOf="@+id/textViewFormulaireTitre"
android:text="@string/formulaire_multilignes"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireTime"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:gravity="center"
android:layout_alignLeft="@+id/textViewFormulaireMultilignes"
android:layout_below="@+id/textViewFormulaireMultilignes"
android:layout_marginTop="30dp"
android:text="@string/formulaire_time"
android:textSize="20sp"/>
<TextView
android:id="@+id/TextViewFormulaireCombo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTime"
android:layout_below="@+id/textViewFormulaireTime"
android:layout_marginTop="30dp"
android:text="@string/formulaire_combo"
android:textSize="20sp"/>
<CheckBox
android:id="@+id/formulaireCheckBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"
android:layout_marginLeft="100dp"
android:layout_toRightOf="@+id/textViewFormulaireCheckBox"
android:text="@string/formulaire_checkbox1"/>
<RadioGroup
android:id="@+id/formulaireRadioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:orientation="horizontal">
<RadioButton
android:id="@+id/formulaireRadioButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton1"/>
<RadioButton
android:id="@+id/formulaireRadioButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radionbutton2"/>
<RadioButton
android:id="@+id/formulaireRadionButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton3"/>
</RadioGroup>
<SeekBar
android:id="@+id/formulaireSeekBar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_alignLeft="@+id/formulaireCheckBox1"/>
<EditText
android:id="@+id/formulaireEditText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:ems="10"
android:inputType="text">
</EditText>
<Switch
android:id="@+id/formulaireSwitch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireBool"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:text="@string/formulaire_switch"
android:textOff="Non"
android:textOn="Oui"/>
<TimePicker
android:id="@+id/formulaireTimePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireTime"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes"
android:timePickerMode="spinner"
/>
<EditText
android:id="@+id/formulaireEditTextMultiLignes"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"
android:layout_alignBottom="@+id/textViewFormulaireMultilignes"
android:layout_marginLeft="50dp"
android:layout_toRightOf="@+id/textViewFormulaireMultilignes"
android:ems="10"
android:inputType="textMultiLine">
</EditText>
<Spinner
android:id="@+id/formulaireDropDownList"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_alignBottom="@+id/TextViewFormulaireCombo"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes">
</Spinner>
<DatePicker
android:id="@+id/formulaireDatePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireDate"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:datePickerMode="spinner"
android:calendarViewShown="false">
</DatePicker>
<TextView
android:id="@+id/textViewSeekBarValue"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@+id/formulaireSeekBar"
android:text=""/>
</RelativeLayout>
المكونات الرئيسية للنموذج هي كما يلي:
| |
| |
| |
| |
| |
| |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
|
1.18.3. سلاسل الأحرف في النموذج
يتم تعريف سلاسل الأحرف في النموذج في الملف [res / values / strings.xml] التالي:
![]() |
<resources>
<string name="app_name">Exemple-17</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- عرض 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="formulaire_checkbox">Cases à cocher</string>
<string name="formulaire_radioButton">Boutons Radio</string>
<string name="formulaire_seekBar">Seek Bar</string>
<string name="formulaire_saisie">Champ de saisie</string>
<string name="formulaire_bool">Booléen</string>
<string name="formulaire_date">Date</string>
<string name="formulaire_time">Heure</string>
<string name="formulaire_multilignes">Champ de saisie multilignes</string>
<string name="formulaire_listview">Liste</string>
<string name="formulaire_combo">Liste déroulante</string>
<string name="formulaire_checkbox1">1</string>
<string name="formulaire_checkbox2">2</string>
<string name="formulaire_radiobutton1">1</string>
<string name="formulaire_radionbutton2">2</string>
<string name="formulaire_radiobutton3">3</string>
<string name="formulaire_switch"></string>
<string name="formulaire_valider">Valider</string>
</resources>
1.18.4. جزء النموذج
![]() |
الفئة [Vue1Fragment] هي كما يلي:
package exemples.android.fragments;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
// الجزء هو عرض يتم عرضه بواسطة حاوية الأجزاء
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// الحقول في العرض الذي يعرضه الجزء
@ViewById(R.id.formulaireDropDownList)
Spinner dropDownList;
@ViewById(R.id.formulaireButtonValider)
Button buttonValider;
@ViewById(R.id.formulaireCheckBox1)
CheckBox checkBox1;
@ViewById(R.id.formulaireRadioGroup)
RadioGroup radioGroup;
@ViewById(R.id.formulaireSeekBar)
SeekBar seekBar;
@ViewById(R.id.formulaireEditText1)
EditText saisie;
@ViewById(R.id.formulaireSwitch1)
Switch switch1;
@ViewById(R.id.formulaireDatePicker1)
DatePicker datePicker1;
@ViewById(R.id.formulaireTimePicker1)
TimePicker timePicker1;
@ViewById(R.id.formulaireEditTextMultiLignes)
EditText multiLignes;
@ViewById(R.id.formulaireRadioButton1)
RadioButton radioButton1;
@ViewById(R.id.formulaireRadioButton2)
RadioButton radioButton2;
@ViewById(R.id.formulaireRadionButton3)
RadioButton radioButton3;
@ViewById(R.id.textViewSeekBarValue)
TextView seekBarValue;
// قائمة منسدلة
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// يتم تحديد الزر الأول
radioButton1.setChecked(true);
// التقويم
datePicker1.setCalendarViewShown(false);
// seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// القائمة المنسدلة
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
@SuppressLint("DefaultLocale")
@Click(R.id.formulaireButtonValider)
protected void doValider() {
...
}
@Override
protected void updateFragment() {
// تهيئة محول القائمة المنسدلة
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
}
- الأسطر 22-49: يتم استرداد مراجع جميع مكونات النموذج XML [vue1] (السطر 18)؛
- السطر 58: تسمح الطريقة [setChecked] بتحديد زر اختيار أو مربع اختيار؛
- السطر 60: بشكل افتراضي، يعرض المكون [DatePicker] مربع إدخال التاريخ والتقويم. السطر 60 يزيل التقويم؛
- السطر 62: تتيح [SeekBar].setMax() تحديد القيمة القصوى لشريط الضبط. القيمة الدنيا هي 0؛
- الأسطر 63-74: يتم إدارة أحداث شريط الضبط. نريد، عند كل تغيير يقوم به المستخدم، عرض قيمة المقياس في [TextView] في السطر 49؛
- السطر 71: يمثل المعلمة [progress] قيمة المقياس؛
- الأسطر 76-79: قائمة بـ [String] التي سنربطها بالقائمة المنسدلة؛
- السطر 90: طريقة [updateFragment] الخاصة بالجزء. عند تنفيذها، تم تهيئة المتغير [activity] الخاص بالفئة الأصلية؛
- السطر 92: مصدر البيانات [list] مرتبط بمحول القائمة المنسدلة؛
- السطران 93-94: يتم ربط المحول [dataAdapter] بالقائمة المنسدلة [dropDownList]؛
- السطر 84: يتم ربط الطريقة [doValider] بالنقر على الزر [Valider]؛
تهدف الطريقة [doValider] إلى عرض القيم التي أدخلها المستخدم. ورمزها هو كما يلي:
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// قائمة الرسائل المراد عرضها
List<String> messages = new ArrayList<>();
// مربع الاختيار
boolean isChecked = checkBox1.isChecked();
messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
// أزرار الاختيار
int id = radioGroup.getCheckedRadioButtonId();
String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
// SeekBar
int progress = seekBar.getProgress();
messages.add(String.format("SeekBar [value=%d]", progress));
// حقل الإدخال
String texte = String.valueOf(saisie.getText());
messages.add(String.format("Saisie simple [value=%s]", texte));
// المفتاح
boolean état = switch1.isChecked();
messages.add(String.format("Switch [value=%s]", état));
// التاريخ
int an = datePicker1.getYear();
int mois = datePicker1.getMonth() + 1;
int jour = datePicker1.getDayOfMonth();
messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
// النص متعدد الأسطر
String lignes = String.valueOf(multiLignes.getText());
messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
// الوقت
int heure = timePicker1.getHour();
int minutes = timePicker1.getMinute();
messages.add(String.format("Heure [%d, %d]", heure, minutes));
// القائمة المنسدلة
int position = dropDownList.getSelectedItemPosition();
String selectedItem = String.valueOf(dropDownList.getSelectedItem());
messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
// العرض
doAfficher(messages);
}
- السطر 4: سيتم تجميع القيم التي تم إدخالها في قائمة رسائل؛
- السطر 6: تسمح الطريقة [CheckBox].isCkecked() بمعرفة ما إذا كان المربع محددًا أم لا؛
- السطر 9: تسمح الطريقة [RadioGroup].getCheckedButtonId() بالحصول على معرف زر الاختيار الذي تم تحديده أو -1 إذا لم يتم تحديد أي زر؛
- السطر 10: يتيح الرمز [activity.findViewById(id)] تحديد زر الاختيار المحدد والحصول على نصه؛
- السطر 13: تسمح الطريقة [SeekBar].getProgress() بالحصول على قيمة شريط الضبط؛
- السطر 19: تسمح الطريقة [Switch].isChecked() بمعرفة ما إذا كان المفتاح هو On (صحيح) أو Off (خطأ)؛
- السطر 22: تتيح الطريقة [DatePicker].getYear() الحصول على السنة المختارة باستخدام كائن [DatePicker]؛
- السطر 23: تتيح الطريقة [DatePicker].getMonth() الحصول على الشهر المحدد باستخدام كائن [DatePicker] في النطاق [0,11]؛
- السطر 24: تتيح الطريقة [DatePicker].getDayOfMonh() الحصول على يوم الشهر المحدد باستخدام كائن [DatePicker] في النطاق [1,31]؛
- السطر 30: تتيح الطريقة [TimePicker].getHour() الحصول على الساعة المحددة باستخدام كائن [TimePicker]؛
- السطر 31: تسمح الطريقة [TimePicker].getMinute() بالحصول على الدقائق المحددة باستخدام كائن [TimePicker]؛
- السطر 34: تتيح الطريقة [Spinner].getSelectedItemPosition() الحصول على موضع العنصر المحدد في قائمة منسدلة؛
- السطر 35: تتيح الطريقة [Spinner].getSelectedItem() الحصول على الكائن المحدد في قائمة منسدلة؛
الطريقة [doAfficher] التي تعرض قائمة القيم المدخلة هي التالية:
private void doAfficher(List<String> messages) {
// يتم إنشاء النص المراد عرضه
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// عرضه
new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
- السطر 1: تتلقى الطريقة قائمة بالرسائل المراد عرضها؛
- الأسطر 3-6: يتم إنشاء كائن [StringBuilder] من هذه الرسائل. لربط السلاسل، يكون النوع [StringBuilder] أكثر فعالية من النوع [String]؛
- السطر 8: تظهر نافذة حوار نص السطر 3:

1.18.5. تنفيذ المشروع
قم بتنفيذ المشروع واختبر مكونات الإدخال المختلفة.
1.19. مثال-18: استخدام نموذج للعروض
1.19.1. إنشاء المشروع
نقوم بإنشاء مشروع جديد [Exemple-18] عن طريق نسخ المشروع [Exemple-13].
![]() | ![]() |
1.19.2. قالب العروض
نريد استعادة العرضين من المشروع وإدراجهما في قالب:
![]() |

سيتم تنظيم كل من العرضين بنفس الطريقة:
- في [1]، رأس الصفحة؛
- في [2]، عمود يسار قد يحتوي على روابط؛
- في [3]، تذييل الصفحة؛
- في [4]، محتوى.
يتم الحصول على ذلك عن طريق تعديل العرض الأساسي [activity_main.xml] للنشاط؛
![]() | ![]() |
الرمز XML للطريقة [main] هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="75dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/lavenderblushh2">
<TextView
android:id="@+id/textViewHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:text="@string/txt_header"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="0.8"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/left"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@color/lightcyan2">
<TextView
android:id="@+id/txt_left"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_left"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/floral_white"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</LinearLayout>
<LinearLayout
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/wheat1">
<TextView
android:id="@+id/textViewBottom"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_bottom"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
- يتم الحصول على العنوان [1] من الأسطر 38-54؛
- يتم الحصول على الشريط الأيسر [2] من الأسطر 56-84؛
- يتم الحصول على تذييل الصفحة [3] من الأسطر 86-101؛
- يتم الحصول على المحتوى [4] من الأسطر 78-84؛
تستخدم طريقة العرض XML [main] المعلومات الموجودة في الملفين [res / values / colors.xml] و [res / values / strings.xml]:
![]() |
الملف [colors.xml] هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
<color name="lavenderblushh2">#EEE0E5</color>
<color name="lightcyan2">#D1EEEE</color>
<color name="wheat1">#FFE7BA</color>
</resources>
والملف [strings.xml] هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">exemple-12</string>
<string name="action_settings">Settings</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="textView_nom">Quel est votre nom :</string>
<string name="btn_Valider">Validez</string>
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
<string name="textView_bonjour">"Bonjour "</string>
<string name="txt_header">Header</string>
<string name="txt_left">Left</string>
<string name="txt_bottom">Bottom</string>
</resources>
قم بإنشاء سياق تشغيل لهذا المشروع وقم بتشغيله.
1.20. مثال-19: المكون [ListView]
يسمح المكون [ListView] بتكرار عرض معين لكل عنصر في قائمة. يمكن أن يكون العرض المتكرر بأي درجة من التعقيد، بدءًا من سلسلة أحرف بسيطة وصولًا إلى عرض يسمح بإدخال معلومات لكل عنصر في القائمة. سنقوم بإنشاء المكون [ListView] التالي:

تحتوي كل طريقة عرض في القائمة على ثلاثة مكونات:
- [TextView] للمعلومات؛
- [CheckBox]؛
- [TextView] قابل للنقر؛
1.20.1. إنشاء المشروع
نقوم بإنشاء مشروع جديد [Exemple-19] عن طريق نسخ المشروع [Exemple-18].
![]() | ![]() |
![]() |
سنقوم بتطوير المشروع كما هو موضح في [3].
1.20.2. الجلسة
![]() |
تخزن الجلسة البيانات المشتركة بين النشاط والأجزاء:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// قائمة بيانات
private List<Data> liste=new ArrayList<>();
// أدوات الاسترجاع والتعيين
...
}
- السطر 11: قائمة البيانات التي تستخدمها كلتا الطريقتين؛
الفئة [Data] هي كما يلي:
package exemples.android.architecture;
public class Data {
// البيانات
private String texte;
private boolean isChecked;
// منشئ
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
// مُستردات ومُعيّنات
...
}
- السطر 6: النص الذي سيغذي أول [TextView] لكل عنصر في القائمة؛
- السطر 7: القيمة المنطقية التي ستُستخدم لتحديد ما إذا كان سيتم تحديد [checkBox] لكل عنصر في القائمة أم لا؛
1.20.3. النشاط [MainActivity]
يصبح كود الطريقة [@AfterInject] كما يلي:
// حقن الجلسة
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// يتم إنشاء قائمة بيانات
List<Data> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new Data("Texte n° " + i, false));
}
}
- الأسطر 12-15: تهيئة قائمة البيانات الموجودة في الجلسة؛
1.20.4. العرض الأولي [Vue1]
![]() | ![]() |
تعرض طريقة العرض XML [vue1.xml] المنطقة [1] أعلاه. ورمزها هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue1"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView_titre"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:text="@string/btn_vue2" />
<ListView
android:id="@+id/listView1"
android:layout_width="600dp"
android:layout_height="200dp"
android:layout_alignParentLeft="true"
android:layout_below="@+id/button_vue2"
android:layout_marginLeft="30dp"
android:layout_marginTop="50dp" >
</ListView>
</RelativeLayout>
- الأسطر 7-16: المكون [TextView] [2]؛
- الأسطر 27-35: المكون [ListView] [4]؛
- الأسطر 18-25: المكون [Button] [3]؛
1.20.5. العرض المتكرر بواسطة [ListView]
![]() |
العرض المتكرر بواسطة [ListView] هو العرض [list_data] التالي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Libellé"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:text="@string/txt_dummy" />
<CheckBox
android:id="@+id/checkBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="37dp"
android:layout_toRightOf="@+id/txt_Libellé"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/textViewRetirer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Libellé"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="68dp"
android:layout_toRightOf="@+id/checkBox1"
android:text="@string/txt_retirer"
android:textColor="@color/blue"
android:textSize="20sp" />
</RelativeLayout>
- الأسطر 8-14: المكون [TextView] [1]؛
- الأسطر 16-23: المكون [CheckBox] [2]؛
- الأسطر 25-35: المكون [TextView] [3]؛
1.20.6. الجزء [Vue1Fragment]
![]() |
الجزء [Vue1Fragment] يدير العرض XML [vue1]. رمزه هو التالي:
package exemples.android.fragments;
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// حقول العرض المعروضة بواسطة الجزء
@ViewById(R.id.listView1)
protected ListView listView;
// محول القائمة
private ListAdapter adapter;
// تمت المبادرة
private boolean initDone = false;
@AfterViews
void afterViews() {
// الذاكرة
afterViewsDone = true;
}
@Click(R.id.button_vue2)
void navigateToView2() {
// التنقل إلى العرض 2
mainActivity.navigateToView(1);
}
public void doRetirer(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
// ربط البيانات بـ [ListView]
adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
initDone = true;
}
// في حالة (إعادة) إنشاء الجزء - في هذه الحالة يجب إعادة ربط ListView بمحوله
listView.setAdapter(adapter);
// في حالة تغيير أجزاء أخرى لمصدر البيانات - في هذه الحالة يجب تحديث ListView
adapter.notifyDataSetChanged();
}
}
- السطر 15: العرض XML [vue1] مرتبط بالجزء؛
- الأسطر 26-30: لا تقوم الطريقة [@AfterViews] بأي شيء. ومع ذلك، فهي ضرورية لتعيين المتغير [afterViewsDone] إلى true لأن هذا الأخير يستخدمه الفصل الأصلي [AbstractFragment]؛
- الأسطر 42-53: الطريقة [updateFragment] التي يتم استدعاؤها في كل مرة يصبح فيها الجزء مرئيًا. تمت كتابة الطريقة هنا كما لو أن الجزء يمكن أن يخرج من جوار الجزء المعروض وبالتالي يعيد تعيين دورة حياته. هذا ليس هو الحال هنا، ولكنه سيكون كذلك إذا كان للتطبيق 3 أجزاء بجوار واحد؛
- السطر 44: لا يحتاج محول [ListView] إلى التهيئة إلا مرة واحدة؛
- السطر 46: نربط هذا [ListView] بمحول من النوع [ListAdapter]. سنقوم بإنشاء هذه الفئة. وهي مشتقة من فئة [ArrayAdapter] التي سبق لنا استخدامها لربط البيانات بـ [ListView]. نمرر معلومات متنوعة إلى منشئ [ListAdapter]:
- مرجع إلى النشاط الحالي،
- معرف العرض الذي سيتم إنشاؤه لكل عنصر في القائمة،
- مصدر بيانات لتغذية القائمة،
- مرجع إلى الجزء. سيتم استخدام هذا المرجع لإدارة النقر على رابط [Retirer] في [ListView] باستخدام طريقة [doRetirer] في السطر 38؛
- السطر 50: يتم ربط المحول بـ [ListView]. وفي الوقت نفسه، يتم ربط مصدر البيانات [listes] بـ [ListView]. ستتم هذه العملية هنا في كل مرة يتم فيها عرض العرض رقم 1. في الواقع، لا يلزم إجراؤها إلا عند تنفيذ الطريقة [@AfterViews]. هنا يتم تنفيذ التعليمات بشكل متكرر للغاية. نحتاج إلى متغير منطقي يخبرنا أن الطريقة [@AfterViews] قد تم تنفيذها للتو، وبالتالي يجب إعادة ربط [ListView] بمحولها؛
- السطر 52: يتم تحديث [ListView]. في هذا المثال، لا فائدة من ذلك لأن العرض رقم 1 هو الوحيد الذي يمكنه تعديل مصدر بيانات [ListView]. ننتقل إلى حالة أكثر عمومية حيث يمكن للعرض رقم 2 أيضًا تغيير مصدر بيانات [ListView]. سنرى أمثلة على ذلك لاحقًا في هذا المستند. في هذه الحالة، عند الانتقال من العرض رقم 2 إلى العرض رقم 1، يجب تحديث [ListView] الخاص بالعرض رقم 1؛
1.20.7. المحول [ListAdapter] من [ListView]
![]() |
تقوم الفئة [ListAdapter]
- تقوم بتكوين مصدر البيانات لـ [ListView]؛
- تدير عرض العناصر المختلفة لـ [ListView]؛
- تدير أحداث هذه العناصر؛
ورمزها هو التالي:
package exemples.android.fragments;
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
// سياق التنفيذ
private Context context;
// معرف تخطيط عرض سطر من القائمة
private int layoutResourceId;
// بيانات القائمة
private List<Data> data;
// الجزء الذي يعرض [ListView]
private Vue1Fragment fragment;
// المحول
final ListAdapter adapter = this;
// الشركة المصنعة
public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
super(context, layoutResourceId, data);
// يتم حفظ المعلومات
this.context = context;
this.layoutResourceId = layoutResourceId;
this.data = data;
this.fragment = fragment;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
}
- السطر 5: الفئة [ListAdapter] توسع الفئة [ArrayAdapter]؛
- السطر 19: المنشئ؛
- السطر 20: لا تنسَ استدعاء منشئ الفئة الأصلية [ArrayAdapter] مع المعلمات الثلاثة الأولى؛
- الأسطر 22-25: يتم تخزين معلومات المنشئ؛
- السطر 29: سيتم استدعاء الطريقة [getView] بشكل متكرر بواسطة [ListView] لتوليد عرض العنصر رقم [position]. النتيجة [View] المعروضة هي مرجع إلى العرض الذي تم إنشاؤه.
رمز طريقة [getView] هو التالي:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// يتم إنشاء السطر الحالي لـ ListView
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// النص
TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
textView.setText(data.get(position).getTexte());
// مربع الاختيار
CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
checkBox.setChecked(data.get(position).isChecked());
// الرابط [Retirer]
TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
txtRetirer.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
fragment.doRetirer(position);
}
});
// إدارة النقر على مربع الاختيار
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.get(position).setChecked(isChecked);
}
});
// نقوم بعرض السطر
return row;
}
- السطر 2: تتلقى الطريقة ثلاثة معلمات. لن نستخدم سوى المعلمة الأولى؛
- السطر 4: يتم إنشاء عرض العنصر رقم [position]. وهو العرض [list_data] الذي تم تمرير معرفه كمعلمة ثانية إلى المنشئ. بعد ذلك، يتم استرداد مراجع مكونات العرض الذي تم إنشاء مثيل له للتو؛
- السطر 6: نسترد مرجع [TextView] رقم 1؛
- السطر 7: يتم تعيين نص له من مصدر البيانات الذي تم تمريره كمعلمة ثالثة إلى المنشئ؛
- السطر 9: يتم استرداد مرجع [CheckBox] رقم 2؛
- السطر 10: يتم تحديدها أو عدم تحديدها بقيمة مستمدة من مصدر بيانات [ListView]؛
- السطر 12: يتم استرداد مرجع [TextView] رقم 3؛
- الأسطر 13-18: إدارة النقر على الرابط [Retirer]؛
- السطر 16: ستتولى طريقة [Vue1Fragment].doRetirer إدارة هذه النقرة. يبدو من المنطقي بالفعل أن يتولى إدارة هذا الحدث الجزء الذي يعرض [ListView]. فهو يتمتع بنظرة عامة لا تمتلكها الفئة [ListAdapter]. وقد تم تمرير مرجع الجزء [Vue1Fragment] كمعلمة رابعة إلى منشئ الفئة؛
- السطور 20-25: يتم إدارة النقر على مربع الاختيار. تنعكس الإجراء الذي يتم عليه على البيانات التي يعرضها. وذلك للسبب التالي. [ListView] هي قائمة لا تعرض سوى جزء من هذه العناصر. وبالتالي، يكون عنصر من القائمة مخفيًا أحيانًا، ومعروضًا أحيانًا أخرى. عندما يجب عرض العنصر رقم i، يتم استدعاء الطريقة [getView] في السطر 2 أعلاه للموضع رقم i. سيقوم السطر 10 بإعادة حساب حالة مربع الاختيار استنادًا إلى البيانات المرتبطة به. لذلك يجب أن تحفظ هذه البيانات حالة مربع الاختيار بمرور الوقت؛
1.20.8. إزالة عنصر من القائمة
يتم إدارة النقر على الرابط [Retirer] في الجزء [Vue1Fragment] بواسطة الطريقة [doRetirer] التالية:
public void doRetirer(int position) {
// إزالة العنصر رقم [position] من القائمة
List<Data> liste = mainActivity.getListe();
liste.remove(position);
// تسجيل موضع التمرير للعودة إليه
// قراءة
// [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// موضع العنصر الأول المرئي بالكامل أو غير المرئي
int firstPosition = listView.getFirstVisiblePosition();
// إزاحة Y لهذا العنصر بالنسبة لأعلى ListView
// قياس ارتفاع الجزء المخفي
View v = listView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// يتم تحديث [ListView]
adapter.notifyDataSetChanged();
// يتم تحديد الموضع الصحيح في ListView
listView.setSelectionFromTop(firstPosition, top);
}
- السطر 1: يتم استلام الموضع في [ListView] للرابط [Retirer] الذي تم النقر عليه؛
- السطر 3: يتم استرداد قائمة البيانات؛
- السطر 4: يتم إزالة العنصر رقم [position]؛
- السطر 15: يتم تحديث [ListView]. بدون ذلك، لا يتغير شيء بصريًا.
- الأسطر 5-13، 17: عملية معقدة إلى حد ما. بدونها، يحدث ما يلي:
- يعرض [ListView] الأسطر 15-18 من قائمة البيانات،
- نحذف السطر 16،
- السطر 15 أعلاه يعيد تعيينه بالكامل ويقوم [ListView] بعرض الأسطر 0-3 من قائمة البيانات؛
مع الأسطر أعلاه، تتم عملية الحذف ويبقى [ListView] متمركزًا على السطر الذي يلي السطر المحذوف.
1.20.9. العرض XML [Vue2]
![]() | ![]() |
الرمز XML للعرض هو التالي:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue2"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textViewResultats"
android:layout_marginTop="25dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="@string/btn_vue1" />
<TextView
android:id="@+id/textViewResultats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="" />
</RelativeLayout>
- الأسطر 6-15: المكون [TextView] رقم 1؛
- الأسطر 26-33: المكون [TextView] رقم 2؛
- الأسطر 17-24: المكون [Button] رقم 3؛
1.20.10. الجزء [Vue2Fragment]
![]() | 123 ![]() |
تدير القطعة [Vue2Fragment] العرض XML [vue2]. رمزها هو التالي:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// حقول العرض
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@AfterViews
void initFragment(){
// الذاكرة
afterViewsDone=true;
}
@Click(R.id.button_vue1)
void navigateToView1() {
// ننتقل إلى العرض 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// يتم عرض عناصر القائمة التي تم تحديدها في العرض 1
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (Data data : mainActivity.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
}
}
يوجد الرمز المهم في الطريقة [updateFragment] في السطر 32:
- السطر 34: يتم حساب النص المراد عرضه في [TextView] رقم 2؛
- الأسطر 35-39: يتم تصفح قائمة البيانات المعروضة بواسطة [ListView]. وهي مخزنة في النشاط؛
- السطر 36: إذا تم تحديد البيانات رقم i، يتم إضافة العنوان المرتبط بها في نوع [StringBuilder]؛
- السطر 41: يعرض [TextView] النص المحسوب؛
1.20.11. التنفيذ
قم بإنشاء تكوين تنفيذ لهذا المشروع وقم بتنفيذه.
1.20.12. تحسين
في المثال السابق، استخدمنا مصدر بيانات List<Data> حيث كانت الفئة [Data] كما يلي:
package exemples.android.fragments;
public class Data {
// البيانات
private String texte;
private boolean isChecked;
// الشركة المصنعة
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
...
}
في السطر 7، استخدمنا قيمة منطقية لإدارة مربع الاختيار الخاص بعناصر [ListView]. غالبًا ما يتعين على [ListView] عرض البيانات التي يمكن تحديدها عن طريق تحديد خانة دون أن يحتوي عنصر مصدر البيانات على حقل منطقي يتوافق مع هذه الخانة. يمكننا عندئذٍ اتباع الخطوات التالية:
تصبح الفئة [Data] كما يلي:
package exemples.android.fragments;
public class Data {
// البيانات
private String texte;
// المنشئ
public Data(String texte) {
this.texte = texte;
}
// مُستردات ومُعيّنات
...
}
يتم إنشاء فئة [CheckedData] مشتقة من الفئة السابقة:
package exemples.android.fragments;
public class CheckedData extends Data {
// عنصر محدد
private boolean isChecked;
// منشئ
public CheckedData(String text, boolean isChecked) {
// الأصل
super(text);
// محلي
this.isChecked = isChecked;
}
// الوصول والضبط
...
}
يكفي بعد ذلك استبدال (MainActivity، ListAdapter، Vue1Fragment، Vue2Fragment) في جميع أنحاء الكود، النوع [Data] بالنوع [CheckedData]. على سبيل المثال في [MainActivity]:
@AfterInject
protected void afterInject() {
// سجل
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// يتم إنشاء قائمة بيانات
List<CheckedData> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new CheckedData("Texte n° " + i, false));
}
}
يتم توفير مشروع هذه النسخة لك تحت الاسم [Exemple-19B].
1.21. مثال-20: استخدام قائمة
1.21.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-19B] في المشروع [Exemple-20]:
![]() | ![]() |
![]() | 3 ![]() |
سنقوم بإزالة الأزرار من العرضين 1 و 2 لاستبدالها بخيارات قائمة [1-2].
1.21.2. تعريف القوائم
![]() |
يحدد الملف [res / menu / menu_vue1] قائمة العرض رقم 1:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuOptions"
app:showAsAction="ifRoom"
android:title="@string/menuOptions">
<menu>
<item
android:id="@+id/actionCacherMontrerTout"
android:title="@string/actionCacherMontrerTout"/>
<item
android:id="@+id/actionCacherMontrerActions"
android:title="@string/actionCacherMontrerActions"/>
<item
android:id="@+id/actionCacherMontrerActionsValider"
android:title="@string/actionCacherMontrerActionsValider"/>
</menu>
</item>
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue2"
android:title="@string/navigationVue2"/>
</menu>
</item>
</menu>
يتم تعريف عناصر القائمة من خلال المعلومات التالية:
- android:id: معرف العنصر؛
- android:title: اسم العنصر؛
- app:showsAsAction: يشير إلى ما إذا كان يمكن وضع عنصر القائمة في شريط الإجراءات الخاص بالنشاط. يشير [ifRoom] إلى أنه يجب وضع العنصر في شريط الإجراءات إذا كان هناك مكان متاح له؛
- يمكن أن يكون خيار القائمة نفسه قائمة فرعية (علامة <menu>، السطور 25 و29)؛
يحدد الملف [res / menu / menu_vue2] قائمة العرض رقم 2:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue1"
android:title="@string/navigationVue1"/>
</menu>
</item>
</menu>
1.21.3. إدارة القائمة في الفئة المجردة [AbstractFragment]
سنقوم بتحليل إدارة القائمة في الفئة الأم [AbstractFragment] للطريقتين:
package exemples.android.architecture;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// بيانات متاحة للفئات الفرعية
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
protected String className;
// النشاط
protected IMainActivity mainActivity;
protected Activity activity;
// الجلسة
protected Session session;
// القائمة
private Menu menu;
private int[] menuOptions;
private boolean initDone;
// منشئ
public AbstractFragment() {
// التشغيل
className = getClass().getSimpleName();
// سجل
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// الذاكرة
this.menu = menu;
// سجل
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
// يتم استرداد # خيارات القائمة إذا لم يتم ذلك من قبل
if (!initDone) {
// يتم استرداد # خيارات القائمة
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// ننقل قائمة الخيارات إلى مصفوفة
menuOptions = new int[menuOptionsIds.size()];
for (int i = 0; i < menuOptions.length; i++) {
menuOptions[i] = menuOptionsIds.get(i);
}
// نشاط
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
// الذاكرة
initDone = true;
}
// نطلب من الجزء الفرعي أن يبدأ
updateFragment();
}
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
...
}
// عرض خيارات القائمة -----------------------------------
protected void setAllMenuOptions(boolean isVisible) {
....
}
protected void setMenuOptions(MenuItemState[] menuItemStates) {
...
}
// تحديث الفئة الفرعية
protected abstract void updateFragment();
}
- السطر 42: تُظهر السجلات أن الطريقة [onCreateOptionsMenu] يتم استدعاؤها في كل مرة يتم فيها عرض الجزء. يتم استدعاؤها في وقت متأخر جدًا، وتحديدًا بعد استدعاء الطريقة [updateFragment]. وهذا يشير إلى أنه يمكن استخدامها لتحديث الجزء. وهذا ما سنفعله هنا (السطر 63)؛
- السطر 42: تحتوي الطريقة على معلمتين:
- [menu]: وهي قائمة فارغة؛
- [inflater]: أداة تسمح بإنشاء القائمة من وصفها الأولي. لن نستخدم هذه الإمكانية هنا لأننا سنستخدم تعليقًا توضيحيًا AA سيقوم بذلك نيابة عنا؛
- السطر 44: نقوم بحفظ القائمة. سنحتاج إليها لاحقًا؛
- السطران 52-53: نقوم بتخزين معرفات جميع عناصر القائمة في المصفوفة الموجودة في السطر 28؛
- الأسطر 55-57: تُظهر السجلات أنه عند استدعاء الطريقة [onCreateOptionsMenu]، تقوم الطريقة [Fragment.getActivity()] بإرجاع النشاط المرتبط بالجزء؛
- السطر 55: نقوم بتخزين النشاط كمثيل لفئة Android [Activity]؛
- السطر 56: نقوم بتخزين النشاط كمثيل لواجهة [IMainActivity]؛
- السطر 57: نقوم بتخزين الجلسة؛
- السطر 59: نلاحظ أن تهيئة الفئة قد تمت حتى لا نضطر إلى إعادة القيام بها (السطر 50)؛
- السطر 63: نطلب من الجزء الفرعي أن يقوم بالتحديث. وهذا ممكن لأن الجزء الفرعي مرئي ومرتبط في نفس الوقت بعرضه وقائمته؛
الطريقة [getMenuOptions] التي تسمح بالحصول على معرفات عناصر القائمة هي التالية:
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// يتم تصفح جميع عناصر القائمة
for (int i = 0; i < menu.size(); i++) {
// العنصر رقم i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// إذا كان العنصر رقم i قائمة فرعية، فإننا نبدأ من جديد
if (menuItem.hasSubMenu()) {
// التكرار
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
تسمح الطريقة [setAllMenuOptions] بإخفاء/إظهار جميع خيارات القائمة؛
protected void setAllMenuOptions(boolean isVisible) {
// يتم تحديث جميع خيارات القائمة
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
تسمح الطريقة [setMenuOptions] بإخفاء/إظهار بعض خيارات القائمة؛
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// يتم تحديث بعض خيارات القائمة
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
الفئة [MenuItemState] هي كما يلي:
![]() |
package exemples.android.architecture;
public class MenuItemState {
// معرف خيار القائمة
private int menuItemId;
// رؤية الخيار
private boolean isVisible;
// المنشئون
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// أدوات الحصول والتعيين
...
}
1.21.4. إدارة القائمة في الجزء [Vue1Fragment]
تصبح الفئة [Vue1Fragment] كما يلي:
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
...
@OptionsItem(R.id.navigationVue2)
void navigateToView2() {
// ننتقل إلى العرض 2
mainActivity.navigateToView(1);
}
@OptionsItem(R.id.actionValider)
void valider() {
// عرض رسالة
Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
}
private boolean actionCacherMontrerTout = true;
@OptionsItem(R.id.actionCacherMontrerTout)
void cacherMontrerTout() {
// تغيير الحالة
actionCacherMontrerTout = !actionCacherMontrerTout;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionCacherMontrerTout), new MenuItemState(R.id.menuActions, actionCacherMontrerTout)});
}
private boolean actionCacherMontrerActions = true;
@OptionsItem(R.id.actionCacherMontrerActions)
void actionCacherMontrerActions() {
// تغيير الحالة
actionCacherMontrerActions = !actionCacherMontrerActions;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
}
private boolean actionCacherMontrerActionsValider = true;
@OptionsItem(R.id.actionCacherMontrerActionsValider)
void actionCacherMontrerActionsValider() {
// تغيير الحالة
actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
}
...
@Override
protected void updateFragment() {
....
// يتم تحديث القائمة
//setMenuOptions(...)
}
}
- السطر 2: يتم ربط القائمة [res / menu / menu_vue1.xml] بالجزء؛
- السطر 48: عند تنفيذ الطريقة [updateFragment]، يمكن تحديث القائمة أيضًا لتعكس الحالة الجديدة للمقتطف؛
- السطر 7: التعليق التوضيحي [@OptionsItem(R.id.navigationVue2)] يوضح الطريقة التي يجب تنفيذها عند النقر على خيار القائمة [Navigation / Vue 2]؛
- الأسطر 19-25: لإخفاء فرع من القائمة، يكفي إخفاء الخيار الجذري الخاص به؛
- السطر 24: يتم إظهار/إخفاء الخيارات الجذرية [menuNavigation, menuActions]؛
- السطر 40: لإظهار خيار من فرع القائمة، لا يجب إظهار هذا الخيار فحسب، بل يجب أيضًا إظهار جميع الخيارات الموجودة عند الصعود من الخيار الفرعي إلى جذر القائمة؛
1.21.5. إدارة القائمة في المقتطف [Vue2Fragment]
نجد كودًا مشابهًا في جزء العرض رقم 2:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {
// حقول العرض
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// التنقل إلى العرض 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// يتم عرض عناصر القائمة التي تم تحديدها في العرض 1
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (CheckedData data : session.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
// يتم تحديث القائمة
// setMenuOptions(...)
}
}
- السطر 35: يتم عرض الخيار [Navigation / Vue 1]؛
- الأسطر 17-20: عند النقر على الخيار [Navigation / Vue1]، يتم استدعاء الطريقة [navigateToView1]؛
1.21.6. التنفيذ
أنشئ سياق تنفيذ لهذا المشروع وقم بتنفيذه.
1.22. مثال-21: إعادة هيكلة الفئة المجردة [AbstractFragment]
أظهر لنا المثال السابق أنه عندما يحتوي المقتطف على قائمة، فإن طريقة [onCreateOptionsMenu] الخاصة به هي المكان المناسب لطلب تحديث المقتطف:
- يتم استدعاؤها مرة واحدة فقط عندما يتم عرض الجزء؛
- عند استدعائها، يتم إجراء ارتباطات الجزء بنشاطه وعرضه وقائمته؛
لإثبات ذلك، نعود إلى المثال 12 الذي يتميز بوجود العديد من الأجزاء التي يمكن تعديل تجاورها. في هذا المثال، لم تكن الأجزاء تحتوي على قائمة. سنقوم بربط قائمة فارغة بها.
1.22.1. إنشاء المشروع
نقوم بنسخ المشروع [Exemple-12] في المشروع [Exemple-21]:
![]() | ![]() |
1.22.2. قائمة الأجزاء
![]() |
ستكون القائمة المضافة للأجزاء فارغة:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
</menu>
ما يجب فهمه هنا هو أن النشاط لديه بالفعل قائمة خاصة به [menu_main]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
عندما يكون للنشاط قائمة بالفعل، تضاف القائمة المرتبطة بالأجزاء إلى قائمة النشاط: وبالتالي تتوفر خيارات قائمتين. هنا، ستكون قائمة الأجزاء فارغة. وبالتالي لن نرى سوى قائمة النشاط.
1.22.3. الأجزاء
![]() |
نستعيد الفئة المجردة [AbstractFragment] من المثال السابق (انظر الفقرة 1.21.3). نربط القائمة [menu_fragment] بالجزأين:
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
في المقطعين [PlaceholderFragment] و [Vue1Fragment]، نتخلص مما يشير إلى الفئة المجردة القديمة [AbstractFragment].
1.22.4. التنفيذ
قم بتشغيل التطبيق وتأكد من أنه يعمل. تابع السجلات لترى متى يتم تنفيذ الطريقة [onCreateOptionsMenu] للفئة [AbstractFragment]. وهي الآن التي تستدعي الطريقة [updateFragment] للأجزاء الفرعية.
1.23. مثال-22: حفظ/استعادة حالة النشاط والأجزاء
1.23.1. المشكلة
نتناول هنا مشكلة تدوير جهاز Android (عمودي <--> أفقي). لتوضيح ذلك، نستعيد المثال 21 السابق:

إذا قمنا بتدوير الجهاز [1]، فسنحصل على العرض الجديد التالي:

نلاحظ أن:
- في [1]، اختفت علامة التبويب [Fragment n° 3]؛
- في [2]، النص المعروض هو بالفعل نص الجزء رقم 3 ولكن عداد الزيارات غير صحيح؛
خلال هذه الدورة، كانت السجلات كما يلي:
- السطر 1: نرى أن النشاط أعيد بناؤه بالكامل؛
- الأسطر 3-7: الأمر نفسه ينطبق على الأجزاء الخمسة التي تديرها النشاط؛
- السطر 21: سيتم عرض الجزء رقم 3. نرى أنه قبل الزيادة، رقم الزيارة هو 0؛
يمكننا إذن تفسير النتيجة التي تم الحصول عليها بعد الدوران على النحو التالي:
- تقوم الفئة [MainActivity] في البداية بإنشاء شريط علامات تبويب يحتوي على علامة تبويب واحدة، تحمل اسم [Vue 1]. هذه هي علامة التبويب التي نراها؛
- بعد تدوير الجهاز، يعيد مدير الصفحات [mViewPager] عرض نفس المقطع، أي المقطع رقم 3 في هذه الحالة. يجب أن نتذكر هنا أن علامات التبويب والمقاطع مفاهيم مختلفة ولها دورة حياة مختلفة. ستُنفَّذ الطريقة [updateFragment] الخاصة بالمقطع رقم 3:
public void updateFragment() {
// سجل
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// زيادة رقم الزيارة
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// نص معدل
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- السطر 7: يتم قراءة رقم الزيارة الأخير في الجلسة. لكن هذه الجلسة، مثل كل شيء آخر، أعيد بناؤها وأعيد رقم الزيارة إلى الصفر. وهذا ما يفسر النتيجة المعروضة في المقطع رقم 3؛
1.23.2. طرق حفظ/استعادة النشاط والأجزاء
1.23.2.1. الحل 1: النسخ الاحتياطي اليدوي
عند تدوير الجهاز، يتم استدعاء طريقتين من النشاط:
// إدارة النسخ الاحتياطي / استعادة النشاط ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// الأصل
super.onSaveInstanceState(outState);
// نسخ احتياطي لحالة النشاط
// ....
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// الأصل
super.onCreate(savedInstanceState);
// استعادة النشاط
// ...
}
- السطور 2-8: يتم استدعاء الطريقة [onSaveInstanceState] بواسطة النظام عند الدوران. هذا هو المكان الذي يمكن فيه حفظ النشاط. إذا لم يتم القيام بأي شيء، فلن يتم حفظ أي شيء. يجب أن يتم حفظ حالة النشاط في المعلمة [Bundle outState] التي يتم تمريرها إلى الطريقة. تشبه الفئة [Bundle] القاموس. وهي تحتوي على طرق [putString, putInt, putLong, putBoolean, putChar, ...] ذات معلمتين: void putT(String key, T value)؛
- الأسطر 10-16: يتم استدعاء الطريقة [onCreate] عند إنشاء النشاط. إذا تم حفظ حالة النشاط، يتم تمرير هذا الحفظ إليه في المعلمة [Bundle savedInstanceState]. لاسترداد القيم المحفوظة، تتوفر طرق مثل [getString, getInt, getLong, geBoolean, getChar, ...] ذات المعلمة الواحدة: T getT(String key)؛
تتوفر هاتان الطريقتان نفسها للأجزاء لحفظ حالتها.
سنستخدم هذه المعلومات لحفظ واستعادة حالة المثال 21. وللقيام بذلك، نقوم بنسخ المشروع [Exemple-21] في [Exemple-22].
1.23.2.2. الحل 2: الحفظ التلقائي
تشير وثائق Android إلى أنه عند تدوير الجهاز، يمكن تجنب تدمير جزء باستخدام الأمر: [Fragment].setRetainInstance(true). توصي العديد من مقالات [StackOverflow] باستخدام هذه التعليمات فقط للأجزاء التي لا تحتوي على واجهة مرئية [http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, http://stackoverflow.com/questions/12640316/further-understanding-setretaininstancetrue, http://stackoverflow.com/questions/21203948/setretaininstancetrue-in-oncreate-fragment-in-android]. لقد اختبرت هذه التعليمات على مثالين: المثال-17 (الفقرة 1.18 - تطبيق مكون من جزء واحد يعرض نموذجًا) والمثال-21 (الفقرة 1.22)، تطبيق مكون من خمسة أجزاء). في كلتا الحالتين، تبين أن هذه التعليمات وحدها، عند تطبيقها على جميع أجزاء التطبيق، غير كافية لاستعادة العرض الذي كان معروضًا بشكل صحيح عند تدوير الجهاز. بدلاً من إنشاء نموذجين، أحدهما يعتمد على [setRetainInstance(true)] والآخر يعتمد على [setRetainInstance(false)] وهو القيمة الافتراضية، قررت اتباع توصيات [StackOverflow] والاحتفاظ بالقيمة false كقيمة افتراضية لطريقة [setRetainInstance(boolean )]. لم يتم استخدام التعليمات: [Fragment].setRetainInstance(true) مطلقًا في بقية هذا المستند.
1.23.3. طريقة النسخ الاحتياطي/الاستعادة للمشروع [Exemple-22]
يتطور المشروع [Exemple-22] على النحو التالي:
![]() |
نرى ظهور فئتين جديدتين:
- [PlaceHolderFragmentState] التي ستخزن حالة جزء من النوع [PlaceHolderFragment]؛
- [Vue1FragmentState] التي ستخزن حالة الجزء من النوع [Vue1Fragment]؛
هذه الفئات هي التالية:
package exemples.android;
public class Vue1FragmentState {
// الحالة Vue1Fragment
private boolean hasBeenVisited=false;
// مُستردات ومُعيّنات
...
}
- السطر 5: يكون القيمة المنطقية [hasBeenVisited] صحيحة إذا تمت زيارة (عرض) الجزء [Vue1Fragment] مرة واحدة على الأقل. تم إنشاء هذا الحقل للمثال لأن الجزء [Vue1Fragment] ليس لديه ما يحفظه؛
الفئة [PlaceHolderFragmentState] هي كما يلي:
package exemples.android;
public class PlaceHolderFragmentState {
// الحالة تمت زيارتها أم لا
private boolean hasBeenVisited;
// النص المعروض
private String text;
// الوصول والإعداد
...
}
- السطر 5: نجد القيمة المنطقية [hasBeenVisited]؛
- السطر 7: النص الذي يعرضه المقتطف في اللحظة التي يجب حفظها. وقد رأينا أن هذا النص قد فُقد أثناء عملية التدوير؛
سيتم تخزين حالة الأجزاء في الجلسة، وستتولى النشاط مسؤولية حفظ/استعادة هذه الجلسة. تتطور الجلسة على النحو التالي:
package exemples.android;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// عدد الأجزاء التي تمت زيارتها
private int numVisit;
// رقم الجزء من النوع [PlaceholderFragment] المعروض في علامة التبويب الثانية
private int numFragment = -1;
// رقم علامة التبويب المحددة
private int selectedTab = 0;
// رقم العرض الحالي
private int currentView;
// نسخ احتياطية للأجزاء ---------------
private Vue1FragmentState vue1FragmentState;
private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
// المنشئ
public Session() {
for (int i = 0; i < placeHolderFragmentStates.length; i++) {
placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
}
vue1FragmentState = new Vue1FragmentState();
}
// المُستردات والمُعيّنات
...
}
- السطر 18: حالة الجزء [Vue1Fragment]؛
- السطر 19: حالة الأجزاء من النوع [PlaceHolderFragment]؛
- الأسطر 22-27: في منشئ الجلسة، يتم تهيئة حقول الأسطر 18 و19؛
- الأسطر 12-15: يظهر حقلان جديدان:
- السطر 13: رقم آخر علامة تبويب تم تحديدها؛
- السطر 15: رقم آخر جزء معروض؛
تقوم النشاط بحفظ/استعادة الجلسة بالطريقة التالية:
// إدارة حفظ/استعادة النشاط ----------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// الأصل
super.onSaveInstanceState(outState);
// حفظ الجلسة
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// السجل
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// الأصل
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// استعادة الجلسة
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// سجل
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
}
- السطر 8: يتم حفظ الجلسة في شكل سلسلة jSON؛
- السطر 29: يتم استعادة الجلسة من سلسلتها jSON؛
لإدارة حفظ/استعادة الأجزاء، تتطور الفئة المجردة [AbstractFragment] على النحو التالي:
// إدارة الحفظ/الاستعادة -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// الأصل
super.setUserVisibleHint(isVisibleToUser);
// النسخ الاحتياطي؟
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// سيتم إخفاء الجزء - يتم نسخه احتياطيًا
saveFragment();
saveFragmentDone = true;
}
// الذاكرة
this.isVisibleToUser = isVisibleToUser;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// الأصل
super.onActivityCreated(savedInstanceState);
// سجل
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// يجب استعادة المقطع
fragmentHasToBeInitialized = true;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// سجل
if (isDebugEnabled) {
Log.d(className, "onSaveInstanceState");
}
// الأصل
super.onSaveInstanceState(outState);
// حفظ الجزء فقط إذا كان مرئيًا
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// الفئات الفرعية
protected abstract void updateFragment();
protected abstract void saveFragment();
- يتم اتخاذ قرار بحفظ حالة الأجزاء في الجلسة في لحظتين:
- الأسطر 2-14: عندما ينتقل الجزء من حالة "مرئي" إلى "مخفي"؛
- الأسطر 29-42: عندما يشير النظام إلى ضرورة إجراء نسخ احتياطي للجزء وأن هذا الجزء مرئي (السطر 38)؛
تتجنب هذه الآلية إجراء عمليات حفظ أكثر من اللازم. في الواقع، نظرًا لأننا قمنا بحفظ حالة الجزء i عندما انتقل من حالة "مرئي" إلى "مخفي"، فعندما يتم عرض الجزء j ونقوم بإجراء دوران، لا داعي لحفظ الجزء i مرة أخرى. إذا لم يتم إعادة عرضه منذ آخر عملية حفظ، فإن حالته لم تتغير. يجب حفظ حالة الجزء j فقط. لهذه الآلية ميزة أخرى أيضًا: لا نحتاج إلى حفظ حالة جزء ما فقط عند تدوير الجهاز. هناك أيضًا حالة التنقل البحت بين الأجزاء، على سبيل المثال في نظام علامات التبويب. نريد عندئذ العثور على جزء في الحالة التي تركناه عليها آخر مرة تم عرضه فيها. قد يكون هذا الوضع قد اختفى جزئيًا إذا خرج هذا الجزء في وقت ما من جوار الأجزاء المعروضة. في هذه الحالة، لا يتم إعادة بناء الجزء بالكامل، ولكن يتم إعادة بناء العرض المرتبط به. سيُستخدم الحفظ الذي تم إجراؤه عندما أصبح الجزء مخفيًا لاستعادة الحالة الأخيرة لهذا العرض؛
- السطران 10 و40: لتجنب إجراء نسختين احتياطيتين متتاليتين، يتم استخدام المتغير المنطقي [saveFragmentDone] للإشارة إلى أنه تم إجراء نسخة احتياطية؛
- السطران 9 و39: يُطلب من الجزء الفرعي حفظ حالته. الطريقة [saveFragment] هي طريقة مجردة (السطر 47). لذا، فإن على الفئات الفرعية تنفيذها؛
- الأسطر 16-26: تُستخدم الطريقة [onActivityCreated] لتعيين القيمة المنطقية [fragmentHasToBeInitialized] على "صحيح". في الواقع، يجب أن تعرف الشريحة الفرعية أنها يجب أن تعيد تعيين حالة الشريحة بالكامل انطلاقًا من حالة ستجدها في الجلسة؛
وفي الفئة [AbstractFragment] أيضًا، تتطور الطريقة [onCreateOptionsMenu] على النحو التالي:
// تحديث الجزء
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// الذاكرة
this.menu = menu;
// السجل
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
...
// يُطلب من الجزء الفرعي التحديث
updateFragment();
// يجب إجراء النسخ الاحتياطي
saveFragmentDone = false;
}
- السطر 14: رأينا أن القيمة المنطقية [saveFragmentDone] قد تغيرت إلى vrai عند إجراء عملية حفظ. يجب أن تعود في وقت ما إلى faux. عندما يتم تنفيذ الطريقة [updateFragment] (السطر 12) للجزء الفرعي، سيصبح هذا الجزء مرئيًا. ولكن يجب حفظ الجزء عندما يكون مرئيًا، في اللحظة المحددة التي ينتقل فيها من الحالة المرئية إلى الحالة المخفية. لذلك، يتم تعيين القيمة المنطقية [saveFragmentDone] إلى false حتى تتم عملية الحفظ؛
1.23.4. حفظ الجزء [Vue1Fragment]
يتم حفظ الأجزاء في الطريقة [saveFragment] التي تستدعيها الفئة الأم [AbstractFragment]:
// نسخ احتياطي لحالة الجزء
@Override
public void saveFragment() {
// سجل
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// نسخ احتياطي لحالة الجزء في الجلسة
Vue1FragmentState state = new Vue1FragmentState();
state.setHasBeenVisited(true);
session.setVue1FragmentState(state);
// سجل
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- الأسطر 9-11: حفظ حالة الجزء في الجلسة. عند استدعاء الطريقة [saveFragment]، يكون الجزء مرئيًا. لذلك يجب تعيين القيمة المنطقية [hasBeenVisited] إلى vrai (السطر 10)؛
1.23.5. حفظ الجزء [PlaceHolderFragment]
يتم حفظ الأجزاء في الطريقة [saveFragment] التي تستدعيها الفئة الأم [AbstractFragment]:
@Override
public void saveFragment() {
// يتم حفظ حالة الجزء في الجلسة
PlaceHolderFragmentState state = new PlaceHolderFragmentState();
state.setText(textViewInfo.getText().toString());
state.setHasBeenVisited(true);
session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
// سجل
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- الأسطر 4-7: حفظ حالة الجزء في الجلسة؛
- السطر 5: يتم حفظ النص المعروض حاليًا بواسطة [TextView] textViewInfo؛
- السطر 6: يتم تغيير القيمة المنطقية [hasBeenVisited] للجزء إلى vrai؛
- السطر 7: يتم تسجيل حالة الجزء في الجلسة في الجدول [placeHolderFragmentStates]. رقم العنصر المراد تهيئته هو رقم قسم الجزء ناقص واحد؛
1.23.6. استعادة الجزء [Vue1Fragment]
تتم استعادة الأجزاء في الطريقة [updateFragment]:
@Override
protected void updateFragment() {
// سجل
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
}
// استعادة؟
if (fragmentHasToBeInitialized) {
// استعادة الحالة
hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
fragmentHasToBeInitialized = false;
}
// سجل
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
}
// التنقل؟
boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
if (navigation) {
// زيادة رقم الزيارة
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// عرض رقم الزيارة
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
// تغيير رقم الصفحة الحالية
session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
}
- الأسطر 8-12: استعادة حالة الجزء. تم تهيئة القيمة المنطقية [fragmentHasToBeInitialized] بواسطة الفئة الأصلية [AbstractFragment]. عندما تكون قيمتها vrai، يكون الجزء قد أعيد بناؤه للتو ويجب إعادة تهيئته. هذا هو المكان الذي يحدث فيه ذلك. في هذا المثال بالتحديد، لا يوجد ما يجب فعله. لقد أظهرنا ببساطة أنه يمكننا العثور على قيمة القيمة المنطقية [hasBeenVisited] في الحالة المحفوظة للجزء (السطر 10)؛
- السطر 11: لا يجب نسيان إعادة تعيين [fragmentHasToBeInitialized] إلى faux، حتى لا نعيد تهيئة الجزء دون داعٍ عندما نعود إليه لاحقًا دون أن يكون الجهاز قد تم تدويره؛
- الأسطر 18-26: زيادة عداد الزيارات. هنا، توجد صعوبة: عند استعادة المقطع، لا نريد زيادة هذا العداد. علينا هنا التمييز بين:
- التنقل البسيط الذي يعيد المستخدم إلى علامة التبويب [Vue 1]؛
- استعادة عندما يقوم المستخدم بتدوير جهازه أثناء عرض علامة التبويب [Vue 1]؛
يمكن التمييز بين هاتين الحالتين بفضل رقم العرض المخزن في الجلسة. هذا الرقم هو رقم آخر عرض تم عرضه (السطر 28).
- السطر 18: يتم التنقل وليس الاستعادة إذا كان رقم آخر عرض مختلفًا عن رقم العرض الحالي؛
- الأسطر 21-25: زيادة عداد الزيارات وعرضه؛
1.23.7. استعادة الجزء [PlaceHolderFragment]
تتم استعادة الأجزاء في الطريقة [updateFragment]:
// البيانات
private String text;
private int numVisit;
private String newText;
private boolean hasBeenVisited = false;
private ObjectMapper jsonMapper = new ObjectMapper();
...
public void updateFragment() {
// سجل
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// ما هو الجزء المعني؟
int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
int numView = numSection - 1;
// هل يجب تهيئة الجزء؟
if (fragmentHasToBeInitialized) {
// النص الأولي
text = getString(R.string.section_format, numSection);
fragmentHasToBeInitialized = false;
}
// التنقل؟
boolean navigation = session.getCurrentView() != numView;
if (navigation) {
// زيادة رقم الزيارة
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// النص المعدل
newText = String.format("%s, visite %s", text, numVisit);
} else {
// نحن نتعامل مع عملية استعادة
PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
newText = state.getText();
}
// عرض النص
textViewInfo.setText(newText);
// العرض الحالي
session.setCurrentView(numView);
}
- السطران 15-16: يتم تحديد رقم العرض الذي يجري تحديثه؛
- السطور 18-22: الحالة التي يكون فيها الجزء في دورة حفظ/استعادة بعد تغيير اتجاه الجهاز. يجب هنا استعادته. يتعلق الأمر عمومًا باستعادة بعض حقول الجزء؛
- السطر 20: يجب أن يحتوي الحقل [text] في السطر 2 على النص الأولي الذي يعرضه الجزء: [Hello world from section i]. يجب إعادة إنشاؤه هنا؛
- السطر 21: نلاحظ أن تهيئة الجزء قد تمت؛
- الأسطر 24-36: كما سبق بالنسبة للجزء [Vue1Fragment]، لا يجب زيادة عداد الزيارات أثناء الاستعادة. وكما سبق، علينا التمييز بين التصفح والاستعادة؛
- الأسطر 32-36: حالة الاستعادة؛
- السطر 34: يتم استرداد حالة الجزء قبل تدوير الجهاز من الجلسة؛
- السطر 35: يتم استرداد النص الذي كان معروضًا في ذلك الوقت؛
- السطر 38: يتم عرض هذا النص مرة أخرى؛
- السطر 40: يتم تسجيل رقم العرض الجديد المعروض في الجلسة؛
1.23.8. إدارة علامات التبويب
لم تتطرق الفقرات السابقة إلى إدارة علامات التبويب. لكننا لاحظنا مشكلة في المثال 21 عند تدوير الجهاز: لم يتم الاحتفاظ سوى بعلامة التبويب الأولى [Vue 1]. أما علامة التبويب الثانية فقد فُقدت.
نحل هذه المشكلة في الفئة [MainActivity] بالطريقة التالية:
@AfterViews
protected void afterViews() {
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "afterViews");
}
// شريط الأدوات
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
// علامة التبويب الأولى
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// علامة التبويب الثانية؟
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
// أي علامة تبويب تختار؟
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- الأسطر 14-16: إنشاء علامة التبويب الأولى؛
- الأسطر 18-23: إنشاء علامة التبويب الثانية. لمعرفة ما إذا كان يجب إنشاؤها، نبحث في الجلسة عن رقم الجزء المعروض في علامة التبويب 2. إذا كان هذا الرقم مختلفًا عن -1، قيمته الأولية، يتم إنشاء علامة التبويب الثانية. في هذه المرحلة، لدينا علامتا تبويب يتم تحديد الأولى منهما افتراضيًا؛
- السطر 26: نبحث في الجلسة عن رقم علامة التبويب التي كانت محددة قبل الحفظ/الاستعادة ونحددها مرة أخرى. إذا لم يتم تهيئة الحقل [selectedTab] بعد بواسطة الكود، فسيتم استخدام قيمته الأولية 0؛











































































































































































































































































































































