1. تعلم برمجة Android
ملف PDF للوثيقة متاح |هنا|.
الأمثلة من المستند متاحة |هنا|.
1.1. مقدمة
1.1.1. المحتويات
هذه الوثيقة هي نسخة معدلة من عدة وثائق موجودة:
- مقدمة إلى برمجة أجهزة Android اللوحية من خلال الأمثلة؛
- التحكم في Arduino باستخدام جهاز لوحي يعمل بنظام Android؛
- مقدمة إلى برمجة أجهزة Android اللوحية من خلال الأمثلة - الإصدار 2
ويقدم الميزات الجديدة التالية:
- عرض المستند 1 بنية تسمى AVAT (Activity-Views-Actions-Tasks) لتسهيل البرمجة غير المتزامنة في تطبيق Android. في هذا المستند، تُستخدم مكتبة RxJava القياسية لإدارة الإجراءات غير المتزامنة؛
- استخدم المستند 2 بيئة تطوير Eclipse مع مكون إضافي لنظام Android. يستخدم هذا المستند Android Studio؛
- تم تضمين الوثيقة 3 كما هي؛
- استخدم المستند 4 مكتبة [Android Annotations] (AA) مع بيئة تطوير البرامج IntelliJ IDEA Community Edition. يعيد هذا المستند إنتاج المستند 4 بالكامل مع الاختلافات التالية:
- أصبح IDE الآن Android Studio؛
- نظام البناء هو Gradle لجميع مشاريع العميل أو الخادم (في الوثيقة 4، تم استخدام Maven في بعض الأحيان)
- يتم تنفيذ البرمجة غير المتزامنة باستخدام مكتبة RxJava (في الوثيقة 4، تم استخدام مكتبة AA)؛
- يستكشف هذا المستند المجالات التي لم يتم تناولها، أو تم تناولها بشكل موجز فقط، في المستندات السابقة:
- مفهوم تجاور الأجزاء؛
- حفظ/استعادة النشاط وأجزائه؛
- دورة حياة الأجزاء؛
وأخيرًا، تقدم هذه الوثيقة الهيكل الأساسي لعميل Android يتواصل مع خدمة ويب/JSON، حيث نقوم بتحديد عدد كبير من العناصر الشائعة في هذا النوع من العملاء. ويُستخدم هذا الهيكل الأساسي في جميع الأمثلة بدءًا من الفصل 2. وهذا هو الجزء المبتكر حقًا في هذه الوثيقة.
فيما يلي الأمثلة المعروضة:
الطبيعة | |
استيراد مشروع Android موجود | |
مشروع Android أساسي | |
مشروع [تعليقات أندرويد] أساسي | |
طرق العرض والأحداث | |
التنقل بين طرق العرض | |
التنقل بين علامات التبويب | |
استخدام مكتبة [Android Annotations] مع Gradle | |
إدارة الأجزاء في تطبيق Android | |
إعادة النظر في التنقل بين طرق العرض | |
بنية من طبقتين | |
بنية العميل/الخادم | |
التعامل مع عدم التزامن باستخدام RxJava | |
مكونات إدخال البيانات | |
استخدام نمط العرض | |
مكون ListView | |
استخدام القائمة | |
استخدام فئة أصلية للأجزاء | |
حفظ واستعادة حالة النشاط والأجزاء | |
عميل الطقس | |
هيكل عميل أندرويد يتواصل مع خدمة ويب / JSON. يأخذ في الاعتبار عددًا كبيرًا من العناصر الشائعة في هذا النوع من عملاء أندرويد. | |
إدارة المواعيد في العيادة الطبية | |
تمرين عملي - إدارة الرواتب الأساسية | |
تمرين عملي - طلب لوحات أردوينو |
استُخدم هذا المستند في السنة النهائية لكلية الهندسة IstiA بجامعة أنجيه [istia.univ-angers.fr]. وهذا ما يفسر النبرة غير المعتادة أحيانًا للنص. التمرينان العمليان هما واجبات معملية لا يُقدم بشأنها سوى الخطوط العريضة للحل. ويجب على القارئ تطوير الحل بنفسه.
يتوفر كود المصدر للأمثلة |هنا|. لتشغيل هذه الأمثلة، يجب اتباع الإجراءات الواردة في القسم 6.12.
هذا المستند هو دليل تمهيدي لبرمجة Android. ولا يُقصد به أن يكون شاملاً. وهو موجه في المقام الأول للمبتدئين.
الموقع المرجعي لبرمجة Android موجود على الرابط [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)؛
- مدير محاكي Genymotion Android (انظر القسم 6.9)؛
- مدير التبعيات Maven (انظر القسم 6.10)؛
- بيئة تطوير [Android Studio] (انظر القسم 6.11)؛
1.2. مثال-01: استيراد مثال لـ Android
1.2.1. إنشاء المشروع
لنقم بإنشاء أول مشروع Android لنا باستخدام Android Studio. أولاً، لنقم بإنشاء مجلد [examples] فارغ حيث سيتم تخزين جميع مشاريعنا:
![]() |
ثم قم بإنشاء مشروع باستخدام Android Studio. سنقوم أولاً باستيراد أحد الأمثلة المضمنة في بيئة التطوير المتكاملة [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"
}
// The sample build uses multiple directories to
// keep boilerplate and common code separate from
// the main sample code.
List<String> dirs = [
'main', // main sample code; look here for the interesting stuff.
'common', // components that are reused by multiple samples
'template'] // boilerplate code that is generated by the sample template process
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-Enter] على الاقتراح:
![]() |
قد تواجه أيضًا خطأً يتعلق بإصدار Gradle:
![]() |
ينشأ هذا الخطأ عن عدم تطابق بين إصدار Gradle المطلوب في ملف [build.gradle] الخاص بالمشروع (الإصدار 2.10 في السطر 6 أدناه):
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
والذي يرد في ملف [<project>/gradle/wrapper/gradle-wrapper.properties]:
#Wed Apr 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.
للوصول إلى الملف [<project>/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 أو إيقافه أو إعادة تشغيله. أولاً، هناك الأزرار الموجودة على شريط الأدوات:
![]() | ![]() | ![]() |
يقوم زر [إعادة التشغيل] [3] بإيقاف المشروع [2] ثم إعادة تشغيله [1].
1.2.2.3. إدارة ذاكرة التخزين المؤقت
يحتفظ Android Studio بذاكرة تخزين مؤقتة للمشاريع التي يديرها لجعل بيئة تطوير التطبيقات (IDE) سريعة الاستجابة قدر الإمكان. مع إصدار Android Studio 2.1 (مايو 2016)، غالبًا ما كانت هذه الذاكرة المؤقتة لا تعكس التغييرات التي تم إجراؤها للتو على الكود. في هذه الحالة، يجب عليك إبطال صلاحية الذاكرة المؤقتة:
![]() | ![]() |
مع Android 2.1 (مايو 2016)، كان لا بد من تنفيذ الخطوة السابقة عدة مرات، وأحيانًا لم يكن ذلك كافيًا لحل المشكلة المكتشفة. كان الحل هو تعطيل [Instant Run]:
![]() | ![]() |
- في [3-4]، تم تعطيل كل شيء؛
في كل ما يلي، عملنا باستخدام تكوين ذاكرة التخزين المؤقت هذه ولم نواجه أي مشكلات.
1.2.2.4. إدارة السجلات
عند تشغيل مشروع، يتم عرض السجلات في Android Monitor:
![]() | ![]() |
في علامة التبويب [Android Monitor] [1]، تُعرض السجلات في علامة التبويب [logcat] [2]. يتيح لك الزر [3] مسح السجلات. هذا الزر مفيد عندما تريد عرض السجلات الخاصة بعملية معينة:
- مسح السجلات؛
- على جهاز Android، قم بتنفيذ الإجراء الذي تريد السجلات الخاصة به؛
- السجلات التي تظهر هي تلك المتعلقة بالإجراء الذي تم تنفيذه؛
هناك عدة مستويات للسجلات [4]. بشكل افتراضي، يتم تحديد الوضع [Verbose]. وهذا يعني أنه يتم عرض السجلات من جميع المستويات. يمكنك استخدام [4] لتحديد مستوى معين.
تعد السجلات مفيدة جدًا لتحديد النقاط التي يتم فيها تنفيذ طرق معينة أثناء تنفيذ المشروع. سنستخدمها بشكل متكرر. دعونا نلقي نظرة على كود فئة [MainActivity] في مشروع [Example-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، السطر 14] و[onCreateOptionsMenu، السطر 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]. الحجة الأولى هي مصدر رسالة السجل. في الواقع، يمكن لمصادر مختلفة إرسال رسائل إلى وحدة تحكم السجل. للتمييز بينها، نستخدم هذه الحجة الأولى. الحجة الثانية هي الرسالة التي سيتم كتابتها إلى وحدة تحكم السجل؛
إذا قمنا بتشغيل مشروع [Example-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-Del] واحذف جميع عمليات VirtualBox:
![]() | ![]() |
بمجرد الانتهاء من ذلك، أعد تشغيل محاكي Genymotion من Android Studio.
1.2.2.6. إدارة ملف APK الثنائي الذي تم إنشاؤه
يؤدي تجميع المشروع إلى إنتاج ملف ثنائي بامتداد .apk:
![]() | ![]() | ![]() |
هناك نسختان: إحداهما تسمى [debug] والأخرى تسمى [debug-unaligned]. يجب عليك استخدام النسخة الأولى؛ أما النسخة الأخرى فهي نسخة انتقالية. يمكن نقل الملف الثنائي .apk الناتج في [4] مباشرةً إلى محاكي أو جهاز أندرويد. لنقله إلى محاكي، ما عليك سوى سحبه وإفلاته على المحاكي باستخدام الماوس.
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] خصائص ملف التطبيق الثنائي لنظام أندرويد. وفيما يلي محتواه:
<?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 من البيان (الحزمة) يأتي من الإدخال [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] إلى فئة [AppCompatActivity] في Android. وسيكون هذا هو الحال بالنسبة لجميع الأنشطة المستقبلية؛
- السطر 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]، حدد [تحرير التكوينات]؛
- تم إنشاء المشروع باستخدام تكوين [تطبيق]، وسنقوم بحذفه [2] لإعادة إنشائه؛
- في [3]، قم بإنشاء تكوين تشغيل جديد؛
![]() |
- في [4]، حدد [تطبيق Android]؛

