2. هيكل عميل Android يتصل بخدمة ويب / jSON
نقدم الآن هيكل تطبيق Android يتصل بخدمة ويب واحدة أو أكثر / jSON. هذا هو المشروع [client-android-skel] الذي يمكن العثور عليه في مجلد [architecture] الخاص بالأمثلة:
![]() |
ستكون دراسة هذا التطبيق الإطاري فرصة لمراجعة بعض النقاط التي تناولناها في الأمثلة السابقة. سيكون هذا التطبيق بمثابة هيكل أساسي لجميع التطبيقات القادمة. وقد تم بناؤه بعد العديد من المراجعات. ويهدف إلى تجميع أكبر عدد ممكن من عناصر التطبيقات التي سنقوم ببنائها قريبًا في فئات مجردة لتجنب الحاجة إلى كتابة نفس النوع من الكود الذي يختلف فقط في التفاصيل. وفيما يلي خصائصه:
- يتم الاتصال غير المتزامن مع خادم الويب / jSON باستخدام مكتبة RxJava؛
- يتم إدارة دورة حياة الجزء (التحديث، الحفظ، الاستعادة) بواسطة فئته الأم [AbstractFragment] التي تستدعي في أوقات محددة بعض أساليب فئاته الفرعية. وبالتالي، لا يتعين على الفئة الفرعية الاهتمام بمراحل دورة الحياة، بل فقط بتنفيذ بعض الأساليب التي تفرضها فئتها الأم؛
- يتم إدارة دورة حياة النشاط (حفظ / استعادة) بواسطة فئة مجردة [AbstractActivity] التي تفرض هي الأخرى على النشاط الفرعي تنفيذ طرق معينة؛
- الفئة [AbstractActivity] قادرة على إدارة تطبيق مع أو بدون علامات تبويب، مع أو بدون صورة انتظار، مع أو بدون مصادقة أساسية لدى خادم الويب / jSON. ويتم تحديد وجود أو عدم وجود هذه العناصر من خلال التكوين؛
تم استخدام هذا الهيكل لجميع الأمثلة القادمة. ونظرًا لتنوع هذه الأمثلة، فإن ما كان يعمل في أحد الأمثلة قد لا يعمل في المثال التالي. وبما أن الهيكل قد استُخدم في سبعة أمثلة إجمالاً، فقد حدثت العديد من التكرارات. وإذا تم استخدامه في مثال ثامن، فمن المحتمل أن نجد مرة أخرى أن خصوصية هذا المثال الجديد تولد أخطاء جديدة. ومع ذلك، فإن استخدام هذا الهيكل سيبسط بشكل كبير كتابة الأمثلة القادمة. في الواقع، تعد إدارة دورة حياة المقطع (التحديث، الحفظ، الاستعادة) مقترنة بمفهوم تجاور المقاطع أمرًا معقدًا للغاية. وهنا، يتم إخفاؤها تمامًا في الفئة [AbstractFragment].
2.1. بنية عميل Android
يعتمد عميل Android المقترح على البنية التالية:
![]() |
- تقوم الطبقة [DAO] بتنفيذ واجهة [IDao]. وهي التي تتواصل مع خادم الويب / jSON؛
- هناك نشاط واحد فقط يقوم أيضًا بتنفيذ واجهة [IDao]. تتصل العروض به للوصول إلى الخادم؛
- يتم تنفيذ العروض بواسطة أجزاء؛
يعكس مشروع Android هذه البنية:
![]() |
سنقدم العناصر المختلفة لهذا المشروع واحدة تلو الأخرى.
2.2. تكوين Gradle
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// منذ إصدار المكون الإضافي Gradle 0.11 لنظام Android، يجب عليك استخدام android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// خيارات التعبئة والتغليف اللازمة لإنتاج APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile 'io.reactivex:rxandroid:1.2.0'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
- جميع أرقام الإصدارات قابلة للتغيير. ومع ذلك، يمكننا البدء بالأرقام الحالية إذا قمنا بتكوين Android Studio بحيث تكون إصدارات أدوات Android هذه (السطور 15-16، 47-48) موجودة بالفعل (انظر الفقرة 6.11)؛
2.3. بيان التطبيق
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="client.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>
- السطر 3: سنقوم بتغيير حزمة التطبيق؛
- السطران 10 و 15: سنقوم بتحديد قيمة العنصر [app_name] في الملف [res / values / strings.xml]. في الوقت الحالي، هذه القيمة هي:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- اسم التطبيق -->
<string name="app_name">[Donnez un nom à votre application]</string>
</resources>
2.4. تنظيم كود Java
![]() |
- يجمع [architecture] العناصر الرئيسية لتنظيم الكود؛
- يحتوي [activity] على النشاط الفريد للتطبيق؛
- [fragments] يجمع أجزاء أو طرق عرض التطبيق؛
- [dao] يجمع عناصر الاتصال بخادم الويب / jSON؛
2.5. عناصر النشاط
![]() | ![]() |

2.5.1. الطريقة المرتبطة بالنشاط
الطريقة [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.AppBarLayout>
<!-- حاوية الأجزاء -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
- السطر 29: يتم استخدام حاوية أجزاء محددة؛
يحتوي النشاط أيضًا على قائمة [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">
</menu>
في الوقت الحالي، هي فارغة. سيقوم المطور بإكمالها إذا لزم الأمر.
2.5.2. حاوية الأجزاء [MyPager]
![]() |
package client.android.architecture;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// يتحكم في التمرير
private boolean isSwipeEnabled;
// التحكم في التمرير
private boolean isScrollingEnabled;
// المنشئون
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// طرق يجب إعادة تعريفها لإدارة التمرير
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// هل التمرير مسموح؟
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// هل التمرير مسموح به؟
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// التحكم في التمرير
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// أدوات الضبط
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
}
توسع هذه الفئة الفئة القياسية Android [ViewPager] فقط لإدارة التمرير (السطر 11) والتمرير (السطر 13) بين العروض.
- الأسطر 26-43: الطرق التي تمنع التمرير إذا تم تعطيله؛
- الأسطر 46-49: إعادة تعريف الطريقة [setCurrentItem] التي تُستخدم لتغيير العرض المعروض. إذا تم تعطيل التمرير، فسيتم تغيير العرض دون تمرير. تجدر الإشارة إلى أنه يمكن للمطور تجاوز هذا النمط من التشغيل باستخدام الطريقة [setCurrentItem(int position, boolean smoothScrolling)] التي تسمح له بتحديد التمرير الذي يرغب فيه؛
2.5.3. الفئة [CoreState]
![]() |
الفئة [CoreState] هي الفئة الأم لحالات الأجزاء المختلفة:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// المهام: إضافة الفئات الفرعية لـ [CoreState] هنا
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// جزء تمت زيارته أو لم تتم زيارته
protected boolean hasBeenVisited = false;
// حالة القائمة المحتملة للجزء
protected MenuItemState[] menuOptionsState;
// أدوات الاسترجاع والتعيين
...
}
- السطر 16: يحتوي كل جزء في حالته على قيمة منطقية [hasBeenVisited] تحدد ما إذا كان قد تمت زيارته من قبل أم لا. وهذا ضروري لأنه في بعض الأحيان، عند عرض جزء لأول مرة، هناك أمور معينة يجب القيام بها؛
- السطر 18: يقوم المشروع [client-android-skel] بحفظ واستعادة قوائم الأجزاء تلقائيًا إذا كانت تحتوي على قوائم. في الجدول MenuItemState[] menuOptionsState، يتم تخزين حالة ظهور أو إخفاء جميع خيارات القائمة؛
- السطور 10-13: كما تم في [Exemple-22]، سيتم حفظ حالة النشاط وأجزائه في الجلسة التي سيتم حفظها بدورها في شكل سلسلة jSON. سنرى أن الجلسة تخزن مصفوفة من العناصر من النوع [CoreState]. إذا لم نفعل شيئًا، فسيتم حفظ السلسلة jSON من النوع [CoreState]. لكننا نريد حفظ حالات الأجزاء، وهي حالات مشتقة من [CoreState]. لكي يتم إنتاج السلسلة jSON من النوع المشتق وليس تلك من النوع الأصلي، يجب إعلان الأنواع المشتقة كما هو موضح في الأسطر 10-13. تعد الفئة [CoreState] إحدى فئات البنية التي يجب على المطور تعديلها لكل تطبيق جديد (الأسطر 10-13)؛
2.5.4. الواجهة [IMainActivity]
![]() |
تحدد الواجهة [IMainActivity] ما يمكن أن تطلبه الأجزاء من النشاط في البنية التالية:

