9. RxJava في بيئة Android
9.1. مقدمة
سنعيد هنا النظر في تطبيق تمت مناقشته بالفعل في عدة وثائق:
- [Android لمطوري JEE: نموذج غير متزامن لعملاء Android] (الفصل 4)؛
- [مقدمة إلى برمجة أجهزة Android اللوحية من خلال الأمثلة] (الفصل 9)؛
- [مقدمة إلى برمجة أجهزة Android اللوحية من خلال الأمثلة - الإصدار 2] (القسم 1.11)؛
يغطي هذا الفصل تطبيق عميل/خادم حيث يقوم الخادم بتسليم أرقام عشوائية بشكل غير متزامن يعرضها عميل أندرويد:
- في الوثيقة 1، يستخدم عميل أندرويد تقنية غير قياسية؛
- في الوثيقة 2، يستخدم عميل Android تقنية Android القياسية للعمليات غير المتزامنة؛
- في الوثيقة 3، يستخدم عميل Android نفس التقنية المستخدمة في الوثيقة 2 ولكن بشكل مبسط من خلال استخدام التعليقات التوضيحية من مكتبة التعليقات التوضيحية لـ Android؛
عميل Android هو كما يلي:
![]() |
تتواصل طبقة [DAO] مع الخادم الذي يولد الأرقام العشوائية التي يعرضها جهاز Android اللوحي. ويتميز هذا الخادم بالبنية ذات المستويين التالية:
![]() |
يقوم العملاء بالاستعلام عن عناوين URL معينة في طبقة [الويب / JSON] ويتلقون استجابة نصية بتنسيق JSON (ترميز كائنات JavaScript).
سنقسم تحليل التطبيق إلى خطوتين:
خادم الويب/JSON
- طبقة [الأعمال] الخاصة به؛
- خدمة [الويب / JSON] الخاصة به والتي تم تنفيذها باستخدام Spring MVC؛
عميل Android
- طبقة [DAO] الخاصة به؛
- نشاطها؛
- طرق العرض الخاصة به؛
9.2. خدمة الويب / JSON
ملاحظة: يتم تنفيذ خدمة الويب / JSON باستخدام تقنية Spring MVC. يمكن للقراء غير المعتادين على هذه التقنية إما:
- قراءة القسم 9.2.1، الذي يشرح كيفية تشغيل الخادم وكيفية الاستعلام عنه؛
- الرجوع إلى الوثيقة [Spring MVC و Thymeleaf بالأمثلة]، ولا سيما الفصل 4، الذي يعرض التعليقات التوضيحية الرئيسية المستخدمة في الكود؛
9.2.1. مشروع IntelliJ IDEA
تتميز خدمة الويب / JSON بالبنية التالية:
![]() |
يتم تنفيذ هذه البنية من خلال مشروع IntelliJ IDEA التالي [1]:
![]() | ![]() |
يتم تشغيل الخادم عبر [2-3]. ثم يتم عرض سجلات وحدة التحكم:
- السطر 12: يشير إلى أن الخدمة متاحة على المنفذ 8080؛
- السطر 10: عنوان URL الفريد لخدمة الويب / JSON المتاحة عبر عملية HTTP GET. معلماتها هي كما يلي:
- [a,b]: نطاق لتوليد أرقام عشوائية؛
- [minCount, maxCount]: عدد الأرقام العشوائية التي يتم إنشاؤها، حيث count هو رقم عشوائي في الفاصل [minCount, maxCount]؛
- [minDelay, maxDelay]: تنتظر الخدمة تأخيرًا بمقدار ميلي ثانية قبل إرجاع الأرقام المطلوبة، حيث يكون التأخير رقمًا عشوائيًا في [minDelay, maxDelay]؛
في المتصفح، دعونا نطلب عنوان URL هذا:
![]() |
طلبنا:
- أرقام عشوائية في الفترة [100، 200]؛
- n أرقام عشوائية حيث n تقع في الفترة [10، 20]؛
- وقت انتظار يبلغ x مللي ثانية، حيث x يقع في الفترة [300، 400]؛
في الرد:
- aleas: قائمة بالأرقام العشوائية التي تم إنشاؤها؛
- delay: وقت الانتظار بالمللي ثانية الذي حدده الخادم؛
- error: رمز خطأ — 0 في حالة عدم وجود خطأ؛
- message: رسالة خطأ - null في حالة عدم وجود خطأ؛
9.2.2. تبعيات Gradle للمشروع
![]() |
مشروع [server] هو مشروع Gradle تم تكوينه بواسطة ملف [build.gradle] التالي [1]:
// généré par http://start.spring.io/ (mai 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 بسيط. استنادًا إلى المكتبات الموجودة في مسار فئات المشروع، يستنتج [Spring Boot] تكوينًا معقولًا أو محتملًا للمشروع. وبالتالي، إذا كان Hibernate موجودًا في مسار فئات المشروع، فسيستنتج [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. طبقة [الأعمال]
![]() |
![]() |
ستحتوي طبقة [الأعمال] على واجهة [IMetier] التالية:
package dvp.rxjava.server.metier;
public interface IMetier {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [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 {
// fields
private int delay;
private List<Integer> aleas;
// manufacturers
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;
}
// getters and setters
...
}
فيما يلي كود فئة [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) {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [minDelay, maxDelay]
// some checks
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;
}
// mistakes?
if (!messages.isEmpty()) {
throw new AleasException(String.join(" [---] ", messages), erreur);
}
// random number generator
Random random = new Random();
// waiting?
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);
}
}
// result generation
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 result
return new AleasMetier(delay,nombres);
}
}
لن نعلق على الفئة: فهي مشابهة لتلك التي صادفناها في بيئة Swing في القسم 8.4. سنكتفي بالإشارة إلى النقاط التالية:
- السطر 10: تعليق Spring [@Service]، الذي يجعل Spring يقوم بإنشاء مثيل للفئة كمثيل واحد (singleton) ويجعل مرجعها متاحًا لمكونات Spring الأخرى. كان من الممكن استخدام تعليقات Spring أخرى هنا لتحقيق نفس التأثير؛
- السطران 13-14: يتم حقن مخطط JSON. Spring عبارة عن حاوية كائنات. يتم إنشاء مثيل لهذه الحاوية عند بدء تشغيل تطبيق الويب، ثم يتم إنشاء مثيلات للكائنات المحددة بواسطة ملف التكوين، بشكل افتراضي كمثيل واحد (singleton). يمكن أن يحتوي كائن Spring الفردي (singleton) على مراجع إلى كائنات Spring أخرى. وهذا هو الحال هنا: سيحتوي الكائن الفردي [business] (السطران 10-11) على مرجع إلى الكائن الفردي [mapper] (السطران 13-14). وهذا ما يُسمى حقن التبعية. هناك طريقتان لحقن كائن فردي في كائن فردي آخر:
- حسب النوع: هذا ممكن إذا كان الكائن الفردي المراد حقنه هو الكائن الوحيد في Spring من هذا النوع. وهذا هو الحال هنا بالنسبة للحقن في الأسطر 13-14 (النوع ObjectMapper)؛
- حسب الاسم إذا كان هناك عدة كائنات Spring من نفس النوع. في هذه الحالة، يجب إضافة التعليق التوضيحي @Qualifier("singletonName") لتحديد اسم الكائن الفردي؛
تقوم فئة [Metier] بإلقاء استثناءات من النوع [AleaException]:
package android.exemples.server.metier;
public class AleaException extends RuntimeException {
// error code
private int code;
// manufacturers
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// getters and setters
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 إلى الإجراء الذي يجب أن يعالجه. هذه الإجراءات هي طرق لفئات محددة تسمى [Controllers]. الحرف C في MVC هنا هو السلسلة [Dispatcher Servlet، Controller، Action]. إذا لم يتم تكوين أي إجراء لمعالجة عنوان URL الوارد، فسيرد [Dispatcher Servlet] بأن عنوان URL المطلوب لم يتم العثور عليه (خطأ 404 NOT FOUND)؛
- معالجة
- يمكن أن تستخدم الإجراء المحدد المعلمات التي مررها [Dispatcher Servlet] إليه. يمكن أن تأتي هذه المعلمات من عدة مصادر:
- مسار [/param1/param2/...] لعنوان URL،
- معلمات عنوان URL [p1=v1&p2=v2]،
- من المعلمات التي أرسلها المتصفح مع طلبه؛
- عند معالجة طلب المستخدم، قد يحتاج الإجراء إلى طبقة [الأعمال] [2b]. بمجرد معالجة طلب العميل، قد يؤدي ذلك إلى استجابات متنوعة. ومن الأمثلة الكلاسيكية على ذلك:
- صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
- صفحة تأكيد في الحالات الأخرى
- يُصدر الإجراء تعليمات لعرض طريقة عرض محددة [3]. ستعرض طريقة العرض هذه البيانات المعروفة باسم نموذج العرض. هذا هو الحرف M في MVC. سيقوم الإجراء بإنشاء نموذج M هذا [2c] وإصدار تعليمات لعرض طريقة عرض V [3]؛
- الاستجابة - تستخدم طريقة العرض V المحددة النموذج M الذي أنشأته الإجراء لتهيئة الأجزاء الديناميكية من استجابة HTML التي يجب إرسالها إلى العميل، ثم ترسل هذه الاستجابة.
بالنسبة لخدمة الويب / JSON، يتم تعديل البنية السابقة بشكل طفيف:
![]() |
- في [4a]، يتم تحويل النموذج، وهو فئة Java، إلى سلسلة JSON بواسطة مكتبة JSON؛
- في [4b]، يتم إرسال سلسلة JSON هذه إلى المتصفح؛
لنعد إلى طبقة [الويب] في تطبيقنا:
![]() |
في تطبيقنا، يوجد وحدة تحكم واحدة فقط:
![]() |
سترسل خدمة الويب / JSON إلى عملائها استجابة من النوع [AleasResponse] على النحو التالي:
package dvp.rxjava.server.web;
import dvp.rxjava.server.metier.AleasMetier;
public class AleasResponse extends AleasMetier {
// error code
private int erreur;
// error message
private String message;
// manufacturers
public AleasResponse() {
}
public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
super(aleasMetier);
this.erreur = erreur;
this.message = message;
}
// getters and setters
public void setAleasMetier(AleasMetier aleasMetier) {
this.setDelay(aleasMetier.getDelay());
this.setAleas(aleasMetier.getAleas());
}
...
}
- السطر 5: تمتد فئة [AleasResponse] من فئة [AleasMetier] وبالتالي ترث جميع سماتها (aleas، delay)؛
- السطر 8: رمز خطأ (0 في حالة عدم وجود خطأ)؛
- السطر 10: إذا كان الخطأ != 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 {
// business layer
@Autowired
private IMetier metier;
@Autowired
private ObjectMapper mapper;
// random numbers in [a,b]
// n numbers are generated with n in the range [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is a random number in the range [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 {
// we prepare the answer
AleasResponse response = new AleasResponse();
// the business layer is used to generate random numbers
try {
response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
} catch (AleasException e) {
// case of error (code and message)
response.setErreur(e.getCode());
response.setMessage(e.getMessage());
}
// we return the answer 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 (سمة الطريقة)؛
- السطر 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: نطلب قائمة بالأرقام العشوائية من طبقة [business]. تذكر أن طريقة [business].getRandom يمكن أن ترمي استثناءً؛
- السطران 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 {
// -------------------------------- layer configuration [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);
}
// mapper 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) {
// application execution
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، والذي يمكن العثور عليه، على سبيل المثال، في [مقدمة إلى برمجة أجهزة Android اللوحية باستخدام Android Studio].
ActivityViewsLayer[DAO]UserServer
سيحتوي العميل على مكونين:
- طبقة [العرض] (طرق العرض + النشاط)؛
- طبقة [DAO] تتواصل مع خدمة [الويب / JSON] التي درسناها سابقًا.
9.3.1. RxAndroid
للتواصل بشكل غير متزامن مع خادم الأرقام العشوائية، سيستخدم عميل Android مكتبة RxAndroid. توسع هذه المكتبة RxJava لتشمل نظام Android. كما فعلنا مع تطبيق Swing، سنستخدم امتدادًا واحدًا فقط مقدمًا من RxAndroid: المجدول [AndroidSchedulers.mainThread()]. تتبع واجهة المستخدم الرسومية في Android نفس القواعد التي تتبعها واجهة Swing:
- تتم معالجة الأحداث في مؤشر ترابط واحد يسمى حلقة الأحداث أو مؤشر ترابط واجهة المستخدم؛
- عندما يؤدي حدث ما إلى تشغيل إجراءات غير متزامنة، يجب استرداد نتائج تلك الإجراءات في مؤشر ترابط واجهة المستخدم إذا كان سيتم استخدامها لتحديث واجهة المستخدم؛
عميل Android:
- سيرسل طلبات غير متزامنة متعددة إلى خادم الأرقام العشوائية. سيتم تنفيذ هذه الطلبات على جانب العميل باستخدام خيوط المجدول [Schedulers.io()]؛
- ستُرجع هذه الطلبات غير المتزامنة عناصر قابلة للمراقبة سيتم دمجها في عنصر واحد قابل للمراقبة؛
- سيتم مراقبة هذه القيمة القابلة للمراقبة على جانب العميل في المجدول [AndroidSchedulers.mainThread()] المقدم من RxAndroid؛
9.3.2. مشروع IntelliJ IDEA
يُسمى مشروع Android [client]:
![]() | ![]() ![]() |
سيتم تشغيله عبر [2].
ملاحظة: يعتمد التنفيذ بشكل كبير على تكوين بيئة تطوير IntelliJ IDEA المستخدمة. من المحتمل ألا يعمل التنفيذ [2] أعلاه من المحاولة الأولى على جهاز غير جهازي. قد يكون تكوين بيئة تطوير IntelliJ IDEA بشكل صحيح لتشغيل هذا المشروع مهمة شاقة للمبتدئين. فيما يلي بعض النقاط التي يجب الانتباه إليها:
- في [3]، قم بالوصول إلى بنية المشروع؛
![]() | ![]() |
- في [4-5]، JDK و Android SDKs المثبتة على جهازي. لاحظ أن JDK 1.8 ليست ضرورية. لا يدعم Android بعض ميزات Java 8، بما في ذلك lambdas. لذلك، لإنشاء مثيلات للواجهات الوظيفية، سنستخدم الفئات المجهولة. وبالتالي، يكفي استخدام JDK 1.6. ومع ذلك، تم تكوين المشروع كما تم توزيعه باستخدام JDK 1.8؛
فيما يلي ملف [build.gradle] [6] الذي يُستخدم لتكوين مشروع Android:
buildscript {
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
// replace with the current version of the Android plugin
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'
}
}
قد يتعين تعديل الإصدارات الواردة في الأسطر 8 و24-25 و29، وذلك حسب حزم أدوات تطوير تطبيقات Android (SDK) المتوفرة.
لتثبيت حزم تطوير برامج Android جديدة، استخدم SDK Manager كما يلي [1]:
![]() ![]() | ![]() |
تم تكوين المشروع لـ:
- SDK API 23 [2]؛
- أدوات إنشاء SDK 23.0.3 [3]؛
- أداة SDK 25.1.3 [4]
أخيرًا، تحقق من مسار Android SDK في ملف [local.properties] [4]، السطر 11 أدناه:
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Thu Apr 07 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk
9.3.3. تشغيل مشروع IntelliJ IDEA
بمجرد إنشاء بيئة مناسبة للمشروع، يمكن تشغيله على النحو التالي:
![]() | ![]() | ![]() |
- في [1]، قم بتشغيل محاكي Genymotion Android؛
- في [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: وقت الانتظار الذي لاحظه الخادم قبل إرجاع الرد؛
- error: رمز الخطأ — 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) التي تستخدمها طبقة واجهة المستخدم للتواصل مع طبقة [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 {
// random numbers in the [a,b] interval
// n numbers are generated with n itself a random number in the interval [minCount, maxCount]
// numbers are generated after a delay of milliseconds,
// where [delay] is itself a random number in the interval [minDelay, maxDelay]
public Observable<AleasDaoResponse> getAleas(final Request request);
// URL of the web service
public void setUrlServiceWebJson(String url);
// max wait time (ms) for server response to connection request
// max wait time (ms) for server response to a request
public void setClientTimeouts(int connectTimeout, int readTimeOut);
}
- السطر 12: طريقة طبقة [DAO] التي تولد أرقامًا عشوائية بشكل غير متزامن؛
- السطر 15: لتزويد تطبيق [DAO] بعنوان URL لخدمة توليد الأرقام العشوائية؛
- السطر 19: لتعيين الحد الأقصى لفترات الانتظار لتنفيذ [DAO]، لتجنب فترات الانتظار الطويلة للغاية عندما لا يستجيب الخادم؛
تستقبل طريقة [getAleas] جميع معلماتها في كائن [Request] التالي:
package android.aleas.fragments;
public class Request {
// request no
int id;
// user input
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// manufacturers
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;
}
// getters and setters
...
}
هنا، يمكننا رؤية معظم المعلمات من عنوان URL للخادم التي يجب الاستعلام عنها.
تُرجع الطريقة [getAleas] نوع Observable<AleasDaoResponse>، حيث تكون فئة [AleasDaoResponse] كما يلي:
package android.aleas.dao;
import java.util.List;
public class AleasDaoResponse {
// error code
private int erreur;
// error message
private String message;
// server waiting time
private int delay;
// random numbers delivered by the server
private List<Integer> aleas;
// customer status
private ClientState clientState;
// manufacturers
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;
}
// getters and setters
...
}
نوع [ClientState] هو كما يلي:
package android.aleas.dao;
import org.codehaus.jackson.map.annotate.JsonFilter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class ClientState {
// name of execution thread
private String on;
// query time
private String requestAt;
// response time
private String responseAt;
// customer id
private int idClient;
// manufacturer
public ClientState() {
on = Thread.currentThread().getName();
requestAt = getTimeStamp();
}
public ClientState(int idClient) {
this();
this.idClient = idClient;
}
// private methods
private String getTimeStamp() {
return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
// getters and setters
...
}
- السطر 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 {
// customer REST
private RestTemplate restTemplate;
// URL service
private String urlServiceWebJson;
// mapper jSON
private ObjectMapper mapper;
// manufacturers
public Dao() {
// mapper jSON
mapper = new ObjectMapper();
}
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// set the URL of the REST service
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] هي كما يلي:
// client REST
private RestTemplate restTemplate;
...
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
// on fixe le timeout des requêtes du client 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()));
// service execution
return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
@Override
public void call(Subscriber<? super AleasDaoResponse> subscriber) {
try {
// URL of the service: /{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());
// customer information
ClientState clientState = new ClientState(request.getId());
// synchronous http request
String response = executeRestService("get", urlService, null);
// deserialization of jSON server response
AleasServerResponse aleasServerResponse = mapper.readValue(
response,
new TypeReference<AleasServerResponse>() {
});
// mistake?
int erreur = aleasServerResponse.getErreur();
if (erreur != 0) {
// we forward the exception
subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
} else {
// enter the time of reception
clientState.setResponseAt();
// we forward the result to the subscriber
subscriber.onNext(
new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
}
} catch (Exception ex) {
// we forward the exception to the subscriber
subscriber.onError(ex);
} finally {
// we signal the end of the observable
// at runtime, we note that this method has no effect if method [onError] has been called previously - in line with theory - so we could place this instruction only in 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 {
// error code
private int erreur;
// error message
private String message;
// server waiting time
private int delay;
// random numbers
private List<Integer> aleas;
// getters and setters
...
}
- السطر 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 {
// ------------- data common to all fragments
protected MainActivity activity;
protected Session session;
public abstract void onRefresh();
}
- السطر 7: فئة [MyFragment] تمتد من فئة [Fragment] في Android؛
- السطران 10-11: البيانات المشتركة بين جميع الأجزاء؛
- السطر 10: كل جزء يعرف النشاط الوحيد للتطبيق؛
- السطر 11: للتواصل مع بعضها البعض، تستخدم الأجزاء جلسة عمل؛
- السطر 13: قبل عرض جزء، سيُطلب منه تحديث نفسه بمحتوى الجلسة. تم إعلان هذه الطريقة على أنها مجردة لأنها يتم تنفيذها بواسطة الفئات الفرعية. ولهذا السبب، تم إعلان الفئة نفسها على أنها مجردة (السطر 7)؛
تحتوي فئة [Session] على البيانات المشتركة بين الأجزاء المختلفة للتطبيق. وفيما يلي كودها:
![]() |
package android.aleas.activity;
import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;
public class Session {
// application activity
private MainActivity activity;
// number of requests
private int nbRequests;
// request characteristics
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// URL web service / jSON
private String urlWebJson;
// operation began
private boolean onAir;
// idem but a little later in time
private boolean operationStarted;
// the name of the example chosen by the user from the list of examples
private String exampleName;
// its number in the list of fragments
private int examplePosition;
// the example spinner adapter in the query view
private ArrayAdapter<CharSequence> spinnerExemplesAdapter;
// methods
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);
}
// getters and setters
...
}
تقوم الطريقة الموجودة في السطر 46 بإنشاء كائن [Request]، الذي يغلف جميع المعلومات التي قدمها المستخدم في عرض الطلب:
![]() |
package android.aleas.fragments;
public class Request {
// request no
int id;
// user input
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// manufacturers
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;
}
// getters and setters
....
}
9.3.7.2. جزء [RequestFragment] من الطلب
تحتوي جزء الطلب على المكونات التالية:

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

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

