9. RxJava في بيئة Android
9.1. Introduction
سنستعرض هنا تطبيقًا سبق تناوله في عدة وثائق:
- [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (الفصل 4)؛
- [Introduction à la programmation de tablettes Android par l'exemple] (الفصل 9)؛
- [Introduction à la programmation de tablettes Android par l'exemple - version 2] (الفقرة 1.11)؛
يتناول هذا التطبيق نظام عميل/خادم حيث يقوم الخادم بتوفير أرقام عشوائية بشكل غير متزامن يعرضها عميل Android:
- في الوثيقة 1، يستخدم العميل الذي يعمل بنظام أندرويد تقنية غير قياسية؛
- في الوثيقة 2، يستخدم العميل الذي يعمل بنظام أندرويد تقنية أندرويد القياسية للعمليات غير المتزامنة؛
- في الوثيقة 3، يستخدم العميل الذي يعمل بنظام أندرويد نفس التقنية المستخدمة في الوثيقة 2، ولكنها مبسطة باستخدام تعليقات مكتبة Android Annotations؛
العميل الذي يعمل بنظام أندرويد هو التالي:
![]() |
تتواصل الطبقة [DAO] مع الخادم الذي يولد الأرقام العشوائية التي تعرضها اللوحة اللوحية التي تعمل بنظام أندرويد. ويتميز هذا الخادم ببنية من طبقتين على النحو التالي:
![]() |
تستعلم العملاء عن بعض URL من الطبقة [web / JSON] وتتلقى ردًا نصيًا بتنسيق JSON (JavaScript Object Notation).
سنقسم دراسة التطبيق إلى مرحلتين:
خادم الويب / jSON
- طبقته [métier]؛
- خدمته [web / JSON] المُنفَّذة باستخدام Spring MVC؛
عميل Android
- طبقته [DAO]؛
- نشاطه؛
- طرق العرض الخاصة به؛
9.2. خدمة الويب / jSON
ملاحظة: يتم تنفيذ خدمة الويب / jSON بواسطة تقنية Spring MVC. يمكن للقارئ الذي لا يعرف هذه التقنية إما:
- يكتفي بقراءة الفقرة 9.2.1 التي تشرح كيفية تشغيل الخادم وكيفية الاستعلام عنه؛
- الرجوع إلى الوثيقة [Spring MVC et Thymeleaf par l'exemple]، ولا سيما الفصل 4 الذي يعرض التعليقات التوضيحية الرئيسية المستخدمة في الكود؛
9.2.1. مشروع IntelliJ IDEA
تتميز خدمة الويب / jSON بالبنية التالية:
![]() |
يتم تنفيذ هذه البنية من خلال مشروع IntelliJ IDEA التالي [1]:
![]() | ![]() |
يتم تشغيل الخادم بواسطة [2-3]. وعندها يتم عرض سجلات على وحدة التحكم:
- السطر 12: يشير إلى أن الخدمة متاحة على المنفذ 8080؛
- السطر 10: الرمز الفريد URL لخدمة الويب / jSON المتاحة عبر العملية HTTP GET. معلماتها هي كما يلي:
- [a,b]: نطاق توليد الأرقام العشوائية؛
- [minCount, maxCount]: يتم توليد count أرقام عشوائية، حيث count هو رقم عشوائي يقع في النطاق [minCount, maxCount]؛
- [minDelay, maxDelay]: تنتظر الخدمة delay مللي ثانية قبل إرجاع الأرقام المطلوبة، حيث delay هو رقم عشوائي في [minDelay, maxDelay]؛
في متصفح الويب، لنطلب هذا URL:
![]() |
لقد طلبنا:
- أرقام عشوائية في النطاق [100, 200]؛
- أرقام عشوائية في النطاق [10, 20] مع n؛
- فترة انتظار تبلغ x مللي ثانية مع x في النطاق [300, 400]؛
في الرد:
- aleas: قائمة الأرقام العشوائية التي تم توليدها؛
- delay: وقت الانتظار بالمللي ثانية الذي حدده الخادم؛
- erreur: رمز خطأ - 0 في حالة عدم وجود خطأ؛
- message: رسالة خطأ - null في حالة عدم وجود خطأ؛
9.2.2. تبعيات Gradle للمشروع
![]() |
المشروع [serveur] هو مشروع Gradle تم تكوينه بواسطة الملف [build.gradle] [1] التالي:
// تم إنشاؤه بواسطة http://start.spring.io/ (مايو 2016)
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'spring-boot'
jar {
baseName = 'serveur'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
}
- السطر 1: تعليق يوضح كيفية إنشاء ملف التكوين هذا؛
- السطران 4 و10: تبعية لإطار العمل [Spring Boot]، وهو فرع من نظام Spring. يتيح إطار العمل [http://projects.spring.io/spring-boot/] تكوينًا بسيطًا لـ Spring. استنادًا إلى الملفات الموجودة في مسار Classpath للمشروع، يستنتج [Spring Boot] تكوينًا معقولًا أو محتملًا للمشروع. وبالتالي، إذا كان Hibernate موجودًا في مسار الفئات (Classpath) للمشروع، فإن [Spring Boot] سيستنتج أن التنفيذ JPA المستخدم سيكون Hibernate وسيقوم بتكوين Spring وفقًا لذلك. لم يعد على المطور القيام بذلك. ولم يتبق له سوى إجراء التكوينات التي لم يقم بها [Spring Boot] افتراضيًا، أو تلك التي قام بها [Spring Boot] افتراضيًا ولكن يجب تحديدها بشكل دقيق. وفي جميع الأحوال، فإن التكوين الذي يقوم به المطور هو الذي له الكلمة الأخيرة؛
- السطران 14-15: مكونان إضافيان لـ Gradle ضروريان لاستخدام محتوى ملف Gradle هذا؛
- الأسطر 17-20: تحدد خصائص الأرشيف الذي تم إنشاؤه لهذا المشروع؛
- السطران 22-23: لضمان التوافق مع Java 8؛
- الأسطر 25-27: سيتم البحث عن التبعيات في مستودع Maven العام أو في المستودع المحلي على الجهاز؛
- السطر 30: يحدد تبعية للأرتيفاكت [spring-boot-starter-web]. يجلب هذا الأرتيفاكت معه جميع الأرشيفات اللازمة لمشروع Spring MVC. ومن بينها أرشيف خادم Tomcat. وهو الذي سيُستخدم لنشر تطبيق الويب. تجدر الإشارة إلى أن إصدار التبعية لم يُذكر. وسيتم استخدام الإصدار المذكور في المشروع المستورد [spring-boot]؛
لتحديث المشروع، يجب فرض تنزيل التبعيات [1-3]:
![]() | ![]() ![]() |
لنلقِ نظرة على [4]، وهي التبعيات التي يجلبها ملف [build.gradle]:
![]() |
وهي كثيرة جدًا. لقد قام Spring Boot للويب بتضمين التبعيات التي من المحتمل أن تحتاجها أي تطبيق ويب Spring MVC. وهذا يعني أن بعضها قد يكون غير ضروري. Spring Boot مثالي للدرس التعليمي:
- فهو يوفر التبعيات التي سنحتاجها على الأرجح؛
- وسنرى أنه يبسط تكوين مشروع Spring MVC بشكل كبير؛
- يحتوي على خادم Tomcat مدمج [1]، مما يوفر علينا عناء نشر التطبيق على خادم ويب خارجي؛
- ويسمح بإنشاء ملف jar قابل للتنفيذ يتضمن جميع التبعيات المذكورة أعلاه. ويمكن نقل ملف jar هذا من منصة إلى أخرى دون الحاجة إلى إعادة التهيئة.
يمكن العثور على العديد من الأمثلة التي تستخدم Spring Boot على موقع نظام Spring البيئي [http://spring.io/guides]. والآن بعد أن تعرفنا على تبعيات المشروع، يمكننا الانتقال إلى الكود.
9.2.3. الطبقة [métier]
![]() |
![]() |
ستحتوي الطبقة [métier] على الواجهة [IMetier] التالية:
package dvp.rxjava.server.metier;
public interface IMetier {
// أرقام عشوائية في الفترة [a,b]
// يتم إنشاء n عددًا من الأرقام، حيث n نفسه رقم عشوائي في النطاق [minCount, maxCount]
// يتم توليد الأعداد بعد انتظار مدته delay مللي ثانية،
// حيث [delay] هو نفسه عدد عشوائي في النطاق [minDelay, maxDelay]
public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}
هذه الواجهة مطابقة تقريبًا لتلك التي تمت دراستها في بيئة Swing في الفقرة 8.4. في السطر 8، تُرجع الطريقة [getAleas] النوع [AleasMetier] التالي:
package dvp.rxjava.server.metier;
import java.util.List;
public class AleasMetier {
// الحقول
private int delay;
private List<Integer> aleas;
// المنشئات
public AleasMetier(){
}
public AleasMetier(int delay, List<Integer> aleas){
this.delay=delay;
this.aleas=aleas;
}
public AleasMetier(AleasMetier aleasMetier){
this.delay=aleasMetier.delay;
this.aleas=aleasMetier.aleas;
}
// دالات الاسترجاع والتعيين
...
}
فيما يلي كود الفئة [Metier] التي تنفذ الواجهة [IMetier]:
package dvp.rxjava.server.metier;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class Metier implements IMetier {
@Autowired
private ObjectMapper mapper;
@Override
public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
// أرقام عشوائية في النطاق [a,b]
// يتم توليد n عددًا، حيث n نفسه عدد عشوائي في النطاق [minCount, maxCount]
// يتم توليد الأعداد بعد انتظار مدته delay مللي ثانية،
// حيث [delay] هو نفسه عدد عشوائي في النطاق [minDelay, maxDelay]
// بعض عمليات التحقق
List<String> messages = new ArrayList<>();
int erreur = 0;
if (a < 0) {
messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
erreur |= 2;
}
if (a >= b) {
messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
erreur |= 4;
}
if (minCount < 0) {
messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
erreur |= 16;
}
if (minCount > maxCount) {
messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
erreur |= 32;
}
if (minDelay < 0) {
messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
erreur |= 64;
}
if (minCount > maxCount) {
messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
erreur |= 128;
}
if (maxDelay > 5000) {
messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
erreur |= 256;
}
// أخطاء؟
if (!messages.isEmpty()) {
throw new AleasException(String.join(" [---] ", messages), erreur);
}
// مولد الأرقام العشوائية
Random random = new Random();
// في انتظار؟
int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
if (delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
String message = null;
try {
message = mapper.writeValueAsString(Arrays.asList(String.format("[%s : %s]", e.getClass().getName(), e.getMessage())));
} catch (JsonProcessingException e1) {
throw new AleasException(e1,512);
}
throw new AleasException(message, 1024);
}
}
// توليد النتيجة
int count = minCount + random.nextInt(maxCount - minCount + 1);
List<Integer> nombres = new ArrayList<Integer>();
for (int i = 0; i < count; i++) {
nombres.add(a + random.nextInt(b - a + 1));
}
// إرجاع النتيجة
return new AleasMetier(delay,nombres);
}
}
لن نعلق على هذه الفئة: فهي مشابهة لتلك التي تمت مناقشتها في بيئة Swing في الفقرة 8.4. سنكتفي بالإشارة إلى النقاط التالية:
- السطر 10: تعليق Spring [@Service] الذي سيجعل Spring يقوم بإنشاء مثيل واحد فقط من الفئة (singleton) وإتاحة مرجعها لمكونات Spring الأخرى. كان من الممكن استخدام تعليقات Spring أخرى هنا لتحقيق نفس الغرض؛
- السطران 13-14: يتم حقن مُخَطِّط jSON. Spring عبارة عن حاوية كائنات. يتم إنشاء مثيل لهذه الحاوية عند بدء تشغيل التطبيق الويب، ومن ثم يتم إنشاء مثيلات للكائنات المُعرَّفة في ملف التكوين، بشكل افتراضي بنسخة واحدة (singleton). يمكن أن يحتوي كائن Spring من نوع «singleton» على مراجع إلى كائنات Spring أخرى. وهذا هو الحال هنا: سيحتوي الكائن «singleton» [metier] (السطران 10-11) على مرجع إلى الكائن «singleton» [mapper] (السطران 13-14). ويُطلق على ذلك اسم «حقن التبعية». هناك طريقتان لحقن كائن سينجلتون في كائن سينجلتون آخر:
- حسب النوع: وهذا ممكن إذا كان الكائن الفردي المراد حقنه هو الكائن الوحيد في Spring الذي ينتمي إلى هذا النوع. وهذا هو الحال هنا بالنسبة للحقن في السطور 13-14 (النوع ObjectMapper)؛
- عن طريق الاسم إذا كان هناك عدة كائنات Spring من نفس النوع. في هذه الحالة، يجب إضافة التعليق التوضيحي @Qualifier(« nomDuSingleton ») لتحديد اسم الكائن الفردي؛
تُطلق الفئة [Metier] استثناءات من النوع [AleaException]:
package android.exemples.server.metier;
public class AleaException extends RuntimeException {
// رمز الخطأ
private int code;
// منشئات
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// مُستردات وقيم
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- السطر 3: [AleasException] تمتد من الفئة [RuntimeException]. لذا فهي استثناء غير خاضع للرقابة (ليس هناك ضرورة للتعامل معها باستخدام try / catch)؛
- السطر 6: تمت إضافة رمز خطأ إلى الفئة [RuntimeException]؛
9.2.4. خدمة الويب / JSON
![]() |
![]() |
يتم تنفيذ خدمة الويب / JSON بواسطة Spring MVC. يقوم Spring MVC بتنفيذ نموذج الهندسة المعمارية المعروف باسم MVC (النموذج – العرض – وحدة التحكم) بالطريقة التالية:
![]() |
تتم معالجة طلب العميل على النحو التالي:
- الطلب - تكون URL المطلوبة على شكل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] هي فئة Spring التي تعالج URL الواردة. وهي «توجه» URL إلى الإجراء الذي يجب أن يعالجها. هذه الإجراءات هي طرق لفئات معينة تسمى [Contrôleurs]. الحرف C في MVC هو هنا السلسلة [Dispatcher Servlet, Contrôleur, Action]. إذا لم يتم تكوين أي إجراء لمعالجة URL الواردة، فسترد السيرفلت [Dispatcher Servlet] بأن URL المطلوبة لم يتم العثور عليها (خطأ 404 NOT FOUND)؛
- المعالجة
- يمكن للإجراء المختار الاستفادة من المعلمات parami التي أرسلتها إليه خدمة [Dispatcher Servlet]. وقد تأتي هذه المعلمات من عدة مصادر:
- مسار [/param1/param2/...] الخاص بـ URL،
- من المعلمات [p1=v1&p2=v2] الخاصة بـ URL,
- من المعلمات التي يرسلها المتصفح مع طلبه؛
- أثناء معالجة طلب المستخدم، قد تحتاج العملية إلى الطبقة [metier] [2b]. بمجرد معالجة طلب العميل، قد يؤدي ذلك إلى استدعاء استجابات متنوعة. ومن الأمثلة النموذجية على ذلك:
- صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
- صفحة تأكيد في الحالات الأخرى
- تطلب الإجراء عرض طريقة عرض معينة [3]. ستعرض طريقة العرض هذه البيانات التي نسميها نموذج طريقة العرض. وهذا هو الحرف M في MVC. ستقوم الإجراء بإنشاء هذا النموذج M [2c] وستطلب عرض طريقة عرض V [3]؛
- الاستجابة - تستخدم طريقة العرض V المختارة النموذج M الذي أنشأته الإجراء لتهيئة الأجزاء الديناميكية من الاستجابة HTML التي يجب أن ترسلها إلى العميل، ثم ترسل هذه الاستجابة.
بالنسبة لخدمة ويب / JSON، يتم تعديل البنية السابقة بشكل طفيف:
![]() |
- في [4a]، يتم تحويل النموذج — وهو فئة Java — إلى سلسلة JSON بواسطة مكتبة JSON؛
- في [4b]، يتم إرسال هذه السلسلة JSON إلى المتصفح؛
لنعد إلى طبقة [web] في تطبيقنا:
![]() |
في تطبيقنا، يوجد عنصر تحكم واحد فقط:
![]() |
سترسل خدمة الويب / JSON إلى عملائها استجابة من النوع [AleasResponse] على النحو التالي:
package dvp.rxjava.server.web;
import dvp.rxjava.server.metier.AleasMetier;
public class AleasResponse extends AleasMetier {
// رمز الخطأ
private int erreur;
// رسالة الخطأ
private String message;
// منشئات
public AleasResponse() {
}
public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
super(aleasMetier);
this.erreur = erreur;
this.message = message;
}
// دالات الاسترجاع والتعيين
public void setAleasMetier(AleasMetier aleasMetier) {
this.setDelay(aleasMetier.getDelay());
this.setAleas(aleasMetier.getAleas());
}
...
}
- السطر 5: الفئة [AleasResponse] تمتد من الفئة [AleasMetier]، وبالتالي ترث جميع سماتها (aleas، delay)؛
- السطر 8: رمز خطأ (0 في حالة عدم وجود خطأ)؛
- السطر 10: إذا كانت erreur!=0، فستظهر رسالة خطأ، أما إذا لم يكن هناك خطأ، فستكون null؛
وحدة التحكم [AleasController] هي كما يلي:
package dvp.rxjava.server.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.rxjava.server.metier.AleasException;
import dvp.rxjava.server.metier.IMetier;
@Controller
public class AleasController {
// طبقة الأعمال
@Autowired
private IMetier metier;
@Autowired
private ObjectMapper mapper;
// أرقام عشوائية في [a,b]
// يتم توليد n أرقام حيث n تقع في النطاق [minCount, maxCount]
// يتم توليد الأعداد بعد انتظار مدته delay مللي ثانية،
// حيث [delay] هو عدد عشوائي في النطاق [minDelay, maxDelay]
@RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
@PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
@PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {
// يتم إعداد الرد
AleasResponse response = new AleasResponse();
// يتم استخدام طبقة الأعمال لتوليد الأرقام العشوائية
try {
response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
} catch (AleasException e) {
// حالة خطأ (الرمز والرسالة)
response.setErreur(e.getCode());
response.setMessage(e.getMessage());
}
// إرجاع الرد jSON
return mapper.writeValueAsString(response);
}
}
- السطر 16: التعليق التوضيحي [@Controller] يجعل الفئة [AleasController] فئة فردية في Spring. كما يشير إلى أن الفئة تحتوي على طرق ستعالج الطلبات لبعض URL في تطبيق الويب. وهنا، لا يوجد سوى واحدة في السطر 29؛
- السطران 20-21: تطلب العلامة التوضيحية [@Autowired] من Spring حقن مكون من النوع [IMetier] في الحقل. وستكون هذه هي الفئة [Metier] السابقة. وذلك لأننا أضفنا إليها التعليق التوضيحي [@Service]، مما يجعلها تُدار كمكون Spring؛
- السطران 22-23: يطلب التعليق التوضيحي [@Autowired] من Spring حقن مكون من النوع [ObjectMapper] في الحقل. سنقوم بتعريف هذا المكون لاحقًا؛
- السطر 31: تُنشئ الطريقة [getAleas] الأرقام العشوائية. ولا يهم اسمها. وعند تنفيذها، تكون المعلمات الواردة في الأسطر 31-33 قد تم تهيئتها بواسطة Spring MVC. سنرى كيف. علاوة على ذلك، فإن سبب تنفيذها هو أن خادم الويب تلقى طلبًا HTTP GET لـ URL في السطر 29 (السمة method)؛
- السطر 30: تشير التعليقات التوضيحية [@ResponseBody] إلى أن نتيجة الأسلوب يجب إرسالها كما هي إلى العميل. هنا، سنرسل إليه سلسلة أحرف ستكون السلسلة jSON من النوع [AleasResponse]؛
- السطر 29: السلسلة URL التي تمت معالجتها هي على الشكل /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay} حيث يمثل {x} متغيرًا. يتم تعيين هذه المتغيرات المختلفة إلى معلمات الأسلوب في السطرين 32 و33. ويتم ذلك عبر التعليق التوضيحي @PathVariable("x"). تجدر الإشارة إلى أن قيم {x} هي مكونات لـ URL، وبالتالي فهي من النوع String. قد يفشل تحويل String إلى نوع معلمات الدالة. وعندئذٍ يطلق Spring MVC استثناءً. لنلخص: إذا طلبتُ عبر متصفح URL /100/200/10/20/300/400، فستُنفَّذ الطريقة getAleas في السطر 31 بالمعلمات a=100 (السطر 31)، b=200 (السطر 31)، minCount=10 (السطر 31)، maxCount=20 (السطر 32)، minDelay=300 (السطر 32)، maxDelay=400 (السطر 33)؛
- السطر 39: يُطلب من الطبقة [métier] قائمة بالأرقام العشوائية. تجدر الإشارة إلى أن الطريقة [metier].getAleas قد تطلق استثناءً؛
- السطران 42-43: حالة الخطأ؛
- السطر 46: يتم إرجاع الاستجابة من النوع [AleasResponse] في شكل سلسلة jSON؛
9.2.5. تكوين مشروع Spring
![]() |
هناك طرق متنوعة لتكوين Spring:
- باستخدام ملفات XML؛
- باستخدام كود Java؛
- باستخدام مزيج من الاثنين؛
نختار تكوين تطبيق الويب الخاص بنا باستخدام كود Java. وتقوم الفئة [Config] المذكورة أعلاه بتنفيذ هذا التكوين:
package dvp.rxjava.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(basePackages = { "dvp.rxjava.server.metier", "dvp.rxjava.server.web" })
@EnableWebMvc
public class Config {
// -------------------------------- تكوين الطبقة [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// أداة التعيين jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- السطر 15: نُعلم Spring في أي حزم سيجد الكائنات التي سيتم إنشاء مثيلات لها. سيجد اثنتين:
- الفئة [Metier] المُعلَّمة بـ [@Service]؛
- الفئة [AleasController] المُعلَّمة بـ [@Controller]؛
- السطر 16: تؤدي العلامة [@EnableWebMvc] إلى تكوينات تلقائية لإطار عمل Spring MVC؛
- السطران 19-20: حقن سياق Spring (حاوية كائنات Spring). هذا الحقن ضروري لأن الكائن الموجود في الأسطر 22-26 يحتاج إليه؛
- يمكن لملف تكوين Spring تعريف كائنات Spring جديدة باستخدام الطرق المُعلَّمة بـ [@Bean]. وبذلك تصبح نتيجة الطريقة كائنًا من Spring؛
- الأسطر 22-26: تعريف سيرفلت إطار عمل Spring MVC، وهو الذي يوجه الطلبات HTTP إلى وحدة التحكم والطريقة الصحيحة. [DispatcherServlet] هي فئة من Spring؛
- الأسطر 28-31: يُشار إلى أن هذه السيرفلت تعالج جميع طلبات URL؛
- الأسطر 33-36: إن وجود هذا «البيان» هو الذي سيقوم بتنشيط خادم Tomcat الموجود في أرشيفات المشروع. وسينتظر الطلبات على المنفذ 8080؛
- الأسطر 39-42: مُخطِّط jSON. وهذا هو الذي تم حقنه في كائنات Spring [Metier] و [AleasController]؛
9.2.6. تشغيل خادم الويب
![]() |
يتم تشغيل المشروع من الفئة القابلة للتنفيذ التالية: [Application]:
package android.exemples.server.boot;
import android.exemples.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Application {
public static void main(String[] args) {
// تشغيل التطبيق
SpringApplication.run(Config.class, args);
}
}
- السطر 6: الفئة [Application] هي فئة قابلة للتنفيذ (الأسطر 7-10)؛
- السطر 9: الطريقة الثابتة [SpringApplication.run] هي طريقة تابعة لـ [spring Boot] (السطر 4) والتي ستقوم بتشغيل التطبيق. المعلمة الأولى لها هي فئة Java التي تقوم بتكوين المشروع. وهنا نجد الفئة [Config] التي وصفناها للتو. المعلمة الثانية هي مصفوفة الحجج التي يتم تمريرها إلى الطريقة [main] (السطر 7). هنا، لن تكون هناك حجج؛
بالنسبة للتنفيذ الفعلي، يُرجى من القارئ الرجوع إلى الفقرة 9.2.1.
9.3. عميل Android
ملاحظة: مشروع Android التالي معقد إلى حد ما. ويتطلب معرفة جيدة بنظام Android، والتي يمكن العثور عليها على سبيل المثال في [Introduction à la programmation de tablettes Android avec Android Studio ].
النشاط
طرق العرض
الطبقة
[DAO]
المستخدم
الخادم
سيكون للعميل مكونان:
- طبقة [Présentation] (طرق العرض + النشاط)؛
- طبقة [DAO] التي تتواصل مع الخدمة [web / JSON] التي درسناها سابقًا.
9.3.1. RxAndroid
للتواصل بشكل غير متزامن مع خادم الأرقام العشوائية، سيستخدم عميل Android المكتبة RxAndroid. تعمل هذه المكتبة على توسيع نطاق RxJava ليشمل بيئة Android. وكما حدث في تطبيق Swing، لن نستخدم سوى امتداد واحد فقط توفره مكتبة RxAndroid، وهو جدولة [AndroidSchedulers.mainThread()]. تخضع واجهة المستخدم الرسومية في Android لنفس القواعد التي تخضع لها واجهة Swing:
- تتم معالجة الأحداث في مؤشر ترابط واحد يُسمى حلقة الأحداث أو مؤشر ترابط واجهة المستخدم؛
- عندما يُطلق حدث ما إجراءات غير متزامنة، يجب استرداد نتائج هذه الإجراءات في خيط واجهة المستخدم (UI thread) إذا كان من المقرر استخدامها لتحديث واجهة المستخدم؛
عميل أندرويد:
- ستُطلق عدة طلبات غير متزامنة إلى خادم الأرقام العشوائية. سيتم تنفيذ هذه الطلبات من جانب العميل باستخدام خيوط المجدول [Schedulers.io()]؛
- وستُرجع هذه الطلبات غير المتزامنة قيمًا قابلة للمراقبة سيتم دمجها في قيمة واحدة (merge)؛
- سيتم مراقبة هذه القيمة القابلة للمراقبة من جانب العميل في المجدول [AndroidSchedulers.mainThread()] الذي يتم إحضاره بواسطة RxAndroid؛
9.3.2. مشروع IntelliJ IDEA
يُسمى مشروع Android [client]:
![]() | ![]() ![]() |
سيتم تشغيله بواسطة [2].
ملاحظة: يعتمد التشغيل بشكل كبير على إعدادات برنامج IntelliJ IDEA المستخدم. ومن المحتمل ألا يعمل التشغيل المذكور أعلاه ([2]) من المرة الأولى على جهاز آخر غير جهازي. قد يكون تكوين برنامج IntelliJ IDEA بشكل صحيح لتشغيل هذا المشروع مهمة شاقة للمبتدئين. فيما يلي بعض النقاط التي يجب الانتباه إليها:
- في [3]، الوصول إلى بنية المشروع؛
![]() | ![]() |
- في [4-5]، JDK و SDK Android الموجودة على جهازي. تجدر الإشارة إلى أن JDK 1.8 ليست ضرورية. لا يدعم Android بعض ميزات Java 8، بما في ذلك لامبدا. لذا، لإنشاء مثيلات للواجهات الوظيفية، سنستخدم فئات مجهولة. ويكفي عندئذٍ استخدام JDK 1.6. ومع ذلك، فقد تم تكوين المشروع كما هو موزع باستخدام JDK 1.8؛
الملف [build.gradle] [6] الذي يقوم بتكوين مشروع Android هو كما يلي:
buildscript {
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
// استبدل بالإصدار الحالي من المكون الإضافي لنظام Android
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
apply plugin: 'com.android.application'
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
jcenter()
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "android.aleas"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
وفقًا لملفات Android الموجودة، قد يلزم تعديل الإصدارات الواردة في الأسطر 8 و24-25 و29.
لتثبيت ملفات Android الجديدة SDK، استخدم SDK Manager كما يلي [1]:
![]() ![]() | ![]() |
تم تكوين المشروع لـ:
- SDK API 23 [2]؛
- SDK أدوات البناء 23.0.3 [3]؛
- SDK Tool 25.1.3 [4]
أخيرًا، تحقق من مسار SDK Android في الملف [local.properties] [4]، السطر 11 أدناه:
## يتم إنشاء هذا الملف تلقائيًا بواسطة Android Studio.
# لا تقم بتعديل هذا الملف -- YOUR CHANGES WILL BE ERASED!
#
# يجب *NOT* تسجيل هذا الملف في أنظمة التحكم في الإصدارات،
# لأنه يحتوي على معلومات خاصة بتكوينك المحلي.
#
# موقع ملف SDK. يستخدم هذا الملف بواسطة Gradle فقط.
# للتخصيص عند استخدام نظام التحكم في الإصدارات، يرجى قراءة
# الملاحظة الواردة في العنوان.
#الخميس 7 أبريل 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk
9.3.3. تشغيل مشروع IntelliJ IDEA
بعد إنشاء بيئة مناسبة للمشروع، يمكن تشغيله على النحو التالي:
![]() | ![]() | ![]() |
- في [1]، يتم تشغيل محاكي Android Genymotion؛
- في [2]، يتم تنفيذ إعداد التشغيل [app]؛
- في [3]، لإنشاء إعداد تشغيل؛
![]() |
- في [1, 3]، تم تسمية التكوين بـ [app]؛
- إلى [2]، وهي تتوافق مع تشغيل الوحدة المسماة [app]؛
- في [4]، يُطلب أن يقترح علينا IDE جهازًا للتنفيذ عند التنفيذ. هنا سيكون دائمًا محاكي Genymotion؛
- في [5]، يُشار إلى الاحتفاظ بهذا الجهاز لجميع عمليات تشغيل التكوين؛
يبدأ تشغيل المشروع على محاكي Genymotion بالخطوة الأولية التالية:

لمعرفة ما يجب إدخاله في [1]، افتح نافذة أوامر DOS واكتب الأمر [ipconfig] التالي:
C:\Program Files\Console2>ipconfig
Configuration IP de Windows
Carte Ethernet Ethernet :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Carte réseau sans fil Connexion au réseau local* 3 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet VirtualBox Host-Only Network :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::8076:36e6:3b38:5e98%16
Adresse IPv4. . . . . . . . . . . . . .: 192.168.56.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet Ethernet 2 :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::d0d9:e01f:ddde:1f4b%14
Adresse IPv4. . . . . . . . . . . . . .: 192.168.95.1
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte réseau sans fil Wi-Fi :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::54b3:afe5:e199:2206%10
Adresse IPv4. . . . . . . . . . . . . .: 192.168.0.13
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . : fe80::523d:e5ff:fe0c:4ad9 192.168.0.1
اكتب في [1] أحد عناوين IP لجهازك (الأسطر 20 و28 و32). إذا كان لديك جدار حماية Windows، فسيتعين عليك على الأرجح تعطيله حتى يتمكن محاكي Android من الوصول إلى خادم الأرقام العشوائية.
يؤدي تنفيذ الطلبات غير المتزامنة باستخدام المعلومات المذكورة أعلاه إلى النتائج التالية:

يؤدي كل طلب إلى استجابة jSON تحتوي على الحقول التالية:
- aleas: الأرقام العشوائية التي يولدها الخادم؛
- idClient: رقم الطلب؛
- on: مؤشر ترابط تنفيذ الطلب من جانب العميل؛
- requestAt: وقت الطلب؛
- responseAt: وقت استلام الرد؛
- delay: فترة الانتظار التي التزم بها الخادم قبل إرسال الرد؛
- erreur: رمز الخطأ - 0 في حالة عدم وجود خطأ؛
- message: رسالة خطأ - null في حالة عدم وجود خطأ؛
- observedAt: وقت مراقبة الرد؛
- observedOn: مؤشر ترابط رصد الرد. سيكون هنا دائمًا [main] الذي يشير إلى مؤشر ترابط واجهة المستخدم؛
نظرًا لأن الطلبات غير متزامنة وأن أوقات الانتظار المفروضة على الخادم عشوائية، فإن الردود تصل بترتيب متفرق.
9.3.4. تبعيات Gradle للمشروع
يحتاج المشروع إلى تبعيات نقوم بتسجيلها في الملف [app / build.gradle]:
![]() |
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
compile 'io.reactivex:rxandroid:1.1.0'
}
- التبعيات الواردة في السطرين 2 و3 هي تبعيات قياسية لمشروع Android مع SDK 23؛
- تستدعي السطر 5 الكائن Spring [RestTemplate] الذي يدير تفاعل الطبقة [DAO] مع الخادم؛
- التبعية في السطر 6 تستدعي المكتبة JSON و [Jackson] التي يستخدمها التطبيق؛
- التبعية في السطر 7 تستدعي المكتبة RxAndroid (ومعها المكتبة RxJava) التي تستخدمها طبقة Ui للتواصل مع الطبقة [DAO]؛
9.3.5. بيان التطبيق Android
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.aleas">
<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="android.aleas.activity.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- السطر 5: يجب السماح بالوصول إلى الإنترنت؛
9.3.6. الطبقة [DAO]
![]() |
![]() | ![]() |
9.3.6.1. واجهة [IDao] للطبقة [DAO]
ستكون واجهة الطبقة [DAO] كما يلي:
package android.aleas.dao;
import android.aleas.fragments.Request;
import rx.Observable;
public interface IDao {
// أرقام عشوائية في النطاق [a,b]
// يتم توليد n عددًا، حيث n نفسه عدد عشوائي في النطاق [minCount, maxCount]
// يتم توليد الأعداد بعد انتظار مدته delay مللي ثانية،
// حيث [delay] هو نفسه عدد عشوائي في النطاق [minDelay, maxDelay]
public Observable<AleasDaoResponse> getAleas(final Request request);
// URL لخدمة الويب
public void setUrlServiceWebJson(String url);
// الحد الأقصى لوقت الانتظار (بالمللي ثانية) لاستجابة الخادم لطلب الاتصال
// الحد الأقصى لفترة الانتظار (بالمللي ثانية) لاستجابة الخادم لطلب
public void setClientTimeouts(int connectTimeout, int readTimeOut);
}
- السطر 12: طريقة الطبقة [DAO] التي توفر الأرقام العشوائية بشكل غير متزامن؛
- السطر 15: لإرشاد التنفيذ [DAO] إلى URL الخاص بخدمة توليد الأرقام العشوائية؛
- السطر 19: لتعيين أوقات انتظار قصوى لتنفيذ [DAO]، وذلك لتجنب وقت انتظار طويل جدًا عندما لا يستجيب الخادم؛
تستقبل الطريقة [getAleas] جميع معلماتها في الكائن [Request] التالي:
package android.aleas.fragments;
public class Request {
// رقم الطلب
int id;
// مدخلات المستخدم
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// المنشئات
public Request() {
}
public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
this.id = id;
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
}
// دالات الاسترجاع والتعيين
...
}
ونلاحظ هنا معظم معلمات الكائن URL الخاص بالخادم الذي يجب استعلامه.
تُرجع الطريقة [getAleas] نوعًا من نوع Observable<AleasDaoResponse>، حيث تكون الفئة [AleasDaoResponse] كما يلي:
package android.aleas.dao;
import java.util.List;
public class AleasDaoResponse {
// رمز الخطأ
private int erreur;
// رسالة الخطأ
private String message;
// وقت انتظار الخادم
private int delay;
// الأرقام العشوائية التي يصدرها الخادم
private List<Integer> aleas;
// حالة العميل
private ClientState clientState;
// المنشئات
public AleasDaoResponse() {
}
public AleasDaoResponse(int erreur, String message, int delay, List<Integer> aleas, ClientState clientState) {
this.erreur = erreur;
this.message = message;
this.delay = delay;
this.aleas = aleas;
this.clientState = clientState;
}
// مُستردات وقيم التعيين
...
}
النوع [ClientState] هو كما يلي:
package android.aleas.dao;
import org.codehaus.jackson.map.annotate.JsonFilter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class ClientState {
// اسم مؤشر الترابط التنفيذي
private String on;
// وقت الطلب
private String requestAt;
// وقت الرد
private String responseAt;
// معرف العميل
private int idClient;
// منشئ
public ClientState() {
on = Thread.currentThread().getName();
requestAt = getTimeStamp();
}
public ClientState(int idClient) {
this();
this.idClient = idClient;
}
// الطرق الخاصة
private String getTimeStamp() {
return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
// أدوات الاسترجاع والتعيين
...
}
- السطر 11: مؤشر ترابط تنفيذ الطبقة [DAO]؛
- السطر 13: وقت الطلب؛
- السطر 15: وقت الرد؛
- السطر 17: رقم الطلب؛
يتم تهيئة الحقول [on, requestAt, idClient] بواسطة العميل في بداية الطلب. يتم تهيئة الحقل [responseAt] عندما يتلقى العميل الرد من الخادم.
9.3.6.2. تنفيذ الطبقة [DAO]
![]() |
يتم تنفيذ الواجهة [IDao] باستخدام الفئة [Dao] التالية:
package android.aleas.dao;
import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class Dao implements IDao {
// العميل REST
private RestTemplate restTemplate;
// URL الخدمة
private String urlServiceWebJson;
// المُعيّن jSON
private ObjectMapper mapper;
// المنشئون
public Dao() {
// أداة التعيين jSON
mapper = new ObjectMapper();
}
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// يتم تعيين URL لخدمة REST
this.urlServiceWebJson = urlServiceWebJson;
}
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
}
}
- السطر 22: الكائن [RestTemplate] الذي سيتولى التواصل مع خادم الأرقام العشوائية؛
- السطر 24: كائن URL الخاص بخدمة التوليد — يتم تعيينه بواسطة الطريقة [setUrlServiceWebJson] في السطر 41؛
- السطر 27: مُعَيِّن jSON الذي سيُستخدم لإلغاء تسلسل السلسلة jSON المرسلة من خادم الأرقام العشوائية؛
- الأسطر 30-33: منشئ الفئة؛
- السطر 32: يتم إنشاء المُعَيِّن jSON الموجود في السطر 27؛
الطريقة [setClientTimeouts] هي كما يلي:
// العميل REST
private RestTemplate restTemplate;
...
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
// يتم تعيين مهلة انتظار طلبات العميل REST
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(readTimeOut);
factory.setConnectTimeout(connectTimeout);
restTemplate = new RestTemplate(factory);
restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
- يتم توفير التفاعل بين العميل وخادم الويب / JSON بواسطة الكائن [RestTemplate] في السطر 2. لم نقم بتهيئته حتى الآن. وتقوم الطريقة [setClientTimeouts] بهذه المهمة؛
- السطر 8: يتم توفير الفئة [HttpComponentsClientHttpRequestFactory] بواسطة التبعية [spring-android-rest-template]. وستسمح لنا هذه الفئة بتحديد المدد الزمنية القصوى لانتظار استجابة الخادم (السطران 9-10)؛
- السطر 11: نقوم بإنشاء الكائن من النوع [RestTemplate] الذي سيكون وسيلة الاتصال مع خدمة الويب. ونمرر إليه كمعلمة الكائن [factory] الذي تم إنشاؤه للتو؛
- السطر 12: يمكن أن يتخذ الحوار بين العميل والخادم أشكالًا متنوعة. تتم عمليات التبادل عبر أسطر نصية، وعلينا أن نحدد للكائن من النوع [RestTemplate] ما يجب عليه فعله بهذه السطر النصي. ولهذا الغرض، نزوده بمحولات، وهي فئات قادرة على معالجة الأسطر النصية. يتم اختيار المحول عمومًا عبر الرؤوس HTTP المصاحبة لسطر النص. وبناءً على هذه الرؤوس، سيختار الكائن [RestTemplate]، من بين محولاته، المحول الأنسب للحالة. هنا، سيكون لدينا محول واحد فقط، وهو محول String --> String، مما يعني أن النوع String المستلم من الخادم لن يخضع لأي تحويل.
تعد الطريقة [getAleas] هي الطريقة الأكثر تعقيدًا:
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
// تشغيل الخدمة
return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
@Override
public void call(Subscriber<? super AleasDaoResponse> subscriber) {
try {
// URL للخدمة: /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}
String urlService = String.format("%s/%s/%s/%s/%s/%s/%s",
urlServiceWebJson, request.getA(), request.getB(), request.getMinCount(),
request.getMaxCount(), request.getMinDelay(), request.getMaxDelay());
// معلومات العميل
ClientState clientState = new ClientState(request.getId());
// طلب HTTP متزامن
String response = executeRestService("get", urlService, null);
// إلغاء تسلسل استجابة الخادم jSON
AleasServerResponse aleasServerResponse = mapper.readValue(
response,
new TypeReference<AleasServerResponse>() {
});
// خطأ؟
int erreur = aleasServerResponse.getErreur();
if (erreur != 0) {
// إعادة توجيه الاستثناء
subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
} else {
// تسجيل وقت الاستلام
clientState.setResponseAt();
// إعادة توجيه النتيجة إلى المشترك
subscriber.onNext(
new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
}
} catch (Exception ex) {
// إعادة توجيه الاستثناء إلى المشترك
subscriber.onError(ex);
} finally {
// يتم الإبلاغ عن انتهاء المراقبة
// عند التنفيذ، نلاحظ أن هذه الطريقة لا تؤثر على الإطلاق إذا تم استدعاء الطريقة [onError] مسبقًا - وفقًا للنظرية - لذا يمكن وضع هذه التعليمات في جملة try فقط
subscriber.onCompleted();
}
}
});
}
- السطر 2: يجب تذكر أنه يتعين إنتاج نوع [Observable<AleasResponse>]؛
- السطر 3: سطر تسجيل في وحدة التحكم Android؛
- السطر 5: يضمن الكائن [RestTemplate] تفاعلًا متزامنًا مع الخادم. وهذا يعني أن مؤشر الترابط التنفيذي الذي يقوم بالطلب يظل معطلاً حتى استلام الرد. في مثال Swing، رأينا كيف يمكن تحويل إجراء متزامن إلى إجراء غير متزامن باستخدام الطريقة [Observable.create]. وهذا هو نفس النهج الذي نتبعه هنا؛
- السطر 7: الطريقة [call] الخاصة بالواجهة [Observable.OnSubscribe<AleasDaoResponse>] في السطر 5. يتم استدعاء هذه الطريقة عندما يشترك مراقب في العنصر القابل للمراقبة؛
- الأسطر 10-12: إنشاء كائن URL الخاص بخدمة الأرقام العشوائية؛
- السطر 14: تهيئة الكائن [ClientState]. ويهدف ذلك هنا إلى تسجيل وقت الطلب؛
- السطر 16: طلب متزامن HTTP. يتم الحصول على استجابة jSON. تتطلب الطريقة [executeRestService] ثلاثة معلمات:
- الطريقة HTTP التي سيتم استخدامها لاستعلام الخدمة؛
- معرف الخدمة URL؛
- الكائن المراد إرساله من النوع Object، أو null إذا كانت الطريقة HTTP ليست من النوع POST؛
- 18-21: تحويل السلسلة jSON المستلمة إلى نوع [AleasServerResponse]. وهذا النوع هو كما يلي:
package android.aleas.dao;
import java.util.List;
public class AleasServerResponse {
// رمز الخطأ
private int erreur;
// رسالة خطأ
private String message;
// وقت انتظار الخادم
private int delay;
// أرقام عشوائية
private List<Integer> aleas;
// أدوات الاسترجاع والتعيين
...
}
- السطر 23: يتم استرداد رمز الخطأ المرسل من الخادم؛
- الأسطر 24-26: في حالة حدوث خطأ، يتم إرسال استثناء إلى المشترك؛
- السطر 29: يتم تحديث [clientState] الذي سيشكل جزءًا من الرد المرسَل إلى المشترك؛
- الأسطر 31-33: إرسال الرد إلى المشترك. ويكون الرد من النوع [AleasDaoResponse]؛
- الأسطر 35-37: تعالج جميع حالات الخطأ دون تمييز. الخطأ الأكثر احتمالًا هو خطأ في الشبكة؛
- السطر 41: إشعار بانتهاء الإرسال؛
9.3.7. طرق عرض التطبيق
![]() |
![]() |
يقدم التطبيق العرضين التاليين:
عرض الطلب