package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// الوصول إلى الجلسة
ISession getSession();
// تغيير العرض
void navigateToView(int position, ISession.Action action);
// إدارة الانتظار
void beginWaiting();
void cancelWaiting();
// ثوابت التطبيق (يجب تعديلها) -------------------------------------
// وضع التصحيح
boolean IS_DEBUG_ENABLED = true;
// الحد الأقصى لوقت انتظار استجابة الخادم
int TIMEOUT = 1000;
// فترة الانتظار قبل تنفيذ طلب العميل
int DELAY = 0;
// المصادقة الأساسية
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// تجاور الأجزاء
int OFF_SCREEN_PAGE_LIMIT = 1;
// شريط علامات التبويب
boolean ARE_TABS_NEEDED = false;
// صورة الانتظار
boolean IS_WAITING_ICON_NEEDED = false;
// عدد أجزاء التطبيق
int FRAGMENTS_COUNT = 0;
// todo أضف هنا الثوابت والأساليب الأخرى
}
- السطر 6: واجهة [IMainActivity] توسع واجهة [IDao] من الطبقة [DAO]؛
- السطر 9: هذه هي النشاط الذي يتيح الوصول إلى الجلسة في شكل مثيل للواجهة [ISession]؛
- السطر 12: يتم تغيير العرض من خلال النشاط. المعلمة الثانية هي الإجراء الذي يؤدي إلى تغيير العرض، وهي إحدى القيم SUBMIT، NAVIGATION، RESTORE؛
- الأسطر 15-17: النشاط هو الذي يدير صورة الانتظار؛
- السطر 22: لتصحيح أخطاء التطبيق؛
- السطر 25: حتى لا ننتظر طويلاً إذا توقف الخادم عن الاستجابة؛
- السطر 28: أثناء التصحيح، سنضع قيمة بضع ثوانٍ لنحصل على الوقت الكافي لإلغاء العملية مع الخادم ونرى ما يحدث؛
- السطر 31: إلى true إذا طلبت الخدمة jSON مصادقة أساسية؛
- السطر 34: تجاور الأجزاء؛
- السطر 37: إلى vrai إذا كان التطبيق يحتوي على علامات تبويب؛
- السطر 39: إلى vrai إذا كان التطبيق يتصل بخادم ويب / jSON ونريد عرض صورة انتظار أثناء التبادل؛
- السطر 43: عدد الأجزاء التي يديرها التطبيق؛
الواجهة [IMainActivity] هي العنصر الثاني في البنية الذي يجب على المطور إكماله (السطر 45).
2.5.5. الواجهة [IDao]
توسع واجهة [IMainActivity] واجهة [IDao] التالية:
![]() |
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// عنوان URL لخدمة الويب
void setUrlServiceWebJson(String url);
// المستخدم
void setUser(String user, String mdp);
// مهلة انتظار العميل
void setTimeout(int timeout);
// المصادقة الأساسية
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// وضع التصحيح
void setDebugMode(boolean isDebugEnabled);
// مهلة انتظار العميل بالمللي ثانية قبل الطلب
void setDelay(int delay);
// مهام: قم بتعريف واجهتك هنا
}
- السطر 24: سيقوم المطور بإكمال الواجهة هنا؛
2.5.6. الجلسة
![]() |
تغلف الفئة [Session] العناصر المشتركة بين النشاط والأجزاء. وهي تنفذ الواجهة [ISession] التالية:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
public interface ISession {
// رقم آخر عرض تم عرضه
int getPreviousView();
void setPreviousView(int numView);
// الحالة الأخيرة لإحدى العروض
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// الإجراء الجاري
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// حالات جميع العروض -
// لا يستخدمه الكود ولكنه ضروري للتسلسل / إزالة التسلسل jSON
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// رقم آخر علامة تبويب تم تحديدها
int getPreviousTab();
void setPreviousTab(int position);
// التنقل عند تحديد علامة التبويب
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
نقدم واجهة [ISession] لفرض وجود طرق معينة في الجلسة:
- الأسطر 7-10: رقم آخر عرض (جزء) تم عرضه؛
- الأسطر 12-15: حالة عرض معين؛
- الأسطر 17-24: نقدم مفهوم الإجراء الجاري. هناك أربعة إجراءات (السطر 17):
- RESTORE: عملية حفظ/استعادة جارية. لا يوجد تغيير في العرض؛
- NAVIGATION: عملية تنقل جارية. سنسمي هنا بالتنقل أي تغيير في العرض حيث يمكن استعادة العرض الجديد من آخر حالة تم تخزينها في الجلسة؛
- SUBMIT: سنعطي النوع [SUBMIT] لعملية جارية، عندما يكون هناك تغيير في العرض ويكون العرض الجديد مرتبطًا بحالة النشاط بشكل عام وليس بحالته وحدها. في بعض الأحيان، يصعب التمييز بين NAVIGATION و SUBMIT. في هذه الحالة، يتم أخذ الحالة الأكثر عمومية وهي SUBMIT؛
- NONE: قيمة الإجراء عندما لم يتلقَ بعد قيمته الأولى؛
- السطور 26-30: سيتم تخزين حالات النشاط والأجزاء في مصفوفة من النوع CoreState[]. لكي تتم إدارتها بشكل صحيح أثناء عمليات التسلسل/إلغاء التسلسل jSON، يجب أن تحتوي على getter و setter؛
- الأسطر 32-35: رقم آخر علامة تبويب تم تحديدها. تُستخدم أثناء دورة الحفظ/الاستعادة لإعادة تحديد علامة التبويب التي كانت محددة قبل تدوير الجهاز؛
- الأسطر 37-40: إدارة قيمة منطقية تشير إلى ما إذا كان اختيار علامة تبويب يجب أن يصاحبه تغيير في المقطع؛
يتم تنفيذ واجهة [ISession] بواسطة الفئة المجردة [AbstractSession] التالية:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class AbstractSession implements ISession {
// رقم المشاهدة السابقة
private int preViousView;
// حالة المشاهدات
private CoreState[] coreStates = new CoreState[0];
// الإجراء الجاري
private Action action = Action.NONE;
// علامة التبويب المحددة سابقًا
private int previousTab;
// التنقل عند تحديد علامة التبويب
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// المنشئ
public AbstractSession() {
// يتم تهيئة جدول حالات الأجزاء
coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
for (int i = 0; i < coreStates.length; i++) {
coreStates[i] = new CoreState();
}
}
// واجهة ISession ---------------------------------------------------------
@Override
public int getPreviousView() {
return preViousView;
}
@Override
public void setPreviousView(int numView) {
this.preViousView = numView;
}
@Override
public CoreState getCoreState(int numView) {
return coreStates[numView];
}
@Override
public void setCoreState(int numView, CoreState coreState) {
coreStates[numView] = coreState;
}
@Override
public Action getAction() {
return action;
}
@Override
public void setAction(Action action) {
this.action = action;
}
@Override
public CoreState[] getCoreStates() {
return coreStates;
}
@Override
public void setCoreStates(CoreState[] coreStates) {
this.coreStates = coreStates;
}
@Override
public int getPreviousTab() {
return previousTab;
}
@Override
public void setPreviousTab(int position) {
this.previousTab = position;
}
@Override
public boolean isNavigationOnTabSelectionNeeded() {
return navigationOnTabSelectionNeeded;
}
@Override
public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
}
}
- السطر 9: رقم العرض الذي كان معروضًا قبل العرض الحالي. هذه المعلومة مفيدة عندما يمكن الوصول إلى عرض من عدة أماكن. وهذا هو الحال عادةً في التنقل عبر علامات التبويب. يمكن للعرض المعروض عندئذ معرفة ما كان العرض السابق؛
- السطر 12: جدول حالات جميع الأجزاء المعروضة بواسطة النشاط؛
- السطر 18: رقم علامة التبويب التي تم تحديدها سابقًا. يلعب دورًا مشابهًا لدور رقم العرض السابق في السطر 9. هذه المعلومة مفيدة عند وجود دوران للجهاز وضرورة العودة إلى علامة التبويب التي كانت محددة قبل الدوران؛
- السطر 22: قيمة منطقية تشير إلى ما إذا كان اختيار علامة تبويب يجب أن يصاحبه تغيير في الجزء المعروض. يجب معرفة أن المشروع [client-android-skel] يدير علامات التبويب والأجزاء بشكل منفصل حتى يمكن استخدامه في الحالات التي يكون فيها عدد علامات التبويب أقل من عدد الأجزاء. هناك نوعان من التحديد:
- التحديد من قبل المستخدم عند النقر على علامة تبويب. في هذه الحالة، يجب تغيير الجزء المعروض عادةً؛
- التحديد البرمجي عبر الطريقة [Tablayout.Tab.select()]. في هذه الحالة، لا يكون تغيير الجزء المعروض مرغوبًا دائمًا. فيما يلي مثالان:
- عند تدوير الجهاز، يتم إعادة إنشاء النشاط وكذلك علامات التبويب. ولكن عند إنشاء علامة التبويب الأولى، تخضع تلقائيًا لعملية برمجية [select]. ولذلك، ليس من المستحسن تغيير الجزء المعروض لأننا في مرحلة إعادة إنشاء النشاط حيث لن يكون الجزء المعروض في النهاية هو بالضرورة الجزء المرتبط بالعلامة التبويبية الأولى؛
- نظرًا لأن إدارة علامات التبويب منفصلة عن إدارة الأجزاء، فقد نرغب في تحديث علامات التبويب (حذف، إضافة) دون التدخل في الأجزاء المرتبطة بها. ومع ذلك، قد تؤدي بعض هذه العمليات مرة أخرى إلى تشغيل عملية برمجية ضمنية [select] على إحدى علامات التبويب. ولا يجب أن يؤدي هذا الاختيار بالضرورة إلى الانتقال إلى الجزء المرتبط؛
- السطر 21: لا يُقصد بحقل [navigationOnTabSelectionNeeded] أن يتم حفظه أثناء عمليات حفظ النشاط وأجزائه. التعليق التوضيحي [@JsonIgnore] يؤدي إلى تجاهل الحقل أثناء عمليات التسلسل/إلغاء التسلسل jSON؛
- السطور 25-31: يقوم المُنشئ بتهيئة مصفوفة حالات أجزاء التطبيق [FRAGMENTS_COUNT]. يتم تهيئة عناصر هذه المصفوفة باستخدام الحقل [hasBeeenVisited=false]. تُستخدم هذه المعلومات لمعرفة ما إذا كانت هذه هي الزيارة الأولى للجزء أم لا؛
الفئة [Session] هي كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// البيانات المراد مشاركتها بين الأجزاء نفسها وبين الأجزاء والنشاط
// العناصر التي لا يمكن تسلسلها في jSON يجب أن تحتوي على التعليق التوضيحي @JsonIgnore
// لا تنسَ استخدام getters و setters اللازمة للتسلسل / إلغاء التسلسل jSON
}
- السطر 5: الفئة [Session] توسع الفئة [AbstractSession] التي رأيناها للتو. سيضع المطور فيها العناصر المراد مشاركتها بين الأجزاء نفسها وبين الأجزاء والنشاط. تجدر الإشارة إلى أن الفئة [Session] لم تعد مرفقة بالتعليق التوضيحي AA [@EBean]. فقد أصبحت فئة عادية؛
2.5.7. الفئة المجردة [AbstractActivity]
![]() |
2.5.7.1. Squelette
الفئة [AbstractActivity] هي فئة تتكون من أكثر من 300 سطر. سنقوم بدراستها على مراحل. هيكلها الأساسي هو كما يلي:
package client.android.architecture;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// طبقة [DAO]
private IDao dao;
// الجلسة
protected Session session;
// حاوية الأجزاء
protected MyPager mViewPager;
// شريط الأدوات
private Toolbar toolbar;
// صورة الانتظار
private ProgressBar loadingPanel;
// شريط علامات التبويب
protected TabLayout tabLayout;
// مدير الأجزاء أو الأقسام
private FragmentPagerAdapter mSectionsPagerAdapter;
// اسم الفئة
protected String className;
// المُخطِط jSON
private ObjectMapper jsonMapper;
// المنشئ
public AbstractActivity() {
// اسم الفئة
className = getClass().getSimpleName();
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructeur");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// التنفيذ IMainActivity --------------------------------------------------------------------
...
// دورة الحياة - النسخ الاحتياطي / استعادة النشاط ------------------------------------
...
// إدارة صورة الانتظار ---------------------------------
...
// واجهة IDao -----------------------------------------------------
...
// مدير الأجزاء --------------------------------
...
// الفئات الفرعية
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
}
الفئة [AbstractActivity]:
- تنفذ واجهة [IMainActivity] (السطران 21 و55)؛
- تدير حفظ واستعادة النشاط وأجزائه عند تدوير الجهاز (السطر 58)؛
- تدير صورة الانتظار عند التبادل مع خادم الويب / jSON (السطر 61)؛
- تنفذ واجهة IDao للطبقة [DAO] (السطر 64)؛
- تنفذ مدير الأجزاء (السطر 67)؛
- يفرض على فئاته الفرعية وجود ست طرق (الأسطر 71-81)؛
2.5.7.2. تنفيذ واجهة [IMainActivity]
تنفيذ واجهة [IMainActivity] (انظر الفقرة 2.5.4) هو كما يلي:
// تنفيذ IMainActivity --------------------------------------------------------------------
@Override
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position, ISession.Action action) {
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
}
// عرض جزء جديد
mViewPager.setCurrentItem(position);
// يتم تسجيل الإجراء الجاري أثناء تغيير العرض
session.setAction(action);
}
2.5.7.3. حفظ حالة النشاط وأجزائه
يوجد حالة النشاط وأجزائه بالكامل في الجلسة. لذا، يتعين حفظ هذه الجلسة. نستأنف هنا ما تم تنفيذه في المشروع [Exemple-22] (انظر الفقرة 1.23):
// إدارة حفظ/استعادة النشاط ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// الأصل
super.onSaveInstanceState(outState);
// حفظ الجلسة في شكل سلسلة jSON
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// سجل
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.5.7.4. استعادة حالة النشاط وأجزائه
يتعلق الأمر باستعادة الجلسة. نقوم بذلك كما هو موضح في [Exemple-22]:
@Override
protected void onCreate(Bundle savedInstanceState) {
// الأصل
super.onCreate(savedInstanceState);
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
// هل هناك شيء ما يجب استعادته؟
if (savedInstanceState != null) {
// استعادة الجلسة
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// سجل
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
} else {
// جلسة
session = new Session();
}
...
- الأسطر 10-26: إذا كان المعامل [Bundle savedInstanceState] في السطر 2 غير null، فسيتم استعادة الجلسة (الأسطر 12-17)؛
- الأسطر 26-29: في حالة ما إذا كان المعامل [Bundle savedInstanceState] في السطر 2 هو null، فإن ذلك يتوافق مع بدء النشاط لأول مرة. عندئذ يتم إنشاء جلسة فارغة؛
2.5.7.5. تهيئة الطبقة [DAO]
@Override
protected void onCreate(Bundle savedInstanceState) {
// الأصل
super.onCreate(savedInstanceState);
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// الطبقة [DAO]
dao = getDao();
if (dao != null) {
// تكوين الطبقة [DAO]
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// الفئات الفرعية
protected abstract IDao getDao();
....
}
- السطر 11: يُطلب من النشاط الفرعي (السطر 21) مرجعًا للطبقة [DAO]؛
- الأسطر 14-17: إذا كانت الطبقة [DAO] موجودة، يتم تكوينها استنادًا إلى المعلومات الموجودة في الواجهة [IMainActivity]؛
2.5.7.6. تهيئة العرض المرتبط بالنشاط
تم عرض العرض المرتبط بالنشاط في الفقرة 2.5.1:
<?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>
<!-- حاوية الأجزاء -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
يتم تهيئة هذه العرض باستخدام الكود التالي:
@Override
protected void onCreate(Bundle savedInstanceState) {
// الأصل
super.onCreate(savedInstanceState);
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// طريقة العرض المرتبطة
setContentView(R.layout.activity_main);
// مكونات العرض ---------------------
// شريط الأدوات
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// صورة الانتظار؟
if (IS_WAITING_ICON_NEEDED) {
// يتم إضافة صورة الانتظار
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// إنشاء ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// إضافة ProgressBar إلى شريط الأدوات
toolbar.addView(loadingPanel);
}
...
- السطر 11: العرض XML [activity_main] مرتبط بالنشاط؛
- السطران 14-15: شريط الأدوات مدمج ومدعوم؛
- السطور 17-27: إضافة صورة انتظار محتملة: إذا كان القيمة المنطقية [IS_WAITING_ICON_NEEDED] صحيحة في واجهة [IMainActivity]؛
- السطر 23: إنشاء صورة الانتظار من النوع [ProgressBar] المشار إليها بواسطة الحقل [loadingPanel]؛
- السطر 24: في البداية، تكون هذه الصورة مخفية؛
- السطر 26: تتم إضافتها إلى شريط الأدوات؛
2.5.7.7. إدارة علامات التبويب
قد تتطلب واجهة [IMainActivity] شريط علامات تبويب. يتم إضافة هذا الشريط وإدارته بالطريقة التالية:
// شريط علامات التبويب
protected TabLayout tabLayout;
...
// شريط علامات التبويب؟
if (ARE_TABS_NEEDED) {
// يتم إضافة شريط علامات التبويب
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// لا توجد تنقل عند التحديد حتى يتم عرض جزء
session.setNavigationOnTabSelectionNeeded(false);
// إنشاء شريط علامات التبويب
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// إضافة شريط علامات التبويب إلى شريط التطبيق
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// مدير أحداث شريط علامات التبويب
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// تم تحديد علامة تبويب
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
}
if (session.isNavigationOnTabSelectionNeeded()) {
// موضع علامة التبويب
int position = tab.getPosition();
// الذاكرة
session.setPreviousTab(position);
// عرض الجزء المرتبط؟
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// الفئات الفرعية
protected abstract void navigateOnTabSelected(int position);
...
- الأسطر 12-48: إضافة وإدارة شريط علامات التبويب؛
- السطر 6: تتم إضافة شريط علامات التبويب إذا تم تعيين الثابت [ARE_TABS_NEEDED] إلى vrai في واجهة [IMainActivity]؛
- السطر 12: عند إنشاء شريط علامات التبويب، قد تحدث عمليات [Tablayout.Tab.select] ضمنية (لا يتسبب فيها المستخدم). نقوم بتعيين القيمة المنطقية [session.navigationOnTabSelectionNeeded] إلى faux لتجنب أي تنقل أثناء هذه الاختيارات الزائفة. سيكون على المطور اختيار الجزء المراد عرضه باستخدام الطريقة [navigateToView]. سيتم إعادة تعيين القيمة المنطقية [session.navigationOnTabSelectionNeeded] إلى vrai عند عرض هذا الجزء (انظر الفئة AbstractFragment)؛
- السطر 14: إنشاء شريط علامات تبويب يُشار إليه بواسطة الحقل [tabLayout]. نستخدم شريط علامات تبويب مخصص [CustomTabLayout] سنعود إليه لاحقًا؛
- السطر 15: نحدد ألوان عناوين علامات التبويب. توجد هذه الألوان في الملف [res / color / tab_txt.xml] التالي:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="#FFFF00" />
<item android:state_selected="false" android:color="#FFFFFF" />
</selector>
- السطر (c): لون عنوان علامة التبويب عندما تكون محددة؛
- السطر (d): لون عنوان علامة التبويب عندما لا تكون محددة؛
هذا الملف قابل للتعديل بالطبع. يمكن العثور على الرموز السداسية العشرية للألوان هنا على سبيل المثال.
- السطران 17-18: إضافة شريط علامات التبويب هذا إلى شريط التطبيق الموجود في العرض XML [activity_main]؛
- الأسطر 20-47: مدير أحداث شريط علامات التبويب؛
- الأسطر 22-36: يتم إدارة الحدث [onTabSelected] فقط. وهو يتوافق مع النقر على علامة التبويب [Tab tab] التي تم تمريرها كمعلمة إلى الأسلوب أو مع عملية برمجية [TabLayout.Tab.select]؛
- السطر 30: موضع علامة التبويب المحددة؛
- السطر 32: يتم حفظ هذا الموضع في الجلسة؛
- السطر 34: يتعلق الأمر الآن بعرض الجزء المرتبط بهذه العلامة. لا يمكن إجراء هذا الربط إلا من خلال الفئة الفرعية (السطر 52). تجدر الإشارة إلى أنه لا يتم ربط شريط علامات التبويب بحاوية الأجزاء [mViewPager] كما تم في بعض الأمثلة التي تمت دراستها. هنا، يتم فصل إدارة شريط علامات التبويب تمامًا عن إدارة الأجزاء. ولهذا السبب، عندما يتم النقر على علامة تبويب، يتعين تحديد العرض الذي نريد عرضه؛
- السطر 28: نميز بين اختيار علامة التبويب مع أو بدون التنقل. بشكل عام، عندما ينقر المستخدم على علامة تبويب، نريد التنقل، وعند الاختيار البرمجي لا نريده. المطور هو الذي يميز بين هاتين الحالتين باستخدام العنصر [session.navigationOnTabSelectionNeeded]. عندما لا يتم التنقل، لا يتم تسجيل رقم آخر علامة تبويب تم اختيارها في الجلسة. سيكون على المطور القيام بذلك؛
2.5.7.8. مدير علامات التبويب [CustomTabLayout]
![]() |
نستخدم مدير علامات تبويب مخصصًا لنتمكن من عرض عناوين علامات التبويب بخطوط مختلفة. الفئة [CustomTabLayout] هي كما يلي:
package client.android.architecture.custom;
import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class CustomTabLayout extends TabLayout {
private Typeface mTypeface;
public CustomTabLayout(Context context) {
super(context);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
}
@Override
public void addTab(Tab tab) {
super.addTab(tab);
ViewGroup mainView = (ViewGroup) getChildAt(0);
ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());
int tabChildCount = tabView.getChildCount();
for (int i = 0; i < tabChildCount; i++) {
View tabViewChild = tabView.getChildAt(i);
if (tabViewChild instanceof TextView) {
((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
}
}
}
}
- يتم تخصيص خط عناوين علامات التبويب في السطرين 30 و44؛
المجلد [fonts] هو التالي:
![]() |
المصادر:
- تم العثور على رمز الفئة [CustomTabLayout] في URL [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout]؛
- تم العثور على خطوط الحروف في URL و [https://www.fontsquirrel.com/fonts/roboto]؛
2.5.7.9. أحدث عمليات التهيئة
@Override
protected void onCreate(Bundle savedInstanceState) {
// الأصل
super.onCreate(savedInstanceState);
// السجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// إنشاء مثيل لمدير الأجزاء
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// حاوية الأجزاء مرتبطة بمدير الأجزاء
// أي أن الجزء رقم i من حاوية الأجزاء هو الجزء رقم i الذي تم إصداره بواسطة مدير الأجزاء
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// يتم منع التمرير بين الأجزاء
mViewPager.setSwipeEnabled(false);
// تجاور الأجزاء
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// يتم عرض العرض الأول
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// نقوم بتمرير السيطرة إلى النشاط الفرعي
onCreateActivity();
}
...
// الفئات الفرعية
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- الأسطر 10-19: نجد هنا كودًا شائعًا في الأمثلة التي درسناها؛
- الأسطر 21-23: عرض أول عرض. هناك بلا شك عدة طرق لتمييز هذه الحالة. هنا، استخدمنا حقيقة أنه بالنسبة لأول عرض، فإن قيمة الإجراء الذي يؤدي إلى تغيير العرض هي NONE؛
- السطر 22: لا نضع أي افتراضات بشأن الجزء الأول المراد عرضه. في أمثلةنا، كان هذا غالبًا الجزء رقم 0، ولكن ليس دائمًا (انظر المثال 22). لذلك سنطلب من النشاط الفرعي (السطر 30) أن يخبرنا ما هي هذه الواجهة الأولى؛
- السطر 25: قمنا هنا بتحليل كل ما يمكن تحليله. الآن، لدى الفئة الفرعية عمليات التهيئة الخاصة بها التي يجب القيام بها (السطر 29)؛
2.5.7.10. إدارة صورة الانتظار
في الفئة [AbstractActivity]، تتم إدارة صورة الانتظار بواسطة الطريقتين التاليتين:
// إدارة صورة الانتظار ---------------------------------
public void cancelWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.INVISIBLE);
}
}
public void beginWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.VISIBLE);
}
}
2.5.7.11. تنفيذ واجهة [IDao]
في الفئة [AbstractActivity]، يتم تنفيذ الواجهة [IDao] (انظر الفقرة 2.5.5) بالطريقة التالية:
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// الطبقة [DAO]
private IDao dao;
...
// واجهة IDao -----------------------------------------------------
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setUser(String user, String mdp) {
dao.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
dao.setBasicAuthentification(isBasicAuthentificationNeeded);
}
@Override
public void setDebugMode(boolean isDebugEnabled) {
dao.setDebugMode(isDebugEnabled);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
- السطر 3: تجدر الإشارة إلى أن قيمة هذا الحقل قد تم توفيرها بواسطة النشاط الفرعي في الطريقة [onCreate]؛
2.5.7.12. تنفيذ مدير الأجزاء
في الفئة [AbstractActivity]، يتم تنفيذ مدير الأجزاء على النحو التالي:
...
// مدير الأجزاء --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// المنشئ
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// أجزاء الفئة الفرعية
fragments = getFragments();
}
// يجب عرض الجزء رقم الموضع
@Override
public AbstractFragment getItem(int position) {
// يتم عرض الجزء
return fragments[position];
}
// يعرض عدد الأجزاء المطلوب إدارتها
@Override
public int getCount() {
return fragments.length;
}
// يعرض عنوان الجزء رقم الموضع
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// الفئات الفرعية
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
...
}
- السطر 5: مصفوفة الأجزاء المرتبطة بالنشاط. ستُشتق جميع الأجزاء من الفئة [AbstractFragment]؛
- الأسطر 8-12: يقوم المنشئ بتهيئة مصفوفة الأجزاء. ويطلبها من الفئة الفرعية للنشاط (السطر 35)؛
- الأسطر 28-31: يمكن استخدام عناوين الأجزاء في تطبيق يحتوي على عدد من علامات التبويب يساوي عدد الأجزاء. في هذه الحالة، يمكن إعطاء علامة التبويب عنوان الجزء. هنا، يتم طلب هذه العناوين من الفئة الفرعية (السطر 37)؛
2.5.7.13. الطريقة [onResume]
يتم تنفيذ الأسلوب [onResume] قبل ظهور العرض المرتبط بالنشاط بقليل. ويُستخدم هنا لاختيار علامة تبويب بعد عملية حفظ/استعادة:
@Override
public void onResume() {
// الأصل
super.onResume();
if (IS_DEBUG_ENABLED) {
Log.d(className, "onResume");
}
// في حالة الاستعادة، يجب استعادة آخر علامة تبويب محددة
if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
tabLayout.getTabAt(session.getPreviousTab()).select();
}
}
- السطر 10: تحديد علامة التبويب التي كانت محددة قبل عملية الحفظ/الاستعادة. يجب أن نتذكر هنا أنه في الطريقة [onCreate]، التي يتم تنفيذها في دورة حياة النشاط قبل الطريقة [onResume]، تم تعطيل التنقل عند تحديد علامة تبويب. لذلك، هنا يتم تحديد علامة تبويب ولكن لا يحدث تغيير في الجزء؛
2.5.7.14. Résumé
ستكون الفئة المجردة [AbstractActivity] هي الفئة الأم للنشاط الوحيد للتطبيق.
يجب أن تنفذ النشاط الفرعي الطرق الست التالية:
// الفئات الفرعية
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
كما أن النشاط الفرعي لديه حق الوصول إلى العناصر المحمية التالية من فئته الأصلية:
// الجلسة
protected ISession session;
// حاوية الأجزاء
protected MyPager mViewPager;
// شريط علامات التبويب
protected CustomTabLayout tabLayout;
// اسم الفئة
protected String className;
2.5.8. النشاط [MainActivity]
![]() |
يمكن تسمية الفئة [MainActivity] باسم مختلف. الشرط الوحيد لها هو تنفيذ واجهة [IMainActivity]. الفئة الأساسية المتوفرة هي التالية:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// الطبقة [DAO]
@Bean(Dao.class)
protected IDao dao;
// الجلسة
private Session session;
// طرق الفئة الأصلية -----------------------
@Override
protected void onCreateActivity() {
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// الجلسة
this.session = (Session) super.session;
// المهام المطلوبة: نواصل عمليات التهيئة التي بدأتها الفئة الأصلية
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// المهام المطلوبة: تعريف الأجزاء هنا
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// المهام: تحديد عناوين الأجزاء هنا
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// المهام: التنقل عبر علامات التبويب - تحديد العرض المطلوب
}
@Override
protected int getFirstView() {
// المهام: التنقل عبر علامات التبويب - تحديد العرض الأول المراد عرضه
return 0;
}
}
- السطر 14: لكي يتم فهم الترميز AA [@Bean] في السطر 19، يجب أن يكون للنشاط الترميز AA [@EActivity]؛
- السطر 15: النشاط مرتبط بالقائمة XML [menu_main]. هذه القائمة فارغة حاليًا. سيتعين على المطور إكمالها إذا احتاج إلى ذلك؛
- السطر 16: الفئة تمتد من الفئة [AbstractActivity]؛
- السطران 19-20: مرجع إلى الطبقة [DAO]. سيتم إنشاء مثيل لهذه الطبقة بواسطة المكتبة AA قبل تهيئة هذا الحقل. وهذا يعني أن الفاصوليا AA [Dao] يجب أن تكون موجودة. وهذا هو الحال دائمًا مع التطبيق النموذجي الذي نقدمه. حتى في تطبيق لا يحتوي على طبقة [DAO]، يمكن ترك الحزمة [dao] موجودة. وهذا لا يسبب أي تعقيدات؛
- السطر 22: الجلسة كمثيل من النوع [Session]. توجد الجلسة في الفئة الأم [AbstractActivity] ولكن كمثيل للواجهة [ISession] (السطر 32)؛
- الأسطر 24-63: الطرق الست التي تفرضها الفئة الأم [AbstractActivity]؛
- الأسطر 36-39: الطريقة [getDao] تُرجع مرجعًا إلى الطبقة [DAO]. هنا، لا يكون هذا المرجع أبدًا null. ولكن في الفئة الأم [AbstractActivity]، تم توقع الحالة التي تعرض فيها الفئة الفرعية مرجعًا null للإشارة إلى عدم وجود طبقة [DAO]. إذا أردنا الاستفادة من هذه الإمكانية (التي لا أعتبرها مفيدة جدًا في رأيي)، فيجب هنا إرجاع المؤشر null؛
2.6. الطبقة [DAO]