رقم | النوع | الاسم | الدور |
1 | TextView | infoResponses | عدد الردود المستلمة |
2 | ListView | listResponses | قائمة سلاسل JSON المستلمة من الخادم |
3 | زر | btnCancel | لإلغاء الطلبات المرسلة إلى الخادم |
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">
<!-- application bar -->
<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">
<!-- toolbar -->
<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">
<!-- waiting image -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- tab container -->
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<!-- view container -->
<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>
مكونات هذا العرض هي كما يلي:
lines | النوع | الاسم | الدور |
20-34 | شريط الأدوات | شريط الأدوات | شريط أدوات التطبيق |
29-34 | شريط التقدم | لوحة التحميل | صورة مؤقتة تُعرض أثناء معالجة طلب المستخدم |
37-40 | TabLayout | علامات التبويب | شريط علامات التبويب في التطبيق |
44-51 | MyPager | الحاوية | الحاوية التي تُعرض فيها الأجزاء المختلفة للتطبيق |
فئة [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 {
// swipe control
private boolean isSwipeEnabled;
// manufacturers
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// method redefinition
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe authorized?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe authorized?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- توسع فئة [MyPager] فئة [ViewPager] القياسية في Android. نستخدم فئة [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 {
// layer [DAO]
private IDao dao;
// the session
private Session session;
// manufacturer
public MainActivity() {
// parent
super();
// session
session = new Session();
// DAO
dao = new Dao();
}
// getters
public Session getSession() {
return session;
}
// implémentation 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]: حيث ستتواصل معالجات الأحداث الخاصة بها مع طبقة [Activity] عندما تحتاج إلى التفاعل مع الخادم.
- السطر 24: إشارة إلى طبقة [DAO] التي تم تهيئتها بواسطة المنشئ في السطر 35؛
- السطر 26: إشارة إلى الجلسة المشتركة بين الأجزاء، والتي تم تهيئتها بواسطة المنشئ في السطر 33؛
- الأسطر 46-59: تنفيذ واجهة [IDao]؛
تقوم فئة [MainActivity] بتهيئة مكونات العرض المرتبط بها على النحو التالي:
// barre d'outils
private Toolbar toolbar;
// gestionnaire de fragments
private MyPager mViewPager;
// conteneur d'onglets
private TabLayout tabLayout;
// image d'attente
private ProgressBar loadingPanel;
...
@Override
public void onCreate(Bundle savedInstanceState) {
// classique
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// session
session.setActivity(this);
// configuration timeouts de la couche [DAO]
setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);
// composants
mViewPager = (MyPager) findViewById(R.id.container);
toolbar = (Toolbar) findViewById(R.id.toolbar);
loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
tabLayout = (TabLayout) findViewById(R.id.tabs);
// toolbar
setSupportActionBar(toolbar);
// au départ on n'a qu'un seul onglet
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Request");
tabLayout.addTab(tab);
// gestionnaire d'évt
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
int position = tab.getPosition();
if (position == 0) {
// onglet requête
showView(0);
} else {
// onglet réponse - dépend de l'exemple choisi
showView(session.getExamplePosition());
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
// création des fragments des réponses
createResponseFragments();
// gestion image d'attente
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):
// fragment manager - must define getItem, getCount methods
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// managed fragments
private MyFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
super(fm);
this.fragments = fragments;
}
// must render fragment no. position
@Override
public MyFragment getItem(int position) {
// the fragment
return fragments[position];
}
// makes the number of fragments to manage
@Override
public int getCount() {
// no. of fragments
return fragments.length;
}
}
}
- تُوسّع فئة [SectionsPagerAdapter] فئة [FragmentPagerAdapter] في Android. وهي تتجاوز طريقتين من فئتها الأم:
- طريقة [getItem]، السطر 15؛
- طريقة [getCount]، السطر 22؛
- تحتوي فئة [SectionsPagerAdapter] على جميع أجزاء التطبيق. يتم تخزينها في السطر 5. لاحظ أنها من النوع [MyFragment]، كما هو موضح في القسم 9.3.7.1؛
- السطر 8: لإنشاء نفسها، تتلقى فئة [SectionsPagerAdapter] الأجزاء التي يجب أن تديرها؛
- الأسطر 14-18: تُرجع طريقة [getItem] الجزء الموجود في الموضع [position]؛
- الأسطر 21–25: تُرجع الطريقة [getCount] العدد الإجمالي للأجزاء؛
تقوم الطريقة [createResponseFragments] بإنشاء جميع الأجزاء التي يحتاجها التطبيق:
private void createResponseFragments() {
// spinner examples
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Specify the layout to use when the list of choices appears
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// put the adapter in the session so that the [Request] view can retrieve it
session.setSpinnerExemplesAdapter(adapter);
...
}
- السطر 3: نقوم بإنشاء محول لمؤشر التمرير الخاص بالأمثلة، وهو في هذه الحالة قائمة من سلاسل الأحرف التي تمثل أسماء الأمثلة. توجد هذه الأسماء في ملف [layout/exemples.xml]:
![]() |
يحتوي ملف [examples.xml] على الكود التالي:
<!-- exemples -->
<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.examples]، [examples] هو اسم المصفوفة (انظر السطر 3 أعلاه)، وليس اسم الملف.
- السطر 5: نربط تخطيطًا (مدير العرض) بالمحول. الآن أصبح المحول يحتوي على كل من البيانات ووضع العرض الخاص بها؛
- السطر 7: نضيف المُهايئ إلى الجلسة. ومن هنا سيقوم [RequestFragment] الذي يحتاج إليه باسترداده؛
لنواصل مع كود طريقة [createResponseFragments]:
private void createResponseFragments() {
// spinner examples
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Specify the layout to use when the list of choices appears
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// put the adapter in the session so that the [Request] view can retrieve it
session.setSpinnerExemplesAdapter(adapter);
// create fragment table (1 query, n responses)
MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
// query fragment
tFragments[0] = new RequestFragment();
// answer fragments
for (int i = 1; i < tFragments.length; i++) {
// we construct the name of the fragment to be instantiated, corresponding to the example chosen by the user
// this name must be the full name with its package - here it is directly associated with the example number in the spinner
String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
// instantiate the fragment associated with the example
MyFragment fragment;
try {
// class instantiation
fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
} catch (Exception e) {
e.printStackTrace();
return;
}
// the fragment has been created - we put it in the table
tFragments[i] = fragment;
}
// instantiation of the fragment manager with these new fragments
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
// Set up the ViewPager with the sections adapter.
mViewPager.setAdapter(mSectionsPagerAdapter);
// page navigation - this instruction is important
// here we say that on both sides of the displayed view, we must keep [tFragments.length] views initialized
// this means that all fragments used by the application are in memory and initialized
// if you don't do this then the default [OffscreenPageLimit] is 1
// so if the fragment displayed is no. 3, only fragments 2 and 4 will be initialized
// this is done by calling the [onCreateView] method of these 2 fragments - this means that in this method, you must plan to
// regenerate the visual appearance of the fragment the last time it was used
// there's code that can't stand being run twice - it creates a huge mess and is complex to manage
// here we've chosen to avoid these difficulties - in the logs, we can see that when the application starts, all fragments are created
// and their method [onCreateView] executed - it's never executed again -
mViewPager.setOffscreenPageLimit(tFragments.length);
// inhibit swiping between fragments
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] هي كما يلي:
// display view n° [position]
private void showView(int position) {
// refresh the fragment before displaying it
mSectionsPagerAdapter.getItem(position).onRefresh();
// displays the requested view - goes directly to the view (second parameter set to false)
// without this parameter, the user defaults to the desired view, quickly displaying intermediate views - undesirable behavior
mViewPager.setCurrentItem(position, false);
}
- السطر 3: نريد عرض الجزء #position؛
- السطر 4: يتم طلب هذا الجزء من مدير الأجزاء ثم يتم تحديثه. منذ آخر مرة تم عرضه فيها، ربما تكون الجلسة قد تغيرت. لذلك يجب على الجزء فحص الجلسة لمعرفة ما إذا كان يحتاج إلى التحديث؛
- السطر 7: يتم عرض الجزء بواسطة [ViewPager]. نظرًا لأن هذا قد تم ربطه بمدير الأجزاء، فسيتم عرض الجزء #[position] — وهو الجزء الذي قمنا بتحديثه للتو في السطر 4؛
لننتهي بالطريقتين لإدارة الانتظار:
public void beginWaiting() {
// gestion image d'attente
loadingPanel.setVisibility(View.VISIBLE);
}
public void cancelWaiting() {
// gestion image d'attente
loadingPanel.setVisibility(View.INVISIBLE);
// fin exécution
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 of the web service
private EditText edtUrlServiceRest;
private TextView txtMsgErreurUrlServiceWeb;
// number of requests
private EditText edtNbRequests;
private TextView txtErrorRequests;
// generation interval
private EditText edtA;
private EditText edtB;
private TextView txtErrorIntervalle;
// delay
private EditText edtMinDelay;
private EditText edtMaxDelay;
private TextView txtErrorDelay;
// number of values generated
private EditText edtMinCount;
private EditText edtMaxCount;
private TextView txtErrorCount;
// button
private Button btnExecuter;
// list of answers
private ListView listReponses;
private TextView infoReponses;
// spinner examples
private Spinner spinnerExemples;
// seizures
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
// manufacturer
public RequestFragment() {
super();
Log.d("rxjava", "RequestFragment constructor");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d("rxjava", "RequestFragment onCreateView");
// recover activity and session
activity = (MainActivity) getActivity();
session = activity.getSession();
// create the fragment view from its definition XML
View view = inflater.inflate(R.layout.request, container, false);
// components
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);
// execute] button
btnExecuter.setVisibility(View.VISIBLE);
btnExecuter.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doExecuter();
}
});
// initially no error messages
txtErrorRequests.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
txtErrorCount.setVisibility(View.INVISIBLE);
txtErrorDelay.setVisibility(View.INVISIBLE);
// spinner examples
spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
// result
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 adapter) المستخدم في الأمثلة يتم استرداده من الجلسة. لاحظ أيضًا في السطر 91 أن النقر على زر [Execute] تتم معالجته بواسطة طريقة [doExecute]؛
- السطران 64-65: تنتمي الحقول [activity] و[session] إلى الفئة الأصلية [MyFragment]؛
طريقة [doExecute] هي كما يلي:
// seizures
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() {
// valid entries?
if (isPageValid()) {
// we put info in session
session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
// store the URL of the web service
activity.setUrlServiceWebJson(session.getUrlWebJson());
Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
// action in progress
session.setOnAir(true);
// but not started
session.setOperationStarted(false);
// the answer fragment is displayed
activity.selectTab(Constants.VUE_RESPONSE);
// we start waiting
beginWaiting();
}
}
- السطر 15: لن نعلق على طريقة [ispageValid]. فهي تتحقق من صحة الإدخالات ولا تُرجع القيمة true إلا إذا كانت جميعها صحيحة. وفي هذه الحالة، تُستخدم لتهيئة الحقول في الأسطر 2–9؛
- السطر 17: يتم حفظ الإدخالات المختلفة في الجلسة:
- [spinnerExemples.getSelectedItem().toString()] هو اسم المثال الذي اختاره المستخدم ويتم تخزينه في [session.exampleName]؛
- [spinnerExemples.getSelectedItemPosition() + 1] هو معرف الجزء المرتبط بالمثال، والذي تم تخزينه (الجزء) بواسطة مدير الأجزاء. يتم تخزين هذا المعرف في [session.examplePosition]؛
- السطر 19: يتم تمرير عنوان URL لخدمة الويب / JSON إلى النشاط، والذي بدوره يمرره إلى طبقة [DAO]؛
- الأسطر 21-24: لاحظ أن العملية على وشك البدء؛
- السطر 26: سيتم عرض علامة تبويب الاستجابة. لفهم ما سيحدث، تذكر الكود [MainActivity.selectTab]:
// sélection d'un onglet
public void selectTab(int position) {
// il y a au plus 2 onglets
// au départ il n'y en a qu'un, celui de la requête
// si l'onglet demandé est le n° 1 et que celui-ci n'existe pas encore, alors il faut le créer
if (position == 1 && tabLayout.getTabCount() == 1) {
// 1 onglet de +
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Response");
tabLayout.addTab(tab);
}
// on sélectionne par programme l'onglet, ce qui va déclencher l'événement [onTabSelected]
// qui va associer la bonne vue à cet onglet
tabLayout.getTabAt(position).select();
}
- في البداية، لم يكن النشاط قد أنشأ سوى علامة تبويب الطلب (علامة التبويب رقم 0)؛
- الأسطر 6-11: إنشاء علامة تبويب الاستجابة (علامة التبويب رقم 1) إذا لم يتم إنشاؤها بعد؛
- السطر 14: نختار رقم علامة التبويب (0 أو 1). يؤدي هذا إلى وضع الحدث [onTabSelected] في قائمة انتظار حلقة أحداث تطبيق Android؛
معالج حدث [onTabSelected] في [MainActivity] هو كما يلي:
@Override
public void onTabSelected(TabLayout.Tab tab) {
// un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
int position = tab.getPosition();
if (position == 0) {
// onglet requête
showView(0);
} else {
// onglet réponse - dépend de l'exemple choisi
showView(session.getExamplePosition());
}
}
في حالة علامة التبويب [Response]، يتم تنفيذ السطر 9. وسيتم عرض الجزء الذي يحمل المعرف [session.getExamplePosition()]. على سبيل المثال، بالنسبة لـ [example-03]، فإن المعرف المخزن في [session.examplePosition] هو 3. ثم يعرض السطر 10 المقطع ذي المعرف 3. مصفوفة المقاطع التي أنشأتها النشاط في البداية هي [RequestFragment، Example01Fragment، Example02Fragment، Example03Fragment،..]. لذلك، فإن [Example03Fragment] هو بالفعل ما سيتم عرضه. ويتم عرضه بواسطة الكود التالي:
// display view n° [position]
private void showView(int position) {
// refresh the fragment before displaying it
mSectionsPagerAdapter.getItem(position).onRefresh();
// displays the requested view - goes directly to the view (second parameter set to false)
// without this parameter, the user defaults to the desired view, quickly displaying intermediate views - undesirable behavior
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 {
// list of answers
private ListView listReponses;
private TextView infoReponses;
// button
private Button btnAnnuler;
// mapper 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) {
// recover activity and session
activity = (MainActivity) getActivity();
session = activity.getSession();
Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
// create the fragment view from its definition XML
View view = inflater.inflate(R.layout.response, container, false);
// components
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
// cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnAnnuler.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doAnnuler();
}
});
// result
return view;
}
...
// method to be executed (by explicit code) before each fragment display
public void onRefresh() {
...
}
}
- السطر 21: تمتد فئة [ResponseFragment] من فئة [MyFragment]؛
- الأسطر 23–27: مكونات الجزء؛
- الأسطر 32–36: يتم تنفيذ المنشئ مرة واحدة فقط، أثناء الإنشاء الأولي لأجزاء المثال بواسطة النشاط. وذلك لأن جميع أجزاء المثال تمتد من جزء [ResponseFragment]. وعندما يتم إنشاء مثيل لها، يتم استدعاء منشئ فئتها الأم [ResponseFragment]؛
- السطر 35: تهيئة أداة تعيين JSON من السطر 30 المستخدمة لعرض سلسلة JSON لمكدس الاستثناءات؛
- الأسطر 38-59: يتم تنفيذ طريقة [onCreateView] مرة واحدة فقط، أثناء الإنشاء الأولي للأجزاء النموذجية بواسطة النشاط. وهي تحتوي على كود قياسي موجود في تطبيق Android؛
- الأسطر 52-56: الطريقة التي يتم تنفيذها عند النقر على زر [Cancel] هي طريقة [doCancel]؛
- الأسطر 62–64: يتم تنفيذ الأسلوب [onRefresh] في كل مرة يتم فيها عرض علامة التبويب [Response]؛
بفضل السجلات المختلفة الموضوعة في الطرق الرئيسية، يمكننا رؤية ما يحدث عند بدء تشغيل التطبيق:
- السطر 1: إنشاء الجزء [RequestFragment]؛
- الأسطر 2–9: إنشاء الأجزاء للأمثلة الأربعة في التطبيق؛
- السطر 10: تهيئة الجزء [RequestFragment]؛
- الأسطر 11–14: تهيئة الأجزاء الخاصة بالأمثلة الأربعة في التطبيق؛
بعد ذلك، لا نرى أبدًا أي استدعاءات لهذه الطرق مرة أخرى.
الطريقة [ResponseFragment.onRefresh] هي كما يلي:
// méthode à exécuter (par code explicite) avant chaque visualisation du fragment
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()));
// exécution en cours ?
if (session.isOnAir() && !session.isOperationStarted()) {
// exécution requête
session.setOperationStarted(true);
doExecuter();
}
}
- السطر 5: نتحقق مما إذا كان [RequestFragment] قد أرسل طلبًا (session.isOnAir) وما إذا كان قد بدأ (isOperationStarted). إذا كان [RequestFragment] قد أرسل طلبًا ولم يكن قيد التشغيل بالفعل، يتم تشغيل العملية (السطران 7-8)؛
- بمجرد بدء العملية، ونظرًا لأنها غير متزامنة، يمكن للمستخدم التنقل بين علامتي التبويب. إذا عاد المستخدم إلى علامة التبويب [Response] وكانت العملية قيد التنفيذ، فلن يتم تنفيذ السطرين 7-8؛
تقوم طريقة [doExecute] في السطر 8 بتنفيذ العملية التي طلبها المستخدم:
private void doExecuter() {
Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
// start waiting
beginWaiting();
// preparation execution
subscriptions.clear();
reponses.clear();
nbInfos = 0;
// create and execute observables for the chosen example
createAndExecuteObservables();
}
// method implemented by child classes
protected abstract void createAndExecuteObservables();
- السطر 10: ينشئ ويُنفذ ويراقب العناصر القابلة للمراقبة. تختلف هذه العناصر في كل مثال. ولهذا السبب فإن الطريقة [createAndExecuteObservables] هي طريقة مجردة (السطر 14). سيتم تنفيذها بواسطة شظايا [ExampleXXFragment] التي تمتد من فئة [ResponseFragment]؛
- السطر 6: يتم مسح قائمة الاشتراكات؛
- السطر 7: يتم مسح القائمة التي تعرض الردود؛
- السطر 8: يحسب عدد الردود المستلمة؛
تكلف الفئات الفرعية [ExampleXXFragment] الطريقة [showAlea] التالية بمهمة عرض العناصر التي تراقبها:
protected void showAlea(String data) {
// one more piece of information
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// 1 more answer
reponses.add(0, data);
Log.d("rxjava", data);
// maj of 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() {
// we set the hourglass
activity.beginWaiting();
// the [Cancel] button is displayed
btnAnnuler.setVisibility(View.VISIBLE);
}
protected void cancelWaiting() {
// end of wait
activity.cancelWaiting();
// the [Cancel] button is hidden
btnAnnuler.setVisibility(View.INVISIBLE);
}
تستدعي هذه الطرق الأسماء نفسها في النشاط وتقوم ببساطة بإظهار أو إخفاء زر [إلغاء].
يتم التعامل مع النقر على زر [Cancel] بواسطة الكود التالي:
protected void doAnnuler() {
// on annule tous les abonnements
for (Subscription s : subscriptions) {
if (!s.isUnsubscribed()) {
s.unsubscribe();
}
}
// fin de l'attente
cancelWaiting();
}
- الأسطر 3–7: إلغاء جميع الاشتراكات واحدًا تلو الآخر؛
9.3.8. أمثلة على العناصر القابلة للمراقبة
9.3.8.1. مثال-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 {
// mappers jSON
private ObjectMapper mapperAleasUiResponse;
// manufacturer
public Example01Fragment() {
super();
Log.d("rxjava", "Example01Fragment constructor");
// filters jSON
mapperAleasUiResponse = new ObjectMapper();
}
@Override
public void createAndExecuteObservables() {
Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
// we ask for the random numbers
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// observable configuration n° i
// request to server
Request request = session.getRequest();
request.setId(i);
// observable executed on computation thread
observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
}
// observation on event loop thread;
observable = observable.observeOn(AndroidSchedulers.mainThread());
// we execute all these observables
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) {
// extract the information to be displayed
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 {
// answer [DAO]
private AleasDaoResponse aleasDaoResponse;
// observation thread
private String observedOn;
// observation time
private String observedAt;
// manufacturers
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;
}
// getters and setters
...
}
- إلى استجابة طبقة [DAO] (السطر 11)، نضيف معلومتين:
- السطر 13: مؤشر ترابط المراقبة؛
- السطر 15: وقت الملاحظة؛
لنعد إلى كود الاشتراك:
@Override
public void createAndExecuteObservables() {
...
// we execute all these observables
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) {
// exception is displayed
showAlea(getMessagesFromThrowable(th));
// after receiving an exception, the observable receives neither onNext nor onCompleted
// forced to cancel the subscription by hand
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// end waiting
cancelWaiting();
}
}));
}
- الأسطر 11–18: الحالة التي يتلقى فيها المراقب استثناءً؛
- السطر 14: نستخدم طريقة [showAlea] للفئة الأصلية مرة أخرى لعرض الاستثناء. طريقة [getMessagesFromThrowable] هي طريقة للفئة الأصلية [ResponseFragment] التي تولد سلسلة من استثناء:
// messages d'une exception
protected String getMessagesFromThrowable(Throwable ex) {
// on crée une liste avec les msg d'erreur de la pile d'exceptions
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: التأخير الذي لاحظه الخادم؛
- error: رمز الخطأ الذي أعاده الخادم (0=لا يوجد خطأ)؛
- message: رسالة الخطأ التي أرجعها الخادم (null=لا يوجد خطأ)؛
- observedAt: وقت ملاحظة القيمة الملاحظة؛
- الملاحظة على: مؤشر الترابط الذي يراقب القيمة الملاحظة؛
9.3.8.2. مثال-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 {
// mappers jSON
private ObjectMapper mapperAleasUiResponse;
// manufacturer
public Example02Fragment() {
super();
Log.d("rxjava", "Example02Fragment constructor");
// filter jSON
mapperAleasUiResponse = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
// we ask for the random numbers
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// request preparation
Request request = session.getRequest();
request.setId(i);
// only observables with an even customer number are kept
observable = observable
.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
})
// execution on I/O thread
.subscribeOn(Schedulers.io()));
}
// observation on event loop thread
observable = observable.observeOn(AndroidSchedulers.mainThread());
// these observables are executed
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() {
// end waiting
cancelWaiting();
}
}));
}
private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
// extract the information to be displayed
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. المثال-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 {
// mappers jSON
private ObjectMapper mapper;
// manufacturer
public Example03Fragment() {
super();
Log.d("rxjava", "Example03Fragment constructor");
// filter jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// we ask for the random numbers
Observable<List<Integer>> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// request preparation
Request request = session.getRequest();
request.setId(i);
// observable configuration
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();
}
})
// execution on I/O thread
.subscribeOn(Schedulers.io()));
}
// observation on event loop thread
observable = observable.observeOn(AndroidSchedulers.mainThread());
// these observables are executed
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() {
// end waiting
cancelWaiting();
}
}
));
}
private String getDataFrom(List<Integer> aleas) {
// extract the information to be displayed
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. مثال-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 {
// mappers jSON
private ObjectMapper mapper;
// manufacturer
public Example04Fragment() {
super();
Log.d("rxjava", "Example04Fragment constructor");
// filter jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// we ask for the random numbers
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// request preparation
Request request = session.getRequest();
request.setId(i);
// observable configuration
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());
}
})
// execution on an I/O thread
.subscribeOn(Schedulers.io()));
}
// observation on event loop thread
observable = observable.observeOn(AndroidSchedulers.mainThread());
// these observables are executed
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() {
// end waiting
cancelWaiting();
}
}
));
}
}
هذا المثال مشابه للمثال-03، باستثناء أنه بدلاً من استخدام طريقة [map] في السطر 42، نستخدم طريقة [flatMap].
- السطر 55: لاحظ أن نوع القيمة الملاحظة أصبح الآن Integer؛
بالنسبة لـ 10 طلبات، نحصل على النتائج التالية:

هذه المرة، هناك قيم ملاحظة أكثر من الطلبات.
9.3.8.5. مثال-05
سنقوم الآن بتوضيح الإجراء الخاص بإضافة مثال جديد للملاحظات إلى التطبيق.
لنفترض أننا نريد إعادة إنتاج المثال [مثال 22h] من القسم 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 {
// process
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()));
// subscriptions
ProcessUtils.subscribe(1, process);
}
}
- يتم أولاً تجميع قيم المراقب [Observable.range(1, 10)] إلى قيم زوجية وفردية بواسطة طريقة [groupBy] (السطر 11)، ثم يتم دمجها في مراقب واحد بواسطة طريقة [concatMap] (السطر 12)؛
الخطوة 1
نقوم بإنشاء مثال جديد في الملف [examples.xml]:
![]() |
<!-- exemples -->
<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 {
// mappers jSON
private ObjectMapper mapper;
// manufacturer
public Example05Fragment() {
super();
Log.d("rxjava", "Example05Fragment constructor");
// filter jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
// instantiations of functional interfaces
// filter
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();
}
};
// we ask for the random numbers
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// request preparation
Request request = session.getRequest();
request.setId(i);
// observable configuration
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
.groupBy(groupBy).concatMap(concatMap)
// execution on an I/O thread
.subscribeOn(Schedulers.io());
}
// observation on event loop thread
observable = observable.observeOn(AndroidSchedulers.mainThread());
// these observables are executed
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() {
// end waiting
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] مع الخادم الذي يولد الأرقام العشوائية التي يعرضها جهاز Android اللوحي. ويتميز هذا الخادم بالبنية ذات الطبقتين التالية:
![]() |
أرسلت طبقة [DAO] n طلبات HTTP إلى خادم الأرقام العشوائية، وانتظرت طبقة [swing] بشكل غير متزامن نتائج هذه الطلبات لعرضها. تم إرسال هذه الطلبات n HTTP إلى نفس الخادم، الذي أعاد نفس أنواع الاستجابات. سمح لنا ذلك بدمج الاستجابات في ملاحظة واحدة.
في الواقع، تتواصل تطبيقات Android مع خوادم مختلفة، ومن المحتمل ألا ندمج استجاباتها. سيتم التعامل مع طلبات HTTP الموجهة إلى هذه الخوادم بشكل مستقل عن بعضها البعض، وسيتم ملاحظة نتائجها باستخدام طرق منفصلة.

















