عرض الرد

9.3.7.1. الفئة [MyFragment]
هناك جزئان:
- [RequestFragment] للاستعلام؛
- [ResponseFragment] للاستجابة؛
توسع المقطعان الفئة التالية [MyFragment]:
package android.aleas.fragments;
import android.aleas.activity.MainActivity;
import android.aleas.activity.Session;
import android.support.v4.app.Fragment;
public abstract class MyFragment extends Fragment {
// ------------- البيانات المشتركة بين الأجزاء
protected MainActivity activity;
protected Session session;
public abstract void onRefresh();
}
- السطر 7: الفئة [MyFragment] توسع نطاق فئة Android [Fragment]؛
- السطران 10-11: البيانات المشتركة بين جميع الأجزاء؛
- السطر 10: كل جزء يعرف النشاط الوحيد للتطبيق؛
- السطر 11: للتواصل فيما بينها، تستخدم الأجزاء جلسة عمل؛
- السطر 13: قبل عرض جزء، سيُطلب منه تحديث نفسه بمحتوى الجلسة. تم إعلان هذه الطريقة على أنها مجردة لأنها تُنفَّذ بواسطة الفئات الفرعية. ولهذا السبب، تم إعلان الفئة نفسها على أنها مجردة (السطر 7)؛
تحتوي الفئة [Session] على البيانات التي تتشاركها الأجزاء المختلفة للتطبيق. وفيما يلي كودها:
![]() |
package android.aleas.activity;
import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;
public class Session {
// نشاط التطبيق
private MainActivity activity;
// عدد الطلبات
private int nbRequests;
// خصائص الطلبات
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// URL خدمة الويب / jSON
private String urlWebJson;
// بدء العملية
private boolean onAir;
// نفس الشيء ولكن بعد ذلك بقليل
private boolean operationStarted;
// اسم المثال الذي اختاره المستخدم من قائمة الأمثلة
private String exampleName;
// رقمه في قائمة الأجزاء
private int examplePosition;
// محول أداة التمرير في الأمثلة في عرض الاستعلام
private ArrayAdapter<CharSequence> spinnerExemplesAdapter;
// الطرق
public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
this.urlWebJson = urlWebJson;
this.exampleName = exampleName;
this.examplePosition = examplePosition;
}
public Request getRequest() {
return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
}
// طرق الحصول على القيم وتعيينها
...
}
تسمح الطريقة الموجودة في السطر 46 بإنشاء الكائن [Request] الذي يغلف جميع المعلومات التي أدخلها المستخدم في عرض الاستعلام:
![]() |
package android.aleas.fragments;
public class Request {
// رقم الاستعلام
int id;
// مدخلات المستخدم
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// منشئات
public Request() {
}
public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
this.id = id;
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
}
// دالات الاسترجاع والتعيين
....
}
9.3.7.2. جزء الاستعلام [RequestFragment]
يحتوي جزء الاستعلام على المكونات التالية:

يحتوي التطبيق على عرض واحد يتكون من علامتي تبويب:
- [1]: علامة تبويب الطلب؛
- [2]: علامة تبويب الاستجابة؛
مكونات الجزء [RequestFragment] هي التالية:
رقم | النوع | الاسم | الدور |
3 | EditText | edtNbRequests | عدد الطلبات التي يجب إرسالها إلى خدمة توليد الأرقام العشوائية |
4 | EditText | edtA، edtB | حدود [a,b] لفترة توليد الأرقام؛ |
5 | EditText | edtMinCount، edtMaxCount | تقوم الخدمة بتوليد الأرقام count حيث count هو رقم عشوائي ضمن النطاق [minCount, maxCount] |
6 | EditText | edtMinDelay، edtMaxDelay | تنتظر الخدمة delay ميلي ثانية قبل توليد الأرقام حيث delay هو عدد عشوائي في النطاق [minDelay, maxDelay] |
7 | EditText | edtUrlServiceRest | URL لخدمة توليد الأرقام العشوائية؛ |
8 | Spinner | spinnerExemples | قائمة الأمثلة المنسدلة. يوضح كل مثال طريقة معينة من الفئة [Observable]؛ |
8 | Button | btnExecuter | الزر الذي يقوم بتشغيل استدعاءات خدمة توليد الأرقام؛ |
يتم الإبلاغ عن أخطاء الإدخال:

المكونات من 1 إلى 6 هي مكونات [TextView] تحمل الأسماء التالية (بالترتيب): txtErrorRequests، txtErrorIntervalle، txtErrorCount، txtErrorDelay، txtMsgErreurUrlServiceWeb.
9.3.7.3. جزء [ResponseFragment] من الرد
يحتوي جزء الرد على المكونات التالية:

رقم | النوع | الاسم | الدور |
1 | TextView | infoReponses | عدد الردود المستلمة |
2 | ListView | listReponses | قائمة القنوات jSON المستلمة من الخادم |
3 | زر | btnAnnuler | لإلغاء الطلبات الموجهة إلى الخادم |
9.3.7.4. نشاط Android [MainActivity]
![]() |
![]() |
تعرض الفئة [MainActivity] العرض التالي []:
<?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="android.arduinos.ui.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">
<!-- صورة الانتظار -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- حاوية علامات التبويب -->
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<!-- حاوية العروض -->
<android.aleas.activity.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:layout_marginBottom="100dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
مكونات هذه العرض هي كما يلي:
الخطوط | النوع | الاسم | الدور |
20-34 | شريط الأدوات | شريط الأدوات | شريط أدوات التطبيق |
29-34 | ProgressBar | loadingPanel | صورة الانتظار التي تظهر أثناء تنفيذ طلب المستخدم |
37-40 | TabLayout | علامات التبويب | شريط علامات التبويب في التطبيق |
44-51 | MyPager | container | الحاوية التي تُعرض فيها الأجزاء المختلفة للتطبيق |
الفئة [MyPager] هي كما يلي:
package android.aleas.activity;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// عنصر التحكم في التمرير
private boolean isSwipeEnabled;
// المنشئات
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// إعادة تعريف الطرق
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// هل التمرير مسموح به؟
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// هل التمرير مسموح به؟
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// مُعيّن
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- تُوسّع الفئة [MyPager] نطاق الفئة القياسية في Android [ViewPager]. نستخدم الفئة [MyPager] بدلاً من الفئة [ViewPager] فقط لأننا نريد منع التمرير: بشكل افتراضي مع الفئة [ViewPager]، يمكن الانتقال من علامة تبويب إلى أخرى عن طريق التمرير (بالسحب إلى اليسار أو اليمين). هنا، لا نريد هذا السلوك؛
- السطر 11: المتغير المنطقي الذي سيتحكم في التمرير (السطران 26 و36)؛
- الأسطر 44-46: الطريقة التي تسمح بتهيئة الحقل الموجود في السطر 11؛
الهيكل الأساسي لنشاط Android [MainActivity] هو كما يلي:
package android.aleas.activity;
import android.aleas.R;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.dao.Dao;
import android.aleas.dao.IDao;
import android.aleas.fragments.MyFragment;
import android.aleas.fragments.Request;
import android.aleas.fragments.RequestFragment;
import android.os.Bundle;
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.view.View;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar;
import rx.Observable;
public class MainActivity extends AppCompatActivity implements IDao {
// الطبقة [DAO]
private IDao dao;
// الجلسة
private Session session;
// المنشئ
public MainActivity() {
// الأصل
super();
// الجلسة
session = new Session();
// DAO
dao = new Dao();
}
// مُستخرجات
public Session getSession() {
return session;
}
// التنفيذ IDao ----------------------------------------
@Override
public Observable<AleasDaoResponse> getAleas(Request request) {
return dao.getAleas(request);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
dao.setClientTimeouts(connectTimeout, readTimeOut);
}
}
- السطر 21: الفئة [MainActivity] تمتد من الفئة القياسية في Android [AppCompatActivity]. وبالتالي فهي نشاط قياسي في Android؛
- السطر 21: الفئة [MainActivity] تُنفِّذ الواجهة [IDao]؛
إذا عدنا إلى بنية التطبيق:
![]() |
إن قيام النشاط بتنفيذ واجهة الطبقة [DAO] يسمح للعروض بعدم معرفة الطبقة [DAO]: فستتواصل معالجات الأحداث الخاصة بها مع الطبقة [activité] عندما ترغب في التواصل مع الخادم.
- السطر 24: مرجع إلى الطبقة [DAO] التي تم تهيئتها بواسطة مُنشئ السطر 35؛
- السطر 26: مرجع إلى الجلسة المشتركة بين الأجزاء، تم تهيئته بواسطة المنشئ في السطر 33؛
- الأسطر 46-59: تنفيذ واجهة [IDao]؛
تقوم الفئة [MainActivity] بتهيئة مكونات العرض المرتبطة بها على النحو التالي:
// شريط الأدوات
private Toolbar toolbar;
// مدير الأجزاء
private MyPager mViewPager;
// حاوية علامات التبويب
private TabLayout tabLayout;
// صورة الانتظار
private ProgressBar loadingPanel;
...
@Override
public void onCreate(Bundle savedInstanceState) {
// كلاسيكي
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// الجلسة
session.setActivity(this);
// تكوين مهلات الطبقة [DAO]
setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);
// المكونات
mViewPager = (MyPager) findViewById(R.id.container);
toolbar = (Toolbar) findViewById(R.id.toolbar);
loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
tabLayout = (TabLayout) findViewById(R.id.tabs);
// شريط الأدوات
setSupportActionBar(toolbar);
// في البداية لا يوجد سوى علامة تبويب واحدة
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Request");
tabLayout.addTab(tab);
// مدير الأحداث
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// تم تحديد علامة تبويب - يتم تغيير الجزء المعروض بواسطة حاوية الأجزاء
int position = tab.getPosition();
if (position == 0) {
// علامة تبويب الاستعلام
showView(0);
} else {
// علامة تبويب الرد - تعتمد على المثال المختار
showView(session.getExamplePosition());
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
// إنشاء أجزاء الردود
createResponseFragments();
// إدارة صورة الانتظار
loadingPanel.setVisibility(View.INVISIBLE);
}
هذا الكود تقليدي إلى حد ما في نشاط ما. دعونا نوضح بعض النقاط:
- تشير السطر 19 إلى الفئة التالية [Constants]:
package android.aleas.activity;
abstract public class Constants {
final static public int VUE_REQUEST = 0;
final static public int VUE_RESPONSE = 1;
final static public int CONNECT_TIMEOUT = 1000;
final static public int READ_TIMEOUT = 6000;
final static public int DELAY_MAX = 5000;
final static public String EXAMPLES_PACKAGE = "android.aleas.exemples";
}
- الأسطر 31-33: يتم إنشاء علامة التبويب الأولى بعنوان [Request]. في مرحلة ما، سيكون لدينا في الذاكرة:
- المقطع [Request]؛
- n جزءًا من النوع [ExampleXXFragment]؛
ستعرض علامة التبويب الأولى دائمًا المقطع [Request]. وستعرض علامة التبويب الثانية المقطع [ExampleXXFragment] المطابق للمثال الذي اختاره المستخدم. وبالتالي، يتغير المقطع المعروض في علامة التبويب الثانية بمرور الوقت؛
- الأسطر 37-48: الكود الذي يتم تنفيذه عندما ينقر المستخدم على إحدى علامات التبويب؛
- السطر 43: يتم عرض المقتطف رقم 0؛
- السطر 46: يتم عرض المقتطف قيد الاستخدام حاليًا (المعروض). يتم العثور على رقمه في الجلسة؛
- السطر 62: يتم إنشاء أجزاء جميع الأمثلة الموجودة في قائمة الأمثلة في العرض [RequestFragment] (علامة التبويب الأولى)؛
- السطر 65: يتم إخفاء صورة الانتظار في الوقت الحالي؛
لفهم الطريقة [showView] (السطران 43 و46) والطريقة [createResponseFragments]، يتعين علينا أولاً تقديم مدير الأجزاء في الذاكرة (فئة مضمنة في ملف Java الخاص بـ MainActivity):
// مدير الأجزاء - يجب تعريف الطرق getItem، getCount
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// الأجزاء المدارة
private MyFragment[] fragments;
// منشئ
public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
super(fm);
this.fragments = fragments;
}
// يجب أن يعرض الجزء رقم الموضع
@Override
public MyFragment getItem(int position) {
// الجزء
return fragments[position];
}
// يعرض عدد الأجزاء المطلوب إدارتها
@Override
public int getCount() {
// عدد الأجزاء
return fragments.length;
}
}
}
- تُوسّع الفئة [SectionsPagerAdapter] نطاق فئة Android [FragmentPagerAdapter]. وهي تعيد تعريف طريقتين من فئتها الأم:
- الطريقة [getItem]، السطر 15؛
- الطريقة [getCount]، السطر 22؛
- تحتوي الفئة [SectionsPagerAdapter] على جميع أجزاء التطبيق. ويتم تخزين هذه الأجزاء في السطر 5. وتجدر الإشارة إلى أنها من النوع [MyFragment] المذكور في الفقرة 9.3.7.1؛
- السطر 8: لإنشاء الفئة [SectionsPagerAdapter]، تتلقى هذه الفئة الأجزاء التي يتعين عليها إدارتها؛
- الأسطر 14-18: تُرجع الطريقة [getItem] الجزء الموجود في الموضع [position]؛
- الأسطر 21-25: تُرجع الطريقة [getCount] العدد الإجمالي للأجزاء؛
تقوم الطريقة [createResponseFragments] بإنشاء جميع الأجزاء التي يحتاجها التطبيق:
private void createResponseFragments() {
// عجلة اختيار الأمثلة
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// تحديد التخطيط المطلوب استخدامه عند ظهور قائمة الخيارات
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// يتم وضع المحول في الجلسة حتى تقوم طريقة العرض [Request] باسترداده
session.setSpinnerExemplesAdapter(adapter);
...
}
- السطر 3: يتم إنشاء محول لعنصر التمرير (spinner) الخاص بالأمثلة، وهو هنا قائمة من نوع String تمثل أسماء الأمثلة. توجد هذه الأسماء في الملف [layout/exemples.xml]:
![]() |
يحتوي الملف [exemples.xml] على الكود التالي:
<!-- أمثلة -->
<resources>
<string-array name="exemples">
<item>Exemple-01</item>
<item>Exemple-02</item>
<item>Exemple-03</item>
<item>Exemple-04</item>
</string-array>
</resources>
السطر 1، هذا الملف هو المعلمة الثانية للطريقة [createFromResource]. في [R.array.exemples]، [exemples] هو اسم المصفوفة، السطر 3 أعلاه، وليس اسم الملف.
- السطر 5: يتم ربط layout (مدير العرض) بالمحول. الآن أصبح المحول يمتلك البيانات وطريقة عرضها معًا؛
- السطر 7: يتم تشغيل المحول في وضع الجلسة. وهنا سيقوم الجزء [RequestFragment] الذي يحتاج إليه باسترداده؛
لنكمل كود الأسلوب [createResponseFragments]:
private void createResponseFragments() {
// أمثلة على عناصر التحديد
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// تحديد التخطيط المطلوب استخدامه عند ظهور قائمة الخيارات
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// نضع المُهايئ في الجلسة حتى تقوم طريقة العرض [Request] باسترداده
session.setSpinnerExemplesAdapter(adapter);
// إنشاء جدول الأجزاء (استعلام واحد، n إجابات)
MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
// جزء من الاستعلام
tFragments[0] = new RequestFragment();
// أجزاء من الإجابات
for (int i = 1; i < tFragments.length; i++) {
// يتم إنشاء اسم الجزء المراد إنشاؤه والذي يتوافق مع المثال الذي اختاره المستخدم
// يجب أن يكون هذا الاسم هو الاسم الكامل مع الحزمة الخاصة به - وهنا يرتبط مباشرة برقم المثال في قائمة الاختيار
String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
// يتم إنشاء مثيل للجزء المرتبط بالمثال
MyFragment fragment;
try {
// إنشاء مثيل للفئة
fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
} catch (Exception e) {
e.printStackTrace();
return;
}
// تم إنشاء الجزء - يتم وضعه في المصفوفة
tFragments[i] = fragment;
}
// إنشاء مثيل لمدير الأجزاء باستخدام هذه الأجزاء الجديدة
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
// إعداد ViewPager باستخدام محول الأقسام.
mViewPager.setAdapter(mSectionsPagerAdapter);
// التنقل بين الصفحات - هذه التعليمات مهمة
// يُشار هنا إلى أنه على جانبي العرض المعروض، يجب الاحتفاظ بـ [tFragments.length] العروض التي تم تهيئتها
// وهذا يعني هنا أن جميع الأجزاء التي يستخدمها التطبيق موجودة في الذاكرة ومُهيأة
// وإذا لم يتم ذلك، فإن القيمة الافتراضية لـ [OffscreenPageLimit] هي 1
// وبالتالي، إذا كان الجزء المعروض هو رقم 3، فسيتم تهيئة الجزأين 2 و4 فقط
// ويتم ذلك عن طريق استدعاء الطريقة [onCreateView] لهاتين القطعتين - وهذا يعني أنه في هذه الطريقة، يجب توقع
// إعادة إنشاء المظهر المرئي الذي كان عليه الجزء في آخر مرة تم استخدامه فيها — علاوة على ذلك، يجب ألا يحتوي هذا الأسلوب
// يحتوي على كود لا يتحمل التنفيذ مرتين — فهذا يسبب فوضى هائلة ويصعب إدارته
// هنا فضلنا تجنب هذه الصعوبات - في سجلات التشغيل، نرى أنه عند بدء تشغيل التطبيق، يتم إنشاء جميع الأجزاء
// ويتم تنفيذ طريقتها [onCreateView] — ولا يتم تنفيذها أبدًا بعد ذلك —
mViewPager.setOffscreenPageLimit(tFragments.length);
// يتم منع التمرير بين الأجزاء
mViewPager.setSwipeEnabled(false);
}
- السطر 9: إنشاء المصفوفة التي ستحتوي على جميع أجزاء التطبيق؛
- السطر 11: الجزء الأول هو جزء الاستعلام؛
- الأسطر 13-28: سنقوم بإنشاء عدد من الأجزاء يساوي عدد الأمثلة. جميع هذه الأجزاء تمتد من جزء الرد [ResponseFragment] ولا تنفذ سوى ما هو خاص بالمثال: إنشاء القيم الملاحظة. فهذه القيم تختلف بالفعل من مثال لآخر؛
- السطر 16: تحمل شريحة المثال اسمًا قياسيًا: ExampleXXFragment، حيث يمثل XX موضعها في قائمة التمرير الخاصة بالأمثلة مضافًا إليه 1. XX هو أيضًا رقم جزء المثال في مدير الأجزاء؛
- السطر 21: إنشاء مثيل لجزء المثال رقم i من قائمة التمرير:
- Class.forName(exampleName): تحميل الجزء في الذاكرة؛
- Class.forName(exampleName).getConstructors()[0]: يحصل على مرجع المنشئ الأول للفئة. تحتوي الفئة ExampleXXFragment على منشئ واحد فقط. وبالتالي، سيتم الحصول على مرجع لهذا المنشئ؛
- Class.forName(exampleName).getConstructors()[0].تقوم newInstance(new Object[]{}) بإنشاء مثيل من نوع ExampleXXFragment باستخدام المنشئ المذكور في الخطوة السابقة. ويمثل new Object[]{} المعلمات التي تم تمريرها إلى هذا المنشئ. نظرًا لأن مُنشئ الفئة ExampleXXFragment لا يتطلب أي معلمات، يتم تمرير مصفوفة كائنات فارغة؛
- السطر 27: تُضاف هذه الشظية إلى مصفوفة الشظايا؛
- السطر 30: سبق أن رأينا أن مُنشئ مُدير الأجزاء [SectionsPagerAdapter] يتوقع في معلماته مصفوفة الأجزاء التي يجب أن يديرها. والآن نمررها إليه؛
- السطر 22: يتم هنا ربط حاوية الأجزاء [mViewPager] الخاصة بالعرض المرتبط بالنشاط [MainActivity] بمدير الأجزاء: تعرض حاوية الأجزاء [mViewPager] أجزاء مدير الأجزاء؛
- السطر 43: سنقرأ التعليقات - تعني هذه التعليمات أن جميع الأجزاء يجب أن تظل في الحالة التي يضعها فيها الكود، بغض النظر عن الجزء المعروض حاليًا. وبالتالي، عندما نعود إليه، نجده في الحالة التي تركناه عليها؛
- السطر 45: حاوية الأجزاء [mViewPager] هي من النوع [MyPager] الذي يسمح بمنع التمرير؛
الطريقة [MainActivity.showView] هي كما يلي:
// عرض الصفحة رقم [position]
private void showView(int position) {
// يتم تحديث الجزء قبل عرضه
mSectionsPagerAdapter.getItem(position).onRefresh();
// يتم عرض العرض المطلوب - يتم الانتقال مباشرةً إلى العرض (المعلمة الثانية على القيمة false)
// بدون هذا المعامل، يتم الانتقال افتراضيًا إلى العرض المطلوب مع عرض سريع للعروض الوسيطة - سلوك غير مرغوب فيه
mViewPager.setCurrentItem(position, false);
}
- السطر 3: نريد عرض الجزء رقم position؛
- السطر 4: يتم طلب هذا المقتطف من مدير المقتطفات ثم يتم تحديثه. في الواقع، منذ آخر مرة تم عرضها، قد تكون الجلسة قد تغيرت. يجب على المقتطف عندئذٍ فحص الجلسة لمعرفة ما إذا كان يجب تحديثه؛
- السطر 7: يتم عرض المقطع بواسطة [ViewPager]. وبما أن هذا الأخير قد تم ربطه بمدير المقاطع، فإن المقطع رقم [position] هو الذي سيتم عرضه، وهو المقطع الذي تم تحديثه للتو في السطر 4؛
ونختتم بذكر طريقتين لإدارة حالة الانتظار:
public void beginWaiting() {
// إدارة صورة الانتظار
loadingPanel.setVisibility(View.VISIBLE);
}
public void cancelWaiting() {
// إدارة صورة الانتظار
loadingPanel.setVisibility(View.INVISIBLE);
// نهاية التنفيذ
session.setOnAir(false);
session.setOperationStarted(false);
}
9.3.7.5. الجزء [RequestFragment]
الفئة [RequestFragment] هي كما يلي:
package android.aleas.fragments;
import android.aleas.R;
import android.aleas.activity.Constants;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import java.net.URI;
import java.net.URISyntaxException;
public class RequestFragment extends MyFragment {
// URL لخدمة الويب
private EditText edtUrlServiceRest;
private TextView txtMsgErreurUrlServiceWeb;
// عدد الطلبات
private EditText edtNbRequests;
private TextView txtErrorRequests;
// الفاصل الزمني للتوليد
private EditText edtA;
private EditText edtB;
private TextView txtErrorIntervalle;
// التأخير
private EditText edtMinDelay;
private EditText edtMaxDelay;
private TextView txtErrorDelay;
// عدد القيم التي تم إنشاؤها
private EditText edtMinCount;
private EditText edtMaxCount;
private TextView txtErrorCount;
// زر
private Button btnExecuter;
// قائمة الإجابات
private ListView listReponses;
private TextView infoReponses;
// عجلة اختيار الأمثلة
private Spinner spinnerExemples;
// المدخلات
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
// منشئ
public RequestFragment() {
super();
Log.d("rxjava", "RequestFragment constructor");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d("rxjava", "RequestFragment onCreateView");
// استرداد النشاط والجلسة
activity = (MainActivity) getActivity();
session = activity.getSession();
// إنشاء عرض الجزء بناءً على تعريفه XML
View view = inflater.inflate(R.layout.request, container, false);
// المكونات
edtUrlServiceRest = (EditText) view.findViewById(R.id.editTextUrlServiceWeb);
txtMsgErreurUrlServiceWeb = (TextView) view.findViewById(R.id.textViewErreurUrl);
edtNbRequests = (EditText) view.findViewById(R.id.edt_nbrequests);
txtErrorRequests = (TextView) view.findViewById(R.id.txt_error_nbrequests);
edtA = (EditText) view.findViewById(R.id.edt_a);
edtB = (EditText) view.findViewById(R.id.edt_b);
txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);
// زر [Exécuter]
btnExecuter.setVisibility(View.VISIBLE);
btnExecuter.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doExecuter();
}
});
// في البداية لا توجد رسائل خطأ
txtErrorRequests.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
txtErrorCount.setVisibility(View.INVISIBLE);
txtErrorDelay.setVisibility(View.INVISIBLE);
// مؤشر التحميل في الأمثلة
spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
// النتيجة
return view;
}
...
}
- السطر 16: الفئة [RequestFragment] تمتد من الفئة [MyFragment] (انظر الفقرة 9.3.7.1)؛
- الأسطر 18-42: المكونات المرئية للجزء (انظر الفقرة 9.3.7.2)؛
- الأسطر 45-52: المدخلات التي أدخلها المستخدم في النموذج؛
- يتم تنفيذ المنشئ (الأسطر 55-58) والطريقة [onCreateView] عندما تقوم النشاط [MainActivity] بإنشاء جميع أجزاء التطبيق. وتحدث هذه العملية مرة واحدة فقط؛
- السطر 61: كود الطريقة [onCreateView] تقليدي. تجدر الإشارة في السطر 102 إلى أن محول عنصر التمرير (spinner) في الأمثلة يتم استدعاؤه من الجلسة. كما تجدر الإشارة في السطر 91 إلى أن النقر على الزر [Exécuter] تتم معالجته بواسطة الطريقة [doExecuter]؛
- السطران 64-65: الحقلان [activity] و [session] ينتميان إلى الفئة الأم [MyFragment]؛
الطريقة [doExecuter] هي كما يلي:
// البيانات المدخلة
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
...
private void doExecuter() {
// هل البيانات صالحة؟
if (isPageValid()) {
// يتم تخزين المعلومات في الجلسة
session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
// يتم حفظ URL الخاص بخدمة الويب
activity.setUrlServiceWebJson(session.getUrlWebJson());
Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
// الإجراء قيد التنفيذ
session.setOnAir(true);
// ولكن لم تبدأ بعد
session.setOperationStarted(false);
// يتم عرض جزء من الرد
activity.selectTab(Constants.VUE_RESPONSE);
// بدء الانتظار
beginWaiting();
}
}
- السطر 15: لن نعلق على الطريقة [ispageValid]. فهي تتحقق من صحة المدخلات وتُرجع القيمة «true» فقط إذا كانت جميعها صحيحة. وفي هذه الحالة، تُستخدم هذه المدخلات لتهيئة الحقول في الأسطر 2-9؛
- السطر 17: يتم تخزين المدخلات المختلفة في الجلسة:
- [spinnerExemples.getSelectedItem().toString()] هو اسم المثال الذي اختاره المستخدم ويتم تخزينه في [session.exampleName]؛
- [spinnerExemples.getSelectedItemPosition() + 1] هو رقم الجزء المرتبط بالمثال والذي تم تخزينه (الجزء) بواسطة مدير الأجزاء. يتم تخزين هذا الرقم في [session.examplePosition]؛
- السطر 19: يتم إرسال l'URL من خدمة الويب / jSON إلى النشاط الذي يقوم بدوره بإرساله إلى الطبقة [DAO]؛
- الأسطر 21-24: نلاحظ أن عملية ما على وشك البدء؛
- السطر 26: سيتم عرض علامة تبويب الرد. لفهم ما سيحدث، يجب تذكر الرمز [MainActivity.selectTab]:
// اختيار علامة تبويب
public void selectTab(int position) {
// يوجد عديد علامات تبويب لا يتجاوز عدده اثنتين
// في البداية لا يوجد سوى علامة تبويب واحدة، وهي علامة تبويب الطلب
// إذا كانت علامة التبويب المطلوبة هي رقم 1 ولم تكن موجودة بعد، فيجب إنشاؤها
if (position == 1 && tabLayout.getTabCount() == 1) {
// علامة تبويب إضافية
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Response");
tabLayout.addTab(tab);
}
// يتم تحديد علامة التبويب برمجيًا، مما سيؤدي إلى تشغيل الحدث [onTabSelected]
// الذي سيقوم بربط العرض الصحيح بهذه العلامة
tabLayout.getTabAt(position).select();
}
- في البداية، لم تنشئ العملية سوى علامة تبويب الطلب (علامة التبويب رقم 0)؛
- الأسطر 6-11: يتم إنشاء علامة تبويب الاستجابة (علامة التبويب رقم 1) إذا لم تكن قد تم إنشاؤها؛
- السطر 14: يتم تحديد علامة التبويب رقم position (0 أو 1). يؤدي ذلك إلى وضع الحدث [onTabSelected] في قائمة انتظار حلقة الأحداث لتطبيق Android؛
مدير الحدث [onTabSelected] في [MainActivity] هو كما يلي:
@Override
public void onTabSelected(TabLayout.Tab tab) {
// تم تحديد علامة تبويب - يتم تغيير الجزء المعروض بواسطة حاوية الأجزاء
int position = tab.getPosition();
if (position == 0) {
// علامة تبويب الاستعلام
showView(0);
} else {
// علامة تبويب «الاستجابة» — تعتمد على المثال المختار
showView(session.getExamplePosition());
}
}
في حالة علامة التبويب [Response]، يتم تنفيذ السطر 9. سيتم عرض الجزء رقم [session.getExamplePosition()]. على سبيل المثال، بالنسبة للمثال [exemple-03]، الرقم الذي تم تسجيله في [session.examplePosition] هو 3. وبالتالي، يعرض السطر 10 الجزء رقم 3. جدول الأجزاء التي تم إنشاؤها مبدئيًا بواسطة النشاط هو [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. وبالتالي، فإن الجزء [Exemple03Fragment] هو الذي سيتم عرضه. ويتم عرضه بواسطة الكود التالي:
// عرض العرض رقم [position]
private void showView(int position) {
// يتم تحديث الجزء قبل عرضه
mSectionsPagerAdapter.getItem(position).onRefresh();
// يتم عرض العرض المطلوب - يتم الانتقال مباشرةً إلى العرض (المعلمة الثانية على القيمة false)
// بدون هذا المعامل، يتم الانتقال افتراضيًا إلى العرض المطلوب مع عرض سريع للعروض الوسيطة - سلوك غير مرغوب فيه
mViewPager.setCurrentItem(position, false);
}
نلاحظ أن الجزء سيتم تحديثه (السطر 4) قبل عرضه (السطر 7).
9.3.7.6. المقتطف [ResponseFragment]
تعرض الفئة [ResponseFragment] ردود الخادم. ورمزها هو التالي:
package android.aleas.fragments;
import android.aleas.R;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Subscription;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public abstract class ResponseFragment extends MyFragment {
// قائمة الإجابات
private ListView listReponses;
private TextView infoReponses;
// زر
private Button btnAnnuler;
// أداة التعيين jSON
private ObjectMapper mapper;
protected ResponseFragment() {
super();
Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
mapper = new ObjectMapper();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// يتم استرداد النشاط والجلسة
activity = (MainActivity) getActivity();
session = activity.getSession();
Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
// يتم إنشاء عرض الجزء بناءً على تعريفه XML
View view = inflater.inflate(R.layout.response, container, false);
// المكونات
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
// زر [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
btnAnnuler.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doAnnuler();
}
});
// النتيجة
return view;
}
...
// الطريقة المطلوب تنفيذها (بواسطة كود صريح) قبل كل عرض للجزء
public void onRefresh() {
...
}
}
- السطر 21: الفئة [ResponseFragment] تمتد من الفئة [MyFragment]؛
- الأسطر 23-27: مكونات الجزء؛
- الأسطر 32-36: لا يتم تنفيذ المنشئ إلا مرة واحدة، عند الإنشاء الأولي لأجزاء الأمثلة بواسطة النشاط. في الواقع، جميع أجزاء الأمثلة تمتد من الجزء [ResponseFragment]. عند إنشاء مثيلاتها، يتم استدعاء منشئ فئتها الأم [ResponseFragment]؛
- السطر 35: يقوم بتهيئة المُعَيِّن jSON الوارد في السطر 30 والمستخدم لعرض السلسلة jSON من مكدس الاستثناءات؛
- الأسطر 38-59: لا يتم تنفيذ الطريقة [onCreateView] إلا مرة واحدة، عند الإنشاء الأولي لأجزاء الأمثلة بواسطة النشاط. ويوجد فيها كود تقليدي في تطبيق Android؛
- الأسطر 52-56: الطريقة التي يتم تنفيذها عند النقر على الزر [Annuler] هي الطريقة [doAnnuler]؛
- الأسطر 62-64: يتم تنفيذ الطريقة [onRefresh] في كل مرة يتم فيها عرض علامة التبويب [Response]؛
بفضل السجلات المختلفة الموضوعة في الطرق المهمة، يمكننا رؤية ما يحدث عند بدء تشغيل التطبيق:
- السطر 1: إنشاء الجزء [RequestFragment]؛
- الأسطر 2-9: إنشاء أجزاء الأمثلة الأربعة للتطبيق؛
- السطر 10: تهيئة الجزء [RequestFragment]؛
- الأسطر 11-14: تهيئة الأجزاء الخاصة بالأمثلة الأربعة للتطبيق؛
بعد ذلك، لا تظهر أي استدعاءات لهذه الطرق مرة أخرى.
الطريقة [ResponseFragment.onRefresh] هي كما يلي:
// الطريقة المطلوب تنفيذها (بواسطة كود صريح) قبل كل عرض للجزء
public void onRefresh() {
Log.d("rxjava", String.format("ResponseFragment (%s) onRefresh for %s, sessionIsOnAir=%s session.isOperationStarted=%s%n", this, activity == null ? null : activity.getSession().getExampleName(), session.isOnAir(), session.isOperationStarted()));
// هل التنفيذ جارٍ؟
if (session.isOnAir() && !session.isOperationStarted()) {
// تنفيذ الطلب
session.setOperationStarted(true);
doExecuter();
}
}
- السطر 5: يتم التحقق مما إذا كان المقطع [RequestFragment] قد أرسل طلبًا (session.isOnAir) وما إذا كان هذا الطلب قد بدأ (isOperationStarted). إذا كان الجزء [RequestFragment] قد أرسل طلبًا ولم يكن هذا الطلب قيد التنفيذ بالفعل، يتم بدء العملية (السطران 7-8)؛
- وبمجرد بدء العملية، ونظرًا لأنها غير متزامنة، يمكن للمستخدم التنقل بين علامتي التبويب. وإذا انتقل مرة أخرى إلى علامة التبويب [Response] وكانت هناك عملية جارية، فلن يتم تنفيذ السطرين 7-8؛
تقوم الطريقة [doExecuter] في السطر 8 بتنفيذ العملية التي طلبها المستخدم:
private void doExecuter() {
Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
// بدء الانتظار
beginWaiting();
// التحضير للتنفيذ
subscriptions.clear();
reponses.clear();
nbInfos = 0;
// يتم إنشاء وتنفيذ الملاحظات الخاصة بالمثال المختار
createAndExecuteObservables();
}
// طريقة مُنفَّذة بواسطة الفئات الفرعية
protected abstract void createAndExecuteObservables();
- السطر 10: ينشئ الملاحظات وينفذها ويراقبها. وتختلف هذه الملاحظات باختلاف كل مثال. ولهذا السبب، فإن الطريقة [createAndExecuteObservables] هي طريقة مجردة (السطر 14). وسيتم تنفيذها بواسطة الأجزاء [ExampleXXFragment] التي توسع نطاق الفئة [ResponseFragment]؛
- السطر 6: يتم إفراغ قائمة الاشتراكات؛
- السطر 7: يتم إفراغ القائمة التي تعرض الردود؛
- السطر 8: يحسب عدد الردود المستلمة؛
تكلف الفئات الفرعية [ExampleXXFragment] الطريقة التالية [showAlea] بمهمة عرض العناصر التي تراقبها:
protected void showAlea(String data) {
// معلومة إضافية
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// رد واحد إضافي
reponses.add(0, data);
Log.d("rxjava", data);
// تحديث لـ UI
listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
- السطر 1: نلاحظ أن العنصر المراقب يصل في شكل سلسلة. وستكون هذه في الواقع السلسلة jSON الخاصة بالعنصر المراقب. وهذا يتيح لنا الحصول على طريقة عرض واحدة للعنصر المراقب بغض النظر عن نوعه الدقيق في Java؛
- السطر 6: يُضاف العنصر المراقب [data] في المرتبة الأولى من قائمة الردود. وبالتالي، يرى المستخدم في بداية القائمة أحدث الردود؛
تتم إدارة حالة الانتظار بواسطة الطريقتين التاليتين [beginWaiting] و [cancelWaiting]:
private void beginWaiting() {
// يتم عرض رمز الساعة الرملية
activity.beginWaiting();
// يتم عرض الزر [Annuler]
btnAnnuler.setVisibility(View.VISIBLE);
}
protected void cancelWaiting() {
// انتهى وقت الانتظار
activity.cancelWaiting();
// يتم إخفاء الزر [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
}
وهما تستخدمان الطرق التي تحمل نفس أسماء النشاط، ثم تقتصران على إظهار/إخفاء الزر [Annuler].
يتم إدارة النقر على الزر [Annuler] بواسطة الكود التالي:
protected void doAnnuler() {
// يتم إلغاء جميع الاشتراكات
for (Subscription s : subscriptions) {
if (!s.isUnsubscribed()) {
s.unsubscribe();
}
}
// انتهى الانتظار
cancelWaiting();
}
- الأسطر 3-7: يتم إلغاء جميع الاشتراكات واحدًا تلو الآخر؛
9.3.8. أمثلة على العناصر القابلة للمراقبة
9.3.8.1. Exemple-01
تتمثل وظيفة الفئات [ExampleXXFragment] في إنشاء القيم القابلة للمراقبة وتنفيذها ومراقبتها. يتم عرض القيم المراقبة بواسطة الفئة الأم [ResponseFragment].
الفئة [Example01Fragment] هي كما يلي:
![]() |
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import java.io.IOException;
public class Example01Fragment extends ResponseFragment {
// أدوات التعيين jSON
private ObjectMapper mapperAleasUiResponse;
// المنشئ
public Example01Fragment() {
super();
Log.d("rxjava", "Example01Fragment constructor");
// مرشحات jSON
mapperAleasUiResponse = new ObjectMapper();
}
@Override
public void createAndExecuteObservables() {
Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
// يُطلب توفير أرقام عشوائية
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// التكوين القابل للملاحظة رقم i
// طلب يُرسل إلى الخادم
Request request = session.getRequest();
request.setId(i);
// المراقب الذي يتم تنفيذه على مؤشر ترابط الحساب
observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
}
// مراقبة على مؤشر ترابط حلقة الأحداث؛
observable = observable.observeOn(AndroidSchedulers.mainThread());
// يتم تنفيذ جميع هذه المراقبات
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
...
}, new Action0() {
...
}
private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
// يتم استخراج المعلومات المراد عرضها
String data;
try {
data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
- السطر 36: القيمة القابلة للمراقبة الوحيدة التي سيتم إنشاؤها؛
- الأسطر 37-44: إنشاء وتكوين الملاحظات المختلفة التي يتم دمجها (السطر 43) في الملاحظة الموجودة في السطر 36؛
- السطر 43: يتم تنفيذ المراقب في مؤشر ترابط تابع للمجدول [Schedulers.io()]. وسيتم تنفيذ استدعاء HTTP إلى الخادم في هذا المؤشر الترابط؛
- السطر 46: تتم مراقبة المراقب النهائي في مؤشر الترابط الخاص بحلقة الأحداث؛
- الأسطر 48-57: تنفيذ القيم القابلة للمراقبة، وبالتالي الطلبات الموجهة إلى خادم الأرقام العشوائية. لا يدعم نظام Android حتى الآن Java 8 و lambdas الخاصة به. لذلك نستخدم هنا فئات مجهولة لإنشاء مثيلات للواجهات الوظيفية لـ RxJava؛
- الأسطر 49-52: الإجراء الذي يتم تنفيذه عندما يتلقى المراقب عنصرًا جديدًا من النوع [AleasDaoResponse] من العنصر القابل للمراقبة (انظر الفقرة 9.3.6.1)؛
- السطر 51: استدعاء الطريقة [showAlea] من الفئة الأم. تجدر الإشارة إلى أنها تتوقع سلسلة أحرف. ويتم توفير هذه السلسلة بواسطة الطريقة [getDataFrom] في الأسطر 59-68؛
- السطر 63: يتم إرجاع السلسلة jSON من النوع [AleasUiResponse] على النحو التالي:
package android.aleas.fragments;
import android.aleas.dao.AleasDaoResponse;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class AleasUiResponse {
// الاستجابة [DAO]
private AleasDaoResponse aleasDaoResponse;
// خيط المراقبة
private String observedOn;
// وقت المراقبة
private String observedAt;
// المنشئات
public AleasUiResponse() {
observedOn = Thread.currentThread().getName();
observedAt = new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
public AleasUiResponse(AleasDaoResponse aleasDaoResponse, String on, String at) {
this.aleasDaoResponse = aleasDaoResponse;
this.observedOn = on;
this.observedAt = at;
}
public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
this();
this.aleasDaoResponse = aleasDaoResponse;
}
// أدوات الاسترجاع والتعيين
...
}
- إلى استجابة الطبقة [DAO] (السطر 11)، تُضاف معلومتان:
- السطر 13: مؤشر تسلسل المراقبة؛
- السطر 15: وقت المراقبة؛
لنعد إلى رمز الاشتراك:
@Override
public void createAndExecuteObservables() {
...
// يتم تنفيذ جميع هذه العناصر القابلة للمراقبة
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// يتم عرض الاستثناء
showAlea(getMessagesFromThrowable(th));
// بعد تلقي استثناء، لا يتلقى المراقب لا onNext ولا onCompleted
// اضطررت إلى إلغاء الاشتراك يدويًّا
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// انتهى الانتظار
cancelWaiting();
}
}));
}
- الأسطر 11-18: الحالة التي يتلقى فيها المراقب استثناءً؛
- السطر 14: نستخدم مرة أخرى الطريقة [showAlea] من الفئة الأم لعرض الاستثناء. الطريقة [getMessagesFromThrowable] هي طريقة تابعة للفئة الأم [ResponseFragment] التي تنتج سلسلة أحرف بناءً على استثناء:
// رسائل استثناء
protected String getMessagesFromThrowable(Throwable ex) {
// يتم إنشاء قائمة برسائل الخطأ من مكدس الاستثناءات
List<String> messages = new ArrayList<String>();
Throwable th = ex;
while (th != null) {
messages.add(String.format("[%s, %s]", th.getClass().getName(), th.getMessage()));
th = th.getCause();
}
try {
return mapper.writeValueAsString(messages);
} catch (IOException e) {
return e.getMessage();
}
}
- السطر 11: يتم إرجاع السلسلة jSON من قائمة رسائل الخطأ (السطر 4)؛
لنعد إلى كود الاشتراك في المراقب:
- الأسطر 19-25: الكود الذي يتم تنفيذه عندما يتلقى المراقب إشعار انتهاء البث. عندها يتم إلغاء الانتظار (السطر 23) مما يؤدي إلى تحديث واجهة المستخدم الرسومية؛
ينتج عن تنفيذ المثال 01 نتيجة مشابهة لما يلي:

كل عنصر في القائمة هو السلسلة jSON لقيمة ملاحظة. حقول السلسلة jSON هي كما يلي:
- aleas: قائمة الأرقام العشوائية التي يقدمها الخادم؛
- idClient: رقم الاستعلام (يمكن ملاحظة أن الردود عادت بترتيب غير متسلسل)؛
- on: مؤشر ترابط تنفيذ المراقب الذي أصدر هذه القيمة؛
- requestAt: وقت طلب العميل؛
- responseAt: وقت استجابة الخادم؛
- delay: فترة الانتظار التي لاحظها الخادم؛
- erreur: رمز الخطأ الذي أرسله الخادم (0 = لا يوجد خطأ)؛
- message: رسالة الخطأ التي أرسلها الخادم (null = لا يوجد خطأ)؛
- observedAt: وقت رصد القيمة الملاحظة؛
- observedOn: مؤشر ترابط رصد القيمة الملاحظة؛
9.3.8.2. Exemple-02
الفئة [Example02Fragment] هي كما يلي:
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import java.io.IOException;
public class Example02Fragment extends ResponseFragment {
// مُخَطِّطو jSON
private ObjectMapper mapperAleasUiResponse;
// منشئ
public Example02Fragment() {
super();
Log.d("rxjava", "Example02Fragment constructor");
// مرشح jSON
mapperAleasUiResponse = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
// يتم طلب الأرقام العشوائية
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// تحضير الطلب
Request request = session.getRequest();
request.setId(i);
// يتم الاحتفاظ فقط بالملاحظات التي لها رقم عميل زوجي
observable = observable
.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
})
// التنفيذ على مؤشر ترابط الإدخال/الإخراج
.subscribeOn(Schedulers.io()));
}
// المراقبة على مؤشر ترابط حلقة الأحداث
observable = observable.observeOn(AndroidSchedulers.mainThread());
// يتم تنفيذ هذه المراقبات
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// نهاية الانتظار
cancelWaiting();
}
}));
}
private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
// استخراج المعلومات المراد عرضها
String data;
try {
data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
هذا المثال مشابه للمثال السابق (السطر 38). ولكن من بين القيم الملاحظة التي تم الحصول عليها في المثال السابق، لا يتم الاحتفاظ إلا بالقيم الملاحظة التي لها رقم عميل زوجي (الأسطر 42-46)، وذلك بفضل الطريقة [filter] (السطر 41).
والنتائج التي تم الحصول عليها هي كما يلي (لـ 10 طلبات):

9.3.8.3. Exemple-03
الفئة [Example03Fragment] هي كما يلي:
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import java.io.IOException;
import java.util.List;
public class Example03Fragment extends ResponseFragment {
// مُخَطِّطو jSON
private ObjectMapper mapper;
// منشئ
public Example03Fragment() {
super();
Log.d("rxjava", "Example03Fragment constructor");
// مرشح jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// يتم طلب الأرقام العشوائية
Observable<List<Integer>> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// تحضير الطلب
Request request = session.getRequest();
request.setId(i);
// تكوين القابل للمراقبة
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
}).map(new Func1<AleasDaoResponse, List<Integer>>() {
@Override
public List<Integer> call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getAleas();
}
})
// التنفيذ على مؤشر ترابط الإدخال/الإخراج
.subscribeOn(Schedulers.io()));
}
// المراقبة على مؤشر ترابط حلقة الأحداث
observable = observable.observeOn(AndroidSchedulers.mainThread());
// يتم تنفيذ هذه المراقبات
subscriptions.add(observable
.subscribe(new Action1<List<Integer>>() {
@Override
public void call(List<Integer> aleas) {
showAlea(getDataFrom(aleas));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// نهاية الانتظار
cancelWaiting();
}
}
));
}
private String getDataFrom(List<Integer> aleas) {
// استخراج المعلومات المراد عرضها
String data;
try {
data = mapper.writeValueAsString(aleas);
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
هذا المثال مشابه للمثال-02:
- السطر 40: يتم تعريف نفس المتغيرات القابلة للرصد كما في المثال-02؛
- السطر 45: يتم تحويل كل قيمة من القيم الصادرة عن المتغيرات القابلة للمراقبة السابقة، باستخدام الطريقة [map]، إلى نوع List<Integer> الذي يمثل قائمة الأرقام العشوائية التي يولدها الخادم؛
- السطر 58: أصبحت القيمة الملاحظة الآن من النوع List<Integer>؛
والنتيجة التي تم الحصول عليها لـ 10 طلبات هي كما يلي:

9.3.8.4. Exemple-04
الفئة [Example04Fragment] هي كما يلي:
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
public class Example04Fragment extends ResponseFragment {
// مُخَطِّطو jSON
private ObjectMapper mapper;
// منشئ
public Example04Fragment() {
super();
Log.d("rxjava", "Example04Fragment constructor");
// مرشح jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// يتم طلب الأرقام العشوائية
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// إعداد الطلب
Request request = session.getRequest();
request.setId(i);
// تكوين المتغيرات القابلة للمراقبة
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
}).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
@Override
public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
return Observable.from(aleasDaoResponse.getAleas());
}
})
// التنفيذ على خيط إدخال/إخراج
.subscribeOn(Schedulers.io()));
}
// المراقبة على مؤشر ترابط حلقة الأحداث
observable = observable.observeOn(AndroidSchedulers.mainThread());
// تنفيذ هذه المراقبات
subscriptions.add(observable
.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
showAlea(String.valueOf(alea));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// نهاية الانتظار
cancelWaiting();
}
}
));
}
}
هذا المثال مشابه للمثال-03، باستثناء أنه بدلاً من استخدام الطريقة [map] في السطر 42، يتم استخدام الطريقة [flatMap].
- السطر 55: تجدر الإشارة إلى أن نوع القيمة الملاحظة أصبح الآن Integer؛
بالنسبة لـ 10 استعلامات، نحصل على النتائج التالية:

هذه المرة، لدينا قيم ملاحظة أكثر من عدد الاستعلامات.
9.3.8.5. Exemple-05
نعرض الآن الإجراء الذي يجب اتباعه لإضافة مثال جديد للملاحظات إلى التطبيق.
لنفترض أننا نريد تكرار المثال [Exemple22h] الوارد في الفقرة 7.6.4:
package dvp.rxjava.observables.exemples;
import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;
public class Exemple22h {
public static void main(String[] args) throws InterruptedException {
// العملية
Observable<GroupedObservable<Boolean, Integer>> obs = Observable.range(1, 10).groupBy(i -> i % 2 == 0);
Process<Integer> process = new Process<>("process", obs.concatMap(g -> g.asObservable()));
// الاشتراكات
ProcessUtils.subscribe(1, process);
}
}
- يتم أولاً تجميع قيم المراقب [Observable.range(1, 10)] إلى قيم زوجية وفردية بواسطة الطريقة [groupBy] (السطر 11)، ثم يتم تجميعها في مراقب واحد بواسطة الطريقة [concatMap] (السطر 12)؛
الخطوة 1
يتم إنشاء مثال جديد في الملف [exemples.xml]:
![]() |
<!-- أمثلة -->
<resources>
<string-array name="exemples">
<item>Exemple-01</item>
<item>Exemple-02</item>
<item>Exemple-03</item>
<item>Exemple-04</item>
<item>Exemple-05</item>
</string-array>
</resources>
في الأعلى، تمت إضافة السطر 8. يمكن تسمية المثال بأي اسم.
الخطوة 2
نقوم بنسخ الفئة [Example04Fragment] إلى [Example05Fragment]. هنا يكون الاسم محددًا.
الخطوة 3
نقوم بتعديل رمز [Example05Fragment] على النحو التالي:
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.GroupedObservable;
import rx.schedulers.Schedulers;
public class Example05Fragment extends ResponseFragment {
// أدوات التعيين jSON
private ObjectMapper mapper;
// المنشئ
public Example05Fragment() {
super();
Log.d("rxjava", "Example05Fragment constructor");
// مرشح jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
// مثيلات الواجهات الوظيفية
// مرشح
Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
};
// flatMap
Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
@Override
public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
return Observable.from(aleasDaoResponse.getAleas());
}
};
// groupBy
Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer integer) {
return integer % 2 == 0;
}
};
// concatMap
Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
return integerIntegerGroupedObservable.asObservable();
}
};
// يُطلب إدخال الأرقام العشوائية
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// إعداد الطلب
Request request = session.getRequest();
request.setId(i);
// تكوين المراقبة
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
.groupBy(groupBy).concatMap(concatMap)
// التنفيذ على مؤشر ترابط الإدخال/الإخراج
.subscribeOn(Schedulers.io());
}
// المراقبة على مؤشر ترابط حلقة الأحداث
observable = observable.observeOn(AndroidSchedulers.mainThread());
// يتم تنفيذ هذه المراقبات
subscriptions.add(observable
.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
showAlea(String.valueOf(alea));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// نهاية الانتظار
cancelWaiting();
}
}
));
}
}
- السطر 67: يمثل المتغير القابل للمراقبة في المثال 04: تدفق من الأعداد الصحيحة؛
- السطر 68: نقوم بتجميع تدفق الأعداد الصحيحة هذا وفقًا لمعيار منطقي سنقوم بتعريفه. سنحصل على متغير من النوع Observable<GroupedObservable<Boolean, Integer>> الذي ينتج عناصر من النوع GroupedObservable<Boolean, Integer>؛
- السطر 68: ستنتج الطريقة [concatMap] عناصر من النوع Integer انطلاقًا من العناصر من النوع GroupedObservable<Boolean, Integer>؛
- الأسطر 32-59: لتسهيل قراءة عملية إنشاء المراقب في الأسطر 67-69، قمنا بفصل مثيلات الواجهات الوظيفية التي تحتاجها مختلف المشغلات [filter, flatMap, groupBy, concatMap]؛
- الأسطر 47-52: تتوقع الطريقة [groupBy] معلمة من النوع Func1<T,K> حيث T هو نوع العناصر المجمعة وK هو نوع معيار التجميع. انطلاقًا من العنصر T، تتولى مثيل Func1<T,K> مهمة إنتاج مفتاح التجميع K للعنصر؛
- الأسطر 48-51: سيتم تجميع العناصر من النوع Integer حسب التكافؤ. تنتج المثيل Func1<Integer,Boolean> المفتاح true أو false حسب ما إذا كان يجب وضع العنصر في المجموعة الأولى أم الثانية. في النهاية، نحصل على مجموعتين: مجموعة العناصر الزوجية ذات المفتاح true ومجموعة العناصر الفردية ذات المفتاح false؛
- الأسطر 53-59: تتوقع الدالة [concatMap] معلمة من النوع Func1<T,Observable<R>> وتنتج متغيرًا قابلًا للمراقبة من العناصر من النوع R. سيكون النوع T هنا هو النوع الذي يصدره المشغل [groupBy]، وهو في هذه الحالة نوع GroupedObservable<Boolean, Integer>؛
- السطر 57: من العنصر من النوع [GroupedObservable<Boolean, Integer>]، يتم إنتاج نوع Observable<Integer>. ونظرًا لأن المشغل [groupBy] أنتج مجموعتين، فإن المشغل [concatMap] سينتج عنصرين قابلين للمراقبة من النوع [Observable<Integer>]. وكما هو الحال مع [flatMap]، فإنه سيقوم بتسوية هاتين المجموعتين إلى متغير قابل للمراقبة واحد. ولكن على عكس [flatMap]، فإنه لا يخلط عناصر المتغيرات القابلة للمراقبة التي تم تسويتها. لذا يجب أن نلاحظ مجموعتين منفصلتين: الأعداد العشوائية الزوجية والأعداد العشوائية الفردية.
الخطوة 4
نقوم بتشغيل التطبيق:

ونحصل على النتائج التالية:

- في [1]، الأرقام العشوائية الزوجية، وفي [2] الأرقام العشوائية الفردية؛
9.3.8.6. للمتابعة
يُدعى القارئ الآن إلى إنشاء أمثلة خاصة به، وكذلك تجربة قيم مختلفة للبيانات التي يتم إدخالها في النموذج الذي يحدد الاستعلامات الموجهة إلى خادم الأرقام العشوائية.
9.3.9. الخلاصة
لقد أنشأنا في بيئة Android البنية التالية:
عميل Android:
![]() |
تتواصل الطبقة [DAO] مع الخادم الذي يولد الأرقام العشوائية التي تعرضها اللوحة اللوحية التي تعمل بنظام أندرويد. ويتميز هذا الخادم ببنية من طبقتين على النحو التالي:
![]() |
كانت الطبقة [DAO] تُرسل n طلبات HTTP إلى خادم الأرقام العشوائية، بينما كانت الطبقة [swing] تنتظر نتائج هذه الطلبات بشكل غير متزامن لعرضها. كانت هذه الطلبات n HTTP موجهة إلى نفس الخادم الذي كان يقدم نفس أنواع الردود. وقد سمح لنا ذلك بدمج (mergeWith) الردود في متغير قابل للمراقبة واحد.
في الواقع، تتواصل تطبيقات Android مع خوادم مختلفة، ومن غير المرجح أن يتم دمج ردودها. سيتم التعامل مع الطلبات HTTP الموجهة إلى هذه الخوادم بشكل مستقل عن بعضها البعض، وسيتم مراقبة نتائجها باستخدام طرق منفصلة.

















