![]() |
2.6.1. الواجهة IDao
تم عرضها في الفقرة 2.5.5:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// عنوان URL لخدمة الويب
void setUrlServiceWebJson(String url);
// المستخدم
void setUser(String user, String mdp);
// مهلة انتظار العميل
void setTimeout(int timeout);
// المصادقة الأساسية
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// وضع التصحيح
void setDebugMode(boolean isDebugEnabled);
// مهلة انتظار العميل بالمللي ثانية قبل الطلب
void setDelay(int delay);
// مهام: قم بتعريف واجهتك هنا
}
سيضيف المطور أساليب طبقته [DAO] بدءًا من السطر 24.
2.6.2. الواجهة [WebClient]
![]() |
الواجهة [WebClient] هي كما يلي:
package client.android.dao.service;
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.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// المهام المطلوبة: حدد هنا URL المطلوب الوصول إليها
}
سيقوم المطور بإضافة الطرق التي تتواصل مع URL المعروضة بواسطة الخادم jSON بدءًا من السطر 17.
2.6.3. معترض المصادقة [MyAuthInterceptor]
![]() |
الفئة [MyAuthInterceptor] هي كما يلي:
package client.android.dao.service;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
// المستخدم
private String user;
// كلمة المرور
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// رؤوس HTTP للطلب HTTP الذي تم اعتراضه
HttpHeaders headers = request.getHeaders();
// رأس HTTP للمصادقة الأساسية
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
// إضافة إلى رؤوس HTTP
headers.setAuthorization(auth);
// استمرار دورة حياة الطلب HTTP
return execution.execute(request, body);
}
// عناصر المصادقة
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
تولد هذه الفئة رأس المصادقة HTTP التالي:
حيث [code] هو رمز Base64 لسلسلة 'user:mp'. لا تُستخدم هذه الفئة إلا إذا كان الخادم jSON يتوقع هذا النوع من المصادقة. وهناك أنواع أخرى.
ملاحظة: يوضح الفقرة 3.6.3.1 كيفية استخدام هذه الفئة.
2.6.4. الفئة [AbstractDao]
![]() |
الفئة [AbstractDao] هي كما يلي:
package client.android.dao.service;
import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// أداة التعيين jSON
private ObjectMapper mapper = new ObjectMapper();
// وضع التصحيح
protected boolean isDebugEnabled;
// اسم الفئة
protected String className;
// فترة الانتظار قبل تنفيذ الطلب
private int delay;
// المنشئ
public AbstractDao() {
// اسم الفئة
className = getClass().getName();
Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
}
// الطرق المحمية ----------------------------------------------------------
// واجهة عامة
protected interface IRequest<T> {
T getResponse();
}
// طلب عام إلى خدمة ويب / jSON
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// سجل
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("delay=%s", delay));
}
// تنفيذ الخدمة - في انتظار استجابة واحدة
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// تنفيذ الخدمة
try {
// في انتظار؟
if (delay > 0) {
Thread.sleep(delay);
}
// يتم تنفيذ الطلب المتزامن
T response = request.getResponse();
// سجل
if (isDebugEnabled) {
String log;
if (response instanceof String) {
log = (String) response;
} else {
log = mapper.writeValueAsString(response);
}
Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
}
// إرسال الرد إلى المراقب
subscriber.onNext(response);
// الإبلاغ عن انتهاء المراقبة
subscriber.onCompleted();
} catch (InterruptedException | JsonProcessingException | RuntimeException e) {
// سجل
if (isDebugEnabled) {
try {
Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
} catch (JsonProcessingException e1) {
Log.d(className, String.format("Erreur jSON imprévue"));
}
}
// إصدار استثناء
subscriber.onError(new DaoException(e, 100));
}
}
});
}
// وضع التصحيح
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
public void setDelay(int delay) {
this.delay = delay;
}
}
- الأسطر 35-81: تستخدم الطريقة [getResponse] المكتبة RxAndroid لعرض نوع [Observable<T>]. على عكس بعض الأمثلة التي رأيناها سابقًا، لا يتم عرض نوع [Response<T>] الذي هو نوع خاص، بل أي نوع T؛
- السطر 35: تستقبل الطريقة [getResponse] كمعلمة مثيلًا من النوع [IRequest<T>] من الأسطر 30-32، حيث تحصل الطريقة [IRequest.getReponse()] على النوع T من خلال عملية HTTP متزامنة؛
- الأسطر 48-50: ننتظر بشكل مصطنع [delay] ميلي ثانية. في الإنتاج، سنضع [delay=0]. في مرحلة التصحيح، سنضع [delay=qqs secondes] لإعطاء المستخدم فرصة لإلغاء العملية غير المتزامنة وبالتالي رؤية كيف يتصرف الكود عندئذٍ؛
- السطر 52: يتم طلب الاستجابة المتوقعة باستخدام طلب متزامن؛
- السطر 64: بمجرد استلام الرد، يتم تمريره إلى المراقب؛
- السطر 66: يُشار إلى أنه لن يكون هناك مزيد من الإرسال. نحن هنا في حالة خاصة لعملية غير متزامنة لا تُرجع سوى عنصر واحد؛
- الأسطر 67-78: في حالة حدوث استثناء، يتم إرسال الاستثناء إلى المراقب (السطر 77)؛
2.6.5. الفئة [Dao]
![]() |
الفئة [Dao] هي كما يلي:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// عميل خدمة الويب
@RestService
protected WebClient webClient;
// الأمان
@Bean
protected MyAuthInterceptor authInterceptor;
// RestTemplate
private RestTemplate restTemplate;
// مصنع RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// سجل
Log.d(className, "afterInject");
// يتم تصنيع restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// يتم تثبيت المحول jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// يتم تعيين restTemplate لعميل الويب
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// يتم تعيين URL لخدمة الويب
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// يتم تسجيل المستخدم في المعترض
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// تكوين المصنع
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// معترض المصادقة؟
if (isBasicAuthentificationNeeded) {
// يتم إضافة معترض المصادقة
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// طرق خاصة -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// المهام المطلوبة: التنفيذ IDao
}
- السطران 21-22: إدخال bean AA [WebClient] الذي سيضمن التبادل مع خادم الويب / jSON؛
- السطران 24-25: إدخال مانع المصادقة؛
- الأسطر 31-42: يتم تنفيذ هذه الطريقة بعد إدخال حقول الأسطر 21-25؛
- السطر 37: يتم إنشاء الكائن [RestTemplate] الذي يضمن التبادل بين العميل والخادم من factory. هذا ليس ضروريًا، ولكن من خلال factory يمكن تكوين مهلة التبادل. ولهذا السبب، لا نستخدم المنشئ بدون معلمات [RestTemplate()]؛
- السطر 39: نضيف محول jSON إلى محولات [RestTemplate]. سيكون هذا هو المحول الوحيد. وكذلك عندما تتلقى إحدى طرق العميل [WebClient] سلسلة jSON من الخادم، سيتم تحويلها تلقائيًا إلى الكائن الذي يجب أن تعيده الطريقة؛
- السطر 41: يتم تمرير الكائن [RestTemplate] الذي تم تكوينه بهذه الطريقة إلى عميل الويب الذي سيضمن التبادل بين العميل والخادم بفضله؛
- الأسطر 44-48: يتم تعيين جذر خادم الويب URL / jSON. جميع كائنات URL المُعلنة في الفئة [WebClient] هي كائنات URL تابعة لهذا الجذر URL؛
- الأسطر 50-54: تسمح هذه الطريقة بتحديد مالك الاتصال عندما يتم التحكم فيه بواسطة ترخيص من النوع الأساسي (انظر الفقرة 2.6.3)؛
- الأسطر 56-64: تحدد timeouts لتبادل البيانات بين العميل والخادم. ويتم ذلك عبر factory للكائن [RestTemplate] الذي يحكم التبادل؛
- الأسطر 66-78: تسمح هذه الطريقة بالإشارة إلى أن الخادم محمي بمصادقة من النوع الأساسي؛
- الأسطر 72-77: إذا طُلبت مصادقة من النوع الأساسي، يُضاف مُعترض المصادقة المُدرج في السطر 25 إلى مُعترضات الكائن [RestTemplate]. سيضيف هذا المعترض تلقائيًا إلى جميع طلبات عميل الويب، السطر HTTP الخاص بالمصادقة الأساسية التي يتوقعها الخادم؛
- سيقوم المطور بتنفيذ واجهة [IDao] بدءًا من السطر 87؛
2.7. الأجزاء
![]() |
2.7.1. الفئة [MenuItemState]
تغلف الفئة [MenuItemState] حالة خيار القائمة:
package client.android.architecture;
public class MenuItemState {
// معرف خيار القائمة
private int menuItemId;
// رؤية الخيار
private boolean isVisible;
// المنشئات
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// مُستردات ومُعيّنات
...
}
2.7.2. الفئة [Utils]
تجمع الفئة [Utils] بين طرق ثابتة مساعدة:
package client.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// قائمة رسائل استثناء - الإصدار 1
static public List<String> getMessagesFromException(Throwable ex) {
// يتم إنشاء قائمة برسائل الخطأ من مكدس الاستثناءات
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// قائمة رسائل استثناء - الإصدار 2
static public String getMessageForAlert(Throwable th) {
// يتم إنشاء النص المراد عرضه
StringBuilder texte = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// النتيجة
return texte.toString();
}
// قائمة رسائل استثناء - الإصدار 3
static public String getMessageForAlert(List<String> messages) {
// يتم إنشاء النص المراد عرضه
StringBuilder texte = new StringBuilder();
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// النتيجة
return texte.toString();
}
}
2.7.3. الفئة الأصلية [AbstractFragment]
تجمع الفئة [AbstractFragment] ما هو مشترك بين جميع أجزاء التطبيق. كما هو الحال في الفئة [AbstractActivity]، فإن كودها معقد. سنقوم هنا أيضًا بتحليله على مراحل.
2.7.3.1. الهيكل الأساسي
package client.android.architecture.core;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// بيانات خاصة ------------------------------------------------------------
// اشتراكات المراقبة
private List<Subscription> abonnements = new ArrayList<>();
// قائمة الجزء
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// دورة حياة الجزء
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// حالة الجزء
private CoreState previousState;
// المُخطِط jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// دورة حياة الجزء
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// المهام غير المتزامنة
private boolean runningTasksHaveBeenCanceled;
// البيانات المتاحة للفئات الفرعية ---------------------------------------
// وضع التصحيح
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// اسم الفئة
protected String className;
// المهام غير المتزامنة
protected int numberOfRunningTasks;
// النشاط
protected IMainActivity mainActivity;
protected Activity activity;
// الجلسة
protected Session session;
// تحديث الجزء ----------------------------------------------------------------------------------
...
// إدارة القائمة ------------------------------------------
...
// إدارة الانتظار -------------------------------------------------------------
...
// إدارة العمليات غير المتزامنة --------------------------------------------------------------------
...
// إدارة الاستثناءات -------------------------------------------------------------------
....
// إدارة دورة حياة الجزء --------------------------------------------------------
...
// الفئات الفرعية -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
}
- الأسطر 28-45: البيانات الخاصة بالفئة؛
- الأسطر 47-58: البيانات المحمية التي يمكن للفئات الفرعية الوصول إليها؛
- الأسطر 61-62: الكود الذي يقوم بتحديث الجزء الذي سيتم عرضه؛
- السطور 64-65: كود مساعد لإدارة القائمة المحتملة؛
- الأسطر 67-68: كود مساعد لإدارة الانتظار أثناء عملية غير متزامنة؛
- السطور 70-71: كود لتسهيل اتصال الجزء مع الطبقة [DAO]؛
- السطران 73-74: كود مساعد لإدارة أي استثناء بطريقة قياسية؛
- السطران 76-77: كود يدير دورة حياة الجزء؛
- الأسطر 80-94: تفرض الفئة الأم 8 طرق على فئاتها الفرعية؛
2.7.3.2. المنشئ
منشئ الفئة هو كما يلي:
// اسم الفئة
protected String className;
// دورة حياة الجزء
private boolean fragmentHasToBeInitialized = false;
...
// المنشئ ----------------------
public AbstractFragment() {
// التشغيل
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// سجل
if (isDebugEnabled) {
Log.d(className, "constructeur");
}
}
- السطر 9: نلاحظ اسم الفئة الفرعية التي يتم إنشاء مثيل لها هنا. يستخدم هذا الاسم في جميع سجلات الفئة الأم؛
- السطر 10: نلاحظ أن المقطع يخضع لعملية إنشاء. سيتم استخدام هذه المعلومات عندما يُطلب من المقطع الفرعي التحديث؛
2.7.3.3. إدارة القائمة
في بنيتنا، يجب أن يكون لكل جزء قائمة، حتى لو كانت فارغة. فقد أظهرت السجلات أنه عندما يتم تنفيذ الطريقة [onCreateOptionsMenu]، التي يتم تنفيذها عندما يكون للجزء قائمة، يكون الجزء قد تم ربطه بالفعل بنشاطه وعرضه وقائمته وسيصبح مرئيًا. لذلك، هذا هو الوقت الذي يمكن فيه تحديث الواجهة المرئية والقائمة. في هذه الطريقة [onCreateOptionsMenu] نطلب من الجزء الفرعي أن يقوم بالتحديث.
تضم إدارة القائمة طرقًا مساعدة تسمح للجزء الفرعي بعرض عناصر القائمة أو عدم عرضها:
// قائمة الجزء
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// إدارة القائمة ------------------------------------------
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// تصفح جميع عناصر القائمة
for (int i = 0; i < menu.size(); i++) {
// العنصر رقم i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// إذا كان العنصر رقم i قائمة فرعية، فإننا نبدأ من جديد
if (menuItem.hasSubMenu()) {
// التكرار
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// النتيجة
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// نسترد معرفات خيارات القائمة
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// ننقل خيارات القائمة إلى مصفوفة
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// معرف الخيار
int id = menuOptionsIds.get(i);
// حالة الخيار
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// النتيجة
if (isDebugEnabled) {
Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
}
}
// حالات خيارات القائمة
private MenuItemState[] getMenuOptionsStates() {
MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
for (int i = 0; i < menuOptionsStates.length; i++) {
// الحالة
MenuItemState state = this.menuOptionsStates[i];
// معرف القائمة
int id = state.getMenuItemId();
// تهيئة الحالة
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// النتيجة
return menuOptionsStates;
}
// عرض خيارات القائمة -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// يتم تحديث جميع خيارات القائمة
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// يتم تحديث بعض خيارات القائمة
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
- السطر 6-18: تسمح هذه الطريقة بالحصول على المعرفات الرقمية لجميع خيارات القائمة؛
- السطر 6: تتلقى الطريقة [getMenuOptions] معلمتين:
- [Menu menu]: قائمة الجزء؛
- [List<Integer> menuOptionsIds]: قائمة معرفات Android لخيارات القائمة. في البداية تكون هذه القائمة فارغة. ثم يتم ملؤها من خلال مسار متكرر (السطر 15) لشجرة القائمة؛
- الأسطر 20-40: بناءً على القائمة، يتم إنشاء جدول حالات (المعرف، الرؤية) خيارات القائمة. يتم تخزين هذا الجدول في السطر 3. تم وصف الفئة [MenuItemState] في الفقرة 2.7.1؛
- الأسطر 43-55: نسخة معدلة من الطريقة السابقة. تقوم بنفس الشيء، ولكن بدلاً من إعادة حساب معرفات جميع خيارات القائمة، وهو ما تم بالفعل، تستخدم معرفات جدول الحالات في السطر 3؛
- الأسطر 58-63: تسمح الطريقة [setAllMenuOptionsStates] بإخفاء أو إظهار جميع خيارات قائمة المقتطف؛
- الأسطر 65-69: تسمح الطريقة [setMenuOptionsStates]، بشكل انتقائي، بعرض أو إخفاء بعض خيارات القائمة؛
- تم تعريف الطرق [getMenuOptions, getMenuOptionsStates] على أنها خاصة لأنها تُستخدم فقط في [AbstractFragment]. تم تعريف الطرق [setAllMenuOptionsStates] (السطر 58) و [setMenuOptionsStates] (السطر 65) على أنها محمية حتى تكون متاحة للفئات الفرعية؛
2.7.3.4. إدارة انتظار انتهاء مهمة غير متزامنة
// اشتراكات المراقبة
private List<Subscription> abonnements = new ArrayList<>();
// المهام غير المتزامنة
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// إدارة انتظار انتهاء عملية غير متزامنة -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// تسجيل عدد المهام التي سيتم تنفيذها
this.numberOfRunningTasks = numberOfRunningTasks;
// يتم وضع صورة الانتظار
mainActivity.beginWaiting();
// إفراغ قائمة الاشتراكات
abonnements.clear();
// لم يتم الإلغاء بعد
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// يتم إخفاء صورة الانتظار
mainActivity.cancelWaiting();
}
- الأسطر 9-18: لبدء عملية أو عمليات غير متزامنة، سيستدعي الجزء الفرعي الطريقة الأم [beginRunningTasks]. المعلمة لهذه الطريقة هي عدد المهام غير المتزامنة التي سيطلقها الجزء الفرعي؛
- السطر 11: يتم تخزين معلمة الطريقة؛
- السطر 13: يتم إظهار صورة الانتظار؛
- السطر 15: يتم مسح قائمة الاشتراكات في العمليات غير المتزامنة. لم يتم إنشاء هذه العمليات بعد بواسطة الجزء الفرعي؛
- السطر 17: يتم الاحتفاظ بقيمة منطقية للإشارة إلى أن المهام غير المتزامنة التي طلبها الجزء الفرعي قد تم إلغاؤها. في البداية، تكون القيمة المنطقية هي false؛
- الأسطر 20-25: يستدعي الجزء الفرعي الطريقة الأم [cancelWaitingTasks] للإشارة إلى أنه يريد إلغاء المهام التي أطلقها؛
- السطر 22: يتم إخفاء صورة الانتظار؛
2.7.3.5. إدارة الاستثناءات
// إدارة الاستثناء -------------------------------------------------------------------
// عرض تنبيه بشأن الاستثناء
protected void showAlert(Throwable th) {
// عرض رسائل مكدس الاستثناءات لـ Throwable th
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
}
// عرض قائمة الرسائل
protected void showAlert(List<String> messages) {
// عرض قائمة الرسائل
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
- الأسطر 4-7: تسمح الطريقة [showAlert(Throwable)] لجزء فرعي بعرض رسائل مكدس الاستثناءات الخاص بـ Throwable الذي تم تمريره كمعلمة في نافذة؛
- الأسطر 10-13: تسمح الطريقة [showAlert(List<String>] لجزء فرعي بعرض قائمة الرسائل التي تم تمريرها كمعلمة في نافذة؛
- تم وصف الفئة [Utils] المستخدمة في الأسطر 6 و 12 في الفقرة 2.7.2؛
2.7.3.6. إدارة العمليات غير المتزامنة
...
// اشتراكات المراقبة
private List<Subscription> abonnements = new ArrayList<>();
// المهام غير المتزامنة
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// تنفيذ مهمة غير متزامنة باستخدام RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// العملية: المراقبة المطلوب تنفيذها/مراقبتها
// consumeResult: الطريقة التي تستفيد من الاستجابة التي تم الحصول عليها
//
// لا يتم إنشاء اشتراكات جديدة إلا في حالة عدم وجود إلغاء
if (!runningTasksHaveBeenCanceled) {
// التنفيذ على مؤشر ترابط الإدخال/الإخراج والمراقبة على مؤشر ترابط واجهة المستخدم
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// يتم تنفيذ المراقب
try {
abonnements.add(process.subscribe(
// استهلاك النتيجة
consumeResult,
// استهلاك الاستثناء
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
consumeThrowable(th);
}
},
// نهاية المهمة
new Action0() {
@Override
public void call() {
endOfTask();
}
}));
} catch (Throwable th) {
consumeThrowable(th);
}
}
}
private void endOfTask() {
...
}
// أصدرت عملية غير متزامنة استثناءً
// أو حدث استثناء أثناء تنفيذ عملية غير متزامنة
private void consumeThrowable(Throwable th) {
...
}
- الأسطر 9-41: تنفذ مهمة غير متزامنة؛
- السطر 9: تتوقع الطريقة [executeInBackground] معلمتين:
- [Observable<T> process]: العملية غير المتزامنة المطلوب تنفيذها؛
- [Action1<T> consumeResult]: طريقة الجزء الفرعي التي يجب استدعاؤها لنقل العناصر الصادرة عن العملية إليها. في أمثلةنا السابقة، لم تصدر العمليات سوى عنصر واحد فقط. النوع T لـ [Action1<T>] هو النوع T للنتيجة التي أرجعتها العملية المراقبة؛
- السطر 14: لا يتم تشغيل المهمة غير المتزامنة إلا إذا لم يكن قد حدث بالفعل إلغاء من قبل المستخدم أو البرنامج (بسبب استثناء)؛
- السطر 16: تم تكوين العملية للتنفيذ على مؤشر ترابط الإدخال/الإخراج ومراقبتها على مؤشر ترابط واجهة المستخدم؛
- السطر 16: تطلق التعليمات [process.subscribe] تنفيذ العملية في مؤشر ترابط الإدخال/الإخراج. داخل هذا المؤشر، يتم تنفيذ الأمور بشكل متزامن لأننا نستخدم مكتبة HTTP المتزامنة؛
- السطر 19: تحتوي الطريقة [process.subscribe] على ثلاثة معلمات:
- السطر 21: [consumeResult]: طريقة الجزء الفرعي التي ستستهلك العناصر الصادرة عن العملية؛
- الأسطر 22-28: الطريقة التي يتم تنفيذها عند حدوث استثناء أثناء معالجة المهمة غير المتزامنة. يتم تفويض المعالجة إلى الطريقة [consumeThrowable] في السطر 49؛
- الأسطر 29-36: الطريقة التي يتم تنفيذها عندما تصدر المهمة إشعارًا بانتهاء الإرسال. يتم تفويض المعالجة إلى الطريقة [endOfTask] في السطر 43؛
- السطر 19: يتم تسجيل المهمة غير المتزامنة التي تم إطلاقها للتو في الحقل [abonnements] الذي يسجل جميع المهام غير المتزامنة التي تم إطلاقها. سيسمح ذلك بإلغائها إذا لزم الأمر؛
- الأسطر 37-39: طريقة يتم تنفيذها عند حدوث استثناء أثناء معالجة المهمة غير المتزامنة. يتم تفويض المعالجة إلى الطريقة [consumeThrowable] في السطر 49؛
الطريقة [endOfTask] هي كما يلي:
// المهام غير المتزامنة
protected int numberOfRunningTasks;
...
private void endOfTask() {
// مهمة أقل في انتظار
numberOfRunningTasks--;
// انتهى؟
if (numberOfRunningTasks == 0) {
// انتهى الانتظار
cancelWaitingTasks();
// يتم الإبلاغ عن انتهاء المهام إلى الفئة الفرعية
notifyEndOfTasks(false);
}
}
...
// الفئات الفرعية -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- السطر 6: انتهت للتو مهمة غير متزامنة. يتم تقليل عداد المهام النشطة؛
- السطر 8: إذا لم تعد هناك مهام نشطة، فهذا يعني أن الجزء الفرعي قد حصل على جميع إجاباته؛
- السطر 10: يتم إلغاء الانتظار؛
- السطر 12: يتم إخطار الجزء الفرعي بأن جميع المهام التي أطلقها قد انتهت عن طريق استدعاء طريقة [notifyEndOfTasks] الخاصة به. يشير معلمة هذه الطريقة إلى كيفية انتهاء المهام، سواء بشكل طبيعي أو عند الإلغاء من قبل المستخدم أو من قبل الكود بسبب حدوث استثناء. في السطر 12، يتم الإبلاغ عن نهاية طبيعية. تجدر الإشارة إلى أن الجزء الفرعي لا يحتاج إلى الاهتمام بتتبع المهام التي لا تزال نشطة. تقوم فئته الأم بذلك نيابة عنه؛
الطريقة [consumeThrowable] هي كما يلي:
// مهام غير متزامنة
protected int numberOfRunningTasks;
private boolean runningTasksHaveBeenCanceled;
...
// أصدرت عملية غير متزامنة استثناءً
// أو حدثت استثناء أثناء تنفيذ عملية غير متزامنة
private void consumeThrowable(Throwable th) {
// th : الاستثناء المطلوب معالجته
//
// سجل
if (isDebugEnabled) {
Log.d(className, "Exception reçue");
}
// يتم إلغاء المهام التي تم تشغيلها بالفعل
cancelRunningTasks();
// يتم عرض رسائل الخطأ
showAlert(th);
}
// إلغاء المهام
protected void cancelRunningTasks() {
// سجل
if (isDebugEnabled) {
Log.d(className, "Annulation des tâches lancées");
}
// إلغاء جميع المهام غير المتزامنة المسجلة
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
// تسجيل الإلغاء
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// نهاية الانتظار
cancelWaitingTasks();
// الإبلاغ عن إلغاء المهام إلى الجزء الفرعي
notifyEndOfTasks(true);
}
...
// الفئات الفرعية -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- السطر 3: تتلقى الطريقة [consumeThrowable] الاستثناء الذي حدث؛
- السطر 15: يتم إلغاء جميع المهام التي لا تزال نشطة؛
- السطر 17: يتم عرض نص الاستثناء؛
- الأسطر 21-37: إلغاء جميع المهام؛
- الأسطر 27-29: يتم إلغاء جميع الاشتراكات؛
- السطر 31: يتم تسجيل حدوث الإلغاء؛
- السطر 32: إعادة ضبط عداد المهام إلى الصفر؛
- السطر 34: يتم إلغاء الانتظار؛
- السطر 36: يتم إخطار الجزء الفرعي بانتهاء المهام عند الإلغاء؛
2.7.3.7. إدارة دورة حياة الجزء
// دورة الحياة --------------------------------------------------------
@Override
public void onDestroyView() {
// الأصل
super.onDestroyView();
// سجل
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// الأصل
super.onDestroy();
// سجل
if (isDebugEnabled) {
Log.d(className, "onDestroy");
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
private void saveState() {
...
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
...
}
- السطور 2-20: طرق [onDestroyView, onDestroy] موجودة فقط من أجل السجلات. تسمح هذه السجلات للمطور بفهم دورة حياة الأجزاء بشكل أفضل؛
يتم حفظ الجزء عند تدوير الجهاز بواسطة الطرق التالية [setUserVisibleHint, onSaveInstanceState, saveState]:
// دورة حياة الجزء
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// الأصل
super.setUserVisibleHint(isVisibleToUser);
// حفظ؟
if (this.isVisibleToUser && !isVisibleToUser) {
// سيتم إخفاء الجزء - يتم حفظه
if (!saveFragmentDone) {
saveState();
}
}
// الذاكرة
this.isVisibleToUser = isVisibleToUser;
}
private void saveState() {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// سجل
if (isDebugEnabled) {
Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
}
// الأصل
super.onSaveInstanceState(outState);
// حفظ الجزء فقط إذا كان مرئيًا
if (isVisibleToUser) {
// ربما تم الحفظ بالفعل
if (!saveFragmentDone) {
saveState();
}
// يجب إجراء الاستعادة في جميع الأحوال
session.setAction(ISession.Action.RESTORE);
}
}
- الأسطر 6-19: يتم حفظ الجزء إذا انتقل من الحالة المعروضة إلى الحالة المخفية (السطر 11). الطريقة [setUserVisibleHint] هي التي تزودنا بهذه المعلومات؛
- السطر 14: يتم الحفظ بواسطة الطريقة الخاصة في الأسطر 21-23؛
- الأسطر 25-41: عند تدوير الجهاز، سيتم استدعاء الطريقة [onSaveInstanceState]. يتم حفظ الجزء في حالتين:
- أن يكون مرئيًا (السطر 34)؛
- لم يتم حفظه بعد (السطر 36). من الممكن ألا تتمكن الطريقتان [setUserVisibleHint, onSaveInstanceState] من التنفيذ معًا عندما يكون الجزء مرئيًا، وبالتالي تكون إدارة القيمة المنطقية [saveFragmentDone] غير ضرورية. في حالة الشك، فضلت استخدام هذه الطريقة؛
- السطر 40: بعد الحفظ سيأتي الاستعادة. نلاحظ، للمرة القادمة التي سيتعين فيها تحديث الجزء، أنه سيتعين القيام بذلك في عملية [RESTORE]؛
يجب ملاحظة اللحظتين اللتين يُطلب فيهما حفظ المقطع:
- عندما ينتقل من الحالة المرئية إلى الحالة المخفية؛
- عندما يكون هناك دوران للجهاز؛
الطريقة الخاصة [saveState] هي كما يلي:
...
private void saveState() {
// مهام يجب إلغاؤها؟
if (numberOfRunningTasks != 0) {
// يتم إلغاء المهام
cancelRunningTasks();
}
// يتم حفظ حالة الجزء
CoreState currentState = saveFragment();
// تمت زيارة الجزء
currentState.setHasBeenVisited(true);
// حفظ حالة القائمة
currentState.setMenuOptionsState(getMenuOptionsStates());
// تسجيل الدخول
session.setCoreState(getNumView(), currentState);
// تم الحفظ
saveFragmentDone = true;
// سجل
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// الفئات الفرعية -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
- السطر 4-7: قد تحدث عملية تدوير الجهاز أثناء تنفيذ عمليات غير متزامنة. وقد تقرر هنا إلغاء جميع هذه العمليات. هذا ليس قرارًا جيدًا للمستخدم الذي سيضطر إلى إعادة طلب جديد قد يستغرق وقتًا طويلاً في حين أنه قام فقط بتحريك هاتفه أو جهازه اللوحي أو تلقى مكالمة هاتفية. من الممكن الحفاظ على اتصالات الشبكة خلال دورة النسخ الاحتياطي / الاستعادة. لكن الحلول ليست واضحة وقررت عدم تناولها في هذه الدورة للمبتدئين. الطريقة المتبعة هي إجراء اتصالات الشبكة هذه عبر جزء لا يرتبط بواجهة مرئية ولا يتم إتلافه أثناء دورة النسخ الاحتياطي/الاستعادة. يكفي لذلك استخدام الأمر [Fragment.setRetainInstance(true)]؛
- السطر 9: نطلب من الجزء الفرعي حفظ حالته في نوع مشتق من [CoreState] (السطر 31)؛
- السطر 11: يتم تسجيل أن الجزء قد تمت زيارته. هذه المعلومة مفيدة. عندما تتم زيارة جزء للمرة الأولى، قد يختلف تحديثه عن التحديثات التالية لأنه لا يوجد له حالة سابقة في الجلسة؛
- السطر 13: يتم حفظ حالة القائمة مما سيسمح لنا باستعادتها تلقائيًا؛
- السطر 15: يتم حفظ هذه الحالة الحالية في الجلسة. في هذه الجلسة، يتم تجميع الحالات حسب العرض/الجزء، حيث يكون لكل منها حالة. يتم توفير رقم العرض بواسطة الجزء الفرعي (السطر 33)؛
- السطر 17: نلاحظ أن حفظ الجزء قد تم. وذلك لأن هناك طريقتين من المحتمل أن تستدعيان الطريقة [saveState]، ولا داعي لإجراء عمليتي حفظ؛
يتم إعادة إنشاء العرض المرتبط بالجزء بواسطة الطريقة التالية:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// الأصل
super.onActivityCreated(savedInstanceState);
// السجل
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// يجب استعادة العرض
viewHasToBeInitialized = true;
}
في دورة الحياة، يتم تنفيذ الطريقة [onActivityCreated] مباشرة بعد الطريقة [onCreateView]. يشير استدعاء هذه الطريقة الأخيرة إلى أنه يجب إعادة بناء العرض المرتبط بالجزء. نكتفي بتدوين ذلك في السطر 10.
2.7.3.8. تحديث الجزء
يعد تحديث الجزء آخر عملية يتم إجراؤها على الجزء قبل أن يصبح مرئيًا وينتظر إجراءات المستخدم. ويتم ذلك بواسطة الكود التالي:
// قائمة الجزء
private Menu menu;
private MenuItemState[] menuOptionsStates;
// دورة حياة الجزء
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// حالات الجزء
private CoreState previousState;
// المُخطِط jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// دورة حياة الجزء
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// تحديث الجزء ----------------------------------------------------------------------------------
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// سجل
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// الذاكرة
this.menu = menu;
// يتم استرداد # خيارات القائمة إذا لم يتم ذلك من قبل
if (fragmentHasToBeInitialized) {
// يتم استرداد # خيارات القائمة
getMenuOptionsStates(menu);
// النشاط
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = (Session) this.mainActivity.getSession();
}
// يتم استرداد الحالة السابقة للجزء (في المرة الأولى، يمثل فقط القيمة المنطقية hasBeenVisited شيئًا ما)
previousState = session.getCoreState(getNumView());
// تحديث الجزء الفرعي على عدة مراحل
// الخطوة 1 - هل هذه هي الزيارة الأولى؟
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// ليست الزيارة الأولى
// الخطوة 2: هل يجب تهيئة الجزء؟
...
// الخطوة 3: هل يجب تهيئة العرض؟
...
}
// الخطوة 4: إرسال، تنقل، استعادة؟
...
// الخطوة 5: تحديثات نهائية ----------------------
...
}
...
// الفئات الفرعية -----------------------------------------------------
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
- السطر 19: تُستخدم الطريقة [onCreateOptionsMenu] لتحديث الجزء. ولهذا السبب، يجب أن يحتوي الجزء على قائمة، فارغة إذا لزم الأمر. عند تنفيذ هذه الطريقة، يكون الجزء قد تم ربطه بعرضه ونشاطه، كما أنه يكون مرئيًا؛
- السطر 25: يتم حفظ القائمة التي تم تمريرها كمعلمة (السطر 22) إلى الطريقة؛
- الأسطر 27-34: إذا كان يجب تهيئة الجزء:
- السطر 29: يتم وضع حالات خيارات القائمة في المصفوفة [menuOptionsStates] في السطر 3؛
- السطر 31: يتم تخزين النشاط كمثيل من النوع Android [Activity]؛
- السطر 32: يتم تخزين النشاط كمثيل لواجهة [IMainActivity]؛
- السطر 33: يتم تخزين الجلسة. تغيير النوع ضروري، لأن الطريقة [mainActivity.getSession()] تُرجع نوعًا [ISession]؛
- السطر 36: يتم استرداد الحالة السابقة للجزء من الجلسة. إذا كانت هذه هي الزيارة الأولى للجزء، فإن القيمة المنطقية [previousState.hasBeenVisited] هي الوحيدة التي لها معنى؛
- الأسطر 39-44: يتم تنفيذ هذا الكود عندما تكون هذه هي الزيارة الأولى للجزء. في هذه الحالة، لا يكون حالته السابقة ذات أهمية؛
- الأسطر 44-50: يتم تنفيذ الكود عندما لا تكون هذه هي الزيارة الأولى للجزء؛
- السطور 46-47: يتم تنفيذ الكود إذا تم استدعاء منشئ الجزء (fragmentHasToBeInitialized==true)؛
- السطور 48-49: يتم تنفيذ الكود إذا تمت إعادة بناء العرض المرتبط بالجزء (viewHasToBeInitialized==true)؛
- السطران 51-52: يتم تنفيذ الكود وفقًا للإجراء (SUBMIT، NAVIGATION، RESTORE) الجاري؛
- السطران 54-55: لا يزال الكود قيد التنفيذ؛
الخطوات الخمس للتحديث هي كما يلي:
الخطوة 1
// قائمة الجزء
private Menu menu;
private MenuItemState[] menuOptionsStates;
// دورة حياة الجزء
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// حالات الجزء
private CoreState previousState;
// المُخطِط jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// دورة حياة الجزء
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// يتم استرداد الحالة السابقة للجزء (في المرة الأولى، يمثل فقط القيمة المنطقية hasBeenVisited شيئًا ما)
previousState = session.getCoreState(getNumView());
// تحديث الجزء الفرعي على عدة مراحل
// الخطوة 1 - هل هذه هي الزيارة الأولى؟
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// تهيئة الجزء والعرض
initFragment(null);
initView(null);
// إعادة تعيين previousState للمتابعة
previousState = null;
} else {
// هذه ليست الزيارة الأولى
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- السطر 19: يتم استرداد الحالة السابقة للمقتطف من الجلسة؛
- السطور 22-31: يتم تنفيذ الكود إذا لم يتم زيارة الجزء من قبل؛
- السطر 27: يُطلب من الفئة الفرعية تهيئة الجزء. المعلمة الخاصة بالطريقة [initFragment] في السطر 35 هي الحالة السابقة للجزء. هنا، يتم تمرير null لإعلام الجزء الفرعي بأن هذه هي الزيارة الأولى؛
- السطر 28: يُطلب من الفئة الفرعية تهيئة العرض المرتبط بالجزء. المعلمة الخاصة بالطريقة [initView] في السطر 37 هي الحالة السابقة للجزء. هنا، يتم تمرير null لإعلام الجزء الفرعي بأن هذه هي الزيارة الأولى؛
- السطر 30: يتم تعيين الحالة السابقة إلى null للخطوات التالية؛
الخطوتان 2 و 3
// قائمة الجزء
private Menu menu;
private MenuItemState[] menuOptionsStates;
// دورة حياة الجزء
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// حالات الجزء
private CoreState previousState;
// أداة التعيين jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// دورة حياة المقطع
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// يتم استرداد الحالة السابقة للجزء (في المرة الأولى، لا يمثل سوى القيمة المنطقية hasBeenVisited أي شيء)
previousState = session.getCoreState(getNumView());
// تحديث الجزء الفرعي على عدة مراحل
// الخطوة 1 - هل هذه هي الزيارة الأولى؟
if (!previousState.getHasBeenVisited()) {
...
} else {
// ليست الزيارة الأولى
// الخطوة 2: هل يجب تهيئة الجزء؟
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation fragment");
}
// الجزء الفرعي
initFragment(previousState);
}
// الخطوة 3: هل يجب تهيئة العرض؟
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation vue");
}
// جزء فرعي
initView(previousState);
}
}
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- الأسطر 24-42: يتم تنفيذها عندما لا تكون هذه هي الزيارة الأولى للجزء؛
- الأسطر 27-33: إذا تم إعادة بناء الجزء للتو، يتم إعادة تهيئته عن طريق استدعاء الطريقة [initFragment] من الفئة الفرعية (الأسطر 32، 46). يتم تمرير الحالة السابقة للجزء إليها؛
- الأسطر 35-51: إذا كان يجب تهيئة أو إعادة تهيئة العرض المرتبط بالجزء، يُطلب من الجزء الفرعي القيام بذلك (الأسطر 40، 48). وهنا أيضًا، يتم تمرير الحالة الأخيرة المعروفة للجزء إليه؛
الخطوة 4
// قائمة الجزء
private Menu menu;
private MenuItemState[] menuOptionsStates;
// دورة حياة الجزء
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// حالات الجزء
private CoreState previousState;
// مُخطط jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// دورة حياة الجزء
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// يتم استرداد الحالة السابقة للجزء (في المرة الأولى، يمثل فقط القيمة المنطقية hasBeenVisited شيئًا ما)
previousState = session.getCoreState(getNumView());
// تحديث الجزء الفرعي على عدة مراحل
...
// الخطوة 4: إرسال، تنقل، استعادة؟
// سجل
if (isDebugEnabled) {
try {
Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// عملية جارية
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// جزء فرعي
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// استعادة القائمة
setMenuOptionsStates(previousState.getMenuOptionsState());
// جزء فرعي
updateOnRestore(previousState);
} else {
// هذه هي الزيارة الأولى - لا شيء للقيام به
}
break;
case RESTORE:
// استعادة
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// استعادة القائمة (لا يمكن أن تكون previousState فارغة)
setMenuOptionsStates(previousState.getMenuOptionsState());
// جزء فرعي
updateOnRestore(previousState);
break;
}
....
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
- الأسطر 34-66: يتم معالجة الإجراء الجاري الذي قد يكون أحد الإجراءات الثلاثة التالية:
- RESTORE: يتم استعادة الجزء بعد تدوير الجهاز؛
- NAVIGATION: نعود إلى المقطع بهدف العثور عليه في الحالة التي تركناه عليها عند آخر استخدام له؛
- SUBMIT: جميع الحالات الأخرى؛
- السطر 34: يتم استرداد الإجراء الجاري؛
- الأسطر 36-42: بالنسبة لعملية من النوع SUBMIT، يتم استدعاء الطريقة [updateOnSubmit] للجزء الفرعي (الأسطر 41، 68) مع تمرير الحالة الأخيرة المعروفة للجزء إليه؛
- الأسطر 43-55: بالنسبة لعملية من النوع NAVIGATION؛
- الأسطر 47-54: نريد إعادة الجزء إلى آخر حالة معروفة له. يمكن أن تتزامن عملية NAVIGATION مع زيارة أولى. قد يكون هذا هو الحال على سبيل المثال في تطبيق يحتوي على علامات تبويب: إذا انتقلت من علامة التبويب 1 إلى علامة التبويب 4:
- يجب عليّ تهيئة جزء علامة التبويب 4 إذا كانت هذه هي الزيارة الأولى؛
- إعادة الجزء الخاص بالعلامة التبويبية 4 إلى حالته السابقة إذا لم تكن هذه هي الزيارة الأولى؛
- السطور 52-54: لا يتم القيام بأي شيء إذا كانت هذه هي الزيارة الأولى. سيكون على الطريقة الفرعية [initView(CoreState previousState)] القيام بهذه التهيئة. تتميز الزيارة الأولى بالشرط [previousState==null]؛
- السطر 49: إذا لم تكن هذه هي الزيارة الأولى للجزء، يتم استعادة قائمته؛
- السطر 51: يُطلب من الفئة الفرعية تحديث نفسها عن طريق استدعاء الطريقة الموجودة في السطر 70. ويتم تمرير الحالة السابقة للجزء إليها حتى تتمكن من أداء مهمتها؛
- الأسطر 56-66: في حالة عملية استعادة الجزء، يتم القيام بنفس الشيء كما في حالة التنقل خارج الزيارة الأولى؛
الخطوة 5
// قائمة الجزء
private Menu menu;
private MenuItemState[] menuOptionsStates;
// دورة حياة الجزء
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// حالات الجزء
private CoreState previousState;
// المُخطِط jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// دورة حياة الجزء
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// الخطوة 5: تحديثات الأطراف ----------------------
// تم تغيير العرض
session.setPreviousView(getNumView());
// لا توجد إجراءات جارية
session.setAction(ISession.Action.NONE);
// عند مغادرة هذا الجزء، يجب حفظه
saveFragmentDone = false;
// ما لم يتم إعادة بناء الجزء، لا داعي لتهيئته
fragmentHasToBeInitialized = false;
// ما لم يتم إعادة بناء العرض، لا داعي لتهيئته
viewHasToBeInitialized = false;
// نعود إلى التشغيل العادي لاختيار علامات التبويب
session.setNavigationOnTabSelectionNeeded(true);
// يتم إخطار الجزء بأن العرض جاهز
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- الأسطر 18-30: عند الوصول إلى هنا، يكون الجزء قد تم تهيئته ويكون جاهزًا للعرض. ثم نعيد جميع المؤشرات المستخدمة في إدارة دورة حياة الجزء إلى حالتها الأولية؛
- السطر 20: تم تغيير العرض: يتم تسجيل ذلك في الجلسة؛
- السطر 22: لم تعد هناك أي عملية جارية؛
- السطر 24: عند مغادرة الجزء المعروض حاليًا، سيتعين حفظه عند مغادرته؛
- السطر 26: لم يعد هناك حاجة لإعادة بناء الجزء. سيتم إعادة تعيين هذا المؤشر إلى vrai عند تنفيذ منشئ الجزء مرة أخرى؛
- السطر 28: لم يعد هناك حاجة لتهيئة العرض المرتبط بالجزء. سيتم إعادة تعيين هذا المؤشر إلى vrai عند تنفيذ الطريقة [onActivityCreated] مرة أخرى؛
- السطر 30: يتم عرض الجزء ربما في تطبيق ذي علامات تبويب. في هذه الحالة، عندما ينقر المستخدم على إحدى علامات التبويب، يجب أن يحدث تغيير في الجزء؛
- السطر 36: يتم إعلام الفئة الفرعية بأن الجزء جاهز. يمكن لهذه الفئة أن تضع في الطريقة [notifyEndOfUpdates] التحديثات التي يجب إجراؤها في جميع الأحوال، أو تشغيل عملية غير متزامنة للحصول على بيانات جديدة، ...
2.7.4. مثال على جزء
![]() |
أدرجنا في المشروع [client-android-skel] مثالاً على جزء لإطلاع القارئ على البنية النموذجية لجزء من تطبيق قائم على هذا المشروع.
الفئة [DummyFragment] هي كما يلي:
package client.android.fragments.behavior;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
public class DummyFragment extends AbstractFragment {
// الحقول الموروثة من الفئة الأصلية -------------------------------------------------------
// وضع التصحيح
//-- نهائي محمي منطقي isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// اسم الفئة
//-- محمي String className;
// المهام غير المتزامنة
//-- protected int numberOfRunningTasks;
// نشاط
//-- محمي IMainActivity mainActivity;
//-- محمي نشاط activity;
// جلسة
//-- محمي جلسة عمل session;
// طرق موروثة من الفئة الأصلية -------------------------------------------------------
// عرض خيارات القائمة
//-- محمي void setAllMenuOptionsStates(boolean isVisible) {
//-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// إدارة انتظار انتهاء سلسلة من المهام غير المتزامنة
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// تنفيذ مهمة غير متزامنة باستخدام RxAndroid
//-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// إلغاء المهام
//-- protected void cancelRunningTasks() {
// عرض تنبيه عند حدوث استثناء
//-- protected void showAlert(Throwable th) {
// عرض قائمة الرسائل
//-- protected void showAlert(List<String> messages) {
// الطرق المفروضة من قبل الفئة الأم -------------------------------------------------------
@Override
public CoreState saveFragment() {
// يجب حفظ المقتطف
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// إذالا يوجد شيء للحفظ، قم بتنفيذ [return new CoreState();] وحذف الفئة [DummyFragmentState]
}
@Override
protected int getNumView() {
// يجب إعادة رقم الجزء إلى جدول الأجزاء التي تديرها النشاط (انظر MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// يصبح الجزء مرئيًا وقد خضع لعملية إنشاء في هذه الخطوة أو في خطوة سابقة
// يحدث هذا عند بدء تشغيل التطبيق وعند كل دوران لجهاز Android
// يتبعها بالضرورة تنفيذ [initView]
// يجب تهيئة حقول الجزء الذي أعيد بناؤه
// previousState هي أحدث نسخة احتياطية للجزء - تكون قيمتها "null" إذا كانت هذه هي الزيارة الأولى للجزء
}
@Override
protected void initView(CoreState previousState) {
// يصبح الجزء مرئيًا وتم إعادة بناء العرض المرتبط به في هذه الخطوة أو في خطوة سابقة
// يحدث هذا في كل مرة يتم فيها تنفيذ [initFragment] وفي كل مرة يخرج فيها الجزء من المنطقة المجاورة للجزء المعروض
// يجب تهيئة مكونات العرض الذي تمت إعادة بنائه
// previousState هي آخر نسخة احتياطية للجزء - تساوي null إذا كانت هذه هي الزيارة الأولى للجزء
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// يتم تنفيذها بعد [initFragment, initView] إذا تم تنفيذ هذه الطرق
// سيتم عرض العرض بعد عملية من النوع SUBMIT
// يجب عمومًا تهيئة الجزء والعرض المرتبط به من الجلسة
// previousState هو آخر حفظ للجزء - يساوي null إذا كانت هذه هي الزيارة الأولى للجزء
// لا يوجد ما يمكن فعله إذا تعذر الوصول إلى الجزء عبر عملية SUBMIT
// إذا كان من الممكن الوصول إلى الجزء عبر عمليات SUBMIT من أجزاء مختلفة، فيمكن معرفة العرض السابق عبر [session.getPreviousView]
// إذا كان من الممكن الوصول إلى الجزء عبر عدة عمليات SUBMIT انطلاقًا من نفس الجزء، فيجب وضع مؤشر في الجلسة للتمييز بين الأنواع المختلفة من SUBMIT انطلاقًا من هذا الجزء
}
@Override
protected void updateOnRestore(CoreState previousState) {
//بعد [initFragment, initView] إذا تم تنفيذ هذه الطرق
// سيتم عرض العرض بعد عملية من نوع RESTORE أو NAVIGATION
// previousState هو آخر نسخة احتياطية للجزء - لا يساوي أبدًا null
// يجب إعادة العرض إلى حالته السابقة
}
@Override
protected void notifyEndOfUpdates() {
// يتم تنفيذه بعد الطرق [updateOnSubmit, updateOnRestore]
// عند الوصول إلى هنا، تكون الواجهة قد تم إنشاؤها وتهيئتها
// غالبًا ما لا يوجد ما يمكن فعله هنا، ولكن يمكن أيضًا تجميع الإجراءات التي يجب القيام بها بغض النظر عن الطريقة التي يتم بها الوصول إلى هذه الواجهة
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// يتم استدعاؤها عندما تنتهي المهام غير المتزامنة التي أطلقها الجزء أو يتم إلغاؤها
// يمكن التمييز بين هاتين الحالتين بفضل المعلمة runningTasksHaveBeenCanceled
// يجب عمومًا إعادة العرض إلى حالة مختلفة عن تلك التي كان عليها أثناء انتظار استجابات المهام غير المتزامنة
}
}
قد لا تحتوي الفئة [DummyFragment] على حالة. هنا، أضفنا حالة لتذكيرنا بما هو متوقع فيها:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class DummyFragmentState extends CoreState {
// حالة الجزء [DummyFragment]
// لا تضع سوى الحقول القابلة للتسلسل في jSON
// ضع التعليق التوضيحي @JsonIgnore على الحقول الأخرى، لكن من الصعب معرفة الغرض منها
// لا تنسَ getters / setters - فهي تُستخدم للتسلسل / إلغاء التسلسل
}
لتوضيح استخدام المشروع [client-android-skel]، سنستخدم أولاً أمثلة بسيطة قبل الانتقال إلى دراسة حالة أكثر شمولاً.
2.8. تمارين توضيحية
سنبدأ بإعادة هيكلة أمثلة مكتوبة بالفعل.
2.8.1. المثال 17B
نعود إلى المثال 17 الذي تمت دراسته في الفقرة 1.18. إنه تطبيق يحتوي على جزء واحد بدون مهام غير متزامنة وبدون علامات تبويب. ندرسه لنرى كيف يتصرف عند تدوير الجهاز. نقوم بإدخال البيانات التالية:

ثم في [1]، نقوم بتدوير الجهاز مرتين. وتكون العرض الجديد كما يلي:

إذا قارنا بين العرضين، نجد أن كل شيء قد تم الاحتفاظ به باستثناء القائمة [2] التي أصبحت فارغة الآن.
علاوة على ذلك، إذا نقرنا على الزر [Valider]، يظهر مربع حوار يعرض الإدخالات التي تمت في النموذج. إذا قمنا في هذه اللحظة بتدوير الجهاز، نفقد مربع الحوار.
لذلك، سيتعين علينا، عند الدوران، إعادة إنشاء:
- القائمة المنسدلة والعنصر المحدد فيها؛
- مربع الحوار إذا كان معروضًا أثناء الدوران؛
2.8.1.1. المشروع [Exemple-17B]
نقوم بنسخ المشروع [client-android-skel] في examples/Exemple-17B. ثم نقوم بتحميل المشروع الجديد [1]:
![]() | ![]() | ![]() |
- في [2-3]، في المجلد [behavior]، نلصق الجزء [Vue1Fragment] من المشروع [Exemple-17]؛
![]() | ![]() | ![]() |
- في [4-5]، في المجلد [layout] الخاص بـ [Exemple-17B]، نلصق العرض [vue1.xml] من [Exemple-17]. هذه هي العرض المرتبط بالجزء؛
- في [6]، يتم استبدال المجلد [values] من [Exemple-17B] بالمجلد [values] من [Exemple-17]؛
سيتم تعديل الهامش العلوي للعرض [vue1.xml] إلى 80 نقطة:
<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="80dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
في هذه المرحلة، يمكننا محاولة إجراء تجميع أولي لمعرفة الأخطاء. تنشأ الأخطاء الأولى المبلغ عنها من imports من الحزم التي تغير مكانها. نقوم بتصحيحها (Ctrl-Shift-O). تنشأ أخطاء أخرى عن حقيقة أن العرض [Vue1Fragment] لا ينفذ جميع الطرق التي تفرضها فئته الأم [AbstractParent]:

نقوم بإنشاء الطرق المفقودة (Alt-Enter).
هناك خطأ آخر في التحويل البرمجي تم الإبلاغ عنه وهو التالي:

يتم تصحيح ذلك في ملف [build.gradle] الخاص بالوحدة النمطية (السطر 20 أدناه):
![]() |
في هذه المرحلة، يمكن إعادة التجميع لمعرفة الأخطاء المتبقية. الخطأ الوحيد الذي تم الإبلاغ عنه هو في الطريقة [Vue1Fragment.updateFragment]:
![]() |
يجب حذف التعليق التوضيحي [@Override] من السطر 135. لم تعد هناك أخطاء الآن. سنبدأ من هنا لتعديل المشروع.
2.8.1.2. حالة الجزء [Vue1Fragment]
يحتاج الجزء [Vue1Fragment] إلى حفظ المعلومات عند تدوير الجهاز حتى يمكن استعادته بالكامل. ننشئ فئة [Vue1FragmentState] لهذا الغرض:
![]() |
في الوقت الحالي، هذه الفئة فارغة:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.1.3. تخصيص المشروع
![]() |
يحتوي المجلد [custom] على عناصر البنية التي يمكن للمطور تخصيصها.
ستكون ثوابت واجهة [IMainActivity] كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// الوصول إلى الجلسة
ISession getSession();
// تغيير العرض
void navigateToView(int position, ISession.Action action);
// إدارة الانتظار
void beginWaiting();
void cancelWaiting();
// ثوابت التطبيق -------------------------------------
// وضع التصحيح
boolean IS_DEBUG_ENABLED = true;
// الحد الأقصى لوقت انتظار استجابة الخادم
int TIMEOUT = 1000;
// فترة الانتظار قبل تنفيذ طلب العميل
int DELAY = 0;
// المصادقة الأساسية
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// تجاور الأجزاء
int OFF_SCREEN_PAGE_LIMIT = 1;
// شريط علامات التبويب
boolean ARE_TABS_NEEDED = false;
// صورة الانتظار
boolean IS_WAITING_ICON_NEEDED = false;
// عدد أجزاء التطبيق
int FRAGMENTS_COUNT = 1;
}
- الأسطر 24-31: لا يستخدم التطبيق هنا طبقة [DAO]. لن يتم استخدام هذه الثوابت؛
- السطر 34: تجاور شظايا يساوي 1 وهو القيمة الافتراضية. وبما أن التطبيق يحتوي على شظية واحدة فقط (السطر 43)، فإن هذه القيمة لا أهمية لها؛
- السطور 39-40: نظرًا لعدم وجود عمليات مع الطبقة [DAO]، فلا داعي لوجود صورة انتظار؛
- السطر 37: هذا ليس تطبيقًا متعدد علامات التبويب؛
- السطر 43: يوجد جزء واحد فقط؛
الفئة [Session] هي كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// العناصر التي لا يمكن تسلسلها في jSON يجب أن تحتوي على التعليق التوضيحي @JsonIgnore
}
وهي فارغة. في الواقع، نظرًا لوجود جزء واحد فقط، فلا داعي لتوفير اتصال بين الأجزاء باستخدام جلسة عمل.
وأخيرًا، الفئة [CoreState] هي التالية:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// جزء تمت زيارته أم لا
protected boolean hasBeenVisited = false;
// حالة القائمة المحتملة للجزء
protected MenuItemState[] menuOptionsState;
// مُستردات ومُعيّنات
...
}
- السطور 11-13: علينا وضع جميع الفئات المشتقة من [CoreState] التي تخزن حالة الأجزاء المختلفة. هنا، لا يوجد سوى جزء واحد (السطر 12)؛
2.8.1.4. النشاط [MainActivity]
النشاط [MainActivity] هو حاليًا كما يلي:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// طبقة [DAO]
@Bean(Dao.class)
protected IDao dao;
// الجلسة
private Session session;
// طرق الفئة الأصلية -----------------------
@Override
protected void onCreateActivity() {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// الجلسة
this.session = (Session) super.session;
// المهام المطلوبة: نواصل عمليات التهيئة التي بدأتها الفئة الأصلية
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// المهام: تحديد الأجزاء هنا
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// المهام: تحديد عناوين الأجزاء هنا
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// المهام: التنقل عبر علامات التبويب - تحديد العرض الذي سيظهر عند تحديد علامة التبويب رقم [position]
}
@Override
protected int getFirstView() {
// المهام: تحديد رقم العرض الأول (المقطع) المراد عرضه
return 0;
}
}
تشير التعليقات [//todo] إلى ما يجب على المطور القيام به. تتطور الفئة [MainActivity] على النحو التالي:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// طبقة [DAO]
@Bean(Dao.class)
protected IDao dao;
// جلسة
private Session session;
// طرق الفئة الأصلية -----------------------
@Override
protected void onCreateActivity() {
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// الجلسة
this.session = (Session) super.session;
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
}
يجب تعديل الطريقة الواردة في الأسطر 41-44 فقط. يجب أن تعرض جدول أجزاء التطبيق. في السطر 43، لا تنسَ وضع شرطة سفلية بعد اسم الجزء.
2.8.1.5. حالة الجزء [FragmentState]
بعد إجراء اختبارات التناوب على المشروع [Exemple-17]، تقرر حفظ العناصر التالية من الجزء:
- قائمة قيم القائمة المنسدلة؛
- موضع العنصر المحدد في هذه القائمة؛
- الرسالة التي تعرضها مربع الحوار إذا كان موجودًا في وقت الدوران؛
ستكون الفئة [Vue1FragmentState] كما يلي:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// قيم القائمة المنسدلة
private List<String> list;
// العنصر المحدد في القائمة المنسدلة
private int listSelectedPosition;
// الرسالة المعروضة في مربع الحوار
private String message;
// مُستردات ومُعيّنات
...
}
2.8.1.6. الجزء [AbstractFragment]
حاليًا، تتم إدارة دورة حياة الجزء بواسطة طريقتين (السطران 6 و 32):
// القائمة المنسدلة
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// يتم تحديد الزر الأول
radioButton1.setChecked(true);
// التقويم
datePicker1.setCalendarViewShown(false);
// seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// القائمة المنسدلة
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
...
protected void updateFragment() {
// تهيئة محول القائمة المنسدلة
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
سيتم ترحيل كود هاتين الطريقتين إلى الطرق التي تفرضها الفئة [AbstractFragment] على النحو التالي:
// إدارة دورة حياة الجزء ---------------------------------------------------------------------
@Override
public CoreState saveFragment() {
Vue1FragmentState state = new Vue1FragmentState();
state.setList(list);
state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
state.setMessage(message);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// زيارة أولى؟
if (previousState == null) {
// يتم إنشاء قيم القائمة المنسدلة
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// استرجاع قيم القائمة المنسدلة
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// ورسالة مربع الحوار
message = state.getMessage();
}
// تهيئة محول القائمة المنسدلة
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override
protected void initView(CoreState previousState) {
// التقويم
datePicker1.setCalendarViewShown(false);
// seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// تهيئة محول القائمة المنسدلة
dropDownList.setAdapter(dataAdapter);
// زيارة أولى؟
if (previousState == null) {
// يتم تحديد الزر الأول
radioButton1.setChecked(true);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// قيمة شريط التمرير
seekBarValue.setText(String.valueOf(seekBar.getProgress()));
// العنصر المحدد في القائمة المنسدلة
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// هل الحوار مرئي؟
if (message != null) {
// يتم عرضه
showMessage();
}
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
- السطور 2-9: يجب أن تضع الطريقة [saveFragment] عناصر الجزء المراد حفظها في فئة مشتقة من [CoreState] وتُرجع مثيلها؛
- الأسطر 11-14: يجب أن تعرض الطريقة [getNumView] رقم الجزء. هنا، يوجد جزء واحد فقط رقمه هو 0؛
- الأسطر 16-34: يجب أن تقوم الطريقة [initFragment] بتهيئة حقول الجزء. وهي تتلقى الحالة السابقة للجزء. إذا كانت قيمة [previousState] تساوي null، فهذا يعني أنها الزيارة الأولى؛
- الأسطر 19-25: عند الزيارة الأولى، يتم إنشاء قيم القائمة المنسدلة؛
- الأسطر 26-30: إذا لم تكن هذه هي الزيارة الأولى، يتم استعادة حقول [list, message] الخاصة بالجزء من الحالة السابقة؛
- الأسطر 33-34: تهيئة الحقل [dataAdapter] في الجزء. هذا هو مصدر بيانات القائمة المنسدلة؛
- السطور 37-62: تُستخدم الطريقة [initView] لتهيئة مكونات الواجهة المرئية. تتلقى كمعلمة الحالة السابقة [previousState]. إذا كانت [previousState==null]، فهذا يعني أنها الزيارة الأولى؛
- نجد هنا ما كان موجودًا سابقًا في الطريقة [@AfterViews]؛
- الأسطر 57-61: عند الزيارة الأولى، نتأكد من أن زر الاختيار الأول هو الذي تم تحديده؛
- الأسطر 64-67: يتم تنفيذ الطريقة [updateOnSubmit] عندما تكون الإجراء الجاري هو [SUBMIT]. هنا، لا يوجد تنقل بين الأجزاء وبالتالي لا يوجد إجراء جاري؛
- الأسطر 69-81: يتم تنفيذ الطريقة [updateOnRestore] عندما تكون الإجراء الجاري هو [NAVIGATION] أو [RESTORE]. هنا، لا توجد تنقلات بين الأجزاء وبالتالي لا يمكن تنفيذ الإجراء [NAVIGATION]؛
- السطر 72: يتم إعادة حساب (وليس استعادة) قيمة TextView seekBarValue. في الواقع، أثناء عمليات الدوران، كانت القيمة تُفقد أحيانًا؛
- السطران 74-75: يتم وضع القائمة على العنصر الذي كان محددًا قبل الدوران. بدون ذلك، كانت القائمة توضع على العنصر الأول؛
- السطور 76-80: يتم إعادة عرض مربع الحوار إذا كانت رسالة الحالة السابقة غير null. سنعود إلى الطريقة [showMessage] (السطر 79)؛
- الأسطر 83-86: الطريقة [notifyEndOfUpdates] هي آخر طريقة تستدعيها الفئة الأم قبل ترك الجزء الفرعي وشأنه. هنا لا يوجد ما يجب فعله؛
- الأسطر 88-91: تشير الطريقة [notifyEndOfTasks] إلى نهاية المهام غير المتزامنة التي أطلقها الجزء. هنا، لا توجد أي مهام؛
تتم استعادة مربع الحوار بالطريقة التالية:
// رسالة مربع الحوار
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// قائمة الرسائل المراد عرضها
List<String> messages = new ArrayList<>();
...
// العرض
doAfficher(messages);
}
private void doAfficher(final List<String> messages) {
// يتم إنشاء النص المراد عرضه
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// يتم حفظ الرسالة
message = texte.toString();
// عرضها
showMessage();
}
private void showMessage() {
// عرض
new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// إعادة تعيين الرسالة
message = null;
}
}).show();
}
عندما يقوم المستخدم بتأكيد النموذج، تقوم الطريقة [doValider] (السطر 5) بإنشاء قائمة بالرسائل ثم تعرضها (السطر 10) في مربع الحوار.
- الأسطر 14-20: يتم ربط قائمة الرسائل في رسالة واحدة يتم تخزينها في السطر 2؛
- الأسطر 25-33: هذه هي الرسالة التي يعرضها مربع الحوار، وهي نفس الرسالة التي تعرضها الطريقة [updateOnRestore]؛
- السطر 27: المعلمة الثانية للطريقة [setNeutralButton] هي الطريقة التي يتم تنفيذها عندما ينقر المستخدم على الزر [Fermer] في مربع الحوار؛
- السطر 31: عند إغلاق مربع الحوار، يتم إعادة الرسالة إلى null للإشارة إلى أن مربع الحوار لم يعد موجودًا؛
2.8.1.7. Tests
يُطلب من القارئ اختبار هذا المشروع والتحقق من أن المقتطف لا يزال محفوظًا بعد دورة أو عدة دورات متتالية.
2.8.2. مثال-23: عميل الطقس
تتيح بعض المواقع الحصول على معلومات الطقس في شكل سلاسل مثل jSON. إليك مثال على ذلك:

يكون شكل URL كما يلي: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} حيث:
- city: المدينة التي نريد معرفة حالة الطقس فيها، وهي هنا أنجيه؛
- country: البلد الذي تقع فيه المدينة، وهنا فرنسا (fr)؛
- APPID: مفتاح يتم الحصول عليه عند التسجيل في موقع [https://home.openweathermap.org/users/sign_up]؛
2.8.2.1. المشروع
![]() |
تم إنشاء المشروع استنادًا إلى مشروع [client-android-skel]. ويتميز بالميزات التالية:
- يحتوي على جزء واحد فقط لا يلزم الحفاظ على حالته؛
- يقوم بطلبات غير متزامنة؛
2.8.2.2. تخصيص المشروع
![]() |
تسمح واجهة [IMainActivity] بتحديد بعض خصائص المشروع:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// الدخول إلى الجلسة
ISession getSession();
// تغيير العرض
void navigateToView(int position, ISession.Action action);
// إدارة الانتظار
void beginWaiting();
void cancelWaiting();
// ثوابت التطبيق -------------------------------------
// وضع التصحيح
boolean IS_DEBUG_ENABLED = true;
// الحد الأقصى لوقت انتظار استجابة الخادم
int TIMEOUT = 1000;
// فترة الانتظار قبل تنفيذ طلب العميل
int DELAY = 5000;
// المصادقة الأساسية
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// تجاور الأجزاء
int OFF_SCREEN_PAGE_LIMIT = 1;
// شريط علامات التبويب
boolean ARE_TABS_NEEDED = false;
// صورة الانتظار
boolean IS_WAITING_ICON_NEEDED = true;
// عدد أجزاء التطبيق
int FRAGMENTS_COUNT = 1;
}
- السطور 25 و28 و31 و40: خصائص الطبقة [DAO]. السطر 31، لا حاجة إلى مصادقة أساسية؛
- السطر 34: تجاور الأجزاء. هنا لا أهمية لهذه الثابتة نظرًا لوجود جزء واحد فقط؛
- السطر 37: هذا ليس تطبيقًا متعدد علامات التبويب؛
- السطر 43: يوجد جزء واحد فقط؛
ستكون الفئة [CoreState] التي تخزن حالة الأجزاء كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// المهام: أضف هنا الفئات الفرعية لـ [CoreState]
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// جزء تمت زيارته أم لا
protected boolean hasBeenVisited = false;
// حالة القائمة المحتملة للجزء
protected MenuItemState[] menuOptionsState;
// مُستردات ومُعيّنات
...
}
- الأسطر 10-13: لا يوجد ما يُذكر، حيث لا يوجد في هذا التطبيق سوى جزء واحد لا يتم الاحتفاظ بحالته؛
الفئة [Session] هي كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// العناصر التي لا يمكن تسلسلها في jSON يجب أن تحتوي على التعليق التوضيحي @JsonIgnore
}
وهي فارغة لأنه لا يوجد في هذا التطبيق اتصال بين الأجزاء.
2.8.2.3. الطبقة [DAO]
![]() |
في الطبقة [DAO]، يجب تخصيص ثلاث فئات:
- الواجهة IDao؛
- تنفيذها Dao؛
- واجهة WebClient للتواصل مع خادم الويب / jSON؛
وستكون واجهة [WebClient] كما يلي:
package client.android.dao.service;
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.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// خدمة الأرصاد الجوية
@Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
- السطران 18-19: خدمة الأرصاد الجوية URL. يُذكر أن هذه تتعلق بـ URL الجذر (RestClientRootUrl، السطر 12) للعميل. هنا سيكون هذا URL الجذر هو [http://api.openweathermap.org/]؛
وستكون واجهة [IDao] كما يلي:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// عنوان URL لخدمة الويب
void setUrlServiceWebJson(String url);
// المستخدم
void setUser(String user, String mdp);
// مهلة انتظار العميل
void setTimeout(int timeout);
// المصادقة الأساسية
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// وضع التصحيح
void setDebugMode(boolean isDebugEnabled);
// مهلة انتظار العميل بالمللي ثانية قبل الطلب
void setDelay(int delay);
// خدمة الطقس
Observable<String> getWeatherForecast(String city, String country, String APPID);
}
- يُذكر أن الطرق الواردة في الأسطر 6-22 موجودة بشكل افتراضي في واجهة IDao للمشروع [client-android-skel]؛
- السطر 25: تسمح الطريقة [getWeatherForecast] بالحصول على السلسلة jSON الخاصة بالطقس في المدينة [city] في البلد [country]. المعلمة الثالثة هي المفتاح الذي تم الحصول عليه من الموقع [https://home.openweathermap.org/users/sign_up]؛
يتم تنفيذ واجهة [IDao] بواسطة الفئة [Dao] التالية:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// عميل خدمة الويب
@RestService
protected WebClient webClient;
// الأمان
@Bean
protected MyAuthInterceptor authInterceptor;
// RestTemplate
private RestTemplate restTemplate;
// مصنع RestTemplate
private SimpleClientHttpRequestFactory factory;
// انتهى الوقت
private int timeout;
@AfterInject
public void afterInject() {
// سجل
Log.d(className, "afterInject");
// يتم إنشاء restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// يتم تعيين المحول jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// يتم تعيين restTemplate لعميل الويب
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// يتم تعيين URL لخدمة الويب
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// يتم تسجيل المستخدم في المعترض
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// الذاكرة
this.timeout = timeout;
// مصنع التكوين
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// معترض المصادقة؟
if (isBasicAuthentificationNeeded) {
// يتم إضافة معترض المصادقة
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// طرق خاصة -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// خدمة الطقس ---------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
// سجل
if (isDebugEnabled) {
Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
}
// النتيجة
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- يُذكر أن الأسطر 17-90 موجودة بشكل افتراضي في الفئة [Dao] للمشروع [client-android-skel]. ما عليك سوى إضافة طرق تنفيذ واجهة [IDao]، الخاصة بالتطبيق (السطر 92)؛
- السطور 93-105: تنفيذ طريقة [getWeatherForecast]. هذه الطريقة بسيطة للغاية وتتم في 6 أسطر، الأسطر 100-105؛
- السطر 100: الطريقة [getResponse] هي طريقة تابعة للفئة الأم [AbstractDao]. وهي تتوقع معلمة من النوع [IRequest<T>] حيث T هو نوع الاستجابة المتوقعة من الخادم، وهنا هو String لأننا نتوقع سلسلة jSON. يجب أن يكون النوع T لـ [IRequest<T>] هو النوع T للطريقة [Observable<T> getWeatherForecast]؛
- الواجهة [IRequest<T>] لها طريقة واحدة فقط: getResponse. وتتمثل وظيفة هذه الطريقة في توفير الاستجابة من النوع T التي يجب أن تقدمها الطريقة [Observable<T> getWeatherForecast]؛
- السطر 103: واجهة [WebClient] هي التي توفر هذه الاستجابة. يتم تمرير المعلمات الثلاثة المستلمة في السطر 94 إليها. ولهذا السبب، يجب أن تحتوي هذه المعلمات على السمة final؛
2.8.2.4. النشاط [MainActivity]
![]() |
النشاط [MainActivity] هو التالي:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// الطبقة [DAO]
@Bean(Dao.class)
protected IDao dao;
// طرق الفئة الأصلية -----------------------
@Override
protected void onCreateActivity() {
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new MeteoFragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
// واجهة IDao ---------------------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(String city, String country, String APPID) {
return dao.getWeatherForecast(city, country, APPID);
}
}
- يُذكر أن الأسطر 15-55 موجودة بشكل افتراضي في المشروع [client-android-skel]. ما عليك سوى تخصيصها؛
- الأسطر 37-40: جدول الأجزاء. يوجد جزء واحد فقط هنا؛
- الأسطر 43-46: لا حاجة لعناوين للأجزاء؛
- الأسطر 48-50: لا توجد علامات تبويب هنا؛
- الأسطر 52-55: العرض الأول الذي يجب عرضه هو العرض رقم 0، وهو عرض [MeteoFragment]؛
- الأسطر 58-61: تنفيذ واجهة [IDao]. هنا، لا يوجد شيء آخر يمكن فعله سوى تفويض المهمة إلى الطبقة [DAO] في السطر 21؛
2.8.2.5. المقتطف [MeteoFragment]
![]() |
يستعلم المقطع [MeteoFragment] عن خدمة الويب / jSON الخاصة بالطقس. وهيكلها كما يلي:
package client.android.fragments;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
- السطر 14: العرض [res / layout / meteo_fragment.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="Construisez votre interface visuelle"
android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
android:layout_marginTop="120dp"/>
</RelativeLayout>
لا تعرض طريقة العرض سوى نص السطر 10؛
- السطر 15: القائمة [res / menu / menu_meteo.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/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionMeteo"
android:title="@string/actionMeteo"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
<item
android:id="@+id/actionTerminer"
android:title="@string/actionTerminer"/>
</menu>
</item>
</menu>
- الأسطر 10-12: تُستخدم خيار القائمة هذا لطلب توقعات الطقس لمدينة ما؛
- السطور 14-15: يستخدم خيار القائمة هذا لإلغاء هذا الطلب إذا كان قيد التنفيذ؛
- الأسطر 16-18: هذا الخيار من القائمة ينهي التطبيق؛
الرمز الكامل للمقتطف هو كما يلي:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {
// البيانات المحلية
private int nbReponsesRecues;
// إدارة الأحداث ---------------------------------------------------------------------------------------
// المدن التي نريد معرفة حالة الطقس فيها
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
@OptionsItem(R.id.actionMeteo)
protected void doMeteo() {
// بلده
String country = "fr";
// احصل على معرف API عن طريق إنشاء حساب [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// URL من خدمة الويب / jSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// بداية انتظار المهام غير المتزامنة [paysDeLoire.length]
beginWaiting(paysDeLoire.length);
// عدد الردود المستلمة
nbReponsesRecues = 0;
// يتم إجراء المكالمات غير المتزامنة بالتوازي
for (String city : paysDeLoire) {
// الطقس
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// استغلال الرد
consumeResponse(response);
// استجابة إيجابية
nbReponsesRecues++;
}
});
}
}
// استغلال استجابة الخادم
private void consumeResponse(String response) {
// سجل
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// بداية الانتظار
protected void beginWaiting(int numberOfRunningTasks) {
// سجل
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// الأصل
beginRunningTasks(numberOfRunningTasks);
// يتم عرض الخيار [Annuler]
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{
new MenuItemState(R.id.menuActions, true),
new MenuItemState(R.id.actionAnnuler, true)});
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// القائمة
initMenu();
// عرض النتائج
String message;
switch (nbReponsesRecues) {
case 0:
message = "Aucune réponse n'a été reçue";
break;
case 1:
message = "Une réponse a été reçue. Consultez vos logs...";
break;
default:
message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
break;
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
}
// طرق خاصة -----------------------------------
private void initMenu() {
if (isDebugEnabled) {
Log.d(className, "initMenu");
}
// القائمة
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
// إدارة دورة الحياة ---------------------------------------------------------------------------------------
...
}
- الأسطر 25-50: إدارة النقر على خيار القائمة [Météo]؛
- السطر 32: إنشاء URL لخدمة الويب / jSON لخدمة الطقس. يتم بعد ذلك تمريرها إلى الطبقة [DAO] عبر النشاط؛
- السطر 34: نبدأ الانتظار. نمرر عدد المهام التي سيتم تشغيلها، وذلك حتى تتمكن الفئة الأم من إعلامنا بانتهاء هذه المهام. هنا، هناك خمس مهام لأننا سنطلب توقعات الطقس لخمس مدن من السطر 23؛
- السطر 16: سنقوم بحساب عدد الردود المستلمة حتى نتمكن من عرضها؛
- الأسطر 38-50: نكرر العملية على المدن التي نريد الحصول على توقعات الطقس لها؛
- السطر 40: سنقوم بإجراء 5 طلبات HTTP بالتوازي؛
- السطر 40: نطلب من الفئة الأم [AbstractParent] استعلام خدمة الويب / jSON؛
- السطور 40-48: تتوقع الطريقة [executeInBackground] معلمتين:
- السطر 40: يتم توفير العملية المطلوب مراقبة وتنفيذها بواسطة الطريقة [mainActivity.getWeatherForecast]؛
- الأسطر 40-48: المثيل [Action1] الذي يجب تنفيذه عند تلقي الرد من الخدمة غير المتزامنة. يجب أن يكون النوع T لـ [Action1<T>] هو النوع T لنتيجة الطريقة [getWeatherForecast]؛
- السطر 44: تم استلام رد. يتم تمريره إلى الأسلوب [consumeResponse] في السطر 53؛
- السطر 46: يتم زيادة عداد الردود المستلمة؛
- الأسطر 53-56: استهلاك رد jSON من خدمة الأرصاد الجوية؛
- السطر 55: نكتفي بتسجيل السلسلة jSON؛
- السطور 59-72: يتم تنفيذ الكود قبل بدء المهام غير المتزامنة؛
- السطر 65: يتم تمرير عدد المهام المطلوب تنفيذها إلى الفئة الأم [AbstractParent]. وهذا ما يسمح لها بإخطارنا عند انتهاء جميع المهام؛
- الأسطر 67-70: إعداد القائمة للانتظار. نحتفظ فقط بالخيار [Actions/Annuler] الذي سيسمح للمستخدم بإلغاء المهام التي تم تشغيلها؛
- الأسطر 74-92: كود يتم تنفيذه عندما تخطرنا الفئة الأم بأن جميع المهام التي تم تشغيلها قد اكتملت؛
- السطر 77: نعيد القائمة إلى حالتها الأولية. تعرض الطريقة [initMenu] (السطور 95-102) القائمة مع جميع خياراتها باستثناء الخيار [Actions/Annuler] الذي يتم إخفاؤه؛
- الأسطر 80-91: يتم عرض عدد الردود المستلمة؛
يتم التعامل مع النقر على خيار القائمة [Annuler] بواسطة الكود التالي:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// إلغاء المهام غير المتزامنة
cancelRunningTasks();
}
- السطر 7: يُطلب من الفئة الأصلية إلغاء المهام التي لا تزال نشطة؛
يتم إدارة النقر على خيار القائمة [Terminer] بواسطة الكود التالي:
@OptionsItem(R.id.actionTerminer)
protected void doTerminer() {
// إيقاف كل شيء
System.exit(0);
}
تتم إدارة دورة حياة الجزء من خلال الطرق التالية:
// إدارة دورة الحياة ---------------------------------------------------------------------------------------
@Override
public CoreState saveFragment() {
return new CoreState();
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
// زيارة أولى؟
if (previousState == null) {
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
- الأسطر 3-6: تُستخدم لتخزين حالة الجزء في فئة مشتقة من [CoreState]. إذا لم يكن للجزء حالة لتخزينها كما هو الحال هنا، فإننا نكتفي بإرجاع مثيل من [CoreState]. يجب عدم إرجاع null لأن ذلك سيؤدي لاحقًا إلى تعطل النظام؛
- الأسطر 8-11: يجب أن تعرض رقم العرض. هنا، المقطع [MeteoFragment] له الرقم 0؛
- السطور 13-16: تُستخدم لتهيئة الجزء بمجرد إنشائه (previousState==null) أو إعادة إنشائه (previousState!=null). هنا، لا يوجد ما يجب فعله. الحقل الوحيد الذي يمكن تهيئته هو التالي:
// المدن التي نريد معرفة حالة الطقس فيها
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
لكنه يتم تهيئته تلقائيًا؛
- الأسطر 18-24: تُستخدم لتهيئة العرض المرتبط بالجزء بمجرد إنشائه (previousState==null) أو إعادة إنشائه (previousState!=null)؛
- الأسطر 21-23: إذا كانت هذه هي الزيارة الأولى للجزء، يتم تهيئة قائمته لإخفاء الخيار [Annuler]؛
- الأسطر 27-30: يتم استدعاؤها إذا تم الوصول إلى المقطع عن طريق التنقل باستخدام إجراء من النوع [SUBMIT]. هنا، لا يوجد تنقل بين المقاطع نظرًا لوجود مقطع واحد فقط؛
- الأسطر 32-35: يتم استدعاؤها أثناء دورة النسخ الاحتياطي/الاستعادة بسبب دوران الجهاز أو لأي سبب آخر. هنا، نظرًا لعدم حفظ الحالة، لا يوجد ما يجب القيام به؛
- الأسطر 37-40: يتم استدعاؤها عند إتمام جميع التحديثات السابقة. هنا، لا يوجد ما يجب القيام به؛
2.8.2.6. Tests
نقوم الآن بتنفيذ المثال:


وتكون السجلات كما يلي:
07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructeur
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructeur, thread=main
07-23 13:24:32.861 2642-2642/client.android D/client.android.dao.service.Dao_: afterInject
07-23 13:24:32.950 2642-2642/client.android D/MainActivity_: onCreate
07-23 13:24:32.951 2642-2642/client.android D/client.android.dao.service.Dao_: setTimeout thread=main, timeout=1000
07-23 13:24:32.952 2642-2642/client.android D/client.android.dao.service.Dao_: setBasicAuthentification thread=main, isBasicAuthentificationNeeded=false
07-23 13:24:33.041 2642-2642/client.android D/MainActivity_: adding loadingPanel
07-23 13:24:33.043 2642-2642/client.android D/MeteoFragment_: constructeur
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigation vers vue 0 sur action NONE
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: onCreateActivity
07-23 13:24:33.080 2642-2642/client.android D/MainActivity_: onResume
07-23 13:24:33.325 2642-2642/client.android D/MeteoFragment_: onActivityCreated
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: onCreateOptionsMenu
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: getMenuOptionsStates(Menu)
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: Nombre d'options de menu=4
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initFragment initView updateForFirstVisit
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initMenu
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: session={"action":"NONE","coreStates":[{"@type":"CoreState","hasBeenVisited":false,"menuOptionsState":null}],"previousTab":0,"previousView":0}
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: état précédent=null
07-23 13:24:33.558 2642-2642/client.android D/MeteoFragment_: notifyEndOfUpdates
07-23 13:24:39.766 2642-2642/client.android D/MeteoFragment_: beginWaiting
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=angers, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=le mans, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=nantes, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=laval, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=la roche sur yon, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:45.035 2642-2961/client.android D/client.android.dao.service.Dao_: response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200} sur thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-3]
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: initMenu
- الأسطر 32-36: يتم الحصول على الاستجابات jSON على مؤشرات ترابط الإدخال/الإخراج
- الأسطر 37-41: يسترد المقتطف الردود الخمسة من مؤشر ترابط واجهة المستخدم؛
الآن، نقوم بإجراء الاستعلام باستخدام معرف API غير صحيح:
String APIID = "";

وتكون السجلات كما يلي:
07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
- الأسطر 3-6، 10: تسببت المكالمات الخمس HTTP في 5 استثناءات؛
- السطر 7: يتلقى الجزء [MeteoFragment] الاستثناء الأول. وسيقوم عندئذ بإلغاء جميع المهام؛
الآن، لنضع وقت انتظار مدته 5 ثوانٍ [IMainActivity.DELAY] ونلغي العملية. عندئذٍ تكون السجلات كما يلي:
07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
- السطر 3: طلب الإلغاء؛
- السطر 4: تم إلغاء الانتظار لأن عملية إلغاء قد حدثت؛
- الأسطر 6-10: يؤدي إلغاء المهام إلى حدوث استثناء في كل خيط من خيوط المهام الخمس. ويعتمد نوع الاستثناء على التطبيقات. الاستثناء هنا هو [java.lang.InterruptedException] لأن المهام تم إيقافها أثناء تنفيذها للتعليمات [Thread.sleep(delay)] التي تجعلها تنتظر بشكل مصطنع لمدة [delay] ميلي ثانية؛
2.8.3. مثال-16B
نقوم هنا بإعادة هيكلة المثال 16 من الفقرة 1.17. يعرض هذا المثال جزءًا يقوم بإجراء مكالمات غير متزامنة إلى خادم أرقام عشوائية. لنرى كيف يتصرف أثناء دوران الجهاز:

- في [1]، يتم تدوير الجهاز مرتين؛

نلاحظ أننا فقدنا جميع رسائل الخطأ. سنحاول تحسين ذلك.
2.8.3.1. المشروع Example-16B
نقوم بنسخ المشروع [client-android-skel] إلى المشروع [exemples/Exemple-16B] ثم نقوم بتحميل المشروع الجديد:
![]() |
من المشروع الأولي [Exemple-16]، ننسخ العناصر التالية إلى [Exemple-16B]:
- الملف [res/layout/vue1.xml]، والمجلد [res/values]:
![]() |
سنقوم بتعديل الهامش العلوي للعرض [vue1.xml] إلى 80 نقطة:
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
- الجزء [Vue1Fragment]:
![]() |
- الفئة [dao / service / Response]:
![]() |
في هذه المرحلة، يمكننا محاولة إجراء التجميع الأول:
- النوع الأول من الأخطاء هو imports. فقد تغيرت حزم الفئات أثناء الترحيل إلى [Exemple-16B]. نبدأ بتصحيح هذا النوع من الأخطاء؛
- يتم الإبلاغ عن نوع ثانٍ من الأخطاء في الفئة [Vue1Fragment] لأنها لا تنفذ الطرق التي تفرضها الفئة الأم [AbstractParent]. نقوم بإنشاء هذه الطرق تلقائيًا؛
نحاول إجراء تجميع ثانٍ:
- جميع الأخطاء المتبقية تتركز الآن في الفئة [Vue1Fragment]، وهي الفئة التي ستخضع لأكبر عدد من التعديلات؛
2.8.3.2. إنشاء حالة للجزء [Vue1Fragment]
لقد رأينا أن بعض معلومات الجزء يجب حفظها أثناء الدوران من أجل استعادة الجزء كما كان قبل الدوران. لذلك نقوم بإنشاء حالة [Vue1FragmentState] فارغة في الوقت الحالي:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.3.3. تخصيص المشروع
![]() |
تسمح واجهة [IMainActivity] بتحديد بعض خصائص المشروع:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// الدخول إلى الجلسة
ISession getSession();
// تغيير العرض
void navigateToView(int position, ISession.Action action);
// إدارة الانتظار
void beginWaiting();
void cancelWaiting();
// ثوابت التطبيق -------------------------------------
// وضع التصحيح
boolean IS_DEBUG_ENABLED = true;
// الحد الأقصى لوقت انتظار استجابة الخادم
int TIMEOUT = 1000;
// فترة الانتظار قبل تنفيذ طلب العميل
int DELAY = 5000;
// المصادقة الأساسية
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// تجاور الأجزاء
int OFF_SCREEN_PAGE_LIMIT = 1;
// شريط علامات التبويب
boolean ARE_TABS_NEEDED = false;
// صورة الانتظار
boolean IS_WAITING_ICON_NEEDED = true;
// عدد أجزاء التطبيق
int FRAGMENTS_COUNT = 1;
}
- السطور 25 و28 و31 و40: خصائص الطبقة [DAO]. لا حاجة إلى مصادقة أساسية؛
- السطر 34: تجاور الأجزاء. هنا لا أهمية لهذه الثابتة نظرًا لوجود جزء واحد فقط؛
- السطر 37: هذا ليس تطبيقًا متعدد علامات التبويب؛
- السطر 43: لا يوجد سوى جزء واحد؛
ستكون الفئة [CoreState] التي تخزن حالة الأجزاء كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// ما إذا تمت زيارة الجزء أم لا
protected boolean hasBeenVisited = false;
// حالة القائمة المحتملة للجزء
protected MenuItemState[] menuOptionsState;
// أدوات الاسترجاع والتعيين
...
}
- السطر 12: نعلن فئة حالة الجزء [Vue1Fragment]؛
الفئة [Session] هي كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// العناصر التي لا يمكن تسلسلها في jSON يجب أن تحتوي على التعليق التوضيحي @JsonIgnore
}
وهي فارغة لأنه لا يوجد في هذا التطبيق اتصال بين الأجزاء.
2.8.3.4. الطبقة [DAO]
![]() |
في الطبقة [DAO]، يجب تخصيص ثلاث فئات:
- الواجهة IDao؛
- تنفيذها Dao؛
- واجهة WebClient للتواصل مع خادم الويب / jSON؛
تأتي الفئة [Response] من المشروع [Exemple-16] الذي يستخدمها:
package client.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- الخصائص
// حالة العملية
private int status;
// رسائل الخطأ المحتملة
private List<String> messages;
// نص الرد
private T body;
// المنشئات
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// مُستردات ومُعيّنات
...
}
ستكون واجهة [WebClient] كما يلي:
package client.android.dao.service;
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.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// رقم عشوائي واحد في النطاق [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- السطران 18-19: URL لخدمة الأرقام العشوائية. يُذكر أن هذه تتعلق بـ URL الجذر (RestClientRootUrl، السطر 12) للعميل. هنا سيكون هذا URL الجذر هو [http://localhost:8080]؛
وستكون واجهة [IDao] كما يلي:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// عنوان URL لخدمة الويب
void setUrlServiceWebJson(String url);
// المستخدم
void setUser(String user, String mdp);
// مهلة انتظار العميل
void setTimeout(int timeout);
// المصادقة الأساسية
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// وضع التصحيح
void setDebugMode(boolean isDebugEnabled);
// مهلة انتظار العميل بالمللي ثانية قبل الطلب
void setDelay(int delay);
// خدمة الأرقام العشوائية
Observable<Response<Integer>> getAlea(int a, int b);
}
- يُذكر أن الطرق الواردة في الأسطر 6-22 موجودة افتراضيًا في واجهة IDao للمشروع [client-android-skel]؛
- السطر 25: تسمح الطريقة [getAlea] بالحصول على رقم عشوائي في النطاق [a,b]. يتم الحصول على هذا الرقم في استجابة من النوع [Response<Integer>] حيث يوجد الرقم العشوائي في الحقل [body] من هذا النوع؛
يتم تنفيذ واجهة [IDao] بواسطة الفئة [Dao] التالية:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// عميل خدمة الويب
@RestService
protected WebClient webClient;
// الأمان
@Bean
protected MyAuthInterceptor authInterceptor;
// RestTemplate
private RestTemplate restTemplate;
// مصنع RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// سجل
Log.d(className, "afterInject");
// يتم تصنيع restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// يتم تثبيت المحول jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// يتم تعيين restTemplate لعميل الويب
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// يتم تعيين URL لخدمة الويب
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// يتم تسجيل المستخدم في المعترض
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// تكوين المصنع
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// معترض المصادقة؟
if (isBasicAuthentificationNeeded) {
// يتم إضافة معترض المصادقة
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// طرق خاصة -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// خدمة الأرقام العشوائية
@Override
public Observable<Response<Integer>> getAlea(final int a, final int b) {
// تشغيل عميل الويب
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getAlea(a, b);
}
});
}
}
- يُذكر أن الأسطر 17-85 موجودة بشكل افتراضي في الفئة [Dao] للمشروع [client-android-skel]. ما عليك سوى إضافة طرق تنفيذ الواجهة [IDao]؛
- الأسطر 88-97: تنفيذ طريقة [getAlea]. وهي طريقة بسيطة للغاية وتتكون من 6 أسطر، الأسطر 91-96؛
- السطر 91: الطريقة [getResponse] هي طريقة من الفئة الأم [AbstractDao]. وهي تتوقع معلمة من النوع [IRequest<T>] حيث T هو نوع الاستجابة المتوقعة، وهو هنا من النوع Response<Integer>. يجب أن يكون النوع T لـ [IRequest<T>] (السطر 91) هو النوع T للطريقة [Observable<T> getAlea] (السطر 89)؛
- تحتوي الواجهة [IRequest<T>] على طريقة واحدة فقط: getResponse. وتتمثل وظيفة هذه الطريقة في توفير الاستجابة من النوع T التي يجب أن ترجعها الطريقة [Observable<T> getAlea]؛
- السطر 94: واجهة [WebClient] هي التي توفر هذه الاستجابة. يتم تمرير المعلمتين المستلمتين في السطر 89 إليها. ولهذا السبب، يجب أن يكون لهما السمة final؛
2.8.3.5. النشاط [MainActivity]
![]() |
النشاط [MainActivity] هو التالي:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// الطبقة [DAO]
@Bean(Dao.class)
protected IDao dao;
// طرق الفئة الأصلية -----------------------
@Override
protected void onCreateActivity() {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// مواصلة عمليات التهيئة التي بدأتها الفئة الأصلية
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// تحديد الأجزاء هنا
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
// تحديد عناوين الأجزاء هنا
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// التنقل عبر علامات التبويب - تعريف العرض المطلوب
}
@Override
protected int getFirstView() {
return 0;
}
// واجهة IDao ------------------------------------------
@Override
public Observable<Response<Integer>> getAlea(int a, int b) {
return dao.getAlea(a, b);
}
}
- يُذكر أن الأسطر 15-61 موجودة بشكل افتراضي في المشروع [client-android-skel]. ما عليك سوى تخصيصها؛
- الأسطر 40-44: جدول الأجزاء. يوجد جزء واحد فقط هنا؛
- الأسطر 47-51: لا حاجة لعناوين للأجزاء؛
- الأسطر 53-56: لا توجد علامات تبويب هنا؛
- الأسطر 58-61: العرض الأول الذي يجب عرضه هو العرض رقم 0، وهو عرض [Vue1Fragment]؛
- الأسطر 64-67: تنفيذ واجهة [IDao]. هنا، لا يوجد شيء آخر يمكن فعله سوى تفويض المهمة إلى الطبقة [DAO] في السطر 23؛
2.8.3.6. حالة الجزء [Vue1Fragment]
![]() |
ستكون الفئة [Vue1FragmentState] كما يلي:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.ArrayList;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// حالة الجزء ------------------------
// قائمة الإجابات
private List<String> reponses = new ArrayList<>();
// حالة العرض ------------------------
// رسالة خطأ بشأن عدد الأرقام العشوائية المطلوبة
private boolean txtErrorAleasVisible = false;
// رسالة خطأ بشأن الفاصل الزمني [a,b] للتوليد
private boolean txtErrorIntervalleVisible = false;
// رسالة خطأ بشأن URL لخدمة الويب
private boolean txtMsgErreurUrlServiceWebVisible = false;
// رسالة خطأ بشأن مدة الانتظار
private boolean textViewErreurDelayVisible = false;
// حالة ظهور أو إخفاء زر "تنفيذ"
private boolean btnExecuterVisible = true;
// مُستقبلات ومُعيّنات
...
}
للحصول على ما يجب تخزينه في الجزء، قمنا بإجراء دورات على الجهاز في مواقف مختلفة وراقبنا ما اختفى عند الاستعادة. توصلنا إلى استنتاج مفاده أنه يجب تخزين المعلومات الموجودة في الأسطر 10-23.
2.8.3.7. الجزء [Vue1Fragment]
![]() |
تحتوي طريقة العرض [Vue1Fragment] حاليًا على أخطاء متنوعة بسبب تغيير الفئة الأصلية [AbstractFragment] التي تنحدر منها. بدلاً من وصف التغييرات المطلوبة واحدة تلو الأخرى، سنعلق مباشرةً على النسخة النهائية.
هيكل المقطع هو كما يلي:
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {
...
}
- السطر 26 نذكر أن كل جزء يجب أن يحتوي على قائمة، حتى لو كانت فارغة. وهذا هو الحال هنا.
2.8.3.7.1. إدارة النقر على الزر [Exécuter]
@Click(R.id.btn_Executer)
protected void doExecuter() {
// التحقق من البيانات المدخلة
if (!isPageValid()) {
return;
}
// مسح الإجابات السابقة
reponses.clear();
dataAdapterReponses.notifyDataSetChanged();
// إعادة تعيين عداد الردود إلى 0
nbReponses = 0;
infoReponses.setText("Liste des réponses (0)");
// تهيئة النشاط
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// يتم إعداد المهمة العشوائية
beginWaiting(1);
// طلب الأرقام العشوائية
getAleasInBackground(nbAleas, a, b);
}
void getAleasInBackground(int nbAleas, int a, int b) {
// يتم إنشاء العملية المراد ملاحظتها
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getAlea(a, b));
}
// طلب الأرقام العشوائية
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// استهلاك الإجابة
consumeAleaResponse(response);
}
});
}
protected void consumeAleaResponse(Response<Integer> response) {
// سجل
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// رد +
nbReponses++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
// تحليل الإجابة
// خطأ؟
if (response.getStatus() != 0) {
// عرض
showAlert(response.getMessages());
// إلغاء
doAnnuler();
// العودة إلى واجهة المستخدم
return;
}
// إضافة المعلومات إلى قائمة الردود
reponses.add(0, String.valueOf(response.getBody()));
// تحديث الإجابات
dataAdapterReponses.notifyDataSetChanged();
}
// إلغاء ----------
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// إلغاء المهام غير المتزامنة
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// يتم عرض الساعة الرملية
beginRunningTasks(nbRunningTasks);
// الزر [Annuler] يحل محل الزر [Exécuter]
btnExecuter.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
- الأسطر 4-6: يتم أولاً التحقق من صحة المدخلات. قد تظهر رسائل خطأ عندئذٍ؛
- السطور 8-9: يتم مسح قائمة الإجابات. يتم تطبيق هذا التغيير على ListView الذي يعرضها؛
- السطران 11-12: يتم تعيين عدد الردود المستلمة إلى صفر؛
- السطر 14: يتم تعيين URL لخدمة الأرقام العشوائية. سيتم نقل هذه المعلومات إلى الطبقة [DAO]؛
- السطر 15: يتم تحديد مهلة الانتظار قبل إرسال الطلب إلى خدمة الأرقام العشوائية. سيتم إرسال هذه المعلومات إلى الطبقة [DAO]؛
- السطر 17: الاستعداد لتشغيل مهمة غير متزامنة واحدة (وليس N، وسنرى السبب لاحقًا)؛
- الأسطر 24-27: من بين N مهام غير متزامنة، يتم تنفيذ كل منها كسلسلة من العمليات [merge]؛
- الأسطر 29-36: نطلب من الفئة الأم [AbstractParent] استعلام خدمة الويب / jSON عن أرقام عشوائية؛
- الأسطر 29-36: تتوقع الطريقة [executeInBackground] معلمتين:
- السطر 29: العملية المطلوب مراقبة وتنفيذها هي تلك التي تم حسابها في الأسطر السابقة؛
- الأسطر 29-36: المثيل [Action1] الذي يجب تنفيذه عند تلقي الرد من الخدمة غير المتزامنة. يجب أن يكون النوع T لـ [Action1<T>] هو النوع T لنتيجة الطريقة [getAlea]، أي نوع [Response<Integer>]؛
- السطر 34: عند وصول استجابة (رقم عشوائي)، يتم استهلاكها في الطريقة الموجودة في السطر 39؛
- السطران 49-50: يتم تسجيل وإشارة إلى استلام رد جديد؛
- السطور 53-60: النوع [Response<T>] يحتوي على حقل [status] وهو رمز خطأ. إذا كان هذا الرمز مختلفًا عن الصفر، فهذا يعني أن الخادم واجه مشكلة؛
- السطر 55: يتم عرض رسالة خطأ. تنتمي الطريقة [showAlert] إلى الفئة الأصلية؛
- السطر 57: يتم استدعاء الطريقة الموجودة في الأسطر 68-75. ستقوم بإلغاء المهام التي لا تزال نشطة (السطر 74)؛
- السطر 62: تتم إضافة الرد إلى قائمة الردود التي تمثل مصدر بيانات ListView؛
- السطر 64: يتم تحديث ListView؛
- الأسطر 77-83: تقوم الطريقة [beginWaiting(int nbRunningTasks)] بإعداد العرض للانتظار (الأسطر 81-82) وإبلاغ الفئة الأم بأن مهام [nbRunningTasks] ستُنفَّذ قريبًا (السطر 79)؛
2.8.3.7.2. دورة حياة الجزء
يتم ضمان دورة حياة الجزء من خلال الطرق التالية:
// البيانات المحلية
private List<String> reponses;
private ArrayAdapter<String> dataAdapterReponses;
private int nbReponses = 0;
...
// إدارة دورة الحياة ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// الحالة الحالية للعرض
Vue1FragmentState state = new Vue1FragmentState();
state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
state.setReponses(reponses);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// الزيارة الأولى؟
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
reponses = state.getReponses();
} else {
reponses = new ArrayList<>();
}
// مصدر بيانات listView
dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
// عدد الردود
nbReponses = reponses.size();
}
@Override
protected void initView(CoreState previousState) {
// رابط قائمة العرض / المحول
listReponses.setAdapter(dataAdapterReponses);
// زيارة أولى؟
if (previousState == null) {
// إخفاء رسائل الخطأ
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// الأزرار
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// الحالة السابقة للعرض
Vue1FragmentState state = (Vue1FragmentState) previousState;
// إظهار / إخفاء رسائل الخطأ
txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
// الأزرار
btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
// عدد الردود
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// الزر [Exécuter] يحل محل الزر [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
- السطور 7-18: تضمن حفظ الجزء عندما تطلب الفئة الأم ذلك؛
- السطر 11: ظهور رسالة خطأ بشأن مهلة الانتظار؛
- السطر 12: ظهور رسالة خطأ بشأن عدد الأرقام العشوائية المطلوبة؛
- السطر 13: ظهور رسالة الخطأ بشأن URL لخدمة الويب / jSON؛
- السطر 14: ظهور رسالة خطأ بشأن الفاصل الزمني [a,b] لتوليد الأرقام العشوائية؛
- السطر 15: ظهور الزر [Exécuter]؛
- السطر 16: قائمة الردود المستلمة؛
- الأسطر 20-23: يجب أن تعرض رقم العرض. رقم المقطع هنا هو 0 لأنه لا يوجد سوى مقطع واحد؛
- السطور 25-38: تهيئة حقول الجزء، إما في الزيارة الأولى (previousState==null)، أو في زيارة لاحقة؛
- السطور 29-30: إذا لم تكن هذه هي الزيارة الأولى، يتم استعادة الحقل [reponses] من الحالة السابقة للجزء؛
- السطور 31-33: إذا كانت هذه هي الزيارة الأولى، يتم تهيئة الحقل [reponses] بقائمة فارغة؛
- الأسطر 34-37: انطلاقًا من الحقل [reponses]، يمكن إنشاء مصدر بيانات ListView الخاص بالجزء (السطر 35) وكذلك عدد الردود (السطر 37)؛
- الأسطر 40-55: يتم تنفيذها لتهيئة العرض المرتبط بالجزء، إما عند الزيارة الأولى (previousState==null)، أو عند زيارة لاحقة؛
- السطر 43: يتم ربط ListView الخاص بالجزء بمصدر البيانات الذي تم إنشاؤه للتو في الطريقة [initFragment]؛
- الأسطر 45-54: إذا كانت هذه هي الزيارة الأولى، يتم إعداد العرض لعرضه لأول مرة؛
- الأسطر 57-60: يتم تنفيذها أثناء التنقل بين الأجزاء المرتبط بعمل من النوع [SUBMIT]. هنا، لا يوجد سوى جزء واحد وبالتالي لا يوجد تنقل بين الأجزاء؛
- الأسطر 63-76: يتم تنفيذها أثناء التنقل بين الأجزاء المرتبط بإجراء من النوع [NAVIGATION] أو أثناء دورة الحفظ/الاستعادة بسبب دوران الجهاز أو لأي سبب آخر. هنا، يمكن أن يحدث الحالة الأخيرة فقط. يجب أن نتذكر أنه هنا، في جميع الحالات، [previousState] لا يكون أبدًا null؛
- السطر 65: يتم تحويل الحالة السابقة إلى نوع حالة الجزء؛
- الأسطر 66-75: يتم استخدام محتوى الحالة السابقة لاستعادة العرض؛
- الأسطر 78-81: يتم استدعاؤها عند إتمام جميع التحديثات السابقة. هنا، لا يوجد ما يجب القيام به؛
- الأسطر 83-89: يتم تنفيذها عند انتهاء جميع المهام غير المتزامنة. هنا يتم إخفاء الزر [Annuler] لاستبداله بالزر [Exécuter]؛
2.8.3.8. الاختبارات
يُطلب من القارئ إجراء الاختبارات التالية:
- إنشاء أخطاء وتشغيل الجهاز: يجب الاحتفاظ برسائل الخطأ؛
- الحصول على أرقام عشوائية وتشغيل الجهاز: يجب أن تظل الأرقام العشوائية التي تم الحصول عليها معروضة؛
- وضع انتظار لعدة ثوانٍ وتشغيل الجهاز أثناء الانتظار: يجب أن تكون المهام قد ألغيت (يمكن رؤية ذلك في السجلات)؛
2.8.4. مثال-22B
نستأنف هنا المثال 22 لإعادة هيكلته وفقًا لنموذج المشروع [client-android-skel]. نذكر أن مشروع [Exemple-22] يدير بشكل صحيح دورة الحفظ/الاستعادة للأجزاء أثناء الدوران وأنه هو الذي استخدم كأساس لمشروع [client-android-skel].
نقوم بنسخ المشروع [client-android-skel] إلى [exemples/Exemple-22B] ونقوم بتحميل هذا المشروع الأخير:
![]() |
ثم ننسخ عناصر مختلفة من المشروع [Exemple-22] إلى المشروع [Exemple-22B].
أولاً، ننسخ عناصر من المجلد [res]:
- [layout/fragment_main.xml، layout/vue1.xml، menu/menu_fragment.xml، menu/menu_main.xml، المجلد [values]؛
![]() |
سنقوم بتعديل الهامش العلوي للطريقتين إلى 120 نقطة:
[vue1.xml]:
<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="120dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
[fragment_main]:
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>
ثم ننسخ العناصر [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
في هذه المرحلة، يمكننا محاولة إجراء التجميع الأول. يظهر النوع الأول من الأخطاء: أخطاء imports غير الصحيحة لأن الفئات قد تغيرت من الحزمة. نقوم بتصحيح هذه الأخطاء imports. هناك نوع ثانٍ من الأخطاء يرجع إلى أن الأجزاء لا تنفذ جميع طرق فئتها الأم [AbstractFragment]. نقوم بالتصحيح عن طريق (Alt+Enter).
الأخطاء المتبقية ناتجة عن الاختلافات الموجودة بين الفئة القديمة والجديدة [AbstractFragment]. في الوقت الحالي، يتم تجاهلها.
2.8.4.1. تخصيص المشروع
![]() |
يوجد في المجلد [custom] عناصر البنية التي يمكن للمطور تخصيصها.
تسمح واجهة [IMainActivity] بتحديد بعض خصائص المشروع:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// الدخول إلى الجلسة
ISession getSession();
// تغيير العرض
void navigateToView(int position, ISession.Action action);
// إدارة الانتظار
void beginWaiting();
void cancelWaiting();
// وضع التصحيح
boolean IS_DEBUG_ENABLED = true;
// الحد الأقصى لوقت انتظار استجابة الخادم
int TIMEOUT = 1000;
// فترة الانتظار قبل تنفيذ طلب العميل
int DELAY = 0;
// المصادقة الأساسية
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// تجاور الأجزاء
int OFF_SCREEN_PAGE_LIMIT = 1;
// شريط علامات التبويب
boolean ARE_TABS_NEEDED = true;
// صورة الانتظار
boolean IS_WAITING_ICON_NEEDED = false;
// عدد الأجزاء
int FRAGMENTS_COUNT = 5;
}
- السطور 23 و26 و29 و38: خصائص الطبقة [DAO]. لا توجد هنا؛
- السطر 41: يوجد هنا خمسة أجزاء؛
- السطر 32: تجاور الأجزاء. يمكن أن يكون لهذه الثابتة هنا قيمة في [1,4]. يُنصح القارئ بتغيير هذه القيمة لمعرفة ما إذا كان التطبيق سيستمر في العمل؛
- السطر 35: هذا تطبيق ذو علامات تبويب؛
ستكون الفئة [CoreState] التي تخزن حالة الأجزاء كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
// الشظية التي تمت زيارتها أو لم تتم زيارتها
protected boolean hasBeenVisited = false;
// حالة القائمة المحتملة للجزء
protected MenuItemState[] menuOptionsState;
// أدوات الاسترجاع والتعيين
...
}
- السطر 12: نعلن فئة حالة الجزء [PlaceHolderFragment]. أما الجزء [Vue1Fragment] فلا حالة له؛
الفئة [Session] هي كما يلي:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// البيانات المراد مشاركتها بين الأجزاء نفسها وبين الأجزاء والنشاط
// العناصر التي لا يمكن تسلسلها في jSON يجب أن تحتوي على التعليق التوضيحي @JsonIgnore
// لا تنسَ استخدام متغيرات الحصول (getters) والتعيين (setters) اللازمة للتسلسل/إلغاء التسلسل في jSON
// عدد الأجزاء التي تمت زيارتها
private int numVisit;
// رقم الجزء من النوع [PlaceholderFragment] المعروض في علامة التبويب الثانية
private int numFragment = -1;
// المُستردات والمُعيّنات
...
}
هذه هي جلسة مشروع [Exemple-22].
2.8.4.2. النشاط [MainActivity]
![]() |
النشاط [MainActivity] هو التالي:
package client.android.activity;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// طبقة [DAO]
@Bean(Dao.class)
protected IDao dao;
// الجلسة
private Session session;
// إدارة القائمة-----------------------
@Override
public boolean onOptionsItemSelected(MenuItem item) {
...
}
private void showFragment(int i) {
...
}
// تنفيذ طرق الفئة الأم ---------------------------------------------------
...
}
هنا، الفئة [MainActivity] أكثر أهمية من تلك الموجودة في الأمثلة السابقة لسببين:
- هناك علامات تبويب يجب إدارتها؛
- هناك قائمة يجب إدارتها؛
2.8.4.2.1. تنفيذ أساليب الفئة الأم
// طرق الفئة الأصلية -----------------------
@Override
protected void onCreateActivity() {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// نواصل عمليات التهيئة التي بدأتها الفئة الأصلية
// الجلسة
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// رقم الجزء
final String ARG_SECTION_NUMBER = "section_number";
// تهيئة جدول الأجزاء
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// يتم إنشاء جزء
fragments[i] = new PlaceholderFragment_();
// يمكن تمرير معلمات إلى الجزء
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// جزء من +
fragments[i] = new Vue1Fragment_();
// النتيجة
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
// لا توجد عناوين هنا
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
...
}
@Override
protected int getFirstView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
- الأسطر 2-12: يتم استدعاء الطريقة [onCreateActivity] بواسطة الفئة الأم [AbstractActivity] عند إنشاء النشاط لأول مرة أو إعادة إنشائه أثناء دورة الحفظ/الاستعادة. عند استدعاء هذه الطريقة، تكون الفئة الأم قد استعادت الجلسة بالفعل؛
- السطر 10: يتم استرداد مرجع محلي للجلسة. يعود تغيير النوع إلى حقيقة أن جلسة الفئة الأصلية هي من النوع [AbstractSession]؛
- الأسطر 19-38: يجب أن تعيد الطريقة [getFragments] إلى الفئة الأم المصفوفة الخاصة بالأجزاء التي تديرها التطبيق. يوجد هنا [FRAGMENTS_COUNT]، وهو عدد محدد في [IMainActivity]. الأجزاء الأولى من نوع [FRAGMENTS_COUNT-1] والأخيرة من نوع [Vue1Fragment]؛
- الأسطر 41-45: يجب أن تعرض الطريقة [getFragmentTitle] عناوين الأجزاء عندما تكون هذه المعلومات مفيدة. وهذا ليس هو الحال هنا؛
- الأسطر 47-50: يتم استدعاء هذه الطريقة من قبل الفئة الأم عندما ينقر المستخدم على علامة تبويب. سنعود إلى هذا الموضوع في الفقرة التالية؛
- الأسطر 52-55: تعرض رقم العرض الأول الذي يجب عرضه عند بدء تشغيل التطبيق. هنا، يجب عرض الجزء [Vue1Fragment] أولاً. يمكن استبدال الطريقة [getFirstView] بشكل مفيد بثابت في [IMainActivity]؛
2.8.4.2.2. إدارة علامات التبويب
تتم إدارة علامات التبويب من خلال الطرق التالية:
@Override
protected void onCreateActivity() {
// سجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// نواصل عمليات التهيئة التي بدأتها الفئة الأصلية
// جلسة
this.session = (Session) super.session;
// علامة التبويب الأولى
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// علامة التبويب الثانية؟
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
}
@Override
protected void navigateOnTabSelected(int position) {
// رقم الجزء المراد عرضه
int numFragment;
switch (position) {
case 0:
// رقم الجزء [Vue1Fragment]
numFragment = getFirstView();
break;
default:
// رقم الجزء [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// عرض الجزء
if (numFragment != mViewPager.getCurrentItem()) {
navigateToView(numFragment, ISession.Action.SUBMIT);
}
}
}
- السطور 1-20: يتم استدعاء الطريقة [onCreateActivity] بواسطة الفئة الأم [AbstractActivity] عند إنشاء النشاط لأول مرة أو إعادة إنشائه أثناء دورة الحفظ/الاستعادة. عند استدعاء هذه الطريقة، تكون الفئة الأم قد استعادت الجلسة بالفعل؛
- السطر 9: يتم استرداد مرجع محلي للجلسة. يعود تغيير النوع إلى حقيقة أن جلسة الفئة الأصلية هي من النوع [AbstractSession]؛
- الأسطر 11-13: يتم إنشاء علامة التبويب الأولى؛
- الأسطر 15-20: يتم إنشاء علامة التبويب الثانية إذا تم تسجيل رقم جزء في الجلسة (السطر 15). يكون هذا الرقم في البداية -1 عند إنشاء النشاط لأول مرة؛
- الأسطر 23-39: يتم استدعاء هذه الطريقة من قبل الفئة الأم عندما ينقر المستخدم على علامة تبويب؛
- الأسطر 28-31: إذا تم النقر على علامة التبويب 0، فيجب عرض [Vue1Fragment]. نعلم أن هذه هي العرض الأول الذي تم عرضه عند بدء تشغيل التطبيق؛
- الأسطر 32-35: إذا تم النقر على علامة التبويب 1، فيجب عرض الجزء الذي تم تسجيل رقمه في الجلسة؛
- الأسطر 37-39: ننتقل إلى الجزء المختار. الإجراء المرتبط هو [SUBMIT]. هل كان من الممكن أن يكون [NAVIGATION]؟ في هذا المستند، نستخدم [NAVIGATION] فقط عندما لا يتطلب عرض الجزء الجديد سوى معرفة حالته السابقة. هنا، ليس هذا هو الحال لأن عرض الجزء المعروض يجب أن يتغير مقارنة بحالته السابقة لعرض زيارة إضافية؛
2.8.4.2.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"/>
<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>
التي تعرض ما يلي:
![]() |
تتم إدارة القائمة من خلال الطرق التالية:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// السجل
if (IS_DEBUG_ENABLED) {
Log.d(className, "onOptionsItemSelected");
}
// معالجة خيارات القائمة
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d(className, "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// العنصر المعالج
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// لا توجد تنقلات عند اختيار علامة تبويب برمجية
session.setNavigationOnTabSelectionNeeded(false);
// يتم إعادة إنشاء علامتي التبويب بسبب مشكلة تتعلق بخط العناوين
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
// يتم تعيين رقم المقطع المراد عرضه
session.setNumFragment(i);
// يتم تحديد علامة التبويب رقم 2 مع التنقل
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- الأسطر 16-31: إدارة النقر على خيار قائمة من النوع [Fragmenti]؛
- الأسطر 37-50: تعرض الجزء رقم i (وهي أجزاء من النوع PlaceHolderFragment) في علامة التبويب رقم 1 (علامة التبويب الثانية)؛
- الأسطر 42-44: تقرر حذف علامات التبويب الموجودة لإعادة إنشاء علامتي تبويب جديدتين. تم اتخاذ هذا القرار لتجاوز المشكلة التالية: عندما نكتفي بعرض الجزء في علامة التبويب 1 الموجودة (دون حذفها)، فإن عنوانها يبدو بشكل غريب (خط، حجم) مختلفًا عن عنوان علامة التبويب 0؛
- السطور 43-44: يتم إنشاء علامتي التبويب ولكن لا يتم تحديدهما (المعلمة الأخيرة في false)؛
- السطر 40: من المحتمل أن تؤدي العمليات في الأسطر 42-44 إلى إجراء عمليات [select] على علامات التبويب، مما سيؤدي إلى استدعاء المدير [onTabSelected]. إذا لم يتم اتخاذ أي إجراء، فسيتم الانتقال إلى جزء. يمكن تجنب ذلك عن طريق تعيين القيمة المنطقية [navigationOnTabSelectionNeeded] إلى faux في الجلسة. يتم إعادة تعيين هذه القيمة المنطقية تلقائيًا إلى vrai بواسطة الفئة [AbstractFragment] عندما يصبح جزء ما مرئيًا؛
- السطر 46: يتم تسجيل رقم الجزء المراد عرضه في الجلسة؛
- الأسطر 48-50: يتم تحديد علامة التبويب رقم 2 مع التنقل (السطر 48). سيؤدي ذلك إلى تشغيل الإجراء [onTabSelected] الذي سيقوم بما يلي:
- يعرض الجزء الذي تم إدخال رقمه في الجلسة؛
- تخزين رقم علامة التبويب المحددة في الجلسة؛
2.8.4.3. الجزء [Vue1Fragment]
نقدم هنا النسخة النهائية للمقتطف:
package client.android.fragments.behavior;
import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
// عناصر الواجهة المرئية
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// مدير الأحداث
@Click(R.id.buttonValider)
protected void doValider() {
// يتم عرض الاسم الذي تم إدخاله
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// دورة حياة الجزء -----------------------------------------------
private void initFragment() {
// لا شيء للقيام به
}
// حفظ حالة الجزء
@Override
public CoreState saveFragment() {
// حالة العرض - لا يوجد ما يتم حفظه
return new CoreState();
}
@Override
protected int getNumView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// لا شيء للقيام به
}
@Override
protected void initView(CoreState previousState) {
// الزيارة الأولى؟
if (previousState == null) {
// يتم عرض رقم الزيارة
showNumVisit();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// يتم عرض رقم الزيارة
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// طرق خاصة -------------------------------------
// عرض رقم الزيارة
private void showNumVisit() {
// زيادة رقم الزيارة
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// يتم عرض رقم الزيارة
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
الفئة شبه فارغة.
- الأسطر 35-39: يتم استدعاؤها من قبل الفئة الأم عندما يتعين على المقطع حفظ حالته. لا يوجد حالة يجب حفظها في المقطع [Vue1Fragment]. نُرجع ببساطة مثيلًا من الفئة الأساسية [CoreState] (تذكير: لا يجب إرجاع null)؛
- الأسطر 41-44: يجب أن تعرض رقم الجزء. الجزء [Vue1Fragment] له رقم [FRAGMENTS_COUNT-1] بحكم تصميمه؛
- الأسطر 51-59: يتم استدعاؤها من قبل الفئة الأم عند إنشاء المقطع لأول مرة (previousState==null) أو في المرات التالية (previousState!=null)؛
- الأسطر 54-57: إذا كانت هذه هي الزيارة الأولى، يتم زيادة رقم الزيارة وعرضه (الأسطر 85-92)؛
- الأسطر 61-65: يتم استدعاؤها عندما يتم عرض الجزء المرتبط بإجراء [SUBMIT]. يتم زيادة رقم الزيارة وعرضه. هنا، لا يمكن زيادة رقم الزيارة مرتين خلال دورة الحياة. في الواقع، تتم الزيارة الأولى للجزء [Vue1Fragment] عند بدء تشغيل التطبيق عندما تكون قيمة الإجراء هي [NONE] حسب التصميم في الجلسة. وهذا يضمن عدم استدعاء الطريقة [updateOnSubmit]. بعد ذلك، لن تكون هذه هي الزيارة الأولى أبدًا ولن تقوم الطريقة [initView] بأي شيء؛
- الأسطر 68-71: يتم استدعاؤها في دورة النسخ الاحتياطي/الاستعادة. ونظرًا لأن المقتطف ليس له حالة، فلا يوجد هنا ما يستحق الاستعادة؛
- الأسطر 73-76: يتم استدعاؤها عند إتمام جميع التحديثات السابقة. هنا، لا يوجد شيء آخر للقيام به؛
- الأسطر 78-81: يتم استدعاؤها عند انتهاء جميع المهام غير المتزامنة التي تم تشغيلها. هنا، لا توجد مهام غير متزامنة؛
2.8.4.4. الحالة [PlaceHolderFragmentState]
ستكون حالة الجزء [PlaceHolderFragment] كما يلي:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class PlaceHolderFragmentState extends CoreState {
// نص
private String text;
// المنشئون
public PlaceHolderFragmentState() {
}
public PlaceHolderFragmentState(String text) {
super();
this.text = text;
}
// مُستردات ومُعيّنات
...
}
- عندما يتعين حفظ حالة الجزء، سيتم حفظ النص الذي كان يعرضه (السطر 7)؛
2.8.4.5. الجزء [PlaceHolderFragment]
سيكون المقطع [PlaceHolderFragment] كما يلي:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
// مكونات الواجهة المرئية
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// البيانات
private String text;
// رقم الجزء
private static final String ARG_SECTION_NUMBER = "section_number";
// تنفيذ طرق الفئة الأم ----------------------------
@Override
public CoreState saveFragment() {
// يتم حفظ حالة الجزء
PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
placeHolderFragmentState.setText(textViewInfo.getText().toString());
return placeHolderFragmentState;
}
@Override
protected int getNumView() {
return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// النص الأصلي
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// يتم تحديث النص المعروض
// زيادة رقم الزيارة
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// النص المعدل
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
// سجل
if (isDebugEnabled) {
Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
}
}
@Override
protected void updateOnRestore(CoreState previousState) {
// استعادة النص المعروض
PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
textViewInfo.setText(state.getText());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
}
- الأسطر 30-36: عندما تطلب الفئة الأم من المقطع حفظ حالته، يتم حفظ النص المعروض بواسطة المقطع (السطر 34)؛
- الأسطر 38-41: تعرض رقم الجزء. يعتمد هذا الرقم على رقم القسم الذي تم تمريره كحجة عند إنشائه؛
- الأسطر 43-47: يتم استدعاؤها عند إنشاء الجزء لأول مرة (previousState==null) أو في المرات التالية (previousState !=null)؛
- السطر 46: هنا، لا يتم الاستفادة من الحالة السابقة. يتم إعادة حساب النص الأصلي [text] (السطر 24) الذي يتم عرضه عند الزيارة الأولى في كل مرة. وهذا أمر قابل للنقاش. كان من الممكن اختيار إدراج هذه المعلومات أيضًا في حالة المقتطف؛
- الأسطر 49-51: يتم استدعاؤها عند الإنشاء الأول للطريقة المرتبطة بالجزء (previousState==null) أو عند الإنشاءات التالية (previousState!=null). لا يوجد ما يمكن فعله؛
- الأسطر 53-56: يتم استدعاؤها عندما يتم عرض المقتطف مرتبطًا بإجراء [SUBMIT]. هذا هو الحال دائمًا باستثناء دورة الحفظ/الاستعادة حيث يكون الإجراء هو [RESTORE]. لذلك نقوم بزيادة رقم الزيارة وعرضه؛
- الأسطر 68-74: يتم استدعاؤها في دورة الحفظ/الاستعادة. يتم استعادة النص الذي تم حفظه في حالة المقطع؛
- الأسطر 76-79: يتم استدعاؤها عند إتمام جميع التحديثات السابقة. هنا، لا يوجد شيء آخر للقيام به؛
- السطور 82-83: يتم استدعاؤها عند انتهاء جميع المهام غير المتزامنة التي تم تشغيلها. هنا، لا توجد مهام غير متزامنة؛
2.8.4.6. Tests
يُطلب من القارئ اختبار التطبيق عن طريق تدوير الجهاز للتحقق من أن الجزء المعروض لا يفقد حالته. سنلقي نظرة أيضًا على السجلات.
2.9. Conclusion
في نهاية هذا الفصل، لدينا مشروع نموذجي [client-android-skel] لعميل Android يتواصل مع خدمة ويب / jSON بالخصائص التالية:
- يتم الاتصال غير المتزامن مع خادم الويب / jSON باستخدام مكتبة RxJava؛
- يتم إدارة دورة حياة الجزء (التحديث، الحفظ، الاستعادة) بواسطة فئته الأم [AbstractFragment] التي تستدعي في أوقات محددة بعض أساليب فئاته الفرعية. وبالتالي، لا يتعين على الجزء الفرعي الاهتمام بمراحل دورة الحياة، بل فقط بتنفيذ بعض الأساليب التي تفرضها فئته الأم؛
- يتم إدارة دورة حياة النشاط (حفظ / استعادة) بواسطة فئة مجردة [AbstractActivity] التي تفرض هي الأخرى على النشاط الفرعي تنفيذ طرق معينة؛
- الفئة [AbstractActivity] قادرة على إدارة تطبيق مع أو بدون علامات تبويب، مع أو بدون صورة انتظار، مع أو بدون مصادقة أساسية لدى خادم الويب / jSON. ويتم تحديد وجود أو عدم وجود هذه العناصر من خلال التكوين؛
سنقدم الآن دراسة حالة أكثر تعقيدًا من الأمثلة السابقة. سيعتمد التطبيق الجديد على المشروع النموذجي [client-android-skel].



















