- في [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]، قم بتشغيل تكوين تشغيل [التطبيق]؛
- في [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: إعادة كتابة مشروع [المثال-02] باستخدام مكتبة [Android Annotations]
سنقدم الآن مكتبة [Android Annotations]، التي تسهل كتابة تطبيقات Android. للقيام بذلك، قم بنسخ المثال [Example-02] إلى [Example-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>
الآن، نقوم بتشغيل التطبيق الجديد، الذي احتفظ بجميع إعدادات [مثال-02]:
![]() | ![]() |
في [19]، نحصل على نفس النتيجة التي حصلنا عليها في [المثال-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 {
// Since Android's Gradle plugin 0.11, you have to use 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.
// Generated using AndroidAnnotations 4.0.0.
//
// You can create a larger work that contains this file and distribute that work under terms of your choice.
//
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. إنشاء المشروع
سنتبع الإجراء الموصوف لنسخ [المثال-02] في [المثال-03] في القسم 1.4:
نقوم بما يلي:
- نسخ مشروع [المثال-03] إلى [المثال-04] (بعد حذف المجلد [app/build] من [المثال-03])؛
- نقوم بتحميل مشروع [مثال-04]؛
- تغيير اسم المشروع في الملف [app / res / values / strings.xml] (منظور Android)؛
- نحذف الملف [Example-04 / Example-04.iml] (منظور المشروع)؛
- نقوم بتجميع المشروع ثم تشغيله؛
![]() | ![]() |
1.5.2. إنشاء طريقة عرض
سنستخدم الآن المحرر الرسومي لتعديل العرض الذي يعرضه مشروع [Example-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]، حدد [نص كبير] واسحبه إلى العرض [2]؛
- في [3]، انقر نقرًا مزدوجًا على المكون؛
- في [4]، قم بتحرير النص المعروض. بدلاً من ترميزه بشكل ثابت في عرض XML، سنقوم بإخراجه إلى الملف [res/values/string.xml]
![]() | ![]() | ![]() |
- في [5]، أضف قيمة جديدة إلى ملف [strings.xml]؛
- في [8]، قم بتعيين معرف للسلسلة؛
- في [9]، قم بتعيين قيمة السلسلة؛
- في [10]، العرض الجديد بعد التحقق من صحة الخطوة السابقة؛
![]() | ![]() | ![]() |
- بعد النقر المزدوج على المكون، نقوم بتغيير معرفه [11]؛
- في [12]، في خصائص المكون، قم بتغيير حجم الخط [50pt]؛
- في [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]، يتم محاذاة الحافة العلوية للمكون مع الحافة السفلية لمكون [textViewTitreVue1] على مسافة 50 بكسل [3] (أعلى)؛
- في [2]، يتم محاذاة الحافة اليسرى (left) للمكون مع الحافة اليسرى للحاوية بمسافة 50 بكسل [3] (left)؛
يجب وضع المكون [editTextNom] على بعد 60 بكسل إلى يمين المكون [textView1] ومحاذاته في الأسفل مع ذلك المكون نفسه؛
![]() | ![]() |
- في [1]، يتم محاذاة الحافة اليسرى للمكون مع الحافة اليمنى لمكون [textView1] على مسافة 60 بكسل [2] (يسار). ويتم محاذاة الحافة السفلية (bottom:bottom) لمكون [textView1] [1]؛
يجب وضع مكون [buttonValider] على بعد 60 بكسل إلى يمين مكون [editTextNom] ومحاذاته في الأسفل مع ذلك المكون نفسه؛
![]() | ![]() |
- في [1]، يتم محاذاة الحافة اليسرى للمكون مع الحافة اليمنى لمكون [editTextNom] على مسافة 60 بكسل [2] (يسار). ويتم محاذاة الحافة السفلية للمكون مع الحافة السفلية لمكون [editTextNom] (bottom:bottom) [1]؛
يجب وضع المكون [buttonVue2] على بعد 50 بكسل أسفل المكون [textView1] ومحاذاته إلى يسار ذلك المكون؛
![]() | ![]() |
- في [1]، يتم محاذاة الحافة اليسرى للمكون مع الحافة اليسرى لمكون [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]. ويرجع ذلك إلى أن طريقة العرض تحتوي على حقل إدخال يكون محددًا عند عرضها. وبشكل افتراضي، يؤدي هذا التحديد إلى ظهور لوحة المفاتيح الافتراضية؛
قم بتشغيل التطبيق وتحقق من أن عرض [view1.xml] معروض بالفعل:

1.5.3. معالجة الأحداث
الآن دعونا نتعامل مع النقر على زر [Validate] في عرض [View1]:

يتغير كود [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 {
// visual interface elements
@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");
}
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
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)] الطريقة التي تتعامل مع حدث "Click" على الزر الذي يحمل المعرف [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]؟ للإجابة على هذا السؤال، قمنا بإضافة سجلات؛
قم بتشغيل مشروع [Example-04] وتحقق من حدوث شيء ما عند النقر فوق الزر [Validate]. نحصل على السجلات التالية:
نستنتج أنه عند تشغيل طريقة [onCreate]، لا تكون الحقول المُعلَّمة بـ [@ViewById] قد تم تهيئتها بعد. ومرة أخرى، ننصح القراء المبتدئين بتضمين هذا النوع من السجلات في الطرق التي تدير دورة حياة التطبيق.
1.6. مثال-05: التنقل بين العروض
في المشروع السابق، لم يتم استخدام زر [View 2]. نقترح الاستفادة منه عن طريق إنشاء عرض ثانٍ وتوضيح كيفية التنقل بين العروض. هناك عدة طرق لحل هذه المشكلة. النهج المقترح هنا هو ربط كل عرض بنشاط. طريقة أخرى هي وجود [AppCompatActivity] واحد يعرض عروض [Fragment]. ستكون هذه هي الطريقة المستخدمة في التطبيقات المستقبلية.
1.6.1. إنشاء المشروع
نقوم بنسخ مشروع [Example-04] إلى [Example-05]. للقيام بذلك، سنتبع الإجراء الموضح لنسخ [Example-02] إلى [Example-03] في القسم 1.4، والذي تم تكراره في القسم 1.5.
![]() | ![]() |
1.6.2. إضافة نشاط ثانٍ
لإدارة عرض ثانٍ، سننشئ نشاطًا ثانيًا. سيتولى هذا النشاط إدارة العرض رقم 2. نحن نتبع هنا نموذج "عرض واحد لكل نشاط". وهناك نماذج أخرى ممكنة.

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

- في [5]، اسم الفئة التي سيتم إنشاؤها؛
- في [6]، اسم العرض (view2.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 حاليًا:
![]() |
ونحن نتعامل مع الأمر على النحو التالي:
// navigate to view no. 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// navigate to view no. 2 by passing it the name entered in view no. 1
// create an Intent
Intent intent = new Intent();
// we associate this Intent with an activity
intent.setClass(this, SecondActivity.class);
// we associate information with this Intent
intent.putExtra("NOM", editTextNom.getText().toString().trim());
// launch the [SecondActivity] activity by passing it the 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].
قم بتشغيل مشروع [Example-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>
قم بتشغيل مشروع [Example-05] وتأكد من ظهور العرض الجديد عند النقر على زر [View #2].
1.6.5. نشاط [SecondActivity]
في [MainActivity]، كتبنا الكود التالي:
// navigate to view no. 2
protected void navigateToView2() {
// navigate to view no. 2 by passing it the name entered in view no. 1
// create an Intent
Intent intent = new Intent();
// we associate this Intent with an activity
intent.setClass(this, SecondActivity.class);
// we associate information with this Intent
intent.putExtra("NOM", edtNom.getText().toString().trim());
// launch the [SecondActivity] activity by passing it the 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 {
// visual interface components
@ViewById
protected TextView textViewBonjour;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews() {
// recover intent if it exists
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// we retrieve the name
String nom = extras.getString("NOM");
if (nom != null) {
// we display it
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)، لا يمكن استخدام هذه الحقول لأنها لم تُهيَّأ بعد. في مشروع [Example-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]
// navigate to view no. 2
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// we associate this Intent with an activity
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() {
// we create an Intent for activity [MainActivity]
Intent intent1 = new Intent();
intent1.setClass(this, MainActivity_.class);
// retrieve the Intent of the current activity [SecondActivity]
Intent intent2 = getIntent();
if (intent2 != null) {
Bundle extras2 = intent2.getExtras();
if (extras2 != null) {
// we put the name in the Intent of [MainActivity]
intent1.putExtra("NOM", extras2.getString("NOM"));
}
// launch [MainActivity]
startActivity(intent1);
}
}
- السطران 1-2: ربط طريقة [navigateToView1] بالنقر على زر [btn_vue1]؛
- السطر 4: نقوم بإنشاء [Intent] جديد؛
- السطر 5: ربطه بنشاط [MainActivity_]؛
- السطر 7: استرجاع Intent المرتبط بـ [SecondActivity]؛
- السطر 9: استرداد المعلومات من هذا Intent؛
- السطر 12: يتم استرداد المفتاح [NAME] من [intent2] ووضعه في [intent1] بنفس القيمة المرتبطة؛
- السطر 15: يتم تشغيل النشاط [MainActivity_].
في كود [MainActivity]، نضيف طريقة [@AfterViews] التالية:
@AfterViews
protected void afterViews() {
// recover intent if it exists
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// we retrieve the name
String nom = extras.getString("NOM");
if (nom != null) {
// we display it
editTextNom.setText(nom);
}
}
}
}
قم بإجراء هذه التغييرات واختبر تطبيقك. الآن، عند العودة من العرض 2 إلى العرض 1، يجب أن يظهر الاسم الذي أدخلته في الأصل، وهو ما لم يكن يحدث حتى الآن.
![]() | ![]() |
1.6.7. دورة حياة النشاط
في القسم 1.3.5، قدمنا دورة حياة النشاط. لدينا هنا نشاطان، ونقوم بالتبديل بينهما أثناء التنفيذ. تحتوي هذه الأنشطة على طريقتين — [onCreate] و [afterViews] — وليس من الواضح على الفور متى يتم استدعاء إحداهما بالنسبة للأخرى. من المهم معرفة ذلك. لمعرفة ذلك، سنضيف سجلات إلى كلا النشاطين:
لذا في فئة [MainActivity]، نكتب:
// manufacturer
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] للنشاط الأول بهذا الترتيب. عند النقر على زر [View #2]، تكون السجلات الجديدة كما يلي:
تم تنفيذ طريقتي [onCreate، afterViews] للنشاط الثاني بهذا الترتيب. عند النقر على زر [View #1]، تكون السجلات الجديدة كما يلي:
وبالتالي يتم إنشاء مثيل لفئة [MainActivity] مرة أخرى. عند النقر على زر [View #2]، تكون السجلات الجديدة كما يلي:
وبالتالي يتم إنشاء مثيل لفئة [SecondActivity] مرة أخرى.
وبالتالي، يتم إعادة إنشاء كلا النشاطين بشكل منهجي كلما تم تغيير النشاط.
سنستكشف الآن بنية تحتوي على نشاط واحد قادر على إدارة عدة طرق عرض تسمى الأجزاء. سيتم إنشاء النشاط وطرق العرض مرة واحدة فقط، على عكس الطريقة السابقة حيث كان من الممكن إنشاء النشاط عدة مرات.
1.7. مثال-06: التنقل بين علامات التبويب
سنستكشف هنا واجهات علامات التبويب. المثال معقد ولكنه يقدم جميع العناصر التي سنستخدمها لاحقًا: نشاط واحد، مدير الأجزاء (طرق العرض)، حاوية الأجزاء، التنقل بين الأجزاء. يختلف مفهوم علامات التبويب عن مفهوم الأجزاء وهو ثانوي بالنسبة لما نريد توضيحه في هذا المثال.
1.7.1. إنشاء المشروع
نقوم بإنشاء مشروع جديد:
![]() | ![]() |
![]() |
![]() |
- في [7]، حدد نشاطًا مبوبًا (Tabbed Activity)؛
![]() |
- في [10-14]، احتفظ بالقيم الافتراضية؛
- في [15]، حدد علامات التبويب التي تحتوي على شريط العنوان؛
المشروع الناتج هو كما يلي:
![]() | ![]() |
- في [1]، النشاط؛
- في [2]، الآراء؛
تم إنشاء تكوين وقت التشغيل [app]، الذي سمي على اسم الوحدة النمطية، تلقائيًا [2b]:
![]() |
يمكنك تشغيله. ثم تظهر نافذة بها ثلاث علامات تبويب [3-6]:

1.7.2. تكوين Gradle
تم إنشاء المشروع [Example-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)، الذي يشكل العرض بأكمله. يمكننا أن نرى أنه تخطيط [CoordinatorLayout] (السطر 2)؛
- الأسطر 11-33: حاوية [appbar] (السطر 12). هذا هو [AppBarLayout] (السطر 11)؛
- الأسطر 18–24: مكون [toolbar] (السطر 19) من النوع [Toolbar] (السطر 18)؛
- الأسطر 28-31: حاوية [tabs] (السطر 29). هذا تخطيط من النوع [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>
<!-- Default screen margins, per the Android Design guidelines. -->
<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] إلى السطر (و) أعلاه؛
وبالمثل، يشير التعليق التوضيحي:
- [@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 {
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// the fragment container
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// view
setContentView(R.layout.activity_main);
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of the container
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
// a fragment
public static class PlaceholderFragment extends Fragment {
...
}
// the fragment manager
// it is used to request fragments to be displayed in the main view
// must define methods [getItem] and [getCount] - the others are optional
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] هو كما يلي:
// the fragment manager
// it is used to request fragments to be displayed in the main view
// must define methods [getItem] and [getCount] - the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment n° position
@Override
public Fragment getItem(int position) {
// instantiate a fragment [PlaceHolder] and render it
return PlaceholderFragment.newInstance(position + 1);
}
// makes the number of fragments managed
@Override
public int getCount() {
return 3;
}
// optional - gives a title to managed fragments
@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: تخطيط [RelativeLayout]؛
- الأسطر 11-14: المكون الوحيد للعرض (الجزء): [TextView] محدد بـ [section_label]؛
في [MainActivity]، تكون الأجزاء المدارة من النوع [PlaceholderFragment] التالي:
// a fragment
public static class PlaceholderFragment extends Fragment {
// a text displayed in the fragment
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
// renders a fragment with one piece of information: the fragment number passed as a parameter
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// on-board info
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// result
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// view [fragment_main] is instantiated
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// the [TextView] is found
TextView textView = (TextView) rootView.findViewById(R.id.section_label);
// its content is modified
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
// we return the view
return rootView;
}
}
- السطر 2: فئة [PlaceholderFragment] تمتد من فئة [Fragment] في Android. وهذا هو الحال دائمًا بشكل عام؛
- السطر 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 في السطر (d) أعلاه؛
- تُرجع [getArguments] الإشارة إلى حزمة حجج الجزء. من المهم ملاحظة هنا أن كل حجة تم إنشاؤها باستخدام الحزمة التالية (الأسطر f-h):
// renders a fragment with one piece of information: the fragment number passed as a parameter
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// on-board info
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// result
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) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in 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");
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
// parent
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] على النحو التالي:
// floating button
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]:
// manufacturer
public MainActivity(){
Log.d("MainActivity","constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
// parent
super.onCreate(savedInstanceState);
...
}
// a fragment
public static class PlaceholderFragment extends Fragment {
// a text displayed in the fragment
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
// renders a fragment with one piece of information: the fragment number passed as a parameter
public static PlaceholderFragment newInstance(int sectionNumber) {
Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
// fragment
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: إنشاء قائمة النشاط؛
هنا، يجب أن نتذكر الكود المسؤول عن إنشاء الأجزاء:
// the fragment manager
// it is used to request fragments to be displayed in the main view
// must define methods [getItem] and [getCount] - the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment n° position
@Override
public Fragment getItem(int position) {
// instantiate a fragment [PlaceHolder] and render it
return PlaceholderFragment.newInstance(position + 1);
}
...
- الأسطر 11–15: يتم إنشاء مثيل للجزء بواسطة [newInstance] في كل مرة يطلب فيها حاوية الأجزاء ذلك؛
تُظهر السجلات أعلاه أن أول جزأين قد تم إنشاء مثيلين لهما وتهيئتهما.
الآن، لنضغط على علامة التبويب رقم 2. السجلات الجديدة هي كما يلي:
- الأسطر 1–3: يتم إنشاء مثيل Fragment #3 وتهيئته. تذكر أن Fragment #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. إنشاء المشروع
سنقوم بنسخ مشروع [مثال-06] إلى [مثال-07] لإدخال تعليقات Android في الأخير. للقيام بذلك، اتبع الإجراء الوارد في القسم 1.4. نحصل على النتيجة التالية:
![]() | ![]() |
1.8.2. تكوين Gradle
![]() |
نقوم بتحديث ملف [build.gradle] على النحو التالي:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use 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 {
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// the fragment container
@ViewById(R.id.container)
protected MyPager mViewPager;
// the tab manager
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// the floating button
@ViewById(R.id.fab)
protected FloatingActionButton fab;
// manufacturer
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of the container
tabLayout.setupWithViewPager(mViewPager);
// floating button
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 للأجزاء.
يتطور مشروع [Example-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;
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// visual interface component
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
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)));
// parent
super.onResume();
// display
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: أدخل في حقل [textViewInfo] الإشارة إلى المكون في [fragment_main.xml] المحدد بواسطة [R.id.section_label]، وهو من النوع [TextView] (السطر (l) أدناه):
<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. ستخبرنا السجلات التي تم إعدادها لتتبع دورة حياة الجزء بذلك. في الوقت الحالي، كإجراء احترازي، نقوم بإجراء فحص null؛
- السطر 51: نقوم بتحديث المعلومات الموجودة في حقل [textViewInfo] باستخدام الحجة الصحيحة التي تم تمريرها إلى الجزء أثناء إنشائه؛
تفقد فئة [MainActivity] فئتها الداخلية [PlaceholderFragment] وترى مدير الأجزاء الخاص بها يتطور على النحو التالي:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private Fragment[] fragments;
// number of fragments
private static final int FRAGMENTS_COUNT = 3;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialization of fragment table
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
}
// fragment n° position
@Override
public Fragment getItem(int position) {
Log.d("MainActivity", String.format("getItem[%s]", position));
return fragments[position];
}
// makes the number of fragments managed
@Override
public int getCount() {
return fragments.length;
}
// optional - gives a title to managed fragments
@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])؛
قم بتشغيل مشروع [Example-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)));
// parent
super.onResume();
// display
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. تكون السجلات عندئذٍ كما يلي:
يمكننا أن نرى أن جميع الطرق في Fragment 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;
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
...
@Override
public void onDestroyView() {
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// parent
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: يتم إعادة تنفيذ دورة حياة Fragment 1 لأنها خضعت لـ [onDestroyView]؛
- السطر 5: يتم الآن تنفيذ طريقة [onDestroyView] الخاصة بالجزء 3. ومرة أخرى، عندما يعود المستخدم إلى الجزء 3 أو إلى جزء مجاور، سيتم إعادة تنفيذ دورة حياة هذا الجزء؛
1.8.7. setUserVisibleHint
تقوم طريقة [onCreateView] في دورة الحياة بإنشاء مثيل للعرض المرتبط بالجزء ولكنها لا تجعله مرئيًا بالضرورة. وهذا ما سنراه الآن. يتم تنفيذ طريقة [Fragment.setUserVisibleHint] في كل مرة تتغير فيها رؤية الجزء. نضيف هذه الطريقة إلى كود الجزء:
package exemples.android;
....
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// visual interface component
@ViewById(R.id.section_label)
protected TextView textViewInfo;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// log
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 أن Fragment 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 = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// fragment offset
mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of the container
tabLayout.setupWithViewPager(mViewPager);
// floating button
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
- يمكننا أن نرى أنه عندما يصبح Fragment 1 مرئيًا، فإن طريقة العرض الخاصة به لم يتم إنشاؤها بعد. لذلك، لا يمكننا التفاعل معه. يمكن القيام بذلك خلال دورة حياة الجزء، على سبيل المثال في طريقة [onCreateView] (السطر 11) أو طريقة [onResume] (السطران 13-14). نظرًا لأننا نستخدم تعليقات AA، فإننا عادةً لا نحتاج إلى كتابة طريقة [onCreateView]. لذلك، تبدو طريقة [onResume] هي الأنسب هنا لتحديث Fragment 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] الخاصة بـ Fragment 1 (السطران 6–7) لتحديث Fragment 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]، نتعامل مع النقر على خيار [إنهاء]:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in 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");
//we finish the activity
finish();
return true;
}
// parent
return super.onOptionsItemSelected(item);
}
- الأسطر 14–19: انسخ والصق الأسطر 10–13 وقم بتكييف الكود مع الخيار الجديد؛
- السطر 17: يتم إنهاء النشاط بواسطة إجراء برمجي؛
الآن دعونا نشغل هذه النسخة الجديدة، وبمجرد ظهور العرض الأول، انقر على خيار القائمة [إنهاء]. تكون السجلات عندئذٍ كما يلي:
- السطران 1-2: انقر على خيار [إنهاء]؛
- السطر 4: يتم استدعاء طريقة [onDestroy] للنشاط؛
- السطران 4-5: يتم استدعاء طريقة [onDestroyView] للجزء 1، تليها طريقة [onDestroy] الخاصة به؛
- السطور 6-9: تتكرر هذه العملية بالنسبة للجزأين الآخرين؛
من المهم تذكر أن طريقة [onDestroy] للنشاط والجزأين يتم استدعاؤها عندما يكون النشاط على وشك التدمير بواسطة النظام أو المطور أو المستخدم. يمكن استخدام هذه الطريقة لحفظ المعلومات — على سبيل المثال، محليًا على الجهاز اللوحي — بحيث يمكن استردادها عندما يعيد المستخدم تشغيل التطبيق.
1.9. مثال-08: تحديث جزء باستخدام متغير تجاور الأجزاء
1.9.1. إنشاء المشروع
انسخ مشروع [مثال-07] إلى [مثال-08]. للقيام بذلك، اتبع الإجراء الموضح لنسخ [مثال-02] إلى [مثال-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;
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// visual interface component
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// data
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
if (!initDone) {
// initial text
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// init done
initDone = true;
}
// current text display
textViewInfo.setText(text);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
@Override
public void onDestroyView() {
...
}
@Override
public void onResume() {
...
}
// update fragment
public void update() {
// the work to be done depends on the visit number
if (numVisit > 1) {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// modified text
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// local info for logs
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] هما كما يلي:
// data
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) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// memory
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// number of visits
if (isVisibleToUser) {
// increment
numVisit++;
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// the fragment will be hidden
updateDone = false;
}
}
@Override
public void onResume() {
// parent
super.onResume();
// log
Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// update
if (isVisibleToUser && !updateDone) {
update();
updateDone = true;
}
}
- السطر 14: يتم تخزين حالة ظهور الجزء؛
- الأسطر 22–25: إذا كان الجزء مرئيًا وتم تنفيذ الأسلوب [@AfterViews]، يتم تنفيذ الأسلوب [update] ويتم تعيين القيمة المنطقية [updateDone] إلى true؛
- الأسطر 26–28: إذا كان سيتم إخفاء الجزء، يتم إعادة تعيين المتغير المنطقي [updateDone] إلى false. نحتاج إلى حدث لإعادة تعيين المتغير المنطقي [updateDone] —الذي يتم تعيينه إلى true بمجرد استدعاء طريقة [update]—إلى false حتى يمكن إجراء تحديثات جديدة. نستخدم حقيقة أن الجزء لم يعد مرئيًا للقيام بذلك. عندما يصبح مرئيًا مرة أخرى، يجب تحديث الجزء مرة أخرى؛
- الأسطر 32-42: تُظهر السجلات أنه اعتمادًا على الجوار المختار للجزئيات، قد يتم تنفيذ طريقة [onResume] حتى لو لم تكن الجزئية مرئية. إذا لم تكن مرئية، فإننا لا نقوم بالتحديث (السطر 39) ونقوم، كما فعلنا مع [setMenuVisibility]، بإدارة المتغير المنطقي [updateDone].
أخيرًا، طريقة [onDestroyView] هي كما يلي:
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// indicator update
afterViewsDone = false;
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}
يتم تنفيذ طريقة [onDestroyView] عند انتهاء دورة حياة الفراغ. وقد تستأنف دورة حياة أخرى لاحقًا.
- السطر 6: تزيل طريقة [onDestroyView] أي اتصال بالعرض المرتبط بالجزء. وسيتم إعادة إنشاؤه خلال دورة حياة الجزء التالية. في الوقت الحالي، نحتاج إلى تعيين القيمة المنطقية [afterViews] على false للإشارة إلى أن الاتصال بالعرض لم يعد موجودًا؛
سنقوم بتشغيل التطبيق مع 5 أجزاء ذات تجاور يساوي 2. يتم إجراء التغييرات في [MainActivity]:
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT=2;
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
....
// fragment offset
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]:
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
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] على النحو التالي:
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
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++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
هذا الحل غير قابل للاستخدام على الفور هنا. في الواقع، عندما ينقر المستخدم على علامة التبويب j، والتي ستعرض الجزء j، لا يتم استدعاء الكود الخاص بنا. يتم تنفيذ كود النظام فقط. سنرى في مشروع مستقبلي كيفية اعتراض النقر على علامة التبويب، ولكن في الوقت الحالي سنتبع نهجًا مختلفًا.
لقد ناقشنا حالة التطبيق: مجموعة البيانات التي يديرها التطبيق بمرور الوقت. هنا، يتكون التطبيق من نشاط و n أجزاء، يتم إنشاء مثيل لكل منها مرة واحدة عند بدء تشغيل التطبيق وتطابق مدة حياتها مع مدة حياة التطبيق. لذلك، يمكن لأي من هذه العناصر — أو لعدد منها معًا — أن تكون مرشحة لتخزين حالة التطبيق. لكل جزء وصول، عبر طريقة [Fragment.getActivity()]، إلى النشاط الذي أنشأه. وبما أن جميع الأجزاء لديها وصول إلى النشاط، يبدو من الطبيعي تخزين حالة التطبيق داخله.
ومع ذلك، تعتمد نتيجة طريقة [Fragment.getActivity()] على وقت استدعائها في دورة الحياة. نوضح هذه النقطة بإضافة بعض السجلات إلى فئة [PlaceholderFragment]:
// update fragment
public void update() {
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// the work to be done depends on the visit number
if (numVisit > 1) {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// modified text
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// local info for logs
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: تعرض طريقة [getInfo] جزءًا من حالة التطبيق؛
نقوم بتشغيل التطبيق مع تجاور الأجزاء (fragment adjacency) بقيمة 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. إنشاء المشروع
نقوم بنسخ مشروع [Example-07] إلى [Example-08]. وللقيام بذلك، سنتبع الإجراء الموضح لنسخ [Example-02] إلى [Example-03] في القسم 1.4.
![]() | ![]() |
1.10.2. الجلسة
في هذا المشروع الجديد، نريد أن تعرض الأجزاء العدد الإجمالي للأجزاء التي شاهدها المستخدم. هنا، نحتاج إلى الاحتفاظ بعداد يمكن لجميع الأجزاء الوصول إليه. سنسمي الكائن الذي يغلف البيانات المشتركة بين الأجزاء "جلسة". تأتي هذه المصطلحات من تطوير الويب، حيث يتم وضع البيانات المراد مشاركتها عبر طرق العرض المختلفة التي يطلبها نفس المستخدم في جلسة. إن تغليف المعلومات التي تشاركها الأجزاء المختلفة في كائن واحد يجعل الكود أكثر قابلية للقراءة.
ستكون فئة [Session] كما يلي:
![]() |
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// number of fragments visited
private int numVisit;
// getters and setters
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 {
...
// injection session
@Bean(Session.class)
protected Session session;
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 2;
@AfterInject
protected void afterInject(){
Log.d("MainActivity", "afterInject");
// session initialization
session.setNumVisit(0);
}
...
- السطران 7-8: حقن الإشارة إلى عنصر singleton الخاص بالجلسة باستخدام تعليق [@Bean]. معلمة التعليق هي فئة العنصر الذي سيتم حقنه. لا يمكن أن يكون للحقل المُعلَّق عليه بهذه الطريقة نطاق [private]؛
- السطر 15: تُستخدم علامة [@AfterInject] لتعيين طريقة يتم استدعاؤها بمجرد اكتمال جميع عمليات الحقن للفئة. وبالتالي، عند الدخول إلى طريقة [afterInject] في السطر 16، تكون الإشارة من السطر 8 قد تم تهيئتها بالفعل؛
- السطر 20: يتم إعادة تعيين عداد الزيارات إلى الصفر؛
1.10.4. الجزء [PlaceholderFragment]
تتطور شريحة [PlaceholderFragment] على النحو التالي:
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
....
// session
protected Session session;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// memory
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// number of visits
if (isVisibleToUser) {
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// the fragment will be hidden
updateDone = false;
}
}
// update fragment
public void update() {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// session
if (session == null) {
session = ((MainActivity) getActivity()).getSession();
}
// increment visit no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
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 بالماوس إلى اليسار أو اليمين، يتم استبدال العرض الحالي بالعرض الموجود على اليمين أو اليسار، حسب الحالة. هذا السلوك الافتراضي ليس مرغوبًا دائمًا. سنتعلم كيفية تعطيل التمرير.
لنعد إلى عرض 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 {
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// the fragment container
@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 {
// swipe control
private boolean isSwipeEnabled;
// manufacturers
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// methods to be redefined to manage swiping
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- السطر 8: فئة [MyPager] تمتد من فئة [ViewPager] في Android (السطر 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 {
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// the fragment container
@ViewById(R.id.container)
protected MyPager mViewPager;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// the tab bar is also associated with the fragment container
...
- السطر 12: أصبح مدير الصفحات الآن من النوع [MyPager]؛
- السطر 23: نقوم بتمكين أو تعطيل التمرير.
اختبر هذه النسخة الجديدة. قم بتمكين أو تعطيل التمرير ولاحظ الفرق في سلوك العروض عند سحبها إلى اليمين أو اليسار بالماوس. في جميع التطبيقات المستقبلية، سيتم تعطيل التمرير. ولن نذكره مرة أخرى.
1.10.6. تعطيل التمرير بين الأجزاء
لنواصل مع تحسين مدير علامات التبويب. عند التبديل من علامة التبويب 1 إلى علامة التبويب 4، ترى علامتي التبويب الوسيطتين، 2 و 3، تمران. في مصطلحات Android، يُسمى هذا smoothScrolling. قد يصبح هذا السلوك مزعجًا إذا كان هناك العديد من علامات التبويب. يمكن تعطيله بإضافة الكود التالي إلى مدير الأجزاء [MyPager]:
// swipe control
private boolean isSwipeEnabled;
// controls scrolling
private boolean isScrollingEnabled;
...
// scrolling
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setters
...
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
نظرًا لأن مدير علامات التبويب قد تم ربطه بمدير الأجزاء [MyPager]، فعند النقر على علامة التبويب رقم i، يتم عرض الجزء رقم i بواسطة حاوية الأجزاء باستخدام طريقة [setCurrentItem] أعلاه (السطر 9). [position] هو رقم الجزء المراد عرضه؛
- السطر 10: يتم استدعاء طريقة [setCurrentItem] للفئة الأصلية. الحجة الثانية التي تم تعيينها على [false] تطلب انتقالًا فوريًا بين الأجزاء القديمة والجديدة (بدون تمرير)؛ أما تعيينها على [true] فيطلب انتقالًا عبر التمرير. هنا، الحجة الثانية هي قيمة الحقل في السطر 4، وهو حقل يمكن للمطور تعيينه باستخدام الطريقة في الأسطر 16-18؛
إذا كنت تريد تعطيل التمرير، فستبدو فئة [MainActivity] كما يلي:
...
// fragment offset
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// no scrolling
mViewPager.setScrollingEnabled(false);
...
قم بتشغيل المشروع مرة أخرى وتأكد من عدم وجود أي تمرير بين علامات التبويب 1 و 4، على سبيل المثال. من الآن فصاعدًا، سنقوم دائمًا بتعطيل التمرير. ولن نعود إلى هذا الموضوع مرة أخرى.
1.10.7. جزء جديد
في مثالنا، جميع الأجزاء من نفس النوع [PlaceHolderFragment]. سنتعلم الآن كيفية إنشاء جزء جديد وعرضه.
أولاً، انسخ العرض [vue1.xml] من مشروع [Example-04] إلى مشروع [Example-09] [1]:
![]() | ![]() |
- في [1]، العرض [vue1.xml]؛
- في [3]، تعرض الواجهة أخطاءً ناتجة عن وجود نص مفقود في ملف [res/values/strings.xml]؛
في [2]، نضيف النص المفقود بأخذه من ملف [res/values/strings.xml] في مشروع [Example-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>
<!-- vue 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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
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 {
// fragments
private Fragment[] fragments;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialization of fragment table
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// a fragment of +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
...
}
- السطر 13: يوجد [FRAGMENTS_COUNT] جزءًا: [FRAGMENTS_COUNT-1] جزءًا من النوع [PlaceholderFragment] (الأسطر 14-21) وجزء واحد من النوع [Vue1Fragment_]، السطر 23 (لاحظ علامة التسطير السفلية)؛
قم بتجميع مشروع [Example-09] ثم قم بتشغيله. يجب أن تبدو علامة التبويب 5 مختلفة:
![]() |
1.10.8. استمد جميع الأجزاء من نفس الفئة المجردة
يحتاج الجزء الجديد [Vue1Fragment] أيضًا إلى تحديث نفسه عند عرضه. للقيام بذلك، سنحتاج إلى إنشاء كود مشابه للكود الذي تم إنشاؤه للجزء [PlaceholderFragment]. لتجنب التكرار، سنقوم بفصل ما يمكن فصله إلى فئة مجردة سترثها جميع الأجزاء في التطبيق.
للقيام بذلك، نقوم بإنشاء مشروع جديد.
1.11. مثال-10: اشتقاق جميع الأجزاء من فئة مجردة
1.11.1. إنشاء المشروع
نقوم بنسخ مشروع [مثال-09] إلى [مثال-10]:
![]() | ![]() |
1.11.2. إدارة وضع التصحيح
نضيف خيارًا إلى المشروع لإظهار سجلات وضع التصحيح أو إخفائها. للقيام بذلك، نضيف ثابتًا ثابتًا إلى فئة [MainActivity]:
// mode debug
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 data
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private String className;
// data accessible to daughter classes
protected boolean afterViewsDone = false;
protected boolean isDebugEnabled = true;
// activity
protected MainActivity activity;
// session
protected Session session;
// manufacturer
public AbstractFragment() {
// init
isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
...
}
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
...
}
@Override
public void onResume() {
// parent
super.onResume();
...
}
// local news
protected String getParentInfos() {
return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
}
// update fragment
protected void update() {
...
// the daughter class is asked to update itself
updateFragment();
}
protected abstract void updateFragment();
}
- السطر 7: تمتد فئة [AbstractFragment] من فئة [Fragment] في Android؛
- يجب أن يكون كل جزء قادراً على تحديث نفسه. ولهذا السبب تتطلب الفئة الأم [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) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// memory
this.isVisibleToUser = isVisibleToUser;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
}
// when the fragment becomes visible
if (isVisibleToUser) {
// update fragment
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// we leave the fragment
updateDone = false;
}
}
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// indicator update
afterViewsDone = false;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
}
}
@Override
public void onResume() {
// parent
super.onResume();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
}
if (isVisibleToUser) {
// update
if (!updateDone) {
update();
updateDone = true;
}
}
}
طريقة [update] هي كما يلي:
// update fragment
protected void update() {
// recover activity and session
if (activity == null) {
Activity activity = getActivity();
if (activity != null) {
this.activity = (MainActivity) activity;
this.session = this.activity.getSession();
}
}
// the daughter class is asked to update itself
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.*;
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
// visual interface component
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// data
private boolean initDone;
// data
private String text;
private int numVisit;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public PlaceholderFragment() {
super();
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", "constructor");
}
}
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
...
}
// update fragment
public void updateFragment() {
...
}
}
- السطر 10: تمتد فئة [PlaceholderFragment] من فئة [AbstractFragment]. مع هذه البنية، تتضمن كتابة الفراغات ما يلي:
- كتابة طريقة [@AfterViews]، التي تُستخدم لتهيئة الجزء خلال دورة حياته الأولى أو لإعادة تعيينه في حالة حدوث [onDestroyView] مسبقًا. السطر 39 ضروري لإدارة دورة حياة الجزء بشكل صحيح؛
- كتابة طريقة [updateFragment]، التي تقوم بتحديث الجزء قبل عرضه مباشرة. يمكن لهذه الطريقة استخدام جلسة الفئة الأم؛
- كتابة معالجات أحداث الجزء. وهذا ما سنفعله في المشاريع المستقبلية؛
تظل طريقتا [@AfterViews] و [updateFragment] كما كانتا في المشروع السابق:
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
if (!initDone) {
// initial text
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// init done
initDone = true;
}
// current text display
textViewInfo.setText(text);
}
// update fragment
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
// increment visit no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
// local info for logs
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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// data
private int numVisit;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// local info for logs
protected String getLocalInfos() {
return String.format("numVisit=%s", numVisit);
}
// update fragment
@Override
protected void updateFragment() {
// increment visit no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// the visit number is displayed
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]، التي تدير علامات التبويب، يُكتب ما يلي:
// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of the container
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>
<!-- vue 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>
<!-- menu -->
<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) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// processing menu options
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;
}
}
// item processed
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// change the displayed fragment
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. إنشاء المشروع
نقوم بنسخ مشروع [المثال-10] إلى [المثال-11]:
![]() | ![]() |
1.12.2. الأهداف
سيحتوي التطبيق الجديد على علامتي تبويب:
- ستعرض علامة التبويب الأولى دائمًا الجزء [View1]؛
- ستعرض علامة التبويب الثانية جزءًا محددًا من القائمة؛

