Skip to content

9. RxJava در محیط اندروید

9.1. Introduction

در اینجا ما یک برنامه‌ای را که قبلاً در چندین سند مورد بحث قرار گرفته است، مجدداً بررسی خواهیم کرد:

  1. [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (فصل 4);
  2. [Introduction à la programmation de tablettes Android par l'exemple] (فصل ۹)؛
  3. [Introduction à la programmation de tablettes Android par l'exemple - version 2] (بخش 1.11);

این مربوط به یک برنامهٔ کلاینت/سرور است که در آن سرور به‌صورت ناهمزمان اعداد تصادفی را ارسال می‌کند و کلاینت اندروید آن‌ها را نمایش می‌دهد:

  • در سند ۱، کلاینت اندروید از فناوری غیراستاندارد استفاده می‌کند؛
  • در سند ۲، کلاینت اندروید از فناوری استاندارد اندروید برای عملیات ناهمزمان استفاده می‌کند؛
  • در سند ۳، کلاینت اندروید از همان فناوری سند ۲ استفاده می‌کند، اما با استفاده از انوتیشن‌های کتابخانه Android Annotations ساده‌تر شده است؛

کلاینت اندروید به شرح زیر است:

لایه [DAO] با سروری که اعداد تصادفی نمایش‌داده‌شده توسط تبلت اندروید را تولید می‌کند، ارتباط برقرار می‌کند. این سرور دارای معماری دو لایه‌ای زیر است:

کلاینت‌ها در لایه [web / JSON] پرس‌وجوی برخی URL را انجام می‌دهند و پاسخ متنی را در قالب JSON (JavaScript Object Notation) دریافت می‌کنند.

تحلیل برنامه را به دو مرحله تقسیم خواهیم کرد:

سرور وب / jSON

  • لایه [métier] آن؛
  • سرویس [web / JSON] آن که با Spring MVC پیاده‌سازی شده است؛

کلاینت اندروید

  • لایه [DAO] آن؛
  • فعالیت آن؛
  • ویوهای آن؛

9.2. سرویس وب / jSON

توجه: سرویس وب / jSON با استفاده از فناوری Spring MVC پیاده‌سازی شده است. خوانندگانی که با این فناوری آشنایی ندارند می‌توانند:

  • به‌سادگی بخش 9.2.1 را بخوانید که نحوه راه‌اندازی سرور و نحوه پرس‌وجو از آن را توضیح می‌دهد؛
  • به سند [Spring MVC et Thymeleaf par l'exemple]، به‌ویژه فصل ۴، مراجعه کنید که توضیحات اصلی مورد استفاده در کد را تشریح می‌کند؛

9.2.1. پروژه IntelliJ IDEA

سرویس وب / jSON دارای معماری زیر است:

این معماری توسط پروژه IntelliJ IDEA زیر پیاده‌سازی شده است: [1]:

سرور توسط [2-3] راه‌اندازی می‌شود. سپس لاگ‌ها در کنسول نمایش داده می‌شوند:

2016-05-17 10:47:12.642  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Starting Application on st-PC with PID 13116 (D:\data\istia-1516\projets\rxjava\dvp\android\serveur\build\classes\main started by st in D:\data\istia-1516\projets\rxjava\dvp\android\serveur)
2016-05-17 10:47:12.647  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : No active profile set, falling back to default profiles: default
2016-05-17 10:47:12.706  INFO 13116 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:13.736  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-05-17 10:47:13.749  INFO 13116 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-05-17 10:47:13.750  INFO 13116 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.33
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1214 ms
2016-05-17 10:47:13.965  INFO 13116 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/*]
2016-05-17 10:47:14.251  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}],methods=[GET],produces=[application/json]}" onto public java.lang.String dvp.rxjava.server.web.AleasController.getAleas(int,int,int,int,int,int) throws com.fasterxml.jackson.core.JsonProcessingException
2016-05-17 10:47:14.342  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:14.485  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-05-17 10:47:14.489  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Started Application in 2.289 seconds (JVM running for 2.859)
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2016-05-17 10:48:37.087  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 26 ms
  • خط ۱۲: نشان می‌دهد که سرویس روی پورت ۸۰۸۰ در دسترس است؛
  • خط ۱۰: سرویس وب واحد URL / jSON که از طریق عملیات HTTP GET در دسترس است. پارامترهای آن به شرح زیر است:
    • [a,b]: فاصله زمانی تولید اعداد تصادفی؛
    • [minCount, maxCount]: اعداد تصادفی count تولید می‌شوند، که در آن count یک عدد تصادفی در بازه [minCount, maxCount] است؛
    • [minDelay, maxDelay]: سرویس قبل از بازگرداندن اعداد درخواستی، delay میلی‌ثانیه صبر می‌کند، که در آن delay یک عدد تصادفی در بازه [minDelay, maxDelay] است؛

در یک مرورگر، بیایید این URL را درخواست کنیم:

 

ما درخواست کردیم:

  • اعداد تصادفی در بازه [100, 200]؛
  • اعداد تصادفی n با n در بازه [10, 20];
  • زمان انتظار به میلی‌ثانیه x با x در بازه [300, 400]؛

در پاسخ:

  • آدرس‌های جایگزین: فهرست اعداد تصادفی تولیدشده؛
  • delay: زمان انتظار به میلی‌ثانیه که توسط سرور ثبت شده است؛
  • خطا: یک کد خطا – 0 در صورت عدم وجود خطا؛
  • message: یک پیام خطا – null در صورت عدم وجود خطا؛

9.2.2. وابستگی‌های Gradle پروژه

  

پروژه [serveur] یک پروژه Gradle است که توسط فایل‌های زیر پیکربندی شده است: [build.gradle] و [1]:


// تولید شده توسط http://start.spring.io/ (مه ۲۰۱۶)
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')
}
  • خط ۱: یک نظر (comment) که توضیح می‌دهد این فایل پیکربندی چگونه تولید شده است؛
  • خطوط ۴ و ۱۰: وابستگی به فریم‌ورک [Spring Boot]، شاخه‌ای از اکوسیستم Spring. این فریم‌ورک [http://projects.spring.io/spring-boot/] امکان پیکربندی حداقلی Spring را فراهم می‌کند. بر اساس کتابخانه‌های موجود در مسیر کلاس پروژه، [Spring Boot] یک پیکربندی محتمل یا احتمالی برای پروژه استنتاج می‌کند. بنابراین، اگر Hibernate در مسیر کلاس (classpath) پروژه وجود داشته باشد، [Spring Boot] نتیجه می‌گیرد که پیاده‌سازی مورد استفاده JPA، Hibernate است و Spring را بر این اساس پیکربندی می‌کند. توسعه‌دهنده دیگر نیازی به انجام این کار ندارد. تنها کاری که برای او باقی می‌ماند این است که تنظیماتی را که [Spring Boot] به‌صورت پیش‌فرض تنظیم نکرده است، یا آن‌هایی را که [Spring Boot] به‌صورت پیش‌فرض تنظیم کرده اما نیاز به مشخص شدن دارند، پیکربندی کند. در همه موارد، پیکربندی انجام‌شده توسط توسعه‌دهنده در اولویت قرار دارد؛
  • خطوط ۱۴–۱۵: دو افزونه Gradle مورد نیاز برای استفاده از محتویات این فایل Gradle؛
  • خطوط 17–20: مشخصات آرشیو تولیدشده برای این پروژه را تعریف می‌کنند؛
  • خطوط 22–23: برای سازگاری با جاوا 8؛
  • خطوط ۲۵–۲۷: وابستگی‌ها در مخزن جهانی Maven یا در مخزن محلی روی دستگاه جستجو خواهند شد؛
  • خط ۳۰: وابستگی به آرتیفکت [spring-boot-starter-web] را تعریف می‌کند. این آرتیفکت شامل تمام آرشیوهای مورد نیاز برای یک پروژه Spring به نام MVC است. در میان این‌ها، آرشیو سرور Tomcat نیز وجود دارد. این همان آرشیویی است که برای استقرار وب‌اپلیکیشن استفاده خواهد شد. توجه داشته باشید که نسخه وابستگی مشخص نشده است. نسخه‌ای که در پروژه واردشده [spring-boot] مشخص شده است، استفاده خواهد شد؛

برای به‌روزرسانی پروژه، باید دانلود وابستگی‌ها برای [1-3] را اجباری کنید:

بیایید به [4] و وابستگی‌های معرفی‌شده توسط این فایل، [build.gradle]، نگاهی بیندازیم:

 

تعدادشان بسیار زیاد است. Spring Boot برای وب وابستگی‌هایی را که یک برنامه وب Spring MVC احتمالاً به آن‌ها نیاز دارد، شامل شده است. این بدان معناست که برخی از آن‌ها ممکن است غیرضروری باشند. Spring Boot برای یک آموزش ایده‌آل است:

  • وابستگی‌هایی را که احتمالاً به آن‌ها نیاز خواهیم داشت، فراهم می‌کند؛
  • خواهیم دید که این کار پیکربندی پروژه اسپرینگ MVC را به طور قابل توجهی ساده می‌کند؛
  • این شامل یک سرور Tomcat داخلی [1] است که ما را از نیاز به استقرار برنامه روی یک سرور وب خارجی بی‌نیاز می‌کند؛
  • این امکان را به ما می‌دهد که یک فایل JAR قابل اجرا حاوی تمام وابستگی‌های فوق را تولید کنیم. این فایل JAR را می‌توان بدون نیاز به پیکربندی مجدد از یک پلتفرم به پلتفرم دیگر منتقل کرد.

در وب‌سایت اکوسیستم Spring به آدرس [http://spring.io/guides]، نمونه‌های متعددی از استفاده از Spring Boot وجود دارد. اکنون که وابستگی‌های پروژه را می‌شناسیم، می‌توانیم به سراغ کد برویم.

9.2.3. لایه [métier]

  

لایه [métier] دارای رابط زیر [IMetier] خواهد بود:


package dvp.rxjava.server.metier;

public interface IMetier {
  // اعداد تصادفی در بازه [a,b]
  // n عدد تولید می‌شوند، که در آن n خود یک عدد تصادفی در بازه [minCount, maxCount] است
  // اعداد پس از تأخیری به میلی‌ثانیه تولید می‌شوند،
  // جایی که [delay] خود یک عدد تصادفی در بازه [minDelay, maxDelay] است
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

این رابط تقریباً یکسان با رابط مورد بحث در محیط Swing در بخش 8.4 است. در خط 8، متد [getAleas] نوع زیر را برمی‌گرداند: [AleasMetier]:


package dvp.rxjava.server.metier;

import java.util.List;

public class AleasMetier {
  // میدان‌ها
  private int delay;
  private List<Integer> aleas;

  // سازنده‌ها
  public AleasMetier(){

  }

  public AleasMetier(int delay, List<Integer> aleas){
    this.delay=delay;
    this.aleas=aleas;
  }

  public AleasMetier(AleasMetier aleasMetier){
    this.delay=aleasMetier.delay;
    this.aleas=aleasMetier.aleas;
  }

  // گیرنده و تنظیم‌کننده
...
}

کد کلاس [Metier] که رابط [IMetier] را پیاده‌سازی می‌کند، به شرح زیر است:


package dvp.rxjava.server.metier;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
public class Metier implements IMetier {

  @Autowired
  private ObjectMapper mapper;

  @Override
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    // اعداد تصادفی در بازه [a,b]
    // n عدد تولید می‌شوند، که در آن n خود یک عدد تصادفی در بازه [minCount, maxCount] است
    // اعداد پس از تأخیری به میلی‌ثانیه تولید می‌شوند،
    // جایی که [delay] خود یک عدد تصادفی در بازه [minDelay, maxDelay] است

    // برخی بررسی‌ها
    List<String> messages = new ArrayList<>();
    int erreur = 0;
    if (a < 0) {
      messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
      erreur |= 2;
    }
    if (a >= b) {
      messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
      erreur |= 4;
    }
    if (minCount < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
      erreur |= 16;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
      erreur |= 32;
    }
    if (minDelay < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
      erreur |= 64;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
      erreur |= 128;
    }
    if (maxDelay > 5000) {
      messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
      erreur |= 256;
    }
    //خطاها؟
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // تولیدکننده عدد تصادفی
    Random random = new Random();
    // منتظر؟
    int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
    if (delay > 0) {
      try {
        Thread.sleep(delay);
      } catch (InterruptedException e) {
        String message = null;
        try {
          message = mapper.writeValueAsString(Arrays.asList(String.format("[%s : %s]", e.getClass().getName(), e.getMessage())));
        } catch (JsonProcessingException e1) {
          throw new AleasException(e1,512);
        }
        throw new AleasException(message, 1024);
      }
    }
    //تولید نتیجه
    int count = minCount + random.nextInt(maxCount - minCount + 1);
    List<Integer> nombres = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
      nombres.add(a + random.nextInt(b - a + 1));
    }
    // نتیجه بازگردانده شده
    return new AleasMetier(delay,nombres);
  }

}

ما در مورد این کلاس توضیحی نمی‌دهیم: این کلاس مشابه نمونه‌ای است که در بخش 8.4 در محیط Swing با آن مواجه شدیم. ما صرفاً به نکات زیر اشاره می‌کنیم:

  • خط ۱۰: آنوتیشن Spring با نام [@Service]، که تضمین می‌کند Spring یک نمونه واحد (singleton) از کلاس ایجاد کرده و مرجع آن را در اختیار سایر اجزای Spring قرار دهد. می‌توانست از آنوتیشن‌های دیگر Spring نیز برای دستیابی به همین اثر در اینجا استفاده شود؛
  • خطوط ۱۳–۱۴: یک مپر jSON تزریق می‌شود. Spring یک مخزن اشیاء است. این مخزن هنگام راه‌اندازی برنامه وب ایجاد می‌شود و اشیاء تعریف‌شده در فایل پیکربندی نیز instantiate می‌شوند؛ به‌صورت پیش‌فرض تنها یک نمونه (singleton) ایجاد می‌شود. یک سینگل‌تون اسپرینگ می‌تواند شامل ارجاع به سایر اشیاء اسپرینگ باشد. در اینجا نیز همین‌طور است: سینگل‌تون [metier] (خطوط ۱۰–۱۱) دارای ارجاعی به سینگل‌تون [mapper] (خطوط ۱۳–۱۴) خواهد بود. این روش به تزریق وابستگی (dependency injection) معروف است. دو روش برای تزریق یک سینگل‌تون به سینگل‌تون دیگر وجود دارد:
    • بر اساس نوع: این کار زمانی امکان‌پذیر است که singleton مورد تزریق، تنها شیء Spring از آن نوع باشد. این مورد در اینجا برای تزریق در خطوط ۱۳–۱۴ (نوع ObjectMapper) صدق می‌کند؛
    • بر اساس نام آن، اگر چندین شیء Spring از یک نوع یکسان وجود داشته باشند. در این صورت، باید آناوتیشن @Qualifier("nomDuSingleton") اضافه شود تا نام singleton مشخص گردد؛

کلاس [Metier] استثناهایی از نوع [AleaException] را پرتاب می‌کند:


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // کد خطا
  private int code;

  // سازنده‌ها
  public AleaException() {
  }

  public AleaException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public AleaException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  public AleaException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }

  // گیرنده و تنظیم‌کننده

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • خط ۳: [AleasException] کلاس [RuntimeException] را گسترش می‌دهد. بنابراین این یک استثنای بدون بررسی (unchecked) است (الزامی برای مدیریت آن با بلوک try/catch وجود ندارد)؛
  • خط ۶: یک کد خطا به کلاس [RuntimeException] اضافه می‌شود؛

9.2.4. سرویس وب / JSON

  

سرویس وب / JSON توسط Spring MVC پیاده‌سازی شده است. اسپرینگ MVC الگوی معماری موسوم به MVC (مدل–نما–کنترل‌کننده) را به شرح زیر پیاده‌سازی می‌کند:

پردازش درخواست مشتری به شرح زیر انجام می‌شود:

  1. درخواست URL درخواستی به شکل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... است. [Dispatcher Servlet] کلاس Spring است که URL ورودی را پردازش می‌کند. این کلاس، URL را به اکشنی که باید آن را پردازش کند، مسیریابی می‌کند. این اکشن‌ها متدهایی از کلاس‌های خاصی به نام [Contrôleurs] هستند. حرف «C» در MVC در این مورد، رشته [Dispatcher Servlet, Contrôleur, Action] است. اگر هیچ اقدامی برای رسیدگی به URL ورودی پیکربندی نشده باشد، سرولت [Dispatcher Servlet] پاسخ خواهد داد که URL درخواستی یافت نشد (خطای ۴۰۴ NOT FOUND);
  1. پردازش
  • عملکرد انتخاب‌شده می‌تواند از پارامترهای parami که توسط سرولت [Dispatcher Servlet] به آن ارسال شده است، استفاده کند. این پارامترها ممکن است از چندین منبع بیایند:
    • مسیر [/param1/param2/...] از URL،
    • پارامترهای [p1=v1&p2=v2] از URL,
    • از پارامترهای ارسال‌شده توسط مرورگر در درخواست آن؛
  • هنگام پردازش درخواست کاربر، ممکن است اقدام به لایه‌های [metier] و [2b] نیاز داشته باشد. پس از پردازش درخواست مشتری، ممکن است پاسخ‌های مختلفی ایجاد شود. یک مثال کلاسیک این است:
    • یک صفحهٔ خطا اگر درخواست نتوانست به‌درستی پردازش شود
    • در غیر این صورت، یک صفحه تأیید
  • اقدام دستور نمایش یک نمای خاص را صادر می‌کند: [3]. این نما داده‌هایی را که به عنوان مدل نما شناخته می‌شوند، نمایش می‌دهد. این همان M در MVC است. اقدام این مدل M [2c] را ایجاد کرده و دستور نمایش یک نما V [3] را صادر می‌کند؛
  1. پاسخ – نمای انتخاب‌شده V از مدل M ساخته‌شده توسط اقدام برای راه‌اندازی بخش‌های پویا از پاسخی که باید به کلاینت ارسال کند، استفاده می‌کند و سپس این پاسخ را ارسال می‌کند.

برای یک سرویس وب / JSON، معماری قبلی کمی تغییر می‌کند:

  • در [4a]، مدل—که یک کلاس جاوا است—توسط یک کتابخانه JSON به یک رشته JSON تبدیل می‌شود؛
  • در [4b]، این رشته JSON به مرورگر ارسال می‌شود؛

بیایید به لایه [web] برنامه خود بازگردیم:

در برنامه ما تنها یک کنترلر وجود دارد:

  

سرویس وب /JSON پاسخ خود را از نوع [AleasResponse] به مشتریان خود به شرح زیر ارسال می‌کند:


package dvp.rxjava.server.web;

import dvp.rxjava.server.metier.AleasMetier;

public class AleasResponse extends AleasMetier {

  // کد خطا
  private int erreur;
  // پیام خطا
  private String message;

  // سازنده‌ها
  public AleasResponse() {

  }

  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // گیرنده و تنظیم‌کننده

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • خط ۵: کلاس [AleasResponse] از کلاس [AleasMetier] ارث می‌برد و بنابراین تمام ویژگی‌های آن (aleas, delay) را به ارث می‌برد؛
  • خط ۸: یک کد خطا (۰ در صورت عدم وجود خطا)؛
  • خط ۱۰: اگر erreur!=0، یک پیام خطا؛ null اگر خطایی وجود نداشته باشد؛

کنترل‌کننده [AleasController] به شرح زیر است:


package dvp.rxjava.server.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import dvp.rxjava.server.metier.AleasException;
import dvp.rxjava.server.metier.IMetier;

@Controller
public class AleasController {

    // لایه کسب‌وکار
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    //اعداد تصادفی در [a,b]
    //n عدد تولید می‌شوند، که n در بازه [minCount, maxCount] قرار دارد.
    // اعداد پس از تأخیری به میلی‌ثانیه تولید می‌شوند،
    // جایی که [delay] یک عدد تصادفی در بازه [minDelay, maxDelay]
    @RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
            @PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
            @PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {

        //پاسخ آماده می‌شود
        AleasResponse response = new AleasResponse();
        // لایه کسب‌وکار برای تولید اعداد تصادفی استفاده می‌شود
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // مورد خطا (کد و پیام)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // بازگرداندن پاسخ jSON
        return mapper.writeValueAsString(response);
    }
}
  • خط 16: حاشیه‌نویسی [@Controller] کلاس [AleasController] را به یک Spring singleton تبدیل می‌کند. همچنین نشان می‌دهد که این کلاس شامل متدهایی است که درخواست‌های مربوط به برخی URL در برنامه وب را مدیریت خواهند کرد. در اینجا، تنها یک مورد در خط ۲۹ وجود دارد؛
  • خطوط ۲۰–۲۱: آناوتیشن [@Autowired] به Spring دستور می‌دهد تا یک کامپوننت از نوع [IMetier] را در فیلد تزریق کند. این همان کلاس قبلی [Metier] خواهد بود. این به این دلیل است که ما آناوتیشن [@Service] را روی آن اعمال کرده‌ایم، به این معنی که به‌عنوان یک کامپوننت Spring در نظر گرفته می‌شود؛
  • خطوط ۲۲–۲۳: anotation [@Autowired] به Spring دستور می‌دهد تا یک کامپوننت از نوع [ObjectMapper] را در فیلد تزریق کند. ما به زودی این را تعریف خواهیم کرد؛
  • خط ۳۱: متد [getAleas] اعداد تصادفی تولید می‌کند. نام آن اهمیتی ندارد. وقتی اجرا می‌شود، پارامترهای خطوط ۳۱–۳۳ توسط Spring MVC مقداردهی اولیه شده‌اند. خواهیم دید چگونه. علاوه بر این، دلیل اجرای آن این است که وب‌سرور درخواستی HTTP GET برای URL در خط ۲۹ دریافت کرده است (ویژگی method);
  • خط ۳۰: تفسیر [@ResponseBody] نشان می‌دهد که نتیجه متد باید بدون تغییر به کلاینت ارسال شود. در اینجا، ما یک رشته کاراکتری را ارسال خواهیم کرد که همان رشته jSON از نوع [AleasResponse] خواهد بود؛
  • خط ۲۹: 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 در خط ۳۱ با پارامترهای a=100 (خط ۳۱)، اجرا خواهد شد، b=200 (خط 31)، minCount=10 (خط 31)، maxCount=20 (خط 32)، minDelay=300 (خط 32)، maxDelay=400 (خط 33)؛
  • خط ۳۹: از لایه [métier] خواسته شده است فهرستی از اعداد تصادفی را ارائه دهد. توجه داشته باشید که متد [metier].getAleas ممکن است یک استثنا پرتاب کند؛
  • خطوط ۴۲–۴۳: مدیریت خطا؛
  • خط ۴۶: پاسخ از نوع [AleasResponse] به صورت یک رشته jSON بازگردانده می‌شود؛

9.2.5. پیکربندی پروژه Spring

  

راه‌های مختلفی برای پیکربندی Spring وجود دارد:

  • با استفاده از فایل‌های XML؛
  • با استفاده از کد جاوا؛
  • با استفاده از ترکیبی از هر دو؛

ما انتخاب کرده‌ایم که برنامه وب خود را با استفاده از کد جاوا پیکربندی کنیم. این کلاس [Config] که در بالا نشان داده شده است، این پیکربندی را مدیریت می‌کند:


package dvp.rxjava.server.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@ComponentScan(basePackages = { "dvp.rxjava.server.metier", "dvp.rxjava.server.web" })
@EnableWebMvc
public class Config {
  // -------------------------------- پیکربندی لایه [web]
  @Autowired
  private ApplicationContext context;

  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
    return servlet;
  }

  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }

  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }

  // نقشه‌کش jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • خط ۱۵: به اسپرینگ می‌گوییم در کدام پکیج‌ها اشیاء قابل نمونه‌سازی را پیدا کند. آن دو را پیدا خواهد کرد:
    • کلاس [Metier] که با [@Service] علامت‌گذاری شده است؛
    • کلاس [AleasController] که با [@Controller] نشانه‌گذاری شده است؛
  • خط ۱۶: anotation [@EnableWebMvc] پیکربندی‌های خودکار را برای فریم‌ورک Spring با شناسه MVC راه‌اندازی می‌کند؛
  • خطوط ۱۹–۲۰: تزریق کانکست Spring (ظرفیت شیء Spring). این تزریق ضروری است زیرا شیء در خطوط ۲۲–۲۶ به آن نیاز دارد؛
  • فایل پیکربندی Spring می‌تواند با استفاده از متدهای دارای anotation [@Bean]، اشیاء جدید Spring را تعریف کند. نتیجه این متد سپس به یک شیء Spring تبدیل می‌شود؛
  • خطوط 22–26: تعریف servlet چارچوب Spring با شناسه MVC، که درخواست‌ها را با شناسه HTTP به کنترل‌کننده و متد صحیح مسیریابی می‌کند. [DispatcherServlet] یک کلاس Spring است؛
  • خطوط ۲۸–۳۱: این مشخص می‌کند که این سرولت تمام درخواست‌های URL را مدیریت می‌کند؛
  • خطوط 33–36: حضور این بین است که سرور Tomcat موجود در آرشیوهای پروژه را راه‌اندازی می‌کند. این سرور روی پورت 8080 به درخواست‌ها گوش فرا می‌دهد؛
  • خطوط ۳۹–۴۲: یک مپر jSON. این همان مپری است که به اشیاء Spring یعنی [Metier] و [AleasController] تزریق شده است؛

9.2.6. اجرای سرور وب

  

این پروژه از طریق کلاس اجرایی زیر، [Application]، اجرا می‌شود:


package android.exemples.server.boot;

import android.exemples.server.config.Config;
import org.springframework.boot.SpringApplication;

public class Application {
  public static void main(String[] args) {
    //اجرای برنامه
    SpringApplication.run(Config.class, args);
  }

}
  • خط ۶: کلاس [Application] یک کلاس قابل اجرا است (خطوط ۷–۱۰);
  • خط ۹: متد استاتیک [SpringApplication.run] یک متد از کلاس [spring Boot] (خط ۴) است که برنامه را راه‌اندازی می‌کند. اولین پارامتر آن کلاس جاوا است که پروژه را پیکربندی می‌کند. در این مورد، کلاس [Config]، که به تازگی آن را توصیف کرده‌ایم. پارامتر دوم آرایهٔ آرگومان‌هایی است که به متد [main] (خط ۷) پاس می‌شود. در این مورد، هیچ آرگومانی وجود نخواهد داشت؛

برای اجرای واقعی، خواننده دعوت می‌شود تا به بخش 9.2.1 مراجعه کند.

9.3. کلاینت اندروید

توجه: پروژه اندروید زیر بسیار پیچیده است. این پروژه نیازمند درک خوبی از اندروید است که می‌توان آن را، برای مثال، در [Introduction à la programmation de tablettes Android avec Android Studio ] یافت.

فعالیت

ویوها

لایه

[DAO]

کاربر

سرور

کلاینت دو مؤلفه خواهد داشت:

  1. یک لایه [Présentation] (نماها + فعالیت)؛
  2. یک لایه [DAO] که با سرویس [web / JSON] که قبلاً بررسی کردیم، ارتباط برقرار می‌کند.

9.3.1. RxAndroid

برای برقراری ارتباط غیرهمزمان با سرور اعداد تصادفی، کلاینت اندروید از کتابخانه RxAndroid استفاده خواهد کرد. این کتابخانه، RxJava را برای محیط اندروید گسترش می‌دهد. مانند برنامه Swing، ما تنها از یک افزونه ارائه‌شده توسط RxAndroid استفاده خواهیم کرد، یعنی برنامه‌ریز [AndroidSchedulers.mainThread()]. رابط کاربری گرافیکی اندروید از همان اصول رابط Swing پیروی می‌کند:

  • رویدادها در یک نخ واحد به نام حلقه رویداد (event loop) یا نخ رابط کاربری (UI thread) پردازش می‌شوند؛
  • هنگامی که یک رویداد، عملیات غیرهمزمان را تحریک می‌کند، نتایج این عملیات باید در نخ رابط کاربری (UI thread) بازیابی شوند تا در صورت استفاده برای به‌روزرسانی رابط کاربری، در دسترس باشند؛

کلاینت اندروید:

  • چندین درخواست ناهمزمان به سرور اعداد تصادفی ارسال خواهد کرد. این درخواست‌ها در سمت کلاینت با استفاده از نخ‌های زمان‌بندی‌کننده [Schedulers.io()] اجرا خواهند شد؛
  • این درخواست‌های ناهمزمان، قابل‌مشاهده‌هایی را بازمی‌گردانند که در یک قابل‌مشاهده واحد ادغام خواهند شد؛
  • این observable در سمت کلاینت در زمان‌بندی‌کننده [AndroidSchedulers.mainThread()] که توسط RxAndroid هدایت می‌شود، مشاهده خواهد شد؛

9.3.2. پروژه IntelliJ IDEA

پروژه اندروید [client] نام دارد:

این پروژه از طریق [2] اجرا خواهد شد.

توجه: اجرای برنامه به شدت به پیکربندی نسخه‌ی IntelliJ IDEA مورد استفاده بستگی دارد. احتمالاً دستور بالا بلافاصله روی دستگاهی غیر از دستگاه من کار نخواهد کرد. پیکربندی صحیح IntelliJ IDEA برای اجرای این پروژه می‌تواند برای مبتدیان کاری دلهره‌آور باشد. در اینجا چند نکته وجود دارد که باید در نظر داشته باشید:

  • در [3]، به ساختار پروژه بروید؛
  • در [4-5]، نسخه‌های اندروید JDK و SDK روی دستگاه من نصب شده‌اند. توجه داشته باشید که JDK 1.8 ضروری نیست. اندروید از برخی ویژگی‌های جاوا ۸، از جمله لامبداها، پشتیبانی نمی‌کند. بنابراین، برای نمونه‌سازی رابط‌های تابعی، از کلاس‌های ناشناس استفاده خواهیم کرد. بنابراین، JDK 1.6 کافی است. با این حال، پروژه‌ای که توزیع شده است با JDK 1.8 پیکربندی شده است؛

فایل [build.gradle] [6] که پروژه اندروید را پیکربندی می‌کند به شرح زیر است:


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // با نسخهٔ فعلی افزونهٔ اندروید جایگزین کنید
    classpath 'com.android.tools.build:gradle:1.5.0'
  }
}
apply plugin: 'com.android.application'
dependencies {
  compile 'com.android.support:appcompat-v7:23.1.1'
  compile 'com.android.support:design:23.1.1'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
  compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
  compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
  jcenter()
}
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "android.aleas"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_6
    targetCompatibility JavaVersion.VERSION_1_6
  }
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/NOTICE.txt'
    exclude 'META-INF/LICENSE.txt'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

بسته به فایل‌های Android SDK موجود، ممکن است نسخه‌های خطوط ۸، ۲۴–۲۵ و ۲۹ نیاز به اصلاح داشته باشند.

برای نصب فایل‌های جدید SDK اندروید، از SDK Manager به شرح زیر [1] استفاده کنید:

پروژه برای موارد زیر پیکربندی شده است:

  • SDK, API, 23, [2];
  • SDK Build-tools 23.0.3 [3];
  • SDK Tool 25.1.3 [4]

در نهایت، مسیر Android برای SDK را در فایل [local.properties] [4]، در خط 11 زیر بررسی کنید:


## این فایل به‌طور خودکار توسط اندروید استودیو تولید شده است.
# این فایل را ویرایش نکنید -- YOUR CHANGES WILL BE ERASED!
#
# این فایل باید *NOT* در سیستم‌های کنترل نسخه بررسی شود،
# زیرا حاوی اطلاعاتی است که مختص پیکربندی محلی شماست.
#
#مکان SDK. این فقط توسط Gradle استفاده می‌شود.
# برای سفارشی‌سازی هنگام استفاده از سیستم کنترل نسخه، لطفاً بخوانید
# یادداشت سربرگ.
#پنج‌شنبه ۷ آوریل ۱۴:۵۱:۱۴ CEST ۲۰۱۶
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk

9.3.3. اجرای پروژه IntelliJ IDEA

پس از ایجاد یک محیط مناسب برای پروژه، می‌توان آن را به صورت زیر اجرا کرد:

  • در [1]، شبیه‌ساز اندروید Genymotion راه‌اندازی می‌شود؛
  • در [2]، پیکربندی اجرای [app] اجرا می‌شود؛
  • در [3]، برای ایجاد یک پیکربندی اجرا؛
 
  • در [1, 3]، پیکربندی [app] نامگذاری شد؛
  • به [2]؛ این معادل اجرای ماژولی به نام [app] است؛
  • در [4]، مشخص می‌کنیم که هنگام اجرا، IDE باید یک دستگاه اجرایی پیشنهاد دهد. در اینجا، این دستگاه همیشه شبیه‌ساز Genymotion خواهد بود؛
  • در [5]، مشخص می‌کنیم که این دستگاه باید برای تمام اجراهای پیکربندی حفظ شود؛

اجرای پروژه روی شبیه‌ساز Genymotion با ساخت اولیه زیر آغاز می‌شود:

Image

برای اینکه بدانید در [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 را روی دستگاه خود وارد کنید (خطوط ۲۰، ۲۸، ۳۲). اگر فایروال ویندوز دارید، احتمالاً باید آن را غیرفعال کنید تا شبیه‌ساز اندروید بتواند به سرور عدد تصادفی دسترسی پیدا کند.

اجرای درخواست‌های ناهمزمان با اطلاعات بالا نتایج زیر را به دست می‌دهد:

Image

هر درخواست یک پاسخ jSON تولید می‌کند که شامل فیلدهای زیر است:

  • aleas: اعداد تصادفی تولید شده توسط سرور؛
  • idClient: شماره درخواست؛
  • on: نخ اجرای سمت کلاینت برای درخواست؛
  • requestAt: زمان درخواست؛
  • responseAt: زمان دریافت پاسخ؛
  • delay: زمان انتظار مشاهده‌شده توسط سرور قبل از بازگرداندن پاسخ آن؛
  • error: یک کد خطا – 0 در صورت عدم وجود خطا;
  • پیام: یک پیام خطا – null در صورت عدم وجود خطا؛
  • observedAt: زمانی که پاسخ مشاهده شد؛
  • observedOn: تریدی که پاسخ در آن مشاهده شده است. این همیشه [main] خواهد بود که به ترید رابط کاربری (UI thread) اشاره دارد؛

از آنجا که درخواست‌ها غیرهم‌زمان هستند و زمان‌های انتظار تحمیل‌شده بر سرور تصادفی است، پاسخ‌ها به‌صورت پراکنده بازمی‌گردند.

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'
}
  • وابستگی‌های در خطوط ۲–۳، وابستگی‌های استاندارد برای یک پروژه اندروید با SDK 23 هستند؛
  • وابستگی در خط ۵، شیء Spring با شناسه [RestTemplate] را معرفی می‌کند که ارتباط بین لایه [DAO] و سرور را مدیریت می‌کند؛
  • وابستگی در خط ۶ شامل کتابخانه‌های JSON و [Jackson] است که توسط برنامه استفاده می‌شوند؛
  • وابستگی در خط ۷، کتابخانه RxAndroid (و به همراه آن کتابخانه RxJava) را وارد می‌کند، که لایه Ui برای ارتباط با لایه [DAO] از آن استفاده می‌کند؛

9.3.5. بیانیهٔ برنامهٔ اندروید

  

<?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>
  • خط ۵: دسترسی به اینترنت باید مجاز باشد؛

9.3.6. لایه [DAO]

 

9.3.6.1. رابط [IDao] لایه [DAO]

رابط لایه [DAO] به شرح زیر خواهد بود:


package android.aleas.dao;

import android.aleas.fragments.Request;
import rx.Observable;

public interface IDao {

  // اعداد تصادفی در بازه [a,b]
  //n عدد تولید می‌شوند، که در آن n خود یک عدد تصادفی در بازه [minCount, maxCount] است
  // اعداد پس از تأخیری به میلی‌ثانیه تولید می‌شوند،
  // جایی که [delay] خود یک عدد تصادفی در بازه [minDelay, maxDelay] است
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // URL از سرویس وب
  public void setUrlServiceWebJson(String url);

  //حداکثر زمان انتظار (ms) برای پاسخ سرور به درخواست اتصال
  //حداکثر زمان انتظار (ms) برای پاسخ سرور به یک درخواست
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • خط ۱۲: متدی در لایه [DAO] که اعداد تصادفی را به‌صورت ناهمزمان تولید می‌کند؛
  • خط ۱۵: برای اطلاع‌رسانی به پیاده‌سازی [DAO] در مورد سرویس تولید عدد تصادفی URL؛
  • خط ۱۹: برای تنظیم حداکثر زمان‌های انتظار برای پیاده‌سازی [DAO]، به منظور جلوگیری از انتظارهای بیش از حد طولانی هنگامی که سرور پاسخ نمی‌دهد؛

متد [getAleas] تمام پارامترهای خود را در شیء زیر [Request] دریافت می‌کند:


package android.aleas.fragments;

public class Request {

  // شماره درخواست
  int id;
  // ورودی‌های کاربر
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // سازنده‌ها
  public Request() {

  }

  public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    this.id = id;
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
  }

  // گیرنده و تنظیم‌کننده
...
}

اکثر پارامترهای اینجا با پارامترهای شی URL سرور که نیاز به پرس‌وجو دارد، مطابقت دارند.

متد [getAleas] نوع Observable<AleasDaoResponse> را برمی‌گرداند، که در آن کلاس [AleasDaoResponse] به شرح زیر است:


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // کد خطا
  private int erreur;
  // پیام خطا
  private String message;
  // اتمام زمان سرور
  private int delay;
  // اعداد تصادفی تولید شده توسط سرور
  private List<Integer> aleas;
  //وضعیت مشتری
  private ClientState clientState;

  // سازنده‌ها

  public AleasDaoResponse() {
  }

  public AleasDaoResponse(int erreur, String message, int delay, List<Integer> aleas, ClientState clientState) {
    this.erreur = erreur;
    this.message = message;
    this.delay = delay;
    this.aleas = aleas;
    this.clientState = clientState;
  }

  // گیرنده و تنظیم‌کننده
...
}

نوع [ClientState] به شرح زیر است:


package android.aleas.dao;

import org.codehaus.jackson.map.annotate.JsonFilter;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class ClientState {

  // نام نخ اجرایی
  private String on;
  //زمان درخواست
  private String requestAt;
  // زمان پاسخ
  private String responseAt;
  //شناسهٔ مشتری
  private int idClient;

  // سازنده
  public ClientState() {
    on = Thread.currentThread().getName();
    requestAt = getTimeStamp();
  }

  public ClientState(int idClient) {
    this();
    this.idClient = idClient;
  }

  // متدهای خصوصی

  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  // گیرنده‌ها و تنظیم‌کننده‌ها
...
}
  • خط ۱۱: نخ اجرای لایه [DAO
  • خط ۱۳: زمان درخواست؛
  • خط ۱۵: زمان پاسخ؛
  • خط ۱۷: شماره درخواست؛

میدان‌های [on, requestAt, idClient] در آغاز درخواست توسط کلاینت مقداردهی می‌شوند. میدان [responseAt] زمانی مقداردهی می‌شود که کلاینت پاسخ را از سرور دریافت می‌کند.

9.3.6.2. پیاده‌سازی لایه [DAO]

  

رابط [IDao] با استفاده از کلاس زیر [Dao] پیاده‌سازی شده است:


package android.aleas.dao;

import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class Dao implements IDao {

  // مشتری REST
  private RestTemplate restTemplate;
  // URL سرویس
  private String urlServiceWebJson;

  // نقشه‌ساز jSON
  private ObjectMapper mapper;

  // سازنده‌ها
  public Dao() {
    // نقشه‌ساز jSON
    mapper = new ObjectMapper();
  }

  @Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    ...
  }

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // URL برای سرویس REST تنظیم شده است
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • خط ۲۲: شیء [RestTemplate] که ارتباط با سرور اعداد تصادفی را مدیریت خواهد کرد؛
  • خط ۲۴: URL برای سرویس تولید – توسط متد [setUrlServiceWebJson] در خط ۴۱ تنظیم می‌شود؛
  • خط ۲۷: مپر jSON، که برای دِسیریالایز کردن رشته jSON ارسال‌شده توسط سرور اعداد تصادفی استفاده خواهد شد؛
  • خطوط ۳۰–۳۳: سازنده کلاس؛
  • خط ۳۲: مپر jSON از خط ۲۷ ایجاد می‌شود؛

متد [setClientTimeouts] به شرح زیر است:


  // کلاینت REST
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // زمان‌انتظار درخواست برای کلاینت REST تنظیم شده است
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • ارتباط بین کلاینت و وب سرور / JSON توسط شیء [RestTemplate] در خط ۲ مدیریت می‌شود. ما هنوز آن را مقداردهی اولیه نکرده‌ایم. این کار توسط متد [setClientTimeouts] انجام می‌شود؛
  • خط ۸: کلاس [HttpComponentsClientHttpRequestFactory] توسط وابستگی [spring-android-rest-template] فراهم می‌شود. این کلاس به ما امکان می‌دهد حداکثر زمان‌های انتظار (timeout) را برای پاسخ سرور (خطوط ۹–۱۰) تنظیم کنیم؛
  • خط ۱۱: ما شیء از نوع [RestTemplate] را ایجاد می‌کنیم که به‌عنوان واسطه ارتباط با سرویس وب عمل خواهد کرد. ما شیء [factory] را که همین حالا ایجاد شده است، به‌عنوان پارامتر به آن ارسال می‌کنیم؛
  • خط ۱۲: دیالوگ کلاینت/سرور می‌تواند اشکال مختلفی داشته باشد. داده‌ها از طریق خطوط متنی مبادله می‌شوند و ما باید به شیء از نوع [RestTemplate] دستور دهیم که با هر خط متنی چه کاری انجام دهد. برای این کار، ما مبدل‌هایی را در اختیار آن قرار می‌دهیم – کلاس‌هایی که قادر به پردازش خطوط متنی هستند. انتخاب مبدل معمولاً از طریق هدرهای HTTP که همراه خط متن ارسال می‌شوند، انجام می‌گیرد. بر اساس این هدرها، شیء [RestTemplate] از میان مبدل‌های خود، مناسب‌ترین را برای آن شرایط انتخاب خواهد کرد. در اینجا، ما تنها یک مبدل خواهیم داشت، یک مبدل رشته → رشته، که به این معنی است که نوع String دریافتی از سرور هیچ تغییری نخواهد کرد.

متد [getAleas] پیچیده‌ترین است:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    //اجرای سرویس
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL سرویس: /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}
          String urlService = String.format("%s/%s/%s/%s/%s/%s/%s",
            urlServiceWebJson, request.getA(), request.getB(), request.getMinCount(),
            request.getMaxCount(), request.getMinDelay(), request.getMaxDelay());
          //اطلاعات مشتری
          ClientState clientState = new ClientState(request.getId());
          //درخواست هم‌زمان HTTP
          String response = executeRestService("get", urlService, null);
          // دسریالیزه‌سازی پاسخ سرور jSON
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // خطا؟
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // استثناء منتقل می‌شود
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            //ثبت زمان دریافت
            clientState.setResponseAt();
            // نتیجه برای مشترک ارسال می‌شود
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // استثنا را به مشترک فوروارد کنید
          subscriber.onError(ex);
        } finally {
          // نشان‌دادن پایان مشاهده‌پذیر
          //در زمان اجرا، مشخص می‌شود که این متد اگر قبلاً متد [onError] فراخوانی شده باشد – همان‌طور که انتظار می‌رود – هیچ تأثیری ندارد، بنابراین این عبارت تنها می‌تواند درون بلوک try قرار گیرد
          subscriber.onCompleted();
        }
      }
    });
  }
  • خط ۲: به یاد داشته باشید که ما باید یک نوع [Observable<AleasResponse>] تولید کنیم؛
  • خط ۳: یک ورودی لاگ در کنسول اندروید؛
  • خط ۵: شیء [RestTemplate] ارتباط همگام با سرور را تضمین می‌کند. این بدان معناست که نخ اجرایی که درخواست را ارسال می‌کند تا زمانی که پاسخ دریافت نشود، مسدود می‌ماند. در مثال Swing، دیدیم که چگونه با استفاده از متد [Observable.create] یک عملیات همزمان را به یک عملیات غیرهمزمان تبدیل کنیم. در اینجا نیز از همان رویکرد پیروی می‌کنیم؛
  • خط ۷: متد [call] از رابط [Observable.OnSubscribe<AleasDaoResponse>] در خط ۵. این متد است که زمانی فراخوانی می‌شود که یک ناظر به یک مشاهده‌پذیر مشترک می‌شود؛
  • خطوط ۱۰–۱۲: ایجاد شیء URL برای سرویس عدد تصادفی؛
  • خط ۱۴: مقداردهی اولیه شیء [ClientState]. هدف در اینجا ثبت زمان درخواست است؛
  • خط 16: درخواست همزمان HTTP. یک پاسخ jSON بازگردانده می‌شود. متد [executeRestService] سه پارامتر را انتظار دارد:
      1. متد HTTP که باید برای پرس‌وجو از سرویس استفاده شود؛
      2. URL سرویس؛
      3. شیء ارسال‌شده، از نوع Object یا null اگر متد HTTP از نوع POST نباشد؛
  • ۱۸–۲۱: دِسیریالیزه‌سازی رشتهٔ دریافتی jSON به نوع [AleasServerResponse]. این نوع به شرح زیر است:

package android.aleas.dao;

import java.util.List;

public class AleasServerResponse {

  //کد خطا
  private int erreur;
  // پیام خطا
  private String message;
  // اتمام زمان سرور
  private int delay;
  // اعداد تصادفی
  private List<Integer> aleas;

  // گیرنده و تنظیم‌کننده
...
}
  • خط ۲۳: کد خطا ارسال‌شده توسط سرور بازیابی می‌شود؛
  • خطوط ۲۴–۲۶: در صورت بروز خطا، یک استثنا به مشترک ارسال می‌شود؛
  • خط ۲۹: [clientState] به‌روزرسانی می‌شود؛ این بخشی از پاسخ ارسال‌شده به مشترک را تشکیل خواهد داد؛
  • خطوط ۳۱–۳۳: ارسال پاسخ به مشترک. این پاسخ به شکل [AleasDaoResponse] است؛
  • خطوط ۳۵–۳۷: رسیدگی به همهٔ موارد خطا به یک روش. محتمل‌ترین خطا، خطای شبکه است؛
  • خط ۴۱: اعلان پایان انتقال؛

9.3.7. دیدگاه‌های برنامه

  

برنامه دارای دو نمای زیر است:

نما پرس‌وجو

Image

نما پاسخ

Image

9.3.7.1. کلاس [MyFragment]

دو قطعه وجود دارد:

  • [RequestFragment] برای پرسش؛
  • [ResponseFragment] برای پاسخ؛

هر دو قطعه کلاس زیر را گسترش می‌دهند: [MyFragment]:


package android.aleas.fragments;

import android.aleas.activity.MainActivity;
import android.aleas.activity.Session;
import android.support.v4.app.Fragment;

public abstract class MyFragment extends Fragment {

  // ------------- داده‌های مشترک بین قطعات
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • خط ۷: کلاس [MyFragment] از کلاس Android، [Fragment]، ارث می‌برد؛
  • خطوط ۱۰–۱۱: داده‌های مشترک برای همهٔ قطعات؛
  • خط ۱۰: هر فرگمنت از یک فعالیت واحد برنامه آگاه است؛
  • خط ۱۱: برای ارتباط با یکدیگر، قطعات از یک جلسه (session) استفاده می‌کنند؛
  • خط ۱۳: قبل از نمایش یک فرگمنت، از آن خواسته می‌شود که خود را با محتوای جلسه (session) تازه کند. این متد به صورت انتزاعی (abstract) اعلام شده است زیرا توسط کلاس‌های فرزند پیاده‌سازی می‌شود. به همین دلیل، خود کلاس به صورت انتزاعی (abstract) اعلام شده است (خط ۷)؛

کلاس [Session] حاوی داده‌های مشترک بین قطعات مختلف برنامه است. کد آن به شرح زیر است:

  

package android.aleas.activity;

import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;

public class Session {

  // فعالیت برنامه
  private MainActivity activity;
  // تعداد درخواست‌ها
  private int nbRequests;
  // ویژگی‌های درخواست
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL سرویس وب / jSON
  private String urlWebJson;
  // آغاز عملیات
  private boolean onAir;
  // همانند مورد بالا، اما کمی بعد
  private boolean operationStarted;
  // نام مثالی که کاربر از فهرست مثال‌ها انتخاب کرده است
  private String exampleName;
  //شمارهٔ آن در فهرست قطعات
  private int examplePosition;
  // آداپتور چرخاننده برای مثال‌ها در نمای پرس‌وجو
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

  // متدها
  public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
    this.urlWebJson = urlWebJson;
    this.exampleName = exampleName;
    this.examplePosition = examplePosition;
  }

  public Request getRequest() {
    return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
  }

  // گیرنده و تنظیم‌کننده
...
}

متد در خط ۴۶ برای ایجاد شیء [Request] استفاده می‌شود که تمام اطلاعات ارائه‌شده توسط کاربر را در نمای پرس‌وجو در بر می‌گیرد:

  

package android.aleas.fragments;

public class Request {

  // شماره پرس‌وجو
  int id;
  // ورودی کاربر
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // سازنده‌ها
  public Request() {

  }

  public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    this.id = id;
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
  }

  // گیرنده و تنظیم‌کننده
....
}

9.3.7.2. قطعه [RequestFragment] از پرس‌وجو

قطعه پرس‌وجو دارای اجزای زیر است:

Image

این برنامه یک نما دارد که از دو زبانه تشکیل شده است:

  • [1]: زبانه درخواست؛
  • [2]: زبانه پاسخ؛

اجزای قطعه [RequestFragment] به شرح زیر است:

خیر.
نوع
نام
نقش
۳
EditText
edtNbRequests
تعداد درخواست‌هایی که باید به سرویس تولیدکنندهٔ اعداد تصادفی ارسال شود
۴
EditText
edtA, edtB
محدوده‌های [a,b] بازه تولید عدد؛
۵
EditText
edtMinCount, edtMaxCount
این سرویس اعداد count را تولید می‌کند، که در آن count یک عدد تصادفی در بازه [minCount, maxCount] است.
۶
EditText
edtMinDelay, edtMaxDelay
سرویس قبل از تولید اعداد، به مدت delay میلی‌ثانیه صبر می‌کند، که در آن delay یک عدد تصادفی در بازه [minDelay, maxDelay] است
۷
EditText
edtUrlServiceRest
URL از تولیدکننده عدد تصادفی؛
8
چرخ فلک
spinnerExemples
فهرست کشویی مثال‌ها. هر مثال یک روش خاص از کلاس [Observable] را نشان می‌دهد؛
8
دکمه
btnExecuter
دکمه که فراخوانی‌های سرویس تولید شماره را راه‌اندازی می‌کند؛

خطاهای ورودی علامت‌گذاری می‌شوند:

Image

اجزاء ۱ تا ۶، اجزاء [TextView] با نام‌های زیر (به ترتیب) هستند: txtErrorRequests، txtErrorIntervalle، txtErrorCount، txtErrorDelay، txtMsgErreurUrlServiceWeb.

9.3.7.3. قطعه [ResponseFragment] از پاسخ

قطعه پاسخ دارای اجزای زیر است:

Image

خیر.
نوع
نام
نقش
۱
TextView
infoReponses
تعداد پاسخ‌های دریافتی
۲
ListView
listReponses
فهرست کانال‌های jSON دریافت‌شده از سرور
۳
دکمه
btnAnnuler
برای لغو درخواست‌ها به سرور

9.3.7.4. فعالیت اندروید [MainActivity]

  

کلاس [MainActivity] نمای زیر را نمایش می‌دهد []:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context="android.arduinos.ui.activity.MainActivity">

  <!-- میله‌ی کاربردی -->
  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <!--نوار ابزار -->
    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">

      <!-- تصویر نگه‌دارنده جای -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
    </android.support.v7.widget.Toolbar>

    <!-- ظرفیت‌دهندهٔ برگه -->
    <android.support.design.widget.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
  </android.support.design.widget.AppBarLayout>

  <!-- ظرفیت نما -->
  <android.aleas.activity.MyPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:paddingRight="20dp"
    android:layout_marginBottom="100dp"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>

اجزای این نما به شرح زیر هستند:

رده‌ها
نوع
نام
نقش
۲۰–۳۴
ابزارک
ابزارک
ابزارکجایگاه
۲۹–۳۴
ProgressBar
loadingPanel
تصویر نشانگر جایگاهی که در حین پردازش درخواست کاربر نمایش داده می‌شود
۳۷–۴۰
TabLayout
زبانه‌ها
نوار برگه‌های برنامه
۴۴–۵۱
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 {

  //کنترل کشیدن
  private boolean isSwipeEnabled;

  // سازنده‌ها
  public MyPager(Context context) {
    super(context);
  }

  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  // پوشاندن متد
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // آیا کشیدن مجاز است؟
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // آیا کشیدن مجاز است؟
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // تنظیم‌کننده
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

}
  • کلاس [MyPager] از کلاس استاندارد اندروید [ViewPager] ارث می‌برد. ما از کلاس [MyPager] به جای کلاس [ViewPager] صرفاً به این دلیل استفاده می‌کنیم که می‌خواهیم کشیدن را غیرفعال کنیم: به طور پیش‌فرض، با کلاس [ViewPager]، می‌توانید با کشیدن بین برگه‌ها جابجا شوید (کشیدن به چپ یا راست). در اینجا، ما این رفتار را نمی‌خواهیم؛
  • خط ۱۱: بوله‌ای که کنترل لغزاندن را بر عهده دارد (خطوط ۲۶ و ۳۶)؛
  • خطوط ۴۴–۴۶: متدی که برای مقداردهی اولیه فیلد در خط ۱۱ استفاده می‌شود؛

اسکلت فعالیت اندروید [MainActivity] به شرح زیر است:


package android.aleas.activity;

import android.aleas.R;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.dao.Dao;
import android.aleas.dao.IDao;
import android.aleas.fragments.MyFragment;
import android.aleas.fragments.Request;
import android.aleas.fragments.RequestFragment;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar;
import rx.Observable;

public class MainActivity extends AppCompatActivity implements IDao {

  //لایه [DAO]
  private IDao dao;
  // جلسه
  private Session session;

  // سازنده
  public MainActivity() {
    // والد
    super();
    // جلسه
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // گیرنده‌ها

  public Session getSession() {
    return session;
  }

  //پیاده‌سازی IDao ----------------------------------------
  @Override
  public Observable<AleasDaoResponse> getAleas(Request request) {
    return dao.getAleas(request);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    dao.setClientTimeouts(connectTimeout, readTimeOut);
  }

}
  • خط ۲۱: کلاس [MainActivity] از کلاس استاندارد اندروید [AppCompatActivity] ارث می‌برد. بنابراین این یک فعالیت استاندارد اندروید است؛
  • خط ۲۱: کلاس [MainActivity] رابط [IDao] را پیاده‌سازی می‌کند؛

بازگشت به معماری برنامه:

اینکه این فعالیت، رابط لایه [DAO] را پیاده‌سازی می‌کند، به این معناست که ویوها از لایه [DAO] بی‌خبر هستند: پردازشگرهای رویداد آن‌ها زمانی که بخواهند با سرور تعامل داشته باشند، با لایه [activité] ارتباط برقرار خواهند کرد.

  • خط ۲۴: مرجعی به لایه [DAO] که توسط سازنده در خط ۳۵ مقداردهی اولیه شده است؛
  • خط ۲۶: اشاره‌ای به سشن مشترک میان قطعات، که توسط سازنده در خط ۳۳ اولیه شده است؛
  • خطوط ۴۶–۵۹: پیاده‌سازی رابط [IDao

کلاس [MainActivity] اجزای نمای مرتبط خود را به شرح زیر inicialize می‌کند:


  //نوار ابزار
  private Toolbar toolbar;
  // مدیر قطعه
  private MyPager mViewPager;
  // کانتینر تب
  private TabLayout tabLayout;
  // تصویر نگه‌دارنده جای
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    //کلاسیک
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // جلسه
    session.setActivity(this);
    // QZXW2HTML تنظیمات زمان‌بندی لایه BW0RBT10ZQX
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

    // اجزاء
    mViewPager = (MyPager) findViewById(R.id.container);
    toolbar = (Toolbar) findViewById(R.id.toolbar);
    loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
    tabLayout = (TabLayout) findViewById(R.id.tabs);

    //نوار ابزار
    setSupportActionBar(toolbar);

    // در ابتدا تنها یک زبانه وجود دارد
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // پردازشگر رویداد
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        //یک برگه انتخاب شده است – قطعه نمایش‌داده‌شده توسط محفظهٔ قطعه تغییر می‌کند
        int position = tab.getPosition();
        if (position == 0) {
          // زبانه‌ی پرس‌وجو
          showView(0);
        } else {
          // زبانه‌ی پاسخ – بستگی به مثال انتخاب‌شده دارد
          showView(session.getExamplePosition());
        }
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {

      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {

      }
    });

    // ایجاد قطعات پاسخ
    createResponseFragments();

    // مدیریت بارگذاری تصویر
    loadingPanel.setVisibility(View.INVISIBLE);
}

این کد در یک فعالیت نسبتاً استاندارد است. بیایید چند نکته را روشن کنیم:

  • خط ۱۹ به کلاس زیر، [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";
}
  • خطوط ۳۱–۳۳: اولین تب با عنوان [Request] ایجاد می‌شود. در مقطعی، وضعیت حافظه به شکل زیر خواهد بود:
    • قطعه [Request];
    • n قطعه از نوع [ExampleXXFragment

زبانه‌ی اول همیشه قطعه [Request] را نمایش می‌دهد. زبانه‌ی دوم قطعه [ExampleXXFragment] را که مربوط به مثالی است که کاربر انتخاب کرده، نمایش می‌دهد. بنابراین، قطعه‌ی نمایش‌داده‌شده توسط زبانه‌ی دوم با گذشت زمان تغییر می‌کند؛

  • خطوط ۳۷–۴۸: کدی که هنگام کلیک کاربر روی یکی از برگه‌ها اجرا می‌شود؛
  • خط ۴۳: قطعه شماره ۰ نمایش داده می‌شود؛
  • خط ۴۶: قطعه در حال استفاده (روی صفحه) نمایش داده می‌شود. شماره آن از جلسه بازیابی می‌شود؛
  • خط ۶۲: قطعات مربوط به تمام مثال‌ها در چرخ‌وفلک مثال در نما [RequestFragment] (زبانه‌ی اول) ایجاد می‌شوند؛
  • خط ۶۵: تصویر در حال بارگذاری در حال حاضر مخفی است؛

برای درک متد [showView] (خطوط ۴۳، ۴۶) و متد [createResponseFragments]، ابتدا باید مدیر قطعه در حافظه (یک کلاس گنجانده شده در فایل جاوا MainActivity) را معرفی کنیم:


  //مدیر قطعه – باید متدهای getItem و getCount را تعریف کند
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // قطعات مدیریت‌شده
    private MyFragment[] fragments;

    // سازنده
    public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
      super(fm);
      this.fragments = fragments;
    }

    //باید موقعیت قطعه شماره را بازگرداند
    @Override
    public MyFragment getItem(int position) {
      // قطعه
      return fragments[position];
    }

    // تعداد قطعاتی را که باید مدیریت شوند بازمی‌گرداند
    @Override
    public int getCount() {
      // تعداد قطعات
      return fragments.length;
    }
  }
}
  • کلاس [SectionsPagerAdapter] از کلاس اندروید [FragmentPagerAdapter] ارث می‌برد. این کلاس دو متد از کلاس والد خود را بازنویسی می‌کند:
    • متد [getItem]، خط ۱۵؛
    • متد [getCount]، خط 22؛
  • کلاس [SectionsPagerAdapter] شامل تمام قطعات برنامه است. این قطعات در خط 5 ذخیره شده‌اند. شایان ذکر است که آن‌ها از نوع [MyFragment] هستند، همان‌طور که در بخش 9.3.7.1 توضیح داده شده است؛
  • خط ۸: باید ساخته شود، کلاس [SectionsPagerAdapter] قطعاتی را که باید مدیریت کند دریافت می‌کند؛
  • خطوط 14–18: متد [getItem] قطعه را در موقعیت [position] بازمی‌گرداند؛
  • خطوط 21–25: متد [getCount] تعداد کل قطعات را بازمی‌گرداند؛

متد [createResponseFragments] تمام قطعات مورد نیاز برنامه را ایجاد می‌کند:


private void createResponseFragments() {
    // چرخ‌گردان برای مثال‌ها
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // قالب نمایش را زمانی که فهرست گزینه‌ها ظاهر می‌شود مشخص کنید
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // آداپتور به جلسه اضافه می‌شود تا نما [Request] بتواند آن را بازیابی کند
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • خط ۳: یک اداپتور برای اسپینر مثال‌ها ایجاد می‌شود، در این مورد یک لیست از String که نمایانگر نام‌های مثال‌ها است. این نام‌ها در فایل [layout/exemples.xml] قرار دارند:
  

فایل [exemples.xml] حاوی کد زیر است:


<!-- نمونه‌ها -->
<resources>
  <string-array name="exemples">
    <item>Exemple-01</item>
    <item>Exemple-02</item>
    <item>Exemple-03</item>
    <item>Exemple-04</item>
  </string-array>
</resources>

خط ۱: این فایل پارامتر دوم متد [createFromResource] است. در [R.array.exemples]، [exemples] نام آرایه (خط ۳ بالا) است، نه نام فایل.

  • خط ۵: یک layout (مدیر نمایش) به آداپتور مرتبط می‌شود. اکنون آداپتور هم داده‌ها و هم حالت نمایش آن را در اختیار دارد؛
  • خط ۷: آداپتور در جلسه قرار می‌گیرد. اینجاست که قطعه [RequestFragment]، که به آن نیاز دارد، آن را بازیابی خواهد کرد؛

بیایید با کد متد [createResponseFragments] ادامه دهیم:


private void createResponseFragments() {
    // نمونه‌های چرخ‌فلک
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // قالب نمایش را زمانی که فهرست گزینه‌ها ظاهر می‌شود مشخص کنید
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    //ما آداپتور را در جلسه قرار می‌دهیم تا نما [Request] بتواند آن را بازیابی کند
    session.setSpinnerExemplesAdapter(adapter);
    //ایجاد جدول قطعه (۱ پرس‌وجو، n پاسخ)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // قطعه پرس‌وجو
    tFragments[0] = new RequestFragment();
    // قطعات پاسخ‌ها
    for (int i = 1; i < tFragments.length; i++) {
      // نام قطعهٔ مورد نظر برای نمونه‌سازی، مطابق با مثالی که کاربر انتخاب کرده است، ساخته می‌شود
      // این نام باید نام کامل شامل پکیج آن باشد – در اینجا مستقیماً با شماره مثال در چرخ فلک مرتبط است
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      //قطعه مرتبط با مثال نمونه‌سازی می‌شود
      MyFragment fragment;
      try {
        // نمونه سازی کلاس
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      //قطعه ایجاد شده است – آن را به آرایه اضافه می‌کنیم
      tFragments[i] = fragment;
    }
    // نمونه سازی مدیر قطعه با این قطعات جدید
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // ViewPager را با اداپتور بخش‌ها راه‌اندازی کنید.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // پیمایش بین صفحات – این دستور مهم است
    // در اینجا گفته شده است که در هر دو طرف نمای نمایش‌داده‌شده، باید نماهای اولیه را حفظ کنیم [tFragments.length]
    // این بدان معناست که همهٔ قطعات مورد استفادهٔ برنامه در حافظه قرار دارند و مقداردهی شده‌اند
    //اگر این کار انجام نشود، به‌طور پیش‌فرض [OffscreenPageLimit] روی 1 تنظیم می‌شود
    // پس اگر قطعه نمایش داده شده شماره ۳ باشد، تنها قطعات ۲ و ۴ مقداردهی اولیه می‌شوند
    // این کار با فراخوانی متد [onCreateView] برای این دو قطعه انجام می‌شود – این بدان معناست که در داخل این متد باید اطمینان حاصل کنید که
    // بازتولید ظاهر بصری که قطعه آخرین بار که استفاده شد داشت – علاوه بر این، نباید هیچ کدی درون این متد وجود داشته باشد
    // حاوی هر کدی باشد که نتوان آن را دو بار اجرا کرد – این امر آشفتگی عظیمی ایجاد می‌کند و مدیریت آن پیچیده است
    // در اینجا تصمیم گرفتیم از این مشکلات اجتناب کنیم – لاگ‌ها نشان می‌دهند که همهٔ قطعات هنگام راه‌اندازی برنامه ایجاد می‌شوند
    //و روش آنها [onCreateView] اجرا می‌شود – پس از آن هرگز دوباره اجرا نمی‌شود –
    mViewPager.setOffscreenPageLimit(tFragments.length);
    //ما پیمایش بین قطعات را غیرفعال کرده‌ایم
    mViewPager.setSwipeEnabled(false);
  }
  • خط ۹: ایجاد آرایه‌ای که حاوی تمام قطعات برنامه خواهد بود؛
  • خط ۱۱: اولین قطعه، قطعه درخواست است؛
  • خطوط ۱۳–۲۸: ما به تعداد مثال‌ها، قطعه ایجاد خواهیم کرد. تمام این قطعات از قطعه پاسخ [ResponseFragment] ارث‌بری می‌کنند و تنها آنچه را که به مثال خاص مربوط است پیاده‌سازی می‌کنند: ایجاد مقادیر مشاهده‌شده. این موارد واقعاً از یک مثال به مثال دیگر متفاوت هستند؛
  • خط 16: یک قطعه نمونه نام استانداردی دارد: ExampleXXFragment، که در آن XX موقعیت آن در چرخ‌وفلک نمونه‌ها به‌علاوه 1 است. XX همچنین شماره قطعه مثال در مدیر قطعه است؛
  • خط ۲۱: نمونه‌سازی قطعه مثال شماره i از چرخ‌وفلک:
    • Class.forName(exampleName): قطعه را در حافظه بارگذاری می‌کند؛
    • Class.forName(exampleName).getConstructors()[0]: مرجع به اولین سازنده کلاس را بازیابی می‌کند. کلاس ExampleXXFragment تنها یک سازنده دارد. بنابراین یک مرجع به این سازنده به دست می‌آید؛
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) با استفاده از سازنده مرحله قبل، یک شی از نوع ExampleXXFragment را نمونه‌سازی می‌کند. new Object[]{} نشان‌دهنده پارامترهای ارسال‌شده به این سازنده است. از آنجایی که سازنده کلاس ExampleXXFragment هیچ پارامتری را انتظار ندارد، یک آرایه خالی از اشیاء ارسال می‌شود؛
  • خط ۲۷: این قطعه به آرایهٔ قطعات اضافه می‌شود؛
  • خط ۳۰: ما دیدیم که سازندهٔ دست‌کارگر قطعه `[SectionsPagerAdapter]`، آرایهٔ قطعاتی را که قرار است مدیریت کند، به‌عنوان پارامتر انتظار دارد. اکنون این آرایه را به آن می‌دهیم؛
  • خط ۲۲: کانتینر قطعه [mViewPager] از نمای مرتبط با فعالیت [MainActivity] در اینجا با مدیر قطعه مرتبط می‌شود: کانتینر قطعه [mViewPager] قطعات را از مدیر قطعه نمایش می‌دهد؛
  • خط ۴۳: توضیحات را بخوانید – این دستور در عمل بیان می‌کند که همهٔ قطعات باید در حالتی که کد آن‌ها را قرار می‌دهد باقی بمانند، صرف‌نظر از اینکه کدام قطعه در حال حاضر نمایش داده می‌شود. در نتیجه، وقتی به آن بازگردید، آن را در حالتی خواهید یافت که آن را ترک کرده‌اید؛
  • خط ۴۵: کانتینر قطعه [mViewPager] از نوع [MyPager] است که کشیدن را غیرفعال می‌کند؛

متد [MainActivity.showView] به شرح زیر است:


  //نمای نمایش شماره [position]
  private void showView(int position) {
    // بخش را قبل از نمایش آن تازه‌سازی کنید
    mSectionsPagerAdapter.getItem(position).onRefresh();
    //نمای درخواست‌شده نمایش داده می‌شود – نما مستقیماً فراخوانی می‌شود (پارامتر دوم روی false تنظیم شده است)
    //بدون این پارامتر، سیستم به نمای مورد نظر پیش‌فرض می‌رود در حالی که به‌طور موقت نماهای میانی را نمایش می‌دهد – رفتار نامطلوب
    mViewPager.setCurrentItem(position, false);
}
  • خط ۳: ما می‌خواهیم قطعه شماره موقعیت را نمایش دهیم؛
  • خط ۴: این قطعه از مدیر قطعه درخواست می‌شود و سپس تازه می‌شود. این به این دلیل است که جلسه ممکن است از آخرین نمایش آن تغییر کرده باشد. بنابراین قطعه باید جلسه را بررسی کند تا ببیند آیا نیاز به به‌روزرسانی دارد یا خیر؛
  • خط ۷: قطعه توسط [ViewPager] نمایش داده می‌شود. از آنجایی که این با مدیر قطعه مرتبط شده است، قطعه شماره [position] نمایش داده خواهد شد – همان قطعه‌ای که در خط ۴ تازه شده بود؛

بیایید با دو روش برای مدیریت انتظار به پایان برسیم:


  public void beginWaiting() {
    // پردازش بارگذاری تصویر
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // مدیریت بارگذاری تصویر
    loadingPanel.setVisibility(View.INVISIBLE);
    //پایان اجرا
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. قطعه [RequestFragment]

کلاس [RequestFragment] به شرح زیر است:


package android.aleas.fragments;

import android.aleas.R;
import android.aleas.activity.Constants;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;

import java.net.URI;
import java.net.URISyntaxException;

public class RequestFragment extends MyFragment {

  // URL سرویس وب
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // تعداد درخواست‌ها
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // فاصله زمانی تولید
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // تأخیر
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // تعداد مقادیر تولید شده
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // دکمه
  private Button btnExecuter;
  // فهرست پاسخ‌ها
  private ListView listReponses;
  private TextView infoReponses;
  // چرخ فلش نمونه
  private Spinner spinnerExemples;

  // ورودی‌ها
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

  // سازنده
  public RequestFragment() {
    super();
    Log.d("rxjava", "RequestFragment constructor");
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    //بازیابی فعالیت و جلسه
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    //ایجاد نمای قطعه از تعریف آن XML
    View view = inflater.inflate(R.layout.request, container, false);
    // کامپوننت‌ها
    edtUrlServiceRest = (EditText) view.findViewById(R.id.editTextUrlServiceWeb);
    txtMsgErreurUrlServiceWeb = (TextView) view.findViewById(R.id.textViewErreurUrl);
    edtNbRequests = (EditText) view.findViewById(R.id.edt_nbrequests);
    txtErrorRequests = (TextView) view.findViewById(R.id.txt_error_nbrequests);
    edtA = (EditText) view.findViewById(R.id.edt_a);
    edtB = (EditText) view.findViewById(R.id.edt_b);
    txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
    edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
    edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
    txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
    edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
    edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
    txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
    btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);

    // دکمه [Exécuter]
    btnExecuter.setVisibility(View.VISIBLE);
    btnExecuter.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doExecuter();
      }
    });

    //در ابتدا، هیچ پیام خطایی وجود ندارد
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // نمونه‌ی چرخنده‌ی انتخاب
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // نتیجه
    return view;
  }
...
}
  • خط ۱۶: کلاس [RequestFragment] از کلاس [MyFragment] ارث می‌برد (به بخش ۹.۳.۷.۱ مراجعه کنید)؛
  • خطوط ۱۸–۴۲: اجزای بصری قطعه (به بخش ۹.۳.۷.۲ مراجعه کنید)؛
  • خطوط ۴۵–۵۲: ورودی‌های کاربر در فرم؛
  • سازنده (خطوط ۵۵–۵۸) و متد [onCreateView] زمانی اجرا می‌شوند که فعالیت [MainActivity] تمام قطعات برنامه را ایجاد می‌کند. این اتفاق فقط یک بار رخ می‌دهد؛
  • خط 61: کد متد [onCreateView] استاندارد است. توجه کنید که در خط 102، اداپتور اسپینر از مثال‌ها در جلسه گنجانده شده است. همچنین باید در خط ۹۱ توجه داشت که کلیک روی دکمه [Exécuter] توسط متد [doExecuter] مدیریت می‌شود؛
  • خطوط 64–65: فیلدهای [activity] و [session] به کلاس والد [MyFragment] تعلق دارند؛

متد [doExecuter] به شرح زیر است:


  // ورودی‌ها
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

...

  private void doExecuter() {
    //آیا ورودی‌ها معتبر هستند؟
    if (isPageValid()) {
      //ذخیره اطلاعات در جلسه
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      //ذخیره کردن URL از سرویس وب
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // اقدام در حال انجام
      session.setOnAir(true);
      //اما هنوز شروع نشده است
      session.setOperationStarted(false);
      // نمایش بخش پاسخ
      activity.selectTab(Constants.VUE_RESPONSE);
      // انتظار آغاز می‌شود
      beginWaiting();
    }
}
  • خط ۱۵: ما در مورد متد [ispageValid] توضیحی نمی‌دهیم. این متد اعتبار ورودی‌ها را بررسی می‌کند و تنها در صورتی مقدار 'true' را برمی‌گرداند که همه آنها معتبر باشند. در این مورد، از آنها برای مقداردهی اولیه فیلدها در خطوط ۲ تا ۹ استفاده می‌شود؛
  • خط 17: ورودی‌های مختلف در جلسه ذخیره می‌شوند:
    • [spinnerExemples.getSelectedItem().toString()] نام مثالی است که توسط کاربر انتخاب شده و در [session.exampleName] ذخیره می‌شود؛
    • [spinnerExemples.getSelectedItemPosition() + 1] شمارهٔ قطعهٔ مرتبط با مثال است که توسط مدیر قطعه ذخیره شده است. این شماره در [session.examplePosition] ذخیره شده است؛
  • خط ۱۹: URL از سرویس وب / jSON به فعالیت ارسال می‌شود که به نوبه خود آن را به لایه [DAO] ارسال می‌کند؛
  • خطوط 21–24: توجه کنید که یک عملیات در شرف شروع است؛
  • خط ۲۶: برگه پاسخ قرار است نمایش داده شود. برای درک آنچه قرار است رخ دهد، باید کد [MainActivity.selectTab] را به یاد بیاوریم:

  // زبانه‌ی انتخاب‌شده
  public void selectTab(int position) {
    // حداکثر ۲ برگه وجود دارد
    //در ابتدا تنها یک تب وجود دارد: تبی که از درخواست آمده است
    //اگر زبانهٔ درخواست‌شده شمارهٔ ۱ باشد و هنوز وجود نداشته باشد، باید ایجاد شود
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // ۱ تب اضافی
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // برنامه تب را انتخاب می‌کند که رویداد [onTabSelected] را فعال می‌کند
    // که نمای صحیح را با این زبانه مرتبط می‌سازد
    tabLayout.getTabAt(position).select();
}
  • در ابتدا، فعالیت تنها زبانه درخواست (زبانه شماره ۰) را ایجاد کرده بود؛
  • خطوط ۶–۱۱: اگر زبانه پاسخ (زبانه شماره ۱) قبلاً ایجاد نشده باشد، ایجاد می‌شود؛
  • خط ۱۴: برگه شماره position (۰ یا ۱) انتخاب می‌شود. این کار رویداد [onTabSelected] را در صف حلقه رویدادهای برنامه اندروید قرار می‌دهد؛

پردازشگر رویداد [onTabSelected] در [MainActivity] به شرح زیر است:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        //یک تب انتخاب شده است – قطعه نمایش‌داده‌شده توسط محفظهٔ قطعه تغییر می‌کند
        int position = tab.getPosition();
        if (position == 0) {
          // زبانه‌ی درخواست
          showView(0);
        } else {
          // زبانه‌ی پاسخ – بستگی به مثال انتخاب‌شده دارد
          showView(session.getExamplePosition());
        }
}

در مورد برگه [Response]، خط ۹ اجرا می‌شود. قطعه شماره [session.getExamplePosition()] نمایش داده خواهد شد. برای مثال، در مورد [exemple-03]، عدد ثبت‌شده در [session.examplePosition] برابر با ۳ است. سپس خط ۱۰ قطعه شماره ۳ را نمایش می‌دهد. جدول قطعاتی که در ابتدا توسط فعالیت ایجاد شده است [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..] است. بنابراین قطعه [Exemple03Fragment] نمایش داده خواهد شد. این کار با استفاده از کد زیر انجام می‌شود:


  //نمای نمایش شماره [position]
  private void showView(int position) {
    // قطعه قبل از نمایش تازه می‌شود
    mSectionsPagerAdapter.getItem(position).onRefresh();
    //نمای درخواست‌شده نمایش داده می‌شود – سیستم مستقیماً به نما می‌رود (پارامتر دوم روی false تنظیم شده است)
    //بدون این پارامتر، سیستم به‌طور پیش‌فرض به نمای مورد نظر می‌رود در حالی که به‌طور مختصر نماهای میانی را نمایش می‌دهد – رفتاری نامطلوب
    mViewPager.setCurrentItem(position, false);
}

می‌توانیم ببینیم که قطعه قبل از نمایش (خط ۷) تازه می‌شود (خط ۴).

9.3.7.6. قطعه [ResponseFragment]

کلاس [ResponseFragment] پاسخ‌های سرور را نمایش می‌دهد. کد آن به شرح زیر است:


package android.aleas.fragments;

import android.aleas.R;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Subscription;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public abstract class ResponseFragment extends MyFragment {

  // فهرست پاسخ‌ها
  private ListView listReponses;
  private TextView infoReponses;
  // دکمه
  private Button btnAnnuler;

  // نقشه‌کش jSON
  private ObjectMapper mapper;

  protected ResponseFragment() {
    super();
    Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
    mapper = new ObjectMapper();
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // بازیابی فعالیت و جلسه
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    //نمای قطعه را از تعریف آن ایجاد می‌کند XML
    View view = inflater.inflate(R.layout.response, container, false);
    // کامپوننت‌ها
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    //دکمه [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // نتیجه
    return view;
  }
...
  //روشی که باید قبل از هر بار نمایش فرگمنت (از طریق کد صریح) اجرا شود
  public void onRefresh() {
...
  }
}
  • خط ۲۱: کلاس [ResponseFragment] از کلاس [MyFragment] ارث می‌برد؛
  • خطوط ۲۳–۲۷: اجزای قطعه؛
  • خطوط ۳۲–۳۶: سازنده تنها یک بار، زمانی که فعالیت در ابتدا نمونه‌های فرگمنت را ایجاد می‌کند، اجرا می‌شود. این به این دلیل است که تمام نمونه‌های فرگمنت از فرگمنت [ResponseFragment] ارث می‌برند. هنگامی که نمونه‌سازی می‌شوند، سازنده کلاس والد آنها [ResponseFragment] فراخوانی می‌شود؛
  • خط ۳۵: مپر jSON از خط ۳۰ را که برای نمایش رشته jSON از یک استک استثنا استفاده می‌شود، مقداردهی اولیه می‌کند؛
  • خطوط ۳۸–۵۹: متد [onCreateView] تنها یک بار، در حین ایجاد اولیه قطعات نمونه توسط فعالیت، اجرا می‌شود. این متد حاوی کد استاندارد موجود در یک برنامه اندروید است؛
  • خطوط ۵۲–۵۶: متدی که هنگام کلیک روی دکمه [Annuler] اجرا می‌شود، متد [doAnnuler] است؛
  • خطوط ۶۲–۶۴: متد [onRefresh] هر بار که تب [Response] نمایش داده می‌شود، اجرا می‌شود؛

با کمک لاگ‌های مختلفی که در روش‌های کلیدی قرار گرفته‌اند، می‌توانیم ببینیم هنگام راه‌اندازی برنامه چه اتفاقی می‌افتد:

05-17 08:45:05.803 14158-14158/android.aleas D/rxjava: RequestFragment constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7}) constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: Example01Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example02Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example03Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example04Fragment constructor
05-17 08:45:05.934 14158-14158/android.aleas D/rxjava: RequestFragment onCreateView
05-17 08:45:05.962 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7 #1 id=0x7f0d006e android:switcher:2131558510:1}) onCreateView
05-17 08:45:05.969 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654 #2 id=0x7f0d006e android:switcher:2131558510:2}) onCreateView
05-17 08:45:05.972 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd #3 id=0x7f0d006e android:switcher:2131558510:3}) onCreateView
05-17 08:45:05.978 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2 #4 id=0x7f0d006e android:switcher:2131558510:4}) onCreateView
  • خط ۱: ساخت قطعه [RequestFragment
  • خطوط ۲–۹: ساخت قطعات برای چهار مثال برنامه؛
  • خط ۱۰: инициалиزاسيون قطعه [RequestFragment];
  • خطوط ۱۱–۱۴: مقداردهی اولیهٔ قطعات برای چهار مثال در برنامه؛

پس از آن، دیگر هیچ فراخوانی برای این متدها وجود ندارد.

متد [ResponseFragment.onRefresh] به شرح زیر است:


  //متدی که باید قبل از نمایش هر فرگمنت (از طریق کد صریح) اجرا شود
  public void onRefresh() {
    Log.d("rxjava", String.format("ResponseFragment (%s) onRefresh for %s, sessionIsOnAir=%s session.isOperationStarted=%s%n", this, activity == null ? null : activity.getSession().getExampleName(), session.isOnAir(), session.isOperationStarted()));
    //آیا اجرای در حال انجام است؟
    if (session.isOnAir() && !session.isOperationStarted()) {
      // اجرای درخواست
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • خط ۵: بررسی می‌کنیم که آیا قطعه [RequestFragment] درخواستی (session.isOnAir) ارسال کرده است و آیا آن درخواست شروع شده است (isOperationStarted). اگر قطعه [RequestFragment] درخواستی ارسال کرده باشد و آن درخواست هنوز در حال اجرا نباشد، عملیات آغاز می‌شود (خطوط ۷–۸)؛
  • پس از راه‌اندازی عملیات، از آنجا که این عملیات ناهمزمان است، کاربر می‌تواند بین دو زبانه جابجا شود. اگر کاربر در حین انجام عملیات به زبانه [Response] بازگردد، خطوط ۷–۸ اجرا نخواهند شد؛

متد [doExecuter] در خط ۸ عملیات درخواستی کاربر را اجرا می‌کند:


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // شروع انتظار
    beginWaiting();
    //آماده‌سازی اجرا
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    //ایجاد و اجرای مشاهده‌پذیرها از مثال انتخاب‌شده
    createAndExecuteObservables();
}

// روشی که توسط کلاس‌های فرزند پیاده‌سازی شده است
protected abstract void createAndExecuteObservables();
  • خط ۱۰: قابل‌مشاهده‌ها را ایجاد، اجرا و مشاهده می‌کند. این موارد برای هر مثال متفاوت هستند. به همین دلیل متد [createAndExecuteObservables] انتزاعی است (خط 14). این متد توسط قطعات [ExampleXXFragment] که کلاس [ResponseFragment] را گسترش می‌دهند، پیاده‌سازی خواهد شد؛
  • خط ۶: لیست اشتراک‌ها پاک می‌شود؛
  • خط ۷: لیست نمایش‌دهنده پاسخ‌ها پاک می‌شود؛
  • خط ۸: تعداد پاسخ‌های دریافتی را می‌شمارد؛

کلاس‌های فرزند [ExampleXXFragment] وظیفه نمایش عناصری را که نظارت می‌کنند به متد زیر، [showAlea]، واگذار می‌کنند:


  protected void showAlea(String data) {
    // اطلاعات بیشتر
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // ۱ پاسخ دیگر
    reponses.add(0, data);
    Log.d("rxjava", data);
    // به‌روزرسانی برای UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • خط ۱: می‌بینیم که عنصر مشاهده‌شده به صورت یک رشته دریافت می‌شود. این در واقع همان رشته jSON از عنصر مشاهده‌شده خواهد بود. این به ما امکان می‌دهد تا یک متد واحد برای نمایش عنصر مشاهده‌شده، صرف‌نظر از نوع دقیق جاوا آن، داشته باشیم؛
  • خط ۶: عنصر مشاهده‌شده [data] به بالای لیست پاسخ‌ها اضافه می‌شود. بنابراین کاربر جدیدترین پاسخ‌ها را در بالای لیست می‌بیند؛

صف توسط متدهای زیر مدیریت می‌شود: [beginWaiting] و [cancelWaiting]:


  private void beginWaiting() {
    // شکل ساعت شنی نمایش داده می‌شود
    activity.beginWaiting();
    //دکمه [Annuler] نمایش داده می‌شود
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    //پایان انتظار
    activity.cancelWaiting();
    //دکمه [Annuler] پنهان است
    btnAnnuler.setVisibility(View.INVISIBLE);
}

این‌ها متدهایی با همان نام‌ها را در داخل فعالیت فراخوانی می‌کنند و به سادگی دکمه [Annuler] را نشان می‌دهند یا مخفی می‌کنند.

کلیک روی دکمه [Annuler] توسط کد زیر مدیریت می‌شود:


  protected void doAnnuler() {
    // تمام اشتراک‌ها لغو شده‌اند
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    //پایان انتظار
    cancelWaiting();
}
  • خطوط ۳–۷: تمام اشتراک‌ها یکی یکی لغو می‌شوند؛

9.3.8. نمونه‌هایی از مشاهده‌شدنی‌ها

9.3.8.1. Exemple-01

کلاس‌های [ExampleXXFragment] برای ایجاد، اجرا و مشاهدهٔ مقادیر قابل مشاهده طراحی شده‌اند. مقادیر مشاهده‌شده توسط کلاس والد [ResponseFragment] نمایش داده می‌شوند.

کلاس [Example01Fragment] به شرح زیر است:

  

package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import java.io.IOException;

public class Example01Fragment extends ResponseFragment {

    // نقشه‌سازان jSON
    private ObjectMapper mapperAleasUiResponse;

    // سازنده
    public Example01Fragment() {
        super();
        Log.d("rxjava", "Example01Fragment constructor");
        // فیلترها jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // درخواست اعداد تصادفی
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // پیکربندی قابل مشاهده شماره i
            //درخواست ارسال‌شده به سرور
            Request request = session.getRequest();
            request.setId(i);
            //قابل مشاهده‌ای که روی یک نخ محاسباتی اجرا می‌شود
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // مشاهده در نخ حلقه رویداد؛
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // تمام این قابل‌مشاهده‌ها اجرا می‌شوند
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
...
        }, new Action0() {
...
    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // اطلاعات قابل نمایش استخراج می‌شود
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • خط ۳۶: تنها مشاهده‌پذیر واحد که تولید خواهد شد؛
  • خطوط ۳۷–۴۴: تولید و پیکربندی مشاهدات‌پذیرهای مختلف، که در خط ۴۳ با مشاهدات‌پذیر خط ۳۶ ادغام می‌شوند؛
  • خط ۴۳: این observable در یک thread از زمان‌بندی‌کننده [Schedulers.io()] اجرا می‌شود. فراخوانی HTTP به سرور در این thread اجرا خواهد شد؛
  • خط ۴۶: قابل مشاهده نهایی در نخ حلقه رویداد مشاهده می‌شود؛
  • خطوط ۴۸–۵۷: اجرای قابل‌مشاهده‌ها و در نتیجه درخواست‌ها به سرور عدد تصادفی. اندروید هنوز از جاوا ۸ و لامبداهای آن پشتیبانی نمی‌کند. بنابراین ما در اینجا از کلاس‌های ناشناس برای نمونه‌سازی رابط‌های تابعی RxJava استفاده می‌کنیم؛
  • خطوط ۴۹–۵۲: عملی که وقتی ناظر یک عنصر جدید از نوع [AleasDaoResponse] را از مشاهده‌پذیر دریافت می‌کند، اجرا می‌شود (به بخش ۹.۳.۶.۱ مراجعه کنید)؛
  • خط ۵۱: فراخوانی متد [showAlea] کلاس والد. به یاد داشته باشید که این متد یک رشته را انتظار دارد. این رشته توسط متد [getDataFrom] در خطوط ۵۹–۶۸ فراهم می‌شود؛
  • خط ۶۳: رشته jSON از نوع [AleasUiResponse] به شرح زیر بازگردانده می‌شود:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class AleasUiResponse {

  // پاسخ [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // رشتهٔ مشاهده
  private String observedOn;
  //زمان مشاهده
  private String observedAt;

  // سازنده‌ها
  public AleasUiResponse() {
    observedOn = Thread.currentThread().getName();
    observedAt = new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse, String on, String at) {
    this.aleasDaoResponse = aleasDaoResponse;
    this.observedOn = on;
    this.observedAt = at;
  }

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
    this();
    this.aleasDaoResponse = aleasDaoResponse;
  }
// گیرنده و تنظیم‌کننده
...
}
  • به پاسخ از لایه [DAO] (خط ۱۱)، دو مورد اطلاعات اضافه می‌شود:
    • خط ۱۳: نخ مشاهده؛
    • خط ۱۵: زمان مشاهده؛

بیایید به کد اشتراک بازگردیم:


    @Override
    public void createAndExecuteObservables() {
...
        // ما همهٔ این قابل‌مشاهده‌ها را اجرا می‌کنیم
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                // استثناء نمایش داده می‌شود
                showAlea(getMessagesFromThrowable(th));
                //پس از پرتاب یک استثنا، ناظر هیچ‌کدام از onNext یا onCompleted را دریافت نمی‌کند
                // مجبور به لغو دستی اشتراک
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                //پایان انتظار
                cancelWaiting();
            }
        }));
}
  • خطوط ۱۱–۱۸: حالتی که ناظر یک استثنا دریافت می‌کند؛
  • خط 14: متد [showAlea] از کلاس والد دوباره برای نمایش استثنا استفاده می‌شود. متد [getMessagesFromThrowable] یک متد از کلاس والد [ResponseFragment] است که یک رشتهٔ متنی از یک استثنا تولید می‌کند:

  //پیام‌های استثنا
  protected String getMessagesFromThrowable(Throwable ex) {
    // فهرستی ایجاد می‌شود که شامل پیام‌های خطا از پشته استثنا است
    List<String> messages = new ArrayList<String>();
    Throwable th = ex;
    while (th != null) {
      messages.add(String.format("[%s, %s]", th.getClass().getName(), th.getMessage()));
      th = th.getCause();
    }
    try {
      return mapper.writeValueAsString(messages);
    } catch (IOException e) {
      return e.getMessage();
    }
}
  • خط ۱۱: رشته jSON از یک لیست پیام‌های خطا (خط ۴) بازیابی می‌شود؛

بیایید به کد مربوط به اشتراک‌گذاری ناظر بازگردیم:

  • خطوط ۱۹–۲۵: کدی که هنگام دریافت اعلان پایان پخش توسط ناظر اجرا می‌شود. سپس انتظار لغو می‌شود (خط ۲۳)، که رابط کاربری گرافیکی را به‌روزرسانی می‌کند؛

اجرای مثال 01 خروجی‌ای مشابه خروجی زیر تولید می‌کند:

Image

هر عنصر در این لیست، رشته jSON است که یک مقدار مشاهده‌شده را نشان می‌دهد. فیلدهای موجود در رشته jSON به شرح زیر هستند:

  • aleas: لیست اعداد تصادفی بازگردانده‌شده توسط سرور؛
  • idClient: شماره درخواست (مشاهده می‌شود که پاسخ‌ها به ترتیب تصادفی بازگردانده شده‌اند)؛
  • on: نخ اجرایی مشاهده‌پذیری که این مقدار را منتشر کرده است؛
  • requestAt: زمان درخواست کلاینت؛
  • responseAt: زمان پاسخ سرور؛
  • delay: تأخیلی که توسط سرور مشاهده شده است؛
  • error: کد خطا بازگردانده شده توسط سرور (0=بدون خطا);
  • message: پیام خطای بازگردانده شده توسط سرور (null=بدون خطا);
  • observedAt: زمانی که مقدار مشاهده‌شده ثبت شده است؛
  • observedOn: رشته‌ای که مقدار مشاهده‌شده در آن ثبت شده است؛

9.3.8.2. Exemple-02

کلاس [Example02Fragment] به شرح زیر است:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

import java.io.IOException;

public class Example02Fragment extends ResponseFragment {

    // نقشه‌بردارها jSON
    private ObjectMapper mapperAleasUiResponse;

    // سازنده
    public Example02Fragment() {
        super();
        Log.d("rxjava", "Example02Fragment constructor");
        // فیلتر jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // اعداد تصادفی درخواست می‌شوند
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            //در حال آماده‌سازی درخواست
            Request request = session.getRequest();
            request.setId(i);
            // فقط قابل مشاهده‌ها با شماره مشتری زوج حفظ می‌شوند
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // اجرا روی نخ I/O
                            .subscribeOn(Schedulers.io()));
        }
        // مشاهده روی نخ حلقه رویداد
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // اجرای این مشاهدات
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                showAlea(getMessagesFromThrowable(th));
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                //پایان انتظار
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        //استخراج اطلاعات برای نمایش
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

این مثال مشابه مثال قبلی است (خط ۳۸). با این حال، از میان مشاهدات به‌دست‌آمده در مثال قبلی، تنها آن‌هایی که شماره مشتری‌شان زوج است (خطوط ۴۲–۴۶) با استفاده از روش [filter] (خط ۴۱) نگه‌داری می‌شوند.

نتایج به‌دست‌آمده به شرح زیر است (برای ۱۰ پرس‌وجو):

Image

9.3.8.3. Exemple-03

کلاس [Example03Fragment] به شرح زیر است:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

import java.io.IOException;
import java.util.List;

public class Example03Fragment extends ResponseFragment {

  // نقشه‌سازها jSON
  private ObjectMapper mapper;

  // سازنده
  public Example03Fragment() {
    super();
    Log.d("rxjava", "Example03Fragment constructor");
    // فیلتر jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // اعداد تصادفی درخواست می‌شوند
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      //آماده‌سازی درخواست
      Request request = session.getRequest();
      request.setId(i);
      //  پیکربندی قابل مشاهده
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).map(new Func1<AleasDaoResponse, List<Integer>>() {
        @Override
        public List<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getAleas();
        }
      })
        // اجرا روی نخ I/O
        .subscribeOn(Schedulers.io()));
    }
    // مشاهده در نخ حلقه رویداد
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // اجرای این مشاهدات
    subscriptions.add(observable
      .subscribe(new Action1<List<Integer>>() {
                   @Override
                   public void call(List<Integer> aleas) {
                     showAlea(getDataFrom(aleas));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            //پایان انتظار
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    //استخراج اطلاعات برای نمایش
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

این مثال مشابه مثال-۰۲ است:

  • خط ۴۰: همان مشاهدات در مثال-۰۲ تعریف شده‌اند؛
  • خط ۴۵: هر مقداری که توسط مشاهده‌پذیرهای قبلی صادر می‌شود، با استفاده از متد [map] به نوع List<Integer> تبدیل می‌شود، که فهرست اعداد تصادفی تولید شده توسط سرور است؛
  • خط ۵۸: مقدار مشاهده‌شده اکنون از نوع List<Integer> است؛

نتیجه حاصل برای ۱۰ درخواست به شرح زیر است:

Image

9.3.8.4. Exemple-04

کلاس [Example04Fragment] به شرح زیر است:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

public class Example04Fragment extends ResponseFragment {

  // نقشه‌بردارها jSON
  private ObjectMapper mapper;

  // سازنده
  public Example04Fragment() {
    super();
    Log.d("rxjava", "Example04Fragment constructor");
    // فیلتر jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // اعداد تصادفی درخواست می‌شوند
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      //آماده‌سازی درخواست
      Request request = session.getRequest();
      request.setId(i);
      // پیکربندی قابل مشاهده‌ها
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
        @Override
        public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return Observable.from(aleasDaoResponse.getAleas());
        }
      })
        // اجرا روی یک نخ I/O
        .subscribeOn(Schedulers.io()));
    }
    // مشاهده در نخ حلقه رویداد
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // اجرای این قابل‌مشاهده‌ها
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            //پایان انتظار
            cancelWaiting();
          }
        }
      ));

  }
}

این مثال مشابه مثال-۰۳ است، با این تفاوت که به جای استفاده از متد [map] در خط ۴۲، از متد [flatMap] استفاده می‌شود.

  • خط ۵۵: توجه کنید که نوع مقدار مشاهده‌شده اکنون Integer است؛

برای ۱۰ پرس‌وجو، نتایج زیر به دست می‌آیند:

Image

این بار، مقادیر مشاهده‌شده بیشتر از پرس‌وجوها هستند.

9.3.8.5. Exemple-05

اکنون رویهٔ افزودن یک مثال جدید از مشاهدات به برنامه را تشریح می‌کنیم.

فرض کنید می‌خواهیم مثال [Exemple22h] را از بخش 7.6.4 بازتولید کنیم:


package dvp.rxjava.observables.exemples;

import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;

public class Exemple22h {
    public static void main(String[] args) throws InterruptedException {
        // فرآیند
        Observable<GroupedObservable<Boolean, Integer>> obs = Observable.range(1, 10).groupBy(i -> i % 2 == 0);
        Process<Integer> process = new Process<>("process", obs.concatMap(g -> g.asObservable()));
        // اشتراک‌ها
        ProcessUtils.subscribe(1, process);
    }
}
  • مقادیر متغیر مشاهداتی [Observable.range(1, 10)] ابتدا توسط روش [groupBy] (خط ۱۱) به مقادیر زوج و فرد گروه‌بندی می‌شوند و سپس توسط روش [concatMap] (خط ۱۲) به یک متغیر مشاهداتی واحد ترکیب می‌شوند؛

مرحله ۱

یک مثال جدید در فایل [exemples.xml] ایجاد می‌شود:

  

<!-- مثال‌ها -->
<resources>
  <string-array name="exemples">
    <item>Exemple-01</item>
    <item>Exemple-02</item>
    <item>Exemple-03</item>
    <item>Exemple-04</item>
    <item>Exemple-05</item>
  </string-array>
</resources>

در بالا، خط ۸ اضافه شده است. نامی که به مثال داده می‌شود می‌تواند هر چیزی باشد.

مرحله ۲

کلاس [Example04Fragment] را به‌عنوان [Example05Fragment] کپی کنید. در این مورد، نام از پیش تعریف شده است.

مرحله ۳

کد [Example05Fragment] را به شرح زیر تغییر دهید:


package android.aleas.exemples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.GroupedObservable;
import rx.schedulers.Schedulers;

public class Example05Fragment extends ResponseFragment {

  // نقشه‌سازها jSON
  private ObjectMapper mapper;

  // سازنده
  public Example05Fragment() {
    super();
    Log.d("rxjava", "Example05Fragment constructor");
    // فیلتر jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    //نمونه‌هایی از رابط‌های کاربردی
    // فیلتر
    Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
      @Override
      public Boolean call(AleasDaoResponse aleasDaoResponse) {
        return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
      }
    };
    // flatMap
    Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
        return Observable.from(aleasDaoResponse.getAleas());
      }
    };
    // groupBy
    Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
      @Override
      public Boolean call(Integer integer) {
        return integer % 2 == 0;
      }
    };
    // concatMap
    Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
        return integerIntegerGroupedObservable.asObservable();
      }
    };
    // درخواست اعداد تصادفی
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // در حال آماده‌سازی درخواست
      Request request = session.getRequest();
      request.setId(i);
      //پیکربندی قابل مشاهده
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // اجرا روی یک نخ ورودی/خروجی
        .subscribeOn(Schedulers.io());
    }
    // مشاهده در نخ حلقه رویداد
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // اجرای این مشاهدات‌پذیرها
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            //پایان انتظار
            cancelWaiting();
          }
        }
      ));

  }
}
  • خط ۶۷: نشان‌دهنده مشاهده‌پذیر مثال ۰۴ است: یک جریان اعداد صحیح؛
  • خط ۶۸: ما این جریان اعداد صحیح را بر اساس یک معیار بولی که قصد داریم تعریف کنیم، گروه‌بندی می‌کنیم. ما یک مشاهده‌پذیر از نوع Observable<GroupedObservable<Boolean, Integer>> به دست خواهیم آورد، که در نتیجه عناصری از نوع GroupedObservable<Boolean, Integer> را منتشر می‌کند؛
  • خط ۶۸: متد [concatMap] عناصر از نوع Integer را از عناصر از نوع GroupedObservable<Boolean, Integer> تولید خواهد کرد؛
  • خطوط ۳۲–۵۹: برای خواناتر کردن ایجاد `observable` در خطوط ۶۷–۶۹، ما نمونه‌های `functional interface` مورد نیاز اپراتورهای مختلف `[filter, flatMap, groupBy, concatMap]` را جدا کرده‌ایم؛
  • خطوط ۴۷–۵۲: متد [groupBy] منتظر دریافت پارامتری از نوع Func1<T,K> است، که در آن T نوع عناصر گروه‌بندی‌شده و K نوع معیار گروه‌بندی است. بر اساس عنصر T، نمونه Func1<T,K> مسئول تولید کلید گروه‌بندی عنصر K است؛
  • خطوط ۴۸–۵۱: عناصر از نوع Integer بر اساس هم‌مقداری گروه‌بندی می‌شوند. مثال Func1<Integer,Boolean> بسته به اینکه عنصر باید در کدام گروه قرار گیرد، کلید true یا false را تولید می‌کند. نتیجه دو گروه است: گروه عناصر زوج با کلید true و گروه عناصر فرد با کلید false؛
  • خطوط ۵۳–۵۹: متد [concatMap] انتظار یک پارامتر از نوع Func1<T,Observable<R>> را دارد و یک مشاهده‌پذیر از عناصر نوع R تولید می‌کند. نوع T در اینجا، نوعی خواهد بود که توسط اپراتور [groupBy] صادر می‌شود، که در این مورد یک نوع GroupedObservable<Boolean, Integer> است؛
  • خط ۵۷: از عنصر با نوع [GroupedObservable<Boolean, Integer>]، یک Observable<Integer> تولید می‌شود. از آنجایی که اپراتور [groupBy] دو گروه تولید کرده است، اپراتور [concatMap] دو Observable از نوع [Observable<Integer>] تولید خواهد کرد. مانند [flatMap]، آن‌ها را در یک ناظر واحد مسطح می‌کند. با این حال، برخلاف [flatMap]، عناصر ناظرهای مسطح‌شده را با هم مخلوط نمی‌کند. بنابراین باید دو گروه مجزا مشاهده کنیم: اعداد تصادفی زوج و فرد.

مرحله ۴

ما برنامه را اجرا می‌کنیم:

Image

و نتایج زیر را به دست می‌آوریم:

Image

  • در [1]، اعداد زوج تصادفی؛ در [2]، اعداد فرد؛

9.3.8.6. ادامه

اکنون از خوانندگان دعوت می‌شود تا مثال‌های خود را ایجاد کرده و همچنین با مقادیر مختلف برای فیلدهای فرم که درخواست‌های ارسال‌شده به سرور عدد تصادفی را پیکربندی می‌کنند، آزمایش کنند.

9.3.9. نتیجه‌گیری

ما معماری زیر را در محیط اندروید ایجاد کرده‌ایم:

کلاینت اندروید:

لایه [DAO] با سروری که اعداد تصادفی نمایش‌داده‌شده توسط تبلت اندروید را تولید می‌کند، ارتباط برقرار می‌کند. این سرور دارای معماری دو لایه زیر است:

لایه [DAO] n درخواست (HTTP) به سرور اعداد تصادفی ارسال کرد، در حالی که لایه [swing] به صورت غیرهمزمان منتظر نتایج این درخواست‌ها برای نمایش آن‌ها بود. این n درخواست (HTTP) به یک سرور واحد ارسال شدند که پاسخ‌های یکسانی را بازگردانده است. این امر به ما امکان داد تا پاسخ‌ها را (mergeWith) در یک مشاهده‌گر واحد ادغام کنیم.

در واقع، اپلیکیشن‌های اندروید با سرورهای مختلف ارتباط برقرار می‌کنند و پاسخ‌های آن‌ها به احتمال زیاد ادغام نخواهند شد. درخواست‌های HTTP به این سرورها به طور مستقل از یکدیگر پردازش می‌شوند و نتایج آن‌ها از طریق متدهای جداگانه مشاهده خواهند شد.