- في [1]، الجزء [View1]؛
- في [2]، الجزء [PlaceholderFragment] الذي اختاره المستخدم؛
- في [3]، يستمر حساب الزيارات؛
1.12.3. الجلسة
![]() |
ستكون الجلسة الجديدة على النحو التالي:
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// number of fragments visited
private int numVisit;
// n° fragment type [PlaceholderFragment] displayed in second tab
private int numFragment;
// getters and setters
...
}
- السطر 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] الآن علامات التبويب والتنقل بينها، وهو ما لم تكن تفعله سابقًا. يتغير كودها على النحو التالي:
// the tab manager
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
...
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
...
// no scrolling
mViewPager.setScrollingEnabled(false);
// view1 display
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
// at the start there is only one tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// event manager
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// a tab has been selected - change the fragment displayed by the fragment container
...
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
...
}
- السطر 17: سيكون الجزء الأول الذي يعرضه حاوية الأجزاء هو الجزء [Vue1Fragment]. حسب التصميم، سيكون هذا هو الجزء الأخير في الحاوية؛
- الأسطر 20–22: نظرًا لأننا لم ننشئ ارتباطًا بين علامات التبويب وحاوية الأجزاء، يتعين علينا إدارة علامات التبويب بأنفسنا. في البداية، لا يحتوي شريط علامات التبويب [tabLayout] في السطر 3 على أي علامات تبويب؛
- السطر 20: نقوم بإنشاء علامة التبويب الأولى؛
- السطر 21: نمنحها عنوانًا. في الأمثلة السابقة، كانت عناوين علامات التبويب هي نفس عناوين الأجزاء. لم يعد هذا هو الحال. ونتيجة لذلك، نزيل طريقة [getPageTitle] من مدير الأجزاء. لم نعد بحاجة إليها:
// optional - gives a title to managed fragments
@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");
}
// a tab has been selected - change the fragment displayed by the fragment container
// miter position
int position = tab.getPosition();
// fragment number to display
int numFragment;
switch (position) {
case 0:
// fragment no. [Vue1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// fragment no. [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// fragment display
mViewPager.setCurrentItem(numFragment);
}
- السطر 8: نسترد موضع علامة التبويب التي تم النقر عليها. هنا، سنحصل على الرقم 0 أو 1؛
- الأسطر 12-15: إذا تم النقر على علامة التبويب الأولى، فإننا نستعد لعرض الجزء [Vue1Fragment]؛
- الأسطر 16-18: في الحالات الأخرى (الضغط على علامة التبويب رقم 2)، نستعد لإعادة عرض الجزء الذي تم عرضه آخر مرة تم فيها تحديد علامة التبويب رقم 2. ثم تم تخزين معرّفه في جلسة التطبيق؛
- السطر 21: نطلب من حاوية الجزء عرض الجزء المطلوب؛
الآن دعونا نلقي نظرة على إدارة خيارات القائمة (لا تزال في [MainActivity]):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// processing menu options
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;
}
}
// item processed
return true;
}
- الأسطر 16–31: معالجة خيارات القائمة الأربعة. يستدعي كل معالج الطريقة [showFragment] مع رقم الجزء المراد عرضه؛
طريقة [showFragment] هي كما يلي:
// tab n° 2
private TabLayout.Tab tab2 = null;
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// if the 2nd tab doesn't yet exist, we create it
if (tab2 == null) {
tab2 = tabLayout.newTab();
tabLayout.addTab(tab2);
}
// set the title of the second tab
tab2.setText(String.format("Fragment n° %s", (i + 1)));
// change the displayed fragment
mViewPager.setCurrentItem(i);
// the fragment number displayed is set to session
session.setNumFragment(i);
// tab 2 is selected - does nothing if already selected
tab2.select();
}
}
- تذكر أنه عند بدء تشغيل التطبيق، لا يوجد سوى علامة تبويب واحدة؛
- السطر 2: إشارة إلى علامة التبويب رقم 2، وهي فارغة في البداية؛
- السطر 5: لم تتغير شروط العرض عن الإصدار السابق؛
- الأسطر 7–10: إذا لم تكن علامة التبويب رقم 2 موجودة بعد، يتم إنشاؤها (السطر 8) وإضافتها إلى شريط علامات التبويب (السطر 9)؛
- السطر 12: يتم وضع رقم الجزء المراد عرضه في عنوان علامة التبويب الثانية، مع بدء الترقيم من 1؛
- السطر 14: يتم عرض الجزء المطلوب؛
- السطر 16: يتم تخزين رقمها في الجلسة؛
- السطر 18: يتم تحديد علامة التبويب رقم 2. إذا كانت محددة بالفعل، فلن يحدث شيء: لن يتم تنفيذ طريقة [onTabSelected]. إذا لم تكن محددة بالفعل، فسيتم تشغيل طريقة [onTabSelected]. ثم توجه هذه الطريقة حاوية الجزء لعرض الجزء المعروض بالفعل في السطر 14. يمنع فحص بسيط في طريقة [onTabSelected] حدوث هذا السيناريو:
// display fragment only if necessary
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
ندعو القراء إلى تجربة هذه النسخة الجديدة.
1.12.6. التحسينات
لدينا الآن فهم راسخ للأجزاء ودورة حياتها ومفهوم تجاور الأجزاء وعلاقتها بشريط علامات التبويب. كما أن لدينا بنية قوية اجتازت للتو الاختبار في المثال 11:
- نشاط واحد و n شظايا؛
- جميع الأجزاء تمتد من فئة [AbstractFragment]؛
- يتم وضع البيانات المراد مشاركتها بين الأجزاء وبين الأجزاء والنشاط في فئة [Session]؛
في مشروع جديد، سنوضح العلاقات بين النشاط والأجزاء بإضافة واجهة.
1.13. المثال 12: تحديد العلاقات بين النشاط والأجزاء
في هذا المثال، نريد تحديد العلاقات الدنيا بين النشاط والأجزاء. للقيام بذلك، سنستخدم:
- واجهة [IMainActivity] تحدد ما يمكن للأجزاء أن تطلبه من النشاط؛
- فئة مجردة [AbstractFragment] تحدد الحالة والطرق التي يجب أن تتوفر في كل جزء؛
1.13.1. إنشاء المشروع
نقوم بنسخ مشروع [Example-11] إلى [Example-12] باتباع الإجراء الوارد في القسم 1.4. ونحصل على النتيجة التالية:
![]() | ![]() |
1.13.2. واجهة [IMainActivity]
من الأمثلة السابقة، يتضح أن الأجزاء تحتاج إلى الوصول إلى الجلسة التي تم إنشاؤها بواسطة النشاط. علاوة على ذلك، على الرغم من عدم ظهور ذلك في هذه الأمثلة، فمن المتوقع أن تؤدي معالجات الأحداث في الأجزاء أحيانًا إلى تغيير في العرض. سيُطلب من النشاط تنفيذ هذا التغيير. قد تبدو واجهة [IMainActivity] عندئذٍ كما يلي:
![]() |
package exemples.android;
public interface IMainActivity {
// session access
Session getSession();
// change of view
void navigateToView(int position);
// debug mode
boolean IS_DEBUG_ENABLED = true;
}
السطر 12: لاحظ وجود ثابت كان موجودًا سابقًا في فئة [MainActivity]. نريد تقليل الترابط بين الأجزاء والنشاط وقصره على الترابط بين [AbstractFragment] و [IMainActivity]. يمكن عندئذ تسمية النشاط باسم آخر غير [MainActivity]. نظرًا لاستخدام الثابت [IS_DEBUG_ENABLED] في الأجزاء، يتم نقله إلى واجهة [IMainActivity].
1.13.3. الفئة المجردة [AbstractFragment]
لم تتغير الفئة المجردة [AbstractFragment] إلا قليلاً:
// data accessible to daughter classes
protected boolean afterViewsDone = false;
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// activity
protected IMainActivity mainActivity;
protected Activity activity;
...
// update fragment
protected void update() {
// recover activity and session
if (mainActivity == null) {
this.activity = getActivity();
if (this.activity != null) {
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
}
}
// the daughter class is asked to update itself
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 {
// fragments
private AbstractFragment[] fragments;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
// initialization of fragment table
fragments = new AbstractFragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
...
}
// a fragment of +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
...
}
// makes the number of fragments managed
@Override
public int getCount() {
...
}
}
1.13.5. تعديل فئة [MainActivity]
يجب أن تنفذ فئة [MainActivity] واجهة [IMainActivity]:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
...
// injection session
@Bean(Session.class)
protected Session session;
...
// getter session
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// the position view is displayed
if(mViewPager.getCurrentItem()!=position){
// fragment display
mViewPager.setCurrentItem(position);
}
}
- الأسطر 10–12: كانت طريقة [getSession] موجودة بالفعل؛
- الأسطر 15–22: تعرض طريقة [navigateToView] الجزء #[position]؛
- السطر 17: نتحقق مما إذا كان هناك أي شيء يجب القيام به؛
- السطر 19: يتم عرض الجزء #[position]؛
في هذه المرحلة، قم بتشغيل التطبيق. من المفترض أن يعمل.
1.13.6. تعديل عرض الأجزاء في [MainActivity]
حاليًا، تعرض فئة [MainActivity] جزءًا باستخدام العبارة:
// view1 display
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
نظرًا لأن طريقة [navigateToView] تقوم بنفس الشيء، استبدل هذا النوع من العبارات في كل مكان (موقعان) بما يلي:
ثم قم بتشغيل التطبيق. من المفترض أن يعمل بشكل طبيعي.
1.13.7. الخلاصة
من الآن فصاعدًا، سنستخدم دائمًا البنية السابقة:
- نشاط ينفذ واجهة [IMainActivity]؛
- أجزاء تمتد من فئة [AbstractFragment]، مما يتطلب منها تنفيذ طريقة [updateFragment]. يجب أن تحتوي هذه الأجزاء أيضًا على طريقة [@AfterViews] حيث يتم تعيين القيمة المنطقية [afterViewsDone] إلى true؛
- جلسة تعمل على تغليف البيانات المراد مشاركتها بين الأجزاء والنشاط؛
1.14. المثال-13: المثال-05 مع الأجزاء
في مشروع [مثال-05]، قدمنا التنقل بين العروض. في ذلك الوقت، كان الأمر يتعلق بالتنقل بين الأنشطة: عرض واحد = نشاط واحد. هنا، نقترح وجود نشاط واحد مع عروض متعددة من النوع [AbstractFragment].
1.14.1. إنشاء المشروع
نقوم بنسخ المشروع السابق [مثال-12] إلى [مثال-13] باتباع الإجراء الوارد في القسم 1.4. ونحصل على النتيجة التالية:
![]() | ![]() |
1.14.2. هيكل المشروع
سنبدأ في استخدام الحزم لتنظيم الكود. في الوقت الحالي، يمكننا التمييز بين مجالين مختلفين:
- إدارة الأنشطة؛
- إدارة الأجزاء؛
نقوم بإنشاء حزمتين لهما: [examples.android.activity] و [examples.android.fragments]:
![]() |
![]() | ![]() |
ونفعل الشيء نفسه لإنشاء حزمة [examples.android.fragments]:
![]() | ![]() |
في [8]، نقوم بإنشاء حزمة ثالثة تسمى [architecture] سنضع فيها الكيانات [IMainActivity، AbstractFragment، Session، MyPager]، والتي تمثل اللبنات الأساسية لهيكل تطبيقنا. وهذا بمثابة تذكير بأننا اتخذنا خيارًا معماريًا محددًا. بعد ذلك، انقل عناصر المشروع الحالية كما هو موضح في [9]. يجب تأكيد كل عملية نقل بالنقر على زر [Refactor].
في هذه المرحلة، قم بتجميع التطبيق. لدينا الأخطاء التالية في [MainActivity]:
![]() |
عند نقل الفئات إلى الحزم، أجرى Android Studio التغييرات اللازمة على كود التطبيق (الأسطر 18–21، على سبيل المثال). لم يتم نقل الفئات المشار إليها في السطرين 15 و17. يتم إنشاؤها بواسطة مكتبة Android Annotations. بالنسبة لهذه الفئات، يجب عليك تغيير الاستيرادات يدويًا. وبالتالي، تصبح هذه الأسطر كما يلي:
![]() |
بمجرد الانتهاء من ذلك، لن تكون هناك أخطاء تجميع أخرى. قم بتشغيل التطبيق. ستظهر لك بعد ذلك الرسالة التالية:
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 أن النشاط المحدد هو [examples.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 {
// number of fragments visited
private int numVisit;
// n° fragment type [PlaceholderFragment] displayed in second tab
private int numFragment;
// getters and setters
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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// update fragment
@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]، قم بإزالة كل ما يشير إلى علامات التبويب، والزر العائم، وشريط الأدوات، والقائمة. أسهل طريقة للعثور على هذه الإشارات هي إزالة إعلاناتها:
// the tab manager
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// the floating button
@ViewById(R.id.fab)
protected FloatingActionButton fab;
وأعد تجميع التطبيق. الأسطر التي تحتوي على أخطاء هي تلك التي تشير إلى العناصر المفقودة. لذا احذف كل تلك الأسطر. أيضًا، عدّل مدير الأجزاء بحيث لا يشير بعد الآن إلى الجزء [PlaceholderFragment] الذي حذفناه:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// makes the number of fragments managed
@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 {
// the fragment container
@ViewById(R.id.container)
protected MyPager mViewPager;
// the toolbar
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
// injection session
@Bean(Session.class)
protected Session session;
// number of fragments
private final int FRAGMENTS_COUNT = 5;
// fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 2;
// debug mode
public static final boolean IS_DEBUG_ENABLED = true;
// the fragment manager
private SectionsPagerAdapter mSectionsPagerAdapter;
// manufacturer
public MainActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "constructor");
}
}
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
// toolbar - this is where the application name is displayed
setSupportActionBar(toolbar);
// the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
mViewPager.setAdapter(mSectionsPagerAdapter);
// fragment offset
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// no scrolling
mViewPager.setScrollingEnabled(false);
// view1 display
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// getter session
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// the position view is displayed
if (mViewPager.getCurrentItem() != position) {
// fragment display
mViewPager.setCurrentItem(position);
}
}
// the fragment manager
// it is used to request fragments to be displayed in the main view
// must define methods [getItem] and [getCount] - the others are optional
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
// parent
super(fm);
}
// fragment n° position
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// makes the number of fragments managed
@Override
public int getCount() {
return fragments.length;
}
}
}
هناك بعض التغييرات الأخرى التي يجب إجراؤها:
- حذف السطر 31، الذي لم يعد ضروريًا؛
- السطر 33: اضبط تجاور الأجزاء على 1؛
- السطر 76: انتقل إلى العرض 0. سيكون هذا هو أول عرض يتم عرضه؛
- السطر 108: تهيئة المصفوفة باستخدام الجزء [Vue1Fragment_]:
// fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
إذن لدينا جزء واحد فقط. قم بتشغيل التطبيق. يجب أن تحصل على النتيجة التالية:

يجب أن يعمل زر [Validate].
1.14.4. إنشاء الأجزاء (fragments) وطرق العرض المرتبطة بها
سيحتوي التطبيق على عرضين، وهما العرضان الموجودان في مشروع [Example-05]. لدينا بالفعل العرض [vue1.xml] في المشروع الحالي. سنقوم الآن بنسخ [vue2.xml] من [Example-05] إلى [Example-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>
<!-- vue 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<!-- vue 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>
نقوم بنسخ فئة [View1Fragment] إلى [View2Fragment]:
![]() |
وقم بتعديل الكود المنسوخ على النحو التالي:
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() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// update fragment
@Override
protected void updateFragment() {
}
}
- السطر 9: ترتبط القطعة بالعرض [res/layout/view2.xml]؛
- السطر 10: الفئة تمتد من الفئة المجردة [AbstractFragment]؛
- الأسطر 12–20: الطريقة المطلوبة [@AfterViews]؛
- الأسطر 23–25: الطريقة المطلوبة [updateFragment]؛
1.14.5. تنفيذ الأجزاء والتنقل بينها
ستدير النشاط الآن جزأين. يتم تحديث فئة [SectionsPagerAdapter] الخاصة بها على النحو التالي:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
}
تتولى واجهة [IMainActivity] إدارة التنقل بين العروض باستخدام طريقة [navigateToView] الخاصة بها. سنقوم بمعالجة النقر على زر [View 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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// event managers ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
mainActivity.navigateToView(1);
}
// update fragment
@Override
protected void updateFragment() {
}
}
- الأسطر 37–40: تتولى الطريقة [showVue2] معالجة حدث "النقر" على زر [View #2]؛
- السطر 39: يتم تنفيذ التنقل باستخدام طريقة [navigateToView] الخاصة بالنشاط. تذكر أن النشاط تم تخزينه في الفئة الأصلية على النحو التالي:
// activity
protected IMainActivity mainActivity;
وأن هذا النشاط قد تم تهيئته بالفعل عند الدخول إلى أي معالج أحداث.
- السطر 34: يستخدم البيان المتغير [activity] للفئة الأصلية، وهو مرجع إلى النشاط كمثيل لنوع [Activity] في Android؛
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() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestionnaires d'évts ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// update fragment
@Override
protected void updateFragment() {
}
}
- الأسطر 24–27: تعالج الطريقة [showVue1] حدث "النقر" على زر [View 1]؛
قم بتشغيل المشروع وتحقق من أن التنقل بين العروض يعمل.
1.14.6. تحديد الجلسة
يعمل التطبيق على النحو التالي:
- أدخل اسمًا في العرض 1؛
- اعرض هذا الاسم في العرض 2؛
للسماح للطريقة View 1 بتمرير الاسم الذي تم إدخاله إلى الطريقة View 2، سنستخدم الجلسة التالية؛
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// name
private String nom;
// getters and setters
...
}
- السطر 8: الاسم الذي تم إدخاله؛
ستقوم فئة [MainActivity] بتهيئة الجلسة على النحو التالي:
// injection session
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// init session
session.setNom("");
}
1.14.7. الكود النهائي للأجزاء
في الجزء [Vue1Fragment]، نقوم بتعديل الكود الخاص بمعالج النقر على زر [Validate]:
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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
...
// event managers ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// memorizes the name entered
String nom = editTextNom.getText().toString();
// we display it
Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
// enter the name entered in the session
session.setNom(editTextNom.getText().toString());
// navigate to view no. 2
mainActivity.navigateToView(1);
}
// update fragment
@Override
protected void updateFragment() {
}
}
- الأسطر: 31-37: معالجة النقر على زر [View #2]؛
- السطر 34: قبل الانتقال إلى العرض 2، نقوم بتخزين الاسم الذي تم إدخاله في الجلسة حتى يتمكن العرض الجديد من الوصول إليه؛
تتطور طريقة العرض [View2Fragment] على النحو التالي:
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 {
// visual interface components
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gestionnaires d'évts ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// update fragment
@Override
protected void updateFragment() {
// retrieve the name entered in the session
String nom = session.getNom();
// we display it
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() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
هذه الطريقة غير مكتملة. في الواقع، يجب أن نأخذ دائمًا في الاعتبار الحالة التي يتم فيها إعادة تدوير الجزء بعد عملية [onDestroyView]. في هذه الحالة، يتم إعادة إنشاء عرض الجزء 1، وسيختفي أي اسم تم إدخاله مسبقًا من العرض. نحن لا نريد ذلك. حاليًا، يظل الاسم الذي تم إدخاله معروضًا لأن تجاور أجزاء Fragment 1 يعني أن دورة حياة الجزء [Vue1Fragment] يتم تنفيذها مرة واحدة فقط. ومع ذلك، من الأفضل مراعاة إعادة تدوير الجزء.
هناك عدة طرق لحل هذه المشكلة:
- يمكننا الاستفادة من حقيقة أن طريقة [update] يتم تنفيذها بشكل منهجي في كل مرة يتم فيها عرض الجزء لتحديث الاسم الذي تم إدخاله؛
- يمكنك إجراء هذا التحديث فقط عند إعادة تنفيذ طريقة [@AfterViews]. سنتبع النهج الأخير؛
نقوم بتعديل الكود في [View1Fragment] على النحو التالي:
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// data
private String nom;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// the text displayed is (re)initialized
editTextNom.setText(nom);
}
// event managers ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showVue2() {
// the name entered is noted so that it can be retrieved if the fragment is recycled
nom = editTextNom.getText().toString();
// enter the name entered in the session
session.setNom(nom);
// navigate to view no. 2
activity.navigateToView(1);
}
- السطر 27: بما أننا على وشك الانتقال من العرض 1 إلى العرض 2، نقوم بتخزين الاسم الذي تم إدخاله؛
- السطر 17: في كل مرة يتم فيها تنفيذ دورة حياة الجزء، يتم عرض الاسم الأخير الذي تم إدخاله مرة أخرى؛
بالنسبة لجزء [View2Fragment]، يكفي الكود الموجود:
// visual interface components
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// update fragment
@Override
protected void updateFragment() {
// retrieve the name entered in the session
String nom = session.getNom();
// we display it
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. إنشاء المشروع
نقوم بنسخ المشروع السابق [مثال-12] إلى [مثال-13] باتباع الإجراء الوارد في القسم 1.4. ونحصل على النتيجة التالية:
![]() | ![]() |
1.15.2. طريقة العرض [view1]
سيحتوي التطبيق على عرض واحد فقط [view1.xml]. لذلك، سنحذف العرض الآخر [view2.xml] مع الجزء المرتبط به:
![]() | ![]() |
قم بتجميع التطبيق. تظهر أخطاء في [MainActivity]:
![]() |
قم بتصحيح السطر 4 أدناه في مدير المقتطفات [SectionsPagerAdapter]
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
يصبح السطر 4 أعلاه:
// fragments
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>
<!-- vue 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>
<!-- colors applied -->
<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 {
// visual interface elements
@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;
// list of order responses
private List<String> reponses = new ArrayList<>();
// listview adapter
private ArrayAdapter<String> adapterReponses;
// seizures
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// memory
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// hide error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Executer)
void doExecuter() {
// hide any previous error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// test the validity of entries
if (!isPageValid()) {
return;
}
}
// check the validity of the data entered
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- يوجد هنا جزء واحد فقط سيتم تنفيذ دورة حياته مرة واحدة فقط، عند بدء تشغيل التطبيق. ولهذا السبب، سيتم تنفيذ الطريقة [@AfterViews] (الأسطر 46–57) والطريقة [updateFragment] (الأسطر 75–81) مرة واحدة فقط عند بدء تشغيل التطبيق؛
- السطور 55-56: نخفي رسالتي الخطأ من العرض (الموضحة أدناه) [1-2]؛
![]() |
- السطران 59-60: الطريقة التي يتم تنفيذها عند النقر على زر [Execute]؛
- السطور 71-73: يتم التحقق من صحة الإدخالات؛
طريقة [isPageValid] هي كما يلي:
// seizures
private int nbAleas;
private int a;
private int b;
...
// check the validity of the data entered
private boolean isPageValid() {
// enter the number of random numbers
nbAleas = 0;
Boolean erreur;
int nbErreurs = 0;
try {
nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
erreur = (nbAleas < 1);
} catch (Exception ex) {
erreur = true;
}
// mistake?
if (erreur) {
nbErreurs++;
txtErrorAleas.setVisibility(View.VISIBLE);
}
// enter a
a = 0;
erreur = false;
try {
a = Integer.parseInt(edtA.getText().toString());
} catch (Exception ex) {
erreur = true;
}
// mistake?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// b input
b = 0;
erreur = false;
try {
b = Integer.parseInt(edtB.getText().toString());
erreur = b < a;
} catch (Exception ex) {
erreur = true;
}
// mistake?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// return
return (nbErreurs == 0);
}
- الأسطر 2–4: يتم تهيئة هذه الحقول الثلاثة بواسطة الطريقة [isPageValid]. بالإضافة إلى ذلك، تُرجع هذه الطريقة القيمة true إذا كانت جميع الإدخالات صالحة، والقيمة false في حالة عدم صحتها. إذا كانت أي من الإدخالات غير صالحة، يتم عرض رسائل الخطأ المرتبطة بها؛
في هذه المرحلة، يصبح التطبيق قابلاً للتنفيذ. تحقق من وظيفة طريقة [isPageValid] عن طريق إدخال بيانات غير صحيحة.
1.15.5. طبقة [business]
![]() |
![]() |
توفر طبقة [الأعمال] واجهة [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) {
// object list
List<Object> réponses = new ArrayList<Object>();
// some checks
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"));
}
// mistake?
if (réponses.size() != 0) {
return réponses;
}
// random numbers are generated
Random random = new Random();
for (int i = 0; i < n; i++) {
// generate a random exception 1 time / 3
int nombre = random.nextInt(3);
if (nombre == 0) {
réponses.add(new AleaException("Exception aléatoire"));
} else {
// otherwise a random number is returned between two bounds [a,b]
réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
}
}
// result
return réponses;
}
}
- السطر 9: نستخدم تعليق AA [@EBean] على فئة [Business] حتى نتمكن من حقن المراجع إليها في طبقة [Presentation]. تضمن السمة (scope = EBean.Scope.Singleton) إنشاء مثيل واحد فقط من فئة [Business]. لذلك، يتم حقن المرجع نفسه دائمًا إذا تم حقنه عدة مرات في طبقة [Presentation]؛
- أما باقي الكود فهو قياسي؛
نوع [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]
![]() |
[الأعمال] LayerActivityUserView
ستقوم النشاط بتنفيذ واجهة [IMetier] الخاصة بطبقة [الأعمال]. وبالتالي، سيكون النشاط هو النظير الوحيد للجزء/العرض.
ينفذ النشاط [MainActivity] بالفعل واجهة [IMainActivity]. ولجعله ينفذ أيضًا واجهة [IMetier]، يمكننا:
- إضافة واجهة [IMetier] إلى الواجهات التي تنفذها النشاط؛
- التأكد من أن واجهة [IMainActivity] نفسها تمتد إلى واجهة [IMetier]. هذه هي الطريقة التي نتبعها؛
تصبح واجهة [IMainActivity] كما يلي:
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// session access
Session getSession();
// change of view
void navigateToView(int position);
// debug mode
public static final boolean IS_DEBUG_ENABLED = true;
}
- السطر 5: واجهة [IMainActivity] تمتد واجهة [IMetier]
تتطور فئة [MainActivity] على النحو التالي:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
...
// injection session
@Bean(Session.class)
protected Session session;
// injection molding
@Bean(Metier.class)
protected IMetier metier;
...
// implémentation IMetier --------------------------------------------------------------------
@Override
public List<Object> getAleas(int a, int b, int n) {
return metier.getAleas(a, b, n);
}
- السطران 11-12: يتم إدخال طبقة [business] في النشاط. للقيام بذلك، نستخدم التعليق التوضيحي [@Bean]، الذي يكون معلمته هي الفئة التي تحمل التعليق التوضيحي [@EBean]؛
- السطر 2: تنفذ النشاط واجهة [IMainActivity] وبالتالي واجهة [IMetier] لطبقة [business]؛
- الأسطر 16-19: تنفيذ الطريقة الوحيدة لواجهة [IMetier]. نقوم ببساطة بتفويض الاستدعاء إلى طبقة [business]؛
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 {
// visual interface elements
@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;
// list of order responses
private List<String> reponses = new ArrayList<>();
// listview adapter
private ArrayAdapter<String> adapterReponses;
// seizures
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
...
}
@Click(R.id.btn_Executer)
void doExecuter() {
...
}
// check the validity of the data entered
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// will only be executed once when the application is started
// create the ListView adapter - this requires the [activity] variable to have been initialized
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] لا تكون بالضرورة قيمة فارغة. يرتبط المُهايئ هنا بمصدر البيانات [reponses] المُعرَّف في السطر 37؛
تتعامل طريقة [doExecute] مع النقر على زر [Execute]. وفيما يلي كودها:
@Click(R.id.btn_Executer)
void doExecuter() {
// hide any previous error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// delete previous answers
reponses.clear();
adapterReponses.notifyDataSetChanged();
// test the validity of entries
if (!isPageValid()) {
return;
}
// we ask for the random numbers in the activity
List<Object> data = mainActivity.getAleas(a, b, nbAleas);
// we create a list of Strings from this data
for (Object o : data) {
if (o instanceof Exception) {
reponses.add(((Exception) o).getMessage());
} else {
reponses.add(o.toString());
}
}
// refresh listview
adapterReponses.notifyDataSetChanged();
}
- السطران 7-8: نريد مسح قائمة العرض. للقيام بذلك، نقوم بمسح مصدر البيانات [reponses] ونطلب من المحول المرتبط بقائمة العرض التحديث؛
- السطور 10-12: قبل تنفيذ الإجراء المطلوب، نتحقق من صحة القيم المدخلة؛
- السطر 14: يتم طلب قائمة الأرقام العشوائية من النشاط. نحصل على قائمة من الكائنات حيث يكون كل كائن من النوع [Integer] أو [AleaException]؛
- الأسطر 16–22: باستخدام قائمة الكائنات التي تم الحصول عليها، يتم تحديث مصدر البيانات [reponses] المعروض بواسطة ListView؛
- السطر 24: يُطلب من محول ListView التحديث؛
1.15.8. التنفيذ
قم بتشغيل المشروع وتحقق من أنه يعمل بشكل صحيح.
1.16. مثال-15: بنية العميل/الخادم
سنلقي الآن نظرة على بنية شائعة لتطبيق أندرويد، حيث يتواصل التطبيق مع خدمات ويب بعيدة. وستكون لدينا الآن البنية التالية:
![]() |
لقد أضفنا طبقة [DAO] إلى تطبيق Android للتواصل مع الخادم البعيد. ستتواصل هذه الطبقة مع الخادم الذي يولد الأرقام العشوائية التي يعرضها جهاز Android اللوحي. سيكون لهذا الخادم بنية من مستويين كما يلي:
![]() |
يقوم العملاء بالاستعلام عن عناوين URL محددة في طبقة [web/JSON] ويتلقون استجابة نصية بتنسيق JSON (ترميز كائنات JavaScript). هنا، ستتعامل خدمة الويب الخاصة بنا مع عنوان URL واحد بالصيغة [/a/b]، والذي سيعيد رقمًا عشوائيًا في النطاق [a,b]. سنصف التطبيق بالترتيب التالي:
الخادم
- طبقة [الأعمال] الخاصة به؛
- خدمة [web/JSON] الخاصة به والمُنفذة باستخدام Spring MVC؛
العميل
- طبقة [DAO] الخاصة به. لن تكون هناك طبقة [الأعمال]؛
1.16.1. خادم [الويب/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]: قم بإنشاء المشروع. يتم بعد ذلك إنشاء ملف ZIP لمشروع Gradle نموذجي وإتاحته للتنزيل؛
ما الذي يجب أن تضعه في [2-3]؟ لقد استخدمنا بالفعل تبعيات Gradle. على سبيل المثال، كانت التبعية من المشروع السابق كما يلي:
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use 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]؛
قم بفك ضغط ملف zip الذي تم الحصول عليه في المجلد الذي يحتوي على المشاريع الأخرى:
![]() | ![]() ![]() | ![]() |
باستخدام 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 مخصصة لبيئة تطوير 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 Central. وهو حاليًا أكبر مستودع ثنائي مفتوح المصدر متاح؛
- الأسطر 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}")
}
}
// plugins
apply plugin: 'java'
apply plugin: 'spring-boot'
// binaire du projet
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
// versions Java
sourceCompatibility = 1.8
targetCompatibility = 1.8
// dépôts Maven
repositories {
mavenLocal()
mavenCentral()
}
// dépendances
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
...
// dépendances
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
// plugin pour créer un binaire aux normes Maven dans le dépôt Maven local
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 {
// change to point to your repo, 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]:
// créer un binaire avec toutes ses dépendances
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. طبقة [الأعمال]
![]() |
![]() |
تتبع طبقة [الأعمال] نفس النهج الذي اتبعته طبقة [الأعمال] في المثال السابق. وستحتوي على واجهة [IMetier] التالية:
package exemples.android.server.metier;
public interface IMetier {
// random number in [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) {
// some checks
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);
}
// result generation
Random random=new Random();
random.setSeed(new Date().getTime());
return a + random.nextInt(b - a + 1);
}
}
لن نعلق على الفئة: فهي مشابهة لتلك الموجودة في المثال السابق، باستثناء أنها لا ترمي استثناءات عشوائياً. لاحظ تعليق Spring [@Service] في السطر 8، والذي يجعل Spring يقوم بإنشاء مثيل للفئة كمثيل واحد (singleton) ويجعل مرجعها متاحاً لمكونات Spring الأخرى. كان من الممكن استخدام تعليقات Spring أخرى هنا لتحقيق نفس التأثير. تحتوي مكونات Spring على أسماء افتراضية يمكن تحديدها كسمة للتعليق التوضيحي المستخدم. بدون هذه السمة، كما هو الحال هنا، تأخذ مكونة Spring اسم الفئة مع جعل الحرف الأول منها صغيرًا. وبالتالي، هنا، تُسمى مكونة Spring [metier] بشكل افتراضي؛
تُطلق فئة [Metier] استثناءات من النوع [AleaException]:
package exemples.android.server.metier;
public class AleaException extends RuntimeException {
// error code
private int code;
// manufacturers
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;
}
// getters and setters
....
}
- السطر 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 إلى الإجراء الذي يجب أن يتعامل معه. هذه الإجراءات هي طرق لفئات محددة تسمى [Controllers]. يشير الحرف C في MVC هنا إلى السلسلة [Dispatcher Servlet، Controller، Action]. إذا لم يتم تكوين أي إجراء لمعالجة عنوان URL الوارد، فسيرد [Dispatcher Servlet] بأن عنوان URL المطلوب لم يتم العثور عليه (خطأ 404 NOT FOUND)؛
- معالجة
- يمكن للإجراء المحدد استخدام المعلمات التي مررها [Dispatcher Servlet] إليه. يمكن أن تأتي هذه المعلمات من عدة مصادر:
- مسار [/param1/param2/...] لعنوان URL،
- معلمات عنوان URL [p1=v1&p2=v2]،
- من المعلمات التي أرسلها المتصفح مع طلبه؛
- عند معالجة طلب المستخدم، قد تحتاج العملية إلى طبقة [الأعمال] [2b]. وبمجرد معالجة طلب العميل، قد يؤدي ذلك إلى ظهور استجابات متنوعة. ومن الأمثلة الكلاسيكية على ذلك:
- صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
- صفحة تأكيد في الحالات الأخرى
- تقوم الإجراء بتوجيه عرض معين ليتم عرضه [3]. سيعرض هذا العرض البيانات المعروفة باسم نموذج العرض. هذا هو حرف M في MVC. سيقوم الإجراء بإنشاء نموذج M هذا [2c] وتوجيه عرض V ليتم عرضه [3]؛
- الاستجابة - تستخدم طريقة العرض V المحددة النموذج M الذي أنشأته الإجراء لتهيئة الأجزاء الديناميكية من استجابة HTML التي يجب إرسالها إلى العميل، ثم ترسل هذه الاستجابة.
بالنسبة لخدمة الويب / JSON، يتم تعديل البنية السابقة بشكل طفيف:
![]() |
- في [4a]، يتم تحويل النموذج، وهو فئة Java، إلى سلسلة JSON بواسطة مكتبة JSON؛
- في [4b]، يتم إرسال سلسلة JSON هذه إلى المتصفح؛
يتم عرض مثال على تسلسل كائن Java إلى سلسلة JSON وإلغاء تسلسل سلسلة JSON إلى كائن Java في الملاحق في القسم 6.14.
لنعد إلى طبقة [الويب] في تطبيقنا:
![]() |
في تطبيقنا، يوجد وحدة تحكم واحدة فقط:
![]() |
سترسل خدمة الويب/JSON إلى عملائها استجابة من النوع [Response] على النحو التالي:
package exemples.android.server.web;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- السطر 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 {
// business layer
@Autowired
private IMetier metier;
// mapper JSON
@Autowired
private ObjectMapper mapper;
// random numbers
@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 {
// the answer
Response<Integer> response = new Response<>();
// we use the business layer
try {
response.setBody(metier.getAlea(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// we return the answer
return mapper.writeValueAsString(response);
}
private List<String> getMessagesFromException(Throwable e) {
// message list
List<String> messages = new ArrayList<String>();
// browse the exception stack
Throwable th = e;
while (th != null) {
messages.add(e.getMessage());
th = th.getCause();
}
// we return the result
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: يُطلب من طبقة [business] رقم عشوائي في النطاق [a,b]. تذكر أن طريقة [business].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 {
// web configuration ------------------------------------
@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);
}
// mapper jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- السطر 12: نخبر Spring في أي حزم سيجد المكونين اللذين يحتاج إلى إدارتهما:
- مكون [Metier] المُعلَّم بـ [@Service] في الحزمة [exemples.android.server.metier]؛
- مكون [WebController] المُعلَّم بـ [@Controller] في الحزمة [examples.android.server.web]؛
- السطر 13: تسمح العلامة [@EnableWebMvc] لـ Spring Boot بمعالجة عدد من التكوينات القياسية لتطبيق Spring MVC تلقائيًا. وهذا يقلل بشكل كبير من عبء العمل على المطور؛
- الأسطر 16 و22 و27 و33: تحدد علامة [@Bean] أيضًا مكونات Spring (beans) بنفس الطريقة التي تحدد بها العلامتان السابقتان (@Service و@Controller). هنا، يعلق التعليق التوضيحي [@Bean] على طريقة بدلاً من فئة، ونتيجة الطريقة هي مكون Spring. في حالة عدم وجود سمة تسمية داخل التعليق التوضيحي [@Bean]، يأخذ مكون Spring الذي تم إنشاؤه اسم الطريقة المعلقة؛
- الأسطر 16–20: تحدد bean [dispatcherServlet]. هذا اسم محدد مسبقًا في Spring MVC يحدد وحدة التحكم الأمامية لتطبيق MVC، وهو كائن تمر من خلاله جميع طلبات العملاء ويقوم بتوزيعها (ومن هنا جاء اسمه) على مختلف [@Controller]s في تطبيق Spring MVC؛
- السطر 18: bean [dispatcherServlet] هو مثيل لفئة [DispatcherServlet] التي يوفرها Spring MVC؛
- الأسطر 22–25: تُستخدم حبة [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) {
// application execution
SpringApplication.run(Config.class, args);
}
}
- فئة [Boot] هي فئة قابلة للتنفيذ (الأسطر 7–10)؛
- السطر 9: الطريقة الثابتة [SpringApplication.run] هي طريقة تابعة لـ [Spring Boot] (السطر 4) ستقوم بتشغيل التطبيق. المعلمة الأولى لها هي فئة Java التي تهيئ المشروع. وهنا، هي فئة [Config] التي وصفناها للتو. المعلمة الثانية هي مصفوفة الحجج التي يتم تمريرها إلى الطريقة [main] (السطر 7)؛
يمكن تشغيل تطبيق الويب بعدة طرق، منها ما يلي:
![]() |
ثم يظهر عدد من السجلات في وحدة التحكم:
- الأسطر 12-14: تم تشغيل الخادم المدمج Tomcat؛
- الأسطر 15-19: تم تحميل وتكوين سيرفلت Spring MVC [DispatcherServlet]؛
- السطر 20: تم الكشف عن عنوان URL لخادم الويب [/{a}/{b}]؛
الآن، لنفتح متصفحًا ونختبر عنوان URL 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 لإنشاء ملف قابل للتنفيذ للتطبيق مع جميع تبعياته. بعد تكييفه مع التطبيق الحالي، يصبح هذا التكوين كما يلي:
// créer un binaire avec toutes ses dépendances
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">
<!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="info"> <!-- info, debug, warn -->
<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 البنية التالية:
![]() |
سيتألف العميل من مكونين:
- طبقة [العرض] (العرض + النشاط) مشابهة لتلك التي درسناها في المثال [مثال-14]؛
- طبقة [DAO] التي تتفاعل مع خدمة [الويب / JSON] التي درسناها سابقًا.
1.16.2.1. إنشاء المشروع
نقوم بنسخ المشروع السابق [مثال-14] في [مثال-15] باتباع الإجراء الوارد في القسم 1.4. ونحصل على النتيجة التالية:
![]() | ![]() |
بعد ذلك، ندعو القارئ إلى إنشاء المشروع التالي.
1.16.2.2. تكوين Gradle
![]() |
فيما يلي ملف [build.gradle]:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use 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'
}
}
// options de packaging nécessaires pour être capable de produire l'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 {
// random number
int getAlea(int a, int b);
// URL of the web service
void setUrlServiceWebJson(String url);
// max wait time (ms) for server response
void setTimeout(int timeout);
// client wait time in milliseconds before request
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 {
// 1 random number in the range [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- السطر 12: [WebClient] هي واجهة ستقوم مكتبة AA بتنفيذها بنفسها باستخدام التعليقات التوضيحية التي سنضيفها إليها. يجب أن تنفذ هذه الواجهة استدعاءات لعناوين URL التي تعرضها خدمة الويب / JSON:
// random number
@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، يتم فك تسلسلها تلقائيًا. وبالتالي، نرى في السطر (د) أن عنوان URL [/{a}/{b}] يُرجع نوع String، وهو في الواقع سلسلة JSON (السطر ب). باستخدام هذه المعلومات والمعلومات المتعلقة بالنوع المتوقع في السطر 16، ستقوم مثيل [WebClient] الخاص بالعميل بإلغاء تسلسل السلسلة التي يتلقاها إلى نوع [Response<Integer>]؛
- السطر 15: تعليق توضيحي @Get يشير إلى أنه يجب استدعاء عنوان URL باستخدام طريقة HTTP GET. المعلمة الخاصة بالتعليق التوضيحي @Get هي تنسيق عنوان URL المتوقع من قبل خدمة الويب. ما عليك سوى استخدام المعلمة [value] من التعليق التوضيحي @RequestMapping (السطر b) للطريقة التي يتم استدعاؤها في [WebController] الخاص بالخادم. تحيط الأقواس المتعرجة {} بمعلمات عنوان URL التي يجب تمريرها إلى معلمات الطريقة في السطر 16. تؤدي صيغة [@Path("a") int a] إلى تعيين القيمة {a} من عنوان URL لمعلمة [a] الخاصة بالطريقة. عندما يكون لمعلمة عنوان 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. على جانب الخادم، سيكون لدينا التوقيع التالي:
// random numbers
@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> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
هذه هي فئة [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 {
// service customer REST
@RestService
protected WebClient webClient;
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// timeout before request execution
private int delay;
// interface 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] لتحويلها إلى حبة AA يمكننا حقنها في مكان آخر؛
- السطران 19-20: نقوم بحقن تنفيذ واجهة [WebClient] التي وصفناها. تتولى علامة [@RestService] معالجة هذا الحقن؛
- تقوم الطرق الأخرى بتنفيذ واجهة [IDao] (السطور 27–46)؛
طريقة [setTimeout]
طريقة [setTimeout] هي كما يلي:
@Override
public void setTimeout(int timeout) {
// set the client request timeout REST
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// we build the restTemplate
RestTemplate restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
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) {
// service execution
Response<Integer> info;
DaoException ex;
try {
// waiting
waitSomeTime(delay);
// service execution
info = webClient.getAlea(a, b);
int status = info.getStatus();
if (status == 0) {
// we return the result
return info.getBody();
} else {
// we note the exception
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// we note the exception
ex = new DaoException(e, 100);
}
// we launch the exception
throw ex;
}
...
// private methods -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- السطر 8: انتظر [تأخير] ميلي ثانية؛
- السطر 10: نستدعي ببساطة الطريقة التي تحمل نفس التوقيع في الفئة التي تنفذ واجهة [WebClient]؛
- السطر 11: نقوم بتحليل الاستجابة المستلمة من الخادم عن طريق التحقق من [حالتها]؛
- الأسطر 12–14: إذا لم يكن هناك خطأ من جانب الخادم (status = 0)، فعندئذ نُرجع نتيجة الطريقة؛
- السطر 17: إذا كان هناك خطأ من جانب الخادم (الحالة!=0)، فإننا نعد استثناءً دون إلقائه. أرسل الخادم قائمة برسائل الخطأ. ننشئ استثناءً، يكون رسالته الوحيدة هي سلسلة JSON لقائمة رسائل الخادم؛
- الأسطر 19-22: حالات الاستثناء الأخرى؛
- السطر 24: عندما نصل إلى هذه النقطة، يكون الاستثناء قد حدث بالضرورة. لذا نلقي به؛
استثناء [DaoException] المستخدم في هذا الكود هو كما يلي:
package exemples.android.dao;
import java.util.ArrayList;
import java.util.List;
public class DaoException extends RuntimeException {
// error code
private int code;
// manufacturers
public DaoException() {
}
public DaoException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public DaoException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
// getters and setters
...
}
- السطر 6: [DaoException] هو استثناء لم يتم التعامل معه؛
الطريقة [setUrlServiceWebJson]
طريقة [setUrlServiceWebJson] هي كما يلي:
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// we set the URL of the REST service
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 access
Session getSession();
// change of view
void navigateToView(int position);
// waiting
void beginWaiting();
void cancelWaiting();
// debug mode
boolean IS_DEBUG_ENABLED = true;
// response time
int TIMEOUT = 1000;
// fragment adjacency
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 {
// list of exception messages - version 1
static public List<String> getMessagesFromException(Throwable ex) {
// create a list of error msgs from the exception stack
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// exception message list - version 2
static public String getMessagesForAlert(Throwable th) {
// build the text to be displayed
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--;
}
// result
return texte.toString();
}
}
- الأسطر 9–18: تنشئ قائمة برسائل الخطأ الموجودة في Throwable؛
- الأسطر 21-32: تستخدم الطريقة السابقة لإنشاء النص المراد عرضه في رسالة تنبيه Android من قائمة الرسائل التي تم الحصول عليها؛
- السطور 27-28: يتم ترقيم الرسائل. الرقم الأصغر (1) يتوافق مع الاستثناء الأولي، والرقم الأكبر مع الاستثناء الأحدث في مكدس الاستثناءات؛
1.16.2.5.3. الفئة المجردة [AbstractFragment]
تخدم الفئة [AbstractFragment] غرضين:
- ضمان استدعاء طريقة [updateFragments] للفئات الفرعية دائمًا عند عرض الجزء، ومرة واحدة فقط؛
- استخلاص الحالة والطرق الخاصة بالفئات الفرعية التي يمكن استخلاصها؛
والغرض الثاني هو الذي يدفعنا إلى تضمين عمليات إدارة الصور المؤقتة في هذه الفئة: يجب على جميع مكونات تطبيق Android غير المتزامن التعامل مع هذا النوع من المشكلات:
// wait management
protected void beginWaiting() {
// we set the hourglass
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// the hourglass is removed
mainActivity.cancelWaiting();
}
1.16.2.6. العرض
![]() |
1.16.2.6.1. الطريقة [view1.xml]
![]() |
بالمقارنة مع المثال السابق، تغيرت طريقة العرض [view1.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 {
// visual interface elements
@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;
...
// local data
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
@AfterViews
void afterViews() {
// memory
afterViewsDone=true;
// initially no error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// hidden [Cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
// list of answers
reponses = new ArrayList<>();
}
...
- الأسطر 24–49: إشارات إلى مكونات العرض [view1.xml] (السطر 20)؛
- الأسطر 55-69: طريقة [@AfterViews] التي يتم تنفيذها عند تهيئة الإشارات الموجودة في الأسطر 24-49؛
- السطر 58: لا تنسَ هذا — ضروري لدورة حياة الجزء؛
- الأسطر 60–63: يتم إخفاء رسائل الخطأ؛
- الأسطر 65–66: زر [Cancel] مخفي (السطر 65) وزر [Execute] معروض (السطر 66). لاحظ أنهما موجودان فعليًا أحدهما فوق الآخر؛
- السطر 68: سيحتوي الحقل الموجود في السطر 52 على قائمة السلاسل التي سيتم عرضها بواسطة ListView للردود؛
مباشرة بعد طريقة [@AfterViews]، سيتم تنفيذ طريقة [updateFragment] التالية:
@Override
protected void updateFragment() {
// create the response list adapter
adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
- السطران 4-5: إنشاء محول ListView للإجابات. يتم تخزينه في متغير مثيل بحيث يكون متاحًا للطرق الأخرى في الفئة؛
يؤدي النقر على زر [Execute] إلى تشغيل الطريقة التالية:
// seizures
private int nbAleas;
private int a;
private int b;
private String urlServiceWebJson;
private int delay;
// local data
private int nbInfos;
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
private boolean hasBeenCanceled;
@Click(R.id.btn_Executer)
protected void doExecuter() {
// delete previous answers
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// reset the response counter to 0
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// test the validity of entries
if (!isPageValid()) {
return;
}
// activity initialization
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// we ask for the random numbers
for (int i = 0; i < nbAleas; i++) {
getAlea(a, b);
}
// we start waiting
beginWaiting();
}
@Background(id = "alea")
void getAlea(int a, int b) {
// do as little as possible here
// in any case no display - these must take place in the UiThead
try {
// the result is displayed in the UiThread
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
// the exception is displayed in the 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]، التي يطلب كل منها رقمًا عشوائيًا من خدمة الويب. يتم تشغيل هذه الخيوط بشكل غير متزامن: يقوم خيط واجهة المستخدم بتشغيل خيط [getAlea] (السطر 32) الذي يطلب رقمًا عشوائيًا من خدمة الويب ولا ينتظر انتهاء العملية. سيتم إخطاره بالانتهاء عبر حدث. وبالتالي، سيتم تشغيل خيوط [nbAleas] بالتوازي. من الممكن تكوين التطبيق بحيث لا يشغل سوى خيط واحد في كل مرة. في هذه الحالة، توجد قائمة انتظار للخيوط التي سيتم تنفيذها؛
السطر 38: المعلمة [id] تخصص اسمًا للخيط الذي تم إنشاؤه. هنا، تحمل خيوط [nbAleas] جميعها الاسم نفسه [alea]. سيسمح لنا ذلك بإلغائها جميعًا في نفس الوقت. هذه المعلمة اختيارية إذا لم يتم التعامل مع إلغاء الخيط؛
- السطر 44: يتم استدعاء طريقة [getAlea] الخاصة بالنشاط. وبالتالي، سيتم تنفيذها في مؤشر ترابط منفصل عن واجهة المستخدم. سيقوم مؤشر الترابط هذا بإجراء الاتصال بخدمة الويب ولن ينتظر الرد. سيتم إخطاره لاحقًا عبر حدث بأن الرد متاح. في هذه المرحلة، في السطر 44، سيتم استدعاء طريقة [showInfo] مع الرد المستلم كمعلمة؛
- الأسطر 45-47: قد يؤدي تنفيذ طلب الويب إلى إثارة استثناء. ثم نطلب عرض رسائل خطأ الاستثناء في رسالة تنبيه؛
- السطر 35: ننتظر النتائج:
- سيتم عرض مؤشر التحميل؛
- سيحل زر [Cancel] محل زر [Execute]. نظرًا لأن الخيوط التي تم تشغيلها غير متزامنة، فإن خيط واجهة المستخدم لا ينتظرها، ويتم تنفيذ السطر 35 قبل انتهائها. بمجرد انتهاء طريقة [beginWaiting]، يمكن لواجهة المستخدم أن تستجيب مرة أخرى للمستخدم، مثل النقر على زر [إلغاء]. إذا كانت الخيوط التي تم تشغيلها متزامنة، فلن يتم الوصول إلى السطر 35 إلا بعد انتهاء جميع الخيوط. عندئذٍ لن يكون إلغاؤها منطقيًا؛
طريقة [showInfo] هي كما يلي:
@UiThread
protected void showInfo(int alea) {
if (!hasBeenCanceled) {
// one more piece of information
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// are we done?
if (nbInfos == nbAleas) {
// we end the wait
cancelWaiting();
}
// we add the information to the list of answers
reponses.add(0, String.valueOf(alea));
// we display the answers
adapterReponses.notifyDataSetChanged();
}
}
- يتم استدعاء الطريقة [showInfo] داخل مؤشر الترابط [getAlea] المُعلَّم بعلامة [@Background]. ستقوم هذه الطريقة بتحديث واجهة المستخدم. ولا يمكنها القيام بذلك إلا من خلال التشغيل داخل مؤشر ترابط واجهة المستخدم. وهذا هو معنى العلامة [@UiThread] في السطر 1؛
- السطر 2: تتلقى الطريقة رقمًا عشوائيًا؛
- السطر 3: يتم تنفيذ نص الطريقة فقط إذا لم يقم المستخدم بإلغاء طلبه؛
- السطران 5-6: يتم زيادة عداد الاستجابات وعرضه؛
- الأسطر 8-11: إذا تم استلام جميع الاستجابات المتوقعة، يتم إنهاء الانتظار (نهاية إشارة الانتظار؛ ويحل زر [Execute] محل زر [Cancel])؛
- الأسطر 12-15: يُضاف الرقم العشوائي المستلم إلى قائمة الاستجابات المعروضة بواسطة مكون [ListView listReponses]، ويتم تحديث القائمة؛
طريقة [showAlert] هي كما يلي:
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
// we cancel everything
doAnnuler();
// we display it
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
المنطق هنا مشابه لمنطق طريقة [showInfo]:
- السطر 1: التعليق التوضيحي [@UiThread] مطلوب؛
- السطر 2: تتلقى الطريقة الاستثناء الذي حدث؛
- السطر 3: يتم تنفيذ الطريقة فقط إذا لم يقم المستخدم بإلغاء طلبه؛
- السطر 5: يتم إلغاء طلب المستخدم كما لو كان قد نقر على زر [Cancel] بنفسه؛
- السطر 7: يتم عرض التنبيه باستخدام فئة [AlertDialog] في Android:
- [activity]: هو النشاط من نوع [Activity] المخزن في الفئة الأصلية [AbstractFragment]؛
- [setTitle]: يحدد عنوان نافذة التنبيه [1]؛
- [setMessage]: يحدد الرسالة التي تعرضها نافذة التنبيه [2]؛
- [setNeutral]: يحدد الزر الذي سيغلق نافذة التنبيه [3]؛
- [show]: يطلب عرض نافذة التنبيه؛
![]() |
يتم التعامل مع النقر على زر [إلغاء] بالطريقة التالية:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// memory
hasBeenCanceled=true;
// the asynchronous task is cancelled
BackgroundExecutor.cancelAll("alea", true);
// end of wait
cancelWaiting();
}
- السطر 4: لاحظ أن المستخدم قد ألغى طلبه؛
- السطر 6: يلغي جميع المهام المحددة بالسلسلة [alea]. المعلمة الثانية [true] تعني أنه يجب إلغاؤها حتى لو كانت قد بدأت بالفعل. المعرف [alea] هو المعرف المستخدم لتأهيل طريقة [getAlea] للجزء (السطر 1 أدناه):
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
ملاحظة: تبين أن السطر السادس من كود طريقة [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">
<!-- image d'waiting -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- image d'waiting -->
</android.support.design.widget.AppBarLayout>
...
- الأسطر 17-21: صورة العنصر النائب؛
1.16.2.7.2. نشاط [MainActivity]
لم يتغير [MainActivity] كثيرًا عما كان عليه في [المثال-14]. أولاً، نقوم بإدخال طبقة [DAO] فيه:
// dao injection
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// set the [DAO] layer
setTimeout(TIMEOUT);
}
- السطران 2-3: إدخال طبقة [DAO] عبر تعليق AA؛
- السطور 5-13: الكود الذي يتم تنفيذه بعد هذا الإدخال؛
- السطر 12: تعيين مهلة الانتظار لطبقة [DAO]
بالإضافة إلى ذلك، يجب أن تنفذ نشاط [MainActivity] واجهة [IMainActivity]، التي تمتد بدورها واجهة [IDao]:
// implémentation IMainActivity --------------------------------------------------------------------
@Override
public void navigateToView(int position) {
// the position view is displayed
if (mViewPager.getCurrentItem() != position) {
// fragment display
mViewPager.setCurrentItem(position);
}
}
// hold image management
public void cancelWaiting() {
loadingPanel.setVisibility(View.INVISIBLE);
}
public void beginWaiting() {
loadingPanel.setVisibility(View.VISIBLE);
}
// implémentation IDao --------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// execution
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. إذا كنت متصلاً بشبكة Wi-Fi، فاستخدم عنوان Wi-Fi، وفي هذه الحالة أيضًا، قم بتعطيل جدار الحماية إذا كان لديك واحد.
اختبر التطبيق في الحالات التالية:
- 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 {
...
// debug mode
void setDebugMode(boolean isDebugEnabled);
}
في فئة [Dao]، نضيف الكود التالي:
// debug mode
private boolean isDebugEnabled;
// class name
private String className;
..
// manufacturer
public Dao() {
// class name
className = getClass().getSimpleName();
}
...
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// service execution
Response<Integer> info;
...
@Override
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
- السطر 9: نلاحظ اسم الفئة؛
- الأسطر 16–18: نكتب سجلاً في كل مرة يتم فيها استدعاء الأسلوب [getAlea]؛
بالإضافة إلى ذلك، في جزء [Vue1Fragment]، نضيف السجلات التالية:
@UiThread
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
....
}
@UiThread
protected void showAlert(Throwable th) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
...
}
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
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 ثوانٍ، ثم نضغط على [إلغاء] قبل انتهاء فترة التأخير. فيما يلي السجلات:
- الأسطر 1-5: يتم استدعاء طريقة [getAlea] لفئة [Dao] خمس مرات؛
- السطر 7: طلب المستخدم إلغاء الطلبات؛
- السطر 8: نرى أن [Vue1_Fragment] تتلقى 5 قيم. مرة أخرى، تم تنفيذ جميع الطلبات غير المتزامنة المعلقة؛
لهذا السبب كان علينا إدارة متغير منطقي [hasBeenCanceled] لتجنب عرض أي شيء عند طلب الإلغاء. في كود الإلغاء:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// memory
hasBeenCanceled = true;
// the asynchronous task is cancelled
BackgroundExecutor.cancelAll("alea",true);
// end of wait
cancelWaiting();
}
لا يؤدي الكود الموجود في السطر 10 الغرض المطلوب. قد يكون السبب في ذلك أن المهام غير المتزامنة تشترك في نفس الطريقة المُعلَّمة بـ [@Background]:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. مثال-16: التعامل مع عدم التزامن باستخدام RxAndroid
سنقوم الآن بإدارة عدم التزامن المطلوب لتطبيقات Android باستخدام مكتبة تسمى RxJava [http://reactivex.io/] ونسختها المشتقة لبيئة Android [RxAndroid]. للقيام بذلك، سنستخدم الدورة التدريبية [مقدمة إلى RxJava. التطبيق على بيئات Swing و Android].
1.17.1. إنشاء المشروع
نقوم بنسخ مشروع [المثال-1] إلى [المثال-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 {
// random number
Observable<Integer> getAlea(int a, int b);
// URL of the web service
void setUrlServiceWebJson(String url);
// max wait time (ms) for server response
void setTimeout(int timeout);
// client wait time in milliseconds before request
void setDelay(int delay);
// debug mode
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 {
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// méthodes protégées ----------------------------------------------------------
// generic interface
protected interface IRequest<T> {
Response<T> getResponse();
}
// generic request
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// service execution
return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// service execution
try {
// make the synchronous request and forward the response to the subscriber
Response<T> response = request.getResponse();
// mistake?
int status = response.getStatus();
if (status != 0) {
// we note the exception
ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
} else {
// we issue the answer
subscriber.onNext(response.getBody());
// we signal the end of the observable
subscriber.onCompleted();
}
} catch (JsonProcessingException | RuntimeException e) {
// we note the exception
ex = new DaoException(e, 100);
}
// exception?
if (ex != null) {
// we issue the exception
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: يسترد حالة الاستجابة؛
- الأسطر 32-34: إذا كانت هذه الحالة تشير إلى وجود خطأ، يتم إعداد استثناء؛
- الأسطر 36-39: إذا لم تكن الحالة خطأً، يتم إرسال الاستجابة التي يتوقعها العميل فعليًا (السطر 37)، ويتم إخطار المراقب بأنه لن يكون هناك المزيد من الإصدارات (السطر 39)؛
- الأسطر 41-44: إذا انتهى طلب HTTP باستثناء، يتم تسجيله؛
- الأسطر 46-49: إذا لم يكن الاستثناء [ex] فارغًا، يتم إرساله إلى المراقب. لا داعي هنا لاستدعاء الأسلوب [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 {
// service customer REST
@RestService
protected WebClient webClient;
// timeout before request execution
private int delay;
// debug mode
private boolean isDebugEnabled;
// class name
private String className;
// manufacturer
public Dao() {
// class name
className = getClass().getSimpleName();
}
// interface IDao -------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// web client execution
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// waiting
waitSomeTime(delay);
// synchronous HTTP call
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>.getResponse()]؛
- السطر 36: هنا نستخدم ميزة تسمى الإغلاق: وهي القدرة على تغليف القيم الخارجية للمثيل بداخله عند إنشائه، وفي هذه الحالة قيم [a, b] من السطر 24. وهذا ما يسمح للطريقة [IRequest<Integer>.getResponse()] بعدم احتوائها على معلمات. وقد تم تضمين هذه القيم داخل نص الطريقة. وحيثما كنا عادةً نغير معلمات الدالة (a, b) -> (x, y)، ننشئ هنا مثيلًا جديدًا لـ [IRequest<Integer>] يغلف قيم x و y؛
1.17.7. فئة [MainActivity]
تتطور فئة [MainActivity]، التي تنفذ واجهة [IDao]، على النحو التالي:
// implémentation IDao --------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(int a, int b) {
// execution
return dao.getAlea(a, b);
}
1.17.8. فئة [Vue1Fragment]
تتطور فئة [Vue1Fragment] على النحو التالي:
@Click(R.id.btn_Executer)
protected void doExecuter() {
// delete previous answers
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// reset the response counter to 0
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// test the validity of entries
if (!isPageValid()) {
return;
}
// activity initialization
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// we ask for the random numbers
getAleasInBackground(a, b);
// we start waiting
beginWaiting();
}
- السطر 18: طلب أرقام عشوائية من طريقة [getAleasInBackground]، التي سُميت بهذا الاسم لأن الأرقام سيتم طلبها في مؤشر ترابط مختلف عن مؤشر ترابط واجهة المستخدم؛
private int nbReponses = 0;
// subscriptions to observables
private List<Subscription> abonnements;
// annotation [Background] unnecessary
void getAleasInBackground(int a, int b) {
// initially no response and no subscriptions
nbReponses = 0;
abonnements.clear();
// prepare the observable
Observable<Integer> response = Observable.empty();
// merge the results of the various HTTP calls
// they are executed on an I/O thread
for (int i = 0; i < nbAleas; i++) {
response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
}
// the cumulative observable will be observed on the UI thread
response = response.observeOn(AndroidSchedulers.mainThread());
try {
// the observable is executed
abonnements.add(response.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
// we add the information to the list of answers
showInfo(alea);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// error message
showAlert(th);
// end waiting
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// end waiting
cancelWaiting();
}
}));
} catch (RuntimeException e) {
// the exception is displayed in the UiThread
showAlert(e);
}
}
- السطر 3: لكل عنصر قابل للمراقبة مشتركون. يُسمى الرابط بين المشترك والعملية التي يراقبها «اشتراك». هنا، سيكون لدينا عملية واحدة فقط قيد المراقبة ومشترك واحد. لذلك، سيكون لدينا اشتراك واحد فقط. من أجل توضيح المبدأ، نتعامل مع الأمر كما لو كان لدينا عدة عمليات قيد المراقبة يتم رصدها بواسطة مراقبين مختلفين، مما سيؤدي إلى عدة اشتراكات؛
- الأسطر 11–18: نقوم بتكوين العملية المراقبة (المراقبة). من المهم أن نفهم أن هذا مجرد تكوين: لا يتم تنفيذ العملية؛
- السطر 11: نبدأ بمراقبة فارغة، وهي مراقبة لا تصدر أي شيء؛
- الأسطر 14-16: إلى هذه القابلة للمراقبة الفارغة، نضيف [nbAleas] قابلة للمراقبة، والتي ستكون [nbAleas] طلبات HTTP تعيد [nbAleas] أرقام عشوائية؛
- السطر 15: كما في السابق، يُطلب الرقم العشوائي i من فئة [MainActivity]. من المهم أن نفهم أنه لم يتم تنفيذ أي طلب HTTP بعد. يتم تنفيذ الطريقة [mainActivity.getRandom(a, b)] وتُرجع [Observable<Integer>]. هذه عملية سيتم ملاحظتها بمجرد إطلاقها؛
- السطر 15: تطلب الطريقة [subscribeOn(Schedulers.io())] تنفيذ العملية (عندما يتم تنفيذها) على مؤشر ترابط I/O. توفر مكتبة RxJava أنواعًا مختلفة من مؤشرات الترابط. مؤشر ترابط I/O مناسب لطلبات HTTP؛
- السطر 15: يتم دمج المراقب #i مع المراقب الأولي من السطر 11: من [nbAleas] من المراقبين، كل منها يصدر عنصرًا واحدًا، نقوم بإنشاء مراقب سيصدر [nbAleas] عنصرًا. هذا هو المراقب الذي سيتم مراقبته. يصدر هذا المراقب إشعار [onCompleted] عندما تكون جميع المراقبين المكونين له قد أصدروا إشعارات [onCompleted] الخاصة بهم. سيوفر لنا هذا عناء حساب الردود، كما فعلنا في الإصدار السابق، لتحديد ما إذا كنا قد تلقينا جميع الأرقام المتوقعة؛
- السطر 18: في هذه المرحلة، قمنا بتكوين عنصر قابل للمراقبة يتكون من [nbAleas] عناصر قابلة للمراقبة، يعمل كل منها على مؤشر ترابط I/O؛
- السطر 18: تحدد الطريقة [observeOn(AndroidSchedulers.mainThread())] الخيط الذي يجب مراقبة القيم التي تصدرها المراقبة عليه. هنا، ينتمي الخيط [AndroidSchedulers.mainThread())] إلى مكتبة RxAndroid، وليس RxJava. وهو يشير إلى خيط واجهة المستخدم، المعروف أيضًا باسم حلقة الأحداث. هذه النقطة مهمة: في تطبيق 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] على النحو التالي:
// annotation [UiThread] unnecessary
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
if (!hasBeenCanceled) {
// one more piece of information
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// we add the information to the list of answers
reponses.add(0, String.valueOf(alea));
// display answers
adapterReponses.notifyDataSetChanged();
}
}
تحتوي الطريقة على تغييرين:
- السطر 1: أزلنا تعليق AA [@UiThread]؛
- لم نعد نحسب الاستجابات لتحديد ما إذا كان يجب التوقف عن الانتظار أم لا. أصبح الآن حدث [onCompleted] الخاص بالمراقب هو الذي يوفر هذه المعلومات؛
تتغير طريقة [showAlert] على النحو التالي:
// annotation [UiThread] unnecessary
protected void showAlert(Throwable th) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
if (!hasBeenCanceled) {
// we cancel everything
doAnnuler();
// we display it
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() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// memory
hasBeenCanceled = true;
// asynchronous tasks are cancelled
if (abonnements != null) {
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
}
// end of wait
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 ثوانٍ، ثم النقر على [إلغاء] قبل انتهاء فترة التأخير. السجلات هي كما يلي:
بعد السطر 6، لا توجد سجلات أخرى، مما يدل على أن المراقب (Vue1Fragment) لم يعد يتلقى إخطارات من العملية المراقبة.
هذا هو السلوك المتوقع للإلغاء. لذلك يمكننا إزالة المتغير المنطقي [hasBeenCanceled] من كود [Vue1Fragment] الذي قدمناه في المثال السابق لأن الإلغاء لم يكن يعمل كما هو متوقع.
حقيقة أن المراقب لم يعد يتلقى إخطارات بعد إلغاء العنصر القابل للمراقبة لا تعني أن طلبات HTTP نفسها قد تم إلغاؤها. لرؤية ذلك، نقوم بتعديل فئة [Dao] على النحو التالي:
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// web client execution
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// waiting
waitSomeTime(delay);
// synchronous HTTP call
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، مثل جزء (fragment)، تظهر مشاكل خطيرة. عندئذ يضطر المرء إلى التخلي عن AA واستخدام الحل 1 للبرمجة غير المتزامنة؛
يمكن للقراء المهتمين باستكشاف إمكانيات مكتبة RxAndroid بشكل أعمق الرجوع إلى الوثيقة [مقدمة إلى RxJava. التطبيق في بيئات Swing وAndroid]. وهي تستخدم RxAndroid بدون مكتبة AA.
1.18. مثال-17: مكونات إدخال البيانات
سننشئ مشروعًا جديدًا لتوضيح بعض المكونات الشائعة المستخدمة في نماذج إدخال البيانات.
1.18.1. إنشاء المشروع
نقوم بنسخ مشروع [المثال-13] إلى [المثال-17]:
![]() | ![]() |
سيحتوي المشروع الجديد على عرض واحد فقط [view1.xml]. لذلك، سنقوم بحذف العرض [view2.xml] والجزء المرتبط به [View2Fragment] [2]. سنقوم بتطبيق هذا التغيير في مدير الأجزاء في [MainActivity]:
// our fragment manager to be redefined for each application
// must define the following methods: getItem, getCount, getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// fragments
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>
<!-- vue 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. جزء النموذج
![]() |
فيما يلي فئة [View1Fragment]:
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;
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// the fields of the view displayed by the fragment
@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;
// drop-down list
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// check the first button
radioButton1.setChecked(true);
// the calendar
datePicker1.setCalendarViewShown(false);
// on 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));
}
});
// the drop-down list
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() {
// initialize drop-down list adapter
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 [view1] (السطر 18)؛
- السطر 58: تتيح لك طريقة [setChecked] تحديد زر اختيار أو مربع اختيار؛
- السطر 60: بشكل افتراضي، يعرض مكون [DatePicker] حقل إدخال التاريخ والتقويم. السطر 60 يزيل التقويم؛
- السطر 62: [SeekBar].setMax() يحدد القيمة القصوى للمزلاق. القيمة الدنيا هي 0؛
- الأسطر 63-74: نتعامل مع أحداث شريط التمرير. لكل تغيير يقوم به المستخدم، نريد عرض قيمة شريط التمرير في [TextView] في السطر 49؛
- السطر 71: تمثل المعلمة [progress] قيمة شريط التمرير؛
- الأسطر 76-79: قائمة من [String]s التي سيتم ربطها بالقائمة المنسدلة؛
- السطر 90: طريقة [updateFragment] الخاصة بالجزء. عند تنفيذها، يتم تهيئة متغير [activity] الخاص بالفئة الأصلية؛
- السطر 92: مصدر البيانات [list] مرتبط بمحول القائمة المنسدلة؛
- السطران 93-94: يتم ربط [dataAdapter] بقائمة المنسدلة [dropDownList]؛
- السطر 84: ترتبط طريقة [doValider] بالنقر على زر [Valider]؛
الغرض من طريقة [doValider] هو عرض القيم التي أدخلها المستخدم. وفيما يلي كودها:
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// list of messages to display
List<String> messages = new ArrayList<>();
// checkbox
boolean isChecked = checkBox1.isChecked();
messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
// radio buttons
int id = radioGroup.getCheckedRadioButtonId();
String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
// on SeekBar
int progress = seekBar.getProgress();
messages.add(String.format("SeekBar [value=%d]", progress));
// the input field
String texte = String.valueOf(saisie.getText());
messages.add(String.format("Saisie simple [value=%s]", texte));
// the switch
boolean état = switch1.isChecked();
messages.add(String.format("Switch [value=%s]", état));
// the date
int an = datePicker1.getYear();
int mois = datePicker1.getMonth() + 1;
int jour = datePicker1.getDayOfMonth();
messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
// multi-line text
String lignes = String.valueOf(multiLignes.getText());
messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
// by the hour
int heure = timePicker1.getHour();
int minutes = timePicker1.getMinute();
messages.add(String.format("Heure [%d, %d]", heure, minutes));
// drop-down list
int position = dropDownList.getSelectedItemPosition();
String selectedItem = String.valueOf(dropDownList.getSelectedItem());
messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
// display
doAfficher(messages);
}
- السطر 4: ستُضاف القيم المدخلة إلى قائمة الرسائل؛
- السطر 6: تحدد طريقة [CheckBox].isChecked() ما إذا كان مربع الاختيار محددًا أم لا؛
- السطر 9: تُرجع الطريقة [RadioGroup].getCheckedButtonId() معرّف زر الاختيار المحدد أو -1 إذا لم يتم تحديد أي زر؛
- السطر 10: يسترد الرمز [activity.findViewById(id)] زر الاختيار المحدد وبالتالي تسميته؛
- السطر 13: تعرض طريقة [SeekBar].getProgress() قيمة شريط التمرير؛
- السطر 19: تحدد الطريقة [Switch].isChecked() ما إذا كان المفتاح في وضع التشغيل (true) أو إيقاف التشغيل (false)؛
- السطر 22: تسترد الطريقة [DatePicker].getYear() السنة المحددة باستخدام كائن [DatePicker]؛
- السطر 23: تعرض طريقة [DatePicker].getMonth() الشهر المحدد من كائن [DatePicker] ضمن النطاق [0,11]؛
- السطر 24: تعرض طريقة [DatePicker].getDayOfMonth() اليوم المحدد من الشهر باستخدام كائن [DatePicker] ضمن النطاق [1,31]؛
- السطر 30: تعرض طريقة [TimePicker].getHour() الساعة المحددة باستخدام كائن [TimePicker]؛
- السطر 31: تُرجع الطريقة [TimePicker].getMinute() الدقائق المحددة باستخدام كائن [TimePicker]؛
- السطر 34: تُرجع الطريقة [Spinner].getSelectedItemPosition() موضع العنصر المحدد في قائمة منسدلة؛
- السطر 35: تُرجع الطريقة [Spinner].getSelectedItem() العنصر المحدد في قائمة منسدلة؛
فيما يلي طريقة [doAfficher]، التي تعرض قائمة القيم المدخلة:
private void doAfficher(List<String> messages) {
// we build the poster text
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// we display it
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. إنشاء المشروع
نقوم بإنشاء مشروع جديد [مثال-18] عن طريق نسخ مشروع [مثال-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. إنشاء المشروع
نقوم بإنشاء مشروع جديد [Example-19] عن طريق استنساخ مشروع [Example-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 {
// a list of data
private List<Data> liste=new ArrayList<>();
// getters and setters
...
}
- السطر 11: قائمة البيانات المستخدمة من قبل كلا العرضين؛
فئة [Data] هي كما يلي:
package exemples.android.architecture;
public class Data {
// data
private String texte;
private boolean isChecked;
// manufacturer
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
// getters and setters
...
}
- السطر 6: النص الذي سيملأ [TextView] الأول لكل عنصر في القائمة؛
- السطر 7: القيمة المنطقية التي ستُستخدم لتحديد أو إلغاء تحديد [checkBox] لكل عنصر في القائمة؛
1.20.3. [MainActivity]
يصبح كود طريقة [@AfterInject] كما يلي:
// injection session
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// create a list of data
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. طريقة العرض الأولية [View1]
![]() | ![]() |
تعرض طريقة العرض XML [view1.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 {
// the fields of the view displayed by the fragment
@ViewById(R.id.listView1)
protected ListView listView;
// list adapter
private ListAdapter adapter;
// init done
private boolean initDone = false;
@AfterViews
void afterViews() {
// memory
afterViewsDone = true;
}
@Click(R.id.button_vue2)
void navigateToView2() {
// navigate to view 2
mainActivity.navigateToView(1);
}
public void doRetirer(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
// associate data with [ListView]
adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
initDone = true;
}
// if the fragment has been (re)generated - in this case the ListView must be reconnected to its adapter
listView.setAdapter(adapter);
// if other fragments have changed the data source - in this case, refresh the ListView
adapter.notifyDataSetChanged();
}
}
- السطر 15: عرض XML [view1] مرتبط بالجزء؛
- الأسطر 26–30: لا تقوم طريقة [@AfterViews] بأي شيء. ومع ذلك، من الضروري تعيين المتغير [afterViewsDone] إلى true لأنه يستخدمه الفئة الأصلية [AbstractFragment]؛
- الأسطر 42–53: طريقة [updateFragment]، التي يتم استدعاؤها في كل مرة يصبح فيها الجزء مرئيًا. تمت كتابة الطريقة هنا كما لو أن الجزء يمكن أن يغادر جوار الجزء المعروض وبالتالي يعيد تعيين دورة حياته. هذا ليس هو الحال هنا، ولكنه سيكون كذلك إذا كان التطبيق يحتوي على 3 أجزاء بجوار واحد؛
- السطر 44: لا يحتاج محول [ListView] إلا إلى التهيئة مرة واحدة؛
- السطر 46: نربط [ListAdapter] بهذا [ListView]. سنقوم بإنشاء هذه الفئة. وهي مشتقة من فئة [ArrayAdapter]، التي استخدمناها بالفعل لربط البيانات بـ [ListView]. نمرر معلومات متنوعة إلى منشئ [ListAdapter]:
- مرجع إلى النشاط الحالي،
- معرف العرض الذي سيتم إنشاء مثيل له لكل عنصر في القائمة،
- مصدر بيانات لملء القائمة،
- مرجع إلى الجزء. سيُستخدم هذا للتعامل مع النقر على رابط [Remove] في [ListView] عبر طريقة [doRemove] في السطر 38؛
- السطر 50: يتم ربط المُكيّف بـ [ListView]. وفي الوقت نفسه، يتم ربط مصدر البيانات [lists] بـ [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> {
// execution context
private Context context;
// the id of the layout displaying a line in the list
private int layoutResourceId;
// list data
private List<Data> data;
// the fragment that displays the [ListView]
private Vue1Fragment fragment;
// the adapter
final ListAdapter adapter = this;
// manufacturer
public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
super(context, layoutResourceId, data);
// memorize information
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) {
// create the current ListView line
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// the text
TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
textView.setText(data.get(position).getTexte());
// the checkbox
CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
checkBox.setChecked(data.get(position).isChecked());
// the [Remove] link
TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
txtRetirer.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
fragment.doRetirer(position);
}
});
// manage the click on the checkbox
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.get(position).setChecked(isChecked);
}
});
// we return the line
return row;
}
- السطر 2: تأخذ الطريقة ثلاثة معلمات. سنستخدم المعلمة الأولى فقط؛
- السطر 4: نقوم بإنشاء عرض للعنصر #[position]. هذا هو عرض [list_data] الذي تم تمرير معرفه كمعلمة ثانية إلى المنشئ. ثم نسترد المراجع إلى مكونات العرض الذي قمنا بإنشاء مثيل له للتو؛
- السطر 6: نسترد الإشارة إلى [TextView] #1؛
- السطر 7: نخصص له نصًا من مصدر البيانات الذي تم تمريره كمعلمة ثالثة إلى المنشئ؛
- السطر 9: نسترد الإشارة إلى [CheckBox] #2؛
- السطر 10: نقوم بتحديدها أو إلغاء تحديدها باستخدام قيمة من مصدر بيانات [ListView]؛
- السطر 12: نسترد الإشارة إلى [TextView] #3؛
- الأسطر 13-18: معالجة النقر على رابط [Remove]؛
- السطر 16: تعالج طريقة [Vue1Fragment].doRetirer هذا النقر. من المنطقي أن يتولى الجزء الذي يعرض [ListView] معالجة هذا الحدث. فهو يتمتع بنظرة عامة لا تمتلكها فئة [ListAdapter]. تم تمرير الإشارة إلى الجزء [Vue1Fragment] كمعلمة رابعة إلى منشئ الفئة؛
- الأسطر 20–25: التعامل مع النقر على مربع الاختيار. ينعكس الإجراء الذي يتم تنفيذه عليه في البيانات التي يعرضها. ويرجع ذلك إلى السبب التالي: [ListView] هي قائمة تعرض جزءًا فقط من عناصرها. وبالتالي، يتم إخفاء عنصر القائمة أحيانًا وعرضه أحيانًا أخرى. عندما يلزم عرض العنصر #i، يتم استدعاء الأسلوب [getView] من السطر 2 أعلاه للموضع #i. سيقوم السطر 10 بإعادة حساب حالة مربع الاختيار استنادًا إلى البيانات المرتبطة به. ولذلك، يجب عليه تخزين حالة مربع الاختيار بمرور الوقت؛
1.20.8. إزالة عنصر من القائمة
يتم التعامل مع النقر على رابط [Remove] في جزء [Vue1Fragment] بواسطة طريقة [doRetirer] التالية:
public void doRetirer(int position) {
// remove element n° [position] from the list
List<Data> liste = mainActivity.getListe();
liste.remove(position);
// note the scroll position to return to it
// read
// [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// position of 1st element fully visible or not
int firstPosition = listView.getFirstVisiblePosition();
// y offset of this element relative to the top of the ListView
// measures the height of any hidden part
View v = listView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// refresh the [ListView]
adapter.notifyDataSetChanged();
// we position ourselves at the right spot on the ListView
listView.setSelectionFromTop(firstPosition, top);
}
- السطر 1: الحصول على الموضع في [ListView] للرابط [Remove] الذي تم النقر عليه؛
- السطر 3: استرداد قائمة البيانات؛
- السطر 4: إزالة العنصر الموجود في الموضع [position]؛
- السطر 15: نقوم بتحديث [ListView]. بدون ذلك، لن يتغير شيء بصريًا.
- الأسطر 5–13، 17: عملية معقدة إلى حد ما. بدونها، يحدث ما يلي:
- يعرض [ListView] الأسطر 15-18 من قائمة البيانات،
- يتم حذف السطر 16،
- السطر 15 أعلاه يعيد تعيينها بالكامل، ثم يعرض [ListView] الأسطر 0-3 من قائمة البيانات؛
مع الأسطر أعلاه، يتم الحذف ويبقى [ListView] في السطر الذي يلي السطر المحذوف.
1.20.9. عرض XML [View2]
![]() | ![]() |
فيما يلي كود 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 {
// fields of view
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@AfterViews
void initFragment(){
// memory
afterViewsDone=true;
}
@Click(R.id.button_vue1)
void navigateToView1() {
// navigate to view 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// displays list items selected in view 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 {
// data
private String texte;
private boolean isChecked;
// manufacturer
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
...
}
في السطر 7، استخدمنا متغيرًا منطقيًا لإدارة مربعات الاختيار الخاصة بالعناصر الموجودة في [ListView]. غالبًا ما تحتاج [ListView] إلى عرض بيانات يمكن تحديدها عن طريق تحديد مربع، حتى لو لم يكن للعنصر الموجود في مصدر البيانات حقل منطقي مطابق لهذا المربع. في هذه الحالة، يمكنك اتباع الخطوات التالية:
تصبح فئة [Data] كما يلي:
package exemples.android.fragments;
public class Data {
// data
private String texte;
// manufacturer
public Data(String texte) {
this.texte = texte;
}
// getters and setters
...
}
نقوم بإنشاء فئة [CheckedData] مشتقة من الفئة السابقة:
package exemples.android.fragments;
public class CheckedData extends Data {
// checked item
private boolean isChecked;
// manufacturer
public CheckedData(String text, boolean isChecked) {
// parent
super(text);
// local
this.isChecked = isChecked;
}
// getters and setters
...
}
ثم استبدل ببساطة نوع [Data] بنوع [CheckedData] في جميع أنحاء الكود (MainActivity، ListAdapter، View1Fragment، View2Fragment). على سبيل المثال، في [MainActivity]:
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// create a list of data
List<CheckedData> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new CheckedData("Texte n° " + i, false));
}
}
يتم توفير المشروع الخاص بهذا الإصدار تحت اسم [Example-19B].
1.21. مثال-20: استخدام قائمة
1.21.1. إنشاء المشروع
نقوم بنسخ مشروع [Example-19B] إلى مشروع [Example-20]:
![]() | ![]() |
![]() | 3 ![]() |
سنقوم بإزالة الأزرار من العرضين 1 و2 واستبدالها بخيارات القائمة [1-2].
1.21.2. تعريف XML للقوائم
![]() |
يحدد الملف [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 {
// data accessible to daughter classes
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
protected String className;
// activity
protected IMainActivity mainActivity;
protected Activity activity;
// session
protected Session session;
// menu
private Menu menu;
private int[] menuOptions;
private boolean initDone;
// manufacturer
public AbstractFragment() {
// init
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// memory
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
// retrieve # menu options if not already done
if (!initDone) {
// retrieve the # menu options
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transfer the list of options to a table
menuOptions = new int[menuOptionsIds.size()];
for (int i = 0; i < menuOptions.length; i++) {
menuOptions[i] = menuOptionsIds.get(i);
}
// activity
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
// memory
initDone = true;
}
// the girl fragment is asked to stand
updateFragment();
}
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
...
}
// display menu options -----------------------------------
protected void setAllMenuOptions(boolean isVisible) {
....
}
protected void setMenuOptions(MenuItemState[] menuItemStates) {
...
}
// update girl class
protected abstract void updateFragment();
}
- السطر 42: تُظهر السجلات أن طريقة [onCreateOptionsMenu] يتم استدعاؤها في كل مرة يتم فيها عرض الجزء. يتم استدعاؤها في وقت متأخر جدًا، وتحديدًا بعد استدعاء طريقة [updateFragment]. يشير هذا إلى أنه يمكن استخدامها لتحديث الجزء. وهذا ما سنفعله هنا (السطر 63)؛
- السطر 42: تحتوي الطريقة على معلمتين:
- [menu]: وهي قائمة فارغة؛
- [inflater]: أداة تسمح لنا بإنشاء القائمة من وصفها الأولي. لن نستخدم هذا الخيار هنا لأننا سنستخدم تعليق AA الذي سيقوم بذلك نيابة عنا؛
- السطر 44: نقوم بتخزين القائمة. سنحتاجها لاحقًا؛
- السطران 52-53: نقوم بتخزين معرّفات جميع عناصر القائمة في المصفوفة من السطر 28؛
- الأسطر 55–57: تُظهر السجلات أنه عند استدعاء الدالة [onCreateOptionsMenu]، تُرجع الدالة [Fragment.getActivity()] النشاط المرتبط بالجزء؛
- السطر 55: نقوم بتخزين النشاط كمثيل لفئة [Activity] في Android؛
- السطر 56: نقوم بتخزين النشاط كمثيل لواجهة [IMainActivity]؛
- السطر 57: نقوم بتخزين الجلسة؛
- السطر 59: نلاحظ أن الفئة قد تم تهيئتها بالفعل، لذا لا داعي لإعادة التهيئة (السطر 50)؛
- السطر 63: نطلب من الجزء الفرعي تحديث نفسه. وهذا ممكن لأن الجزء مرئي ومرتبط بعرضه وقائمته؛
طريقة [getMenuOptions]، التي تسترد معرّفات عناصر القائمة، هي كما يلي:
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// scroll through all menu items
for (int i = 0; i < menu.size(); i++) {
// item n° i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// if item n° i is a sub-menu, then start again
if (menuItem.hasSubMenu()) {
// recursivity
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
تسمح لك طريقة [setAllMenuOptions] بإخفاء أو إظهار جميع خيارات القائمة؛
protected void setAllMenuOptions(boolean isVisible) {
// update all menu options
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
تسمح لك طريقة [setMenuOptions] بإخفاء أو إظهار خيارات معينة من القائمة؛
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// update certain menu options
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
فئة [MenuItemState] هي كما يلي:
![]() |
package exemples.android.architecture;
public class MenuItemState {
// menu option identify
private int menuItemId;
// option visibility
private boolean isVisible;
// manufacturers
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters and setters
...
}
1.21.4. إدارة القائمة في جزء [View1Fragment]
تصبح فئة [Vue1Fragment] كما يلي:
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
...
@OptionsItem(R.id.navigationVue2)
void navigateToView2() {
// navigate to view 2
mainActivity.navigateToView(1);
}
@OptionsItem(R.id.actionValider)
void valider() {
// a message is displayed
Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
}
private boolean actionCacherMontrerTout = true;
@OptionsItem(R.id.actionCacherMontrerTout)
void cacherMontrerTout() {
// we change state
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() {
// we change state
actionCacherMontrerActions = !actionCacherMontrerActions;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
}
private boolean actionCacherMontrerActionsValider = true;
@OptionsItem(R.id.actionCacherMontrerActionsValider)
void actionCacherMontrerActionsValider() {
// we change state
actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
}
...
@Override
protected void updateFragment() {
....
// update the menu
//setMenuOptions(...)
}
}
- السطر 2: القائمة [res/menu/menu_vue1.xml] مرتبطة بالجزء؛
- السطر 48: عند تنفيذ طريقة [updateFragment]، يمكن أيضًا تحديث القائمة لتعكس الحالة الجديدة للجزء؛
- السطر 7: التعليق التوضيحي [@OptionsItem(R.id.navigationVue2)] يعلق على الطريقة التي يجب تنفيذها عند النقر على خيار القائمة [Navigation / View 2]؛
- الأسطر 19-25: لإخفاء فرع من القائمة، ما عليك سوى إخفاء خيار الجذر الخاص به؛
- السطر 24: يتم إظهار أو إخفاء الخيارات الجذرية [menuNavigation، menuActions]؛
- السطر 40: لإظهار خيار في فرع القائمة، يجب عليك ليس فقط إظهار هذا الخيار، بل أيضًا جميع الخيارات التي تصادفها عند الانتقال من الخيار النهائي عائدًا إلى جذر القائمة؛
1.21.5. إدارة القائمة في جزء [Vue2Fragment]
يمكن العثور على كود مشابه في جزء View 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 {
// fields of view
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// navigate to view 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// displays list items selected in view 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);
// update the menu
// setMenuOptions(...)
}
}
- السطر 35: عرض خيار [التنقل / العرض 1]؛
- الأسطر 17-20: عند النقر على خيار [التنقل / العرض 1]، يتم استدعاء الأسلوب [navigateToView1]؛
1.21.6. التنفيذ
قم بإنشاء سياق وقت تشغيل لهذا المشروع وقم بتشغيله.
1.22. مثال-21: إعادة هيكلة فئة [AbstractFragment]
أظهر لنا المثال السابق أنه عندما تحتوي القطعة على قائمة، فإن طريقة [onCreateOptionsMenu] الخاصة بها هي المكان المناسب لطلب تحديث القطعة لنفسها:
- يتم استدعاؤها مرة واحدة فقط عندما يكون الجزء على وشك العرض؛
- عند استدعائها، يتم إنشاء ارتباطات الجزء بنشاطه وعرضه وقائمته؛
لتوضيح ذلك، سنعود إلى المثال 12، الذي يضم العديد من الأجزاء التي يمكن تعديل تجاورها. في ذلك المثال، لم تكن الأجزاء تحتوي على قائمة. سنربط قائمة فارغة بها.
1.22.1. إنشاء المشروع
نقوم بنسخ مشروع [Example-12] إلى مشروع [Example-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 #3]؛
- في [2]، النص المعروض هو بالفعل نص الجزء رقم 3، لكن عداد الزيارات غير صحيح؛
خلال هذه الدورة، تكون السجلات كما يلي:
- السطر 1: يمكننا أن نرى أن النشاط قد أعيد بناؤه بالكامل؛
- الأسطر 3-7: ينطبق الأمر نفسه على الأجزاء الخمسة التي يديرها النشاط؛
- السطر 21: الجزء رقم 3 على وشك العرض. نرى أن عدد الزيارات يساوي 0 قبل الزيادة؛
يمكننا إذن تفسير النتيجة التي تم الحصول عليها بعد الدوران على النحو التالي:
- تقوم فئة [MainActivity] في البداية بإنشاء شريط علامات تبويب يحتوي على علامة تبويب واحدة تحمل اسم [View 1]. هذه هي علامة التبويب المرئية؛
- بعد تدوير الجهاز، يعيد مدير الصفحات [mViewPager] عرض نفس الجزء، وهو في هذه الحالة الجزء رقم 3. من المهم أن نتذكر هنا أن علامات التبويب والأجزاء مفاهيم مختلفة ولها دورات حياة مختلفة. سيتم تنفيذ طريقة [updateFragment] للجزء رقم 3:
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// increment visit no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- السطر 7: يتم قراءة معرّف الزيارة الأخيرة من الجلسة. ومع ذلك، فقد تمت إعادة تعيين الجلسة — مثل كل شيء آخر —، كما تمت إعادة تعيين معرّف الزيارة إلى الصفر. وهذا يفسر النتيجة المعروضة في الجزء رقم 3؛
1.23.2. طرق حفظ/استعادة النشاط والأجزاء
1.23.2.1. الحل 1: النسخ الاحتياطي اليدوي
عندما يتم تدوير الجهاز، يتم استدعاء طريقتين من النشاط:
// backup / restore management ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// backup activity status
// ....
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// restoring activity
// ...
}
- الأسطر 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، getBoolean، getChar، ...] مع معلمة واحدة: T getT(String key)؛
تحتوي الأجزاء على هاتين الطريقتين نفسهما لحفظ حالتها.
سنستخدم هذه المعلومات لحفظ واستعادة حالة المثال 21. للقيام بذلك، نقوم بنسخ مشروع [Example-21] إلى [Example-22].
1.23.2.2. الحل 2: الحفظ التلقائي
تشير وثائق Android إلى أنه عند تدوير الجهاز، يمكنك منع إزالة "الجزء" (fragment) باستخدام الأمر: [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. طريقة النسخ الاحتياطي/الاستعادة لمشروع [Example-22]
يتطور مشروع [Example-22] على النحو التالي:
![]() |
تظهر فئتان جديدتان:
- [PlaceHolderFragmentState]، والتي ستخزن حالة جزء من النوع [PlaceHolderFragment]؛
- [Vue1FragmentState]، والتي ستخزن حالة جزء من النوع [Vue1Fragment]؛
هذه الفئات هي كما يلي:
package exemples.android;
public class Vue1FragmentState {
// status Vue1Fragment
private boolean hasBeenVisited=false;
// getters and setters
...
}
- السطر 5: تكون القيمة المنطقية [hasBeenVisited] صحيحة إذا تمت زيارة (عرض) الجزء [Vue1Fragment] مرة واحدة على الأقل. تم إنشاء هذا الحقل في المثال لأن الجزء [Vue1Fragment] لا يحتوي على أي شيء لحفظه؛
فئة [PlaceHolderFragmentState] هي كما يلي:
package exemples.android;
public class PlaceHolderFragmentState {
// whether visited or not
private boolean hasBeenVisited;
// display text
private String text;
// getters and setters
...
}
- السطر 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 {
// number of fragments visited
private int numVisit;
// n° fragment type [PlaceholderFragment] displayed in the second tab
private int numFragment = -1;
// selected tab no
private int selectedTab = 0;
// n° current view
private int currentView;
// fragment backups ---------------
private Vue1FragmentState vue1FragmentState;
private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
// manufacturer
public Session() {
for (int i = 0; i < placeHolderFragmentStates.length; i++) {
placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
}
vue1FragmentState = new Vue1FragmentState();
}
// getters and setters
...
}
- السطر 18: حالة الجزء [Vue1Fragment]؛
- السطر 19: حالة الأجزاء من النوع [PlaceHolderFragment]؛
- الأسطر 22–27: في منشئ الجلسة، يتم تهيئة الحقول من الأسطر 18 و 19؛
- الأسطر 12–15: يظهر حقلان جديدان:
- السطر 13: رقم علامة التبويب الأخيرة المحددة؛
- السطر 15: رقم آخر جزء تم عرضه؛
يقوم النشاط بحفظ/استعادة الجلسة على النحو التالي:
// backup / restore management ----------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// save session
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// log
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) {
// parent
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// session recovery
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// log
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] على النحو التالي:
// backup / restore management -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// backup?
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// the fragment will be hidden - save it
saveFragment();
saveFragmentDone = true;
}
// memory
this.isVisibleToUser = isVisibleToUser;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// parent
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// the fragment must be restored
fragmentHasToBeInitialized = true;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// log
if (isDebugEnabled) {
Log.d(className, "onSaveInstanceState");
}
// parent
super.onSaveInstanceState(outState);
// save fragment only if visible
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// girls' classes
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] على القيمة true. وذلك لأن الجزء الفرعي يحتاج إلى معرفة أنه يجب عليه إعادة تهيئة حالة الجزء بالكامل من الحالة التي سيجدها في الجلسة؛
لا يزال في فئة [AbstractFragment]، تتغير طريقة [onCreateOptionsMenu] على النحو التالي:
// fragment update
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// memory
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
...
// the girl fragment is asked to update itself
updateFragment();
// backup to do
saveFragmentDone = false;
}
- السطر 14: رأينا أن المتغير المنطقي [saveFragmentDone] تم تعيينه إلى true عند إجراء عملية الحفظ. في مرحلة ما، يجب إعادة تعيينه إلى false. عند تنفيذ طريقة [updateFragment] (السطر 12) للجزء الفرعي، يصبح مرئيًا. ومع ذلك، يجب حفظ الجزء عندما يكون مرئيًا، وتحديدًا في اللحظة التي ينتقل فيها من الحالة المرئية إلى الحالة المخفية. ثم نقوم بتعيين المتغير المنطقي [saveFragmentDone] إلى false حتى يتم الحفظ؛
1.23.4. حفظ الجزء [Vue1Fragment]
يتم حفظ الأجزاء في طريقة [saveFragment] التي تستدعيها الفئة الأصلية [AbstractFragment]:
// save fragment status
@Override
public void saveFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// in-session saving of fragment status
Vue1FragmentState state = new Vue1FragmentState();
state.setHasBeenVisited(true);
session.setVue1FragmentState(state);
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- الأسطر 9–11: حفظ حالة الجزء في الجلسة. عند استدعاء الطريقة [saveFragment]، يكون الجزء مرئيًا. لذلك، يجب تعيين القيمة المنطقية [hasBeenVisited] إلى true (السطر 10)؛
1.23.5. حفظ الجزء [PlaceHolderFragment]
يتم حفظ الأجزاء في طريقة [saveFragment] التي تستدعيها الفئة الأصلية [AbstractFragment]:
@Override
public void saveFragment() {
// save fragment state in session
PlaceHolderFragmentState state = new PlaceHolderFragmentState();
state.setText(textViewInfo.getText().toString());
state.setHasBeenVisited(true);
session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
// log
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] المنطقية للجزء إلى true؛
- السطر 7: يتم حفظ حالة الجزء في الجلسة في المصفوفة [placeHolderFragmentStates]. مؤشر العنصر المراد تهيئته هو رقم قسم الجزء ناقص واحد؛
1.23.6. استعادة الجزء [Vue1Fragment]
يتم استعادة الأجزاء في طريقة [updateFragment]:
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
}
// restoration?
if (fragmentHasToBeInitialized) {
// restoration condition
hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
fragmentHasToBeInitialized = false;
}
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
}
// navigation?
boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
if (navigation) {
// increment visit no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// the visit number is displayed
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
// change n° current view
session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
}
- الأسطر 8–12: استعادة حالة الجزء. تم تهيئة المتغير المنطقي [fragmentHasToBeInitialized] بواسطة الفئة الأصلية [AbstractFragment]. عندما تكون قيمته صحيحة، فهذا يعني أن الجزء قد أعيد بناؤه للتو ويجب إعادة تهيئته. وهذا هو المكان الذي يحدث فيه ذلك. في هذا المثال المحدد، لا يوجد ما يجب فعله. لقد أظهرنا ببساطة أنه يمكننا استرداد قيمة المتغير المنطقي [hasBeenVisited] من الحالة المحفوظة للجزء (السطر 10)؛
- السطر 11: لا تنسَ إعادة تعيين [fragmentHasToBeInitialized] إلى false، حتى لا نقوم بإجراء تهيئة غير ضرورية للجزء عند العودة إليه لاحقًا دون أن يكون الجهاز قد تم تدويره؛
- الأسطر 18–26: قم بزيادة عداد الزيارات. هنا، هناك تحدي: عند استعادة الجزء، لا نريد زيادة هذا العداد. نحتاج هنا إلى التمييز بين:
- التنقل البسيط الذي يعيد المستخدم إلى علامة التبويب [View 1]؛
- الاستعادة عندما يقوم المستخدم بتدوير جهازه أثناء عرض علامة التبويب [View 1]؛
نميز بين هاتين الحالتين باستخدام رقم العرض المخزن في الجلسة. هذا الرقم هو رقم آخر عرض تم عرضه (السطر 28).
- السطر 18: يتم التنقل بدلاً من التحديث إذا اختلف رقم العرض الأخير عن رقم العرض الحالي؛
- الأسطر 21-25: زيادة عداد الزيارات وعرضه؛
1.23.7. استعادة [PlaceHolderFragment]
يتم استعادة الأجزاء في طريقة [updateFragment]:
// data
private String text;
private int numVisit;
private String newText;
private boolean hasBeenVisited = false;
private ObjectMapper jsonMapper = new ObjectMapper();
...
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// which fragment is it?
int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
int numView = numSection - 1;
// does the fragment need to be initialized?
if (fragmentHasToBeInitialized) {
// initial text
text = getString(R.string.section_format, numSection);
fragmentHasToBeInitialized = false;
}
// navigation?
boolean navigation = session.getCurrentView() != numView;
if (navigation) {
// increment visit no
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
newText = String.format("%s, visite %s", text, numVisit);
} else {
// we are dealing with a restoration
PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
newText = state.getText();
}
// text display
textViewInfo.setText(newText);
// current view
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 عند تدوير الجهاز: تم الاحتفاظ بعلامة التبويب الأولى [عرض 1] فقط. وفُقدت علامة التبويب الثانية.
نحل هذه المشكلة في فئة [MainActivity] على النحو التالي:
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "afterViews");
}
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
// 1st tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// 2nd 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);
}
// which tab to select?
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- الأسطر 14–16: إنشاء علامة التبويب الأولى؛
- الأسطر 18-23: إنشاء علامة التبويب الثانية. لتحديد ما إذا كان سيتم إنشاؤها، نتحقق من الجلسة لمعرفة رقم الجزء المعروض في علامة التبويب 2. إذا لم يكن هذا الرقم -1 (قيمته الأولية)، يتم إنشاء علامة التبويب الثانية. في هذه المرحلة، لدينا علامتا تبويب، مع تحديد الأولى افتراضيًا؛
- السطر 26: نسترد من الجلسة رقم علامة التبويب التي تم تحديدها قبل الحفظ/الاستعادة ونعيد تحديدها. إذا لم يتم تهيئة حقل [selectedTab] بعد بواسطة الكود، يتم استخدام قيمته الأولية 0؛











































































































































































































































































































































