Skip to content

3. دراسة حالة - إدارة المواعيد

3.1. المشروع

في المستند [Tutoriel AngularJS / Spring 4]، تم تطوير تطبيق عميل/خادم لإدارة مواعيد الأطباء. سنشير لاحقًا إلى هذا المستند [rdvmedecins-angular]. كان للتطبيق نوعان من العملاء:

  • عميل HTML / CSS / JS؛
  • عميل Android؛

تم الحصول على عميل Android تلقائيًا من إصدار HTML للعميل باستخدام الأداة [Cordova]. سيكون الهدف هنا هو إعادة إنشاء عميل Android هذا يدويًا باستخدام المعرفة المكتسبة في الفصول السابقة.

تجدر الإشارة إلى وجود فرق مهم بين الحلين:

  • الحل الذي سنقوم بإنشائه لن يكون قابلاً للاستخدام إلا على أجهزة Android اللوحية؛
  • في الإصدار [rdvmedecins-angular]، عميل الويب المحمول (HTML / CSS / JS) قابل للاستخدام على أي منصة (Android، IoS، Windows

3.2. طرق عرض عميل Android

هناك أربع طرق عرض.

طريقة عرض الإعداد

Image

طريقة عرض اختيار الطبيب وتاريخ الموعد

Image

عرض اختيار الفترة الزمنية للموعد

Image

عرض اختيار العميل للموعد

Image

3.3. بنية المشروع

سيكون لدينا بنية عميل/خادم مشابهة لتلك الموجودة في المثال [Exemple-15] (انظر الفقرة 1.16) من هذا المستند:

Image

سيتم إدارة التبادلات غير المتزامنة بين العميل والخادم باستخدام مكتبة RxAndroid.

3.4. قاعدة البيانات

لا تلعب قاعدة البيانات دورًا أساسيًا في هذا المستند. نقدمها هنا على سبيل المعلومات. سنسميها [dbrdvmedecins] . وهي قاعدة بيانات MySQL5 تحتوي على أربع جداول:

  

3.4.1. الجدول [MEDECINS]

تحتوي على معلومات عن الأطباء الذين تديرهم التطبيق [RdvMedecins].

  • ID: رقم تعريف الطبيب - المفتاح الأساسي للجدول
  • VERSION: رقم تعريف إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
  • NOM: اسم الطبيب
  • PRENOM: اسمه الأول
  • TITRE: لقبه (الآنسة، السيدة، السيد)

3.4.2. الجدول [CLIENTS]

يتم تسجيل عملاء الأطباء المختلفين في الجدول [CLIENTS]:

  • ID: رقم تعريف العميل - المفتاح الأساسي للجدول
  • VERSION: رقم يحدد إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
  • NOM: اسم العميل
  • PRENOM: اسمه الأول
  • TITRE: لقبها (الآنسة، السيدة، السيد)

3.4.3. الجدول [CRENEAUX]

تسرد الجدول الفترات الزمنية التي يمكن فيها استخدام RV:

  • ID: رقم تعريف الفترة الزمنية - المفتاح الأساسي للجدول (السطر 8)
  • VERSION: رقم تعريف إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
  • ID_MEDECIN: رقم يحدد الطبيب الذي ينتمي إليه هذا الموعد – مفتاح خارجي في العمود MEDECINS(ID).
  • HDEBUT: وقت بدء الفترة الزمنية
  • MDEBUT: دقائق بداية الفترة
  • HFIN: ساعة انتهاء الموعد
  • MFIN: دقائق نهاية الفترة الزمنية

تشير السطر الثاني من الجدول [CRENEAUX] (انظر [1] أعلاه) ، على سبيل المثال ، إلى أن الفترة رقم 2 تبدأ في الساعة 8:20 وتنتهي في الساعة 8:40 وتخص الطبيب رقم 1 (السيدة ماري PELISSIER).

3.4.4. الجدول [RV]

تسرد المواعيد المحددة لكل طبيب:

  • ID: رقم يحدد RV بشكل فريد – مفتاح أساسي
  • JOUR: يوم RV
  • ID_CRENEAU: الفترة الزمنية لـ RV - مفتاح خارجي في الحقل [ID] من الجدول [CRENEAUX] – يحدد في آن واحد الفترة الزمنية والطبيب المعني.
  • ID_CLIENT: رقم العميل الذي تم الحجز لصالحه – مفتاح خارجي في الحقل [ID] من الجدول [CLIENTS]

يحتوي هذا الجدول على قاعدة بيانات ( ) مقيدة بالتفرد على قيم الأعمدة المرتبطة (JOUR، ID_CRENEAU):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

إذا كان أحد صفوف الجدول [RV] يحتوي على القيمة (JOUR1، ID_CRENEAU1) للأعمدة (JOUR، ID_CRENEAU)، فلا يمكن أن توجد هذه القيمة في أي مكان آخر. وإلا، فهذا يعني أنه تم أخذ RV مرتين في نفس الوقت لنفس الطبيب. من منظور برمجة Java، يقوم برنامج التشغيل JDBC للقاعدة بإطلاق SQLException عند حدوث هذه الحالة.

السطر الذي يساوي 3 في id (انظر [1] أعلاه) يعني أنه تم حجز موعد RV للفترة رقم 20 والعميل رقم 4 في 23/08/2006. يخبرنا الجدول [CRENEAUX] أن الموعد رقم 20 يتوافق مع الفترة الزمنية 16:20 - 16:40 وينتمي إلى الطبيب رقم 1 (السيدة ماري PELISSIER). تُظهر لنا الجدولة [CLIENTS] أن العميل رقم 4 هو الآنسة بريجيت BISTROU.

3.4.5. إنشاء قاعدة البيانات

لإنشاء الجداول وتعبئتها، يمكن استخدام البرنامج النصي [dbrdvmedecins.sql] الموجود في أرشيف الأمثلة |ICI|.

  

باستخدام [WampServer] (انظر الفقرة 6.15)، يمكننا القيام بما يلي:

 
  • في [1]، انقر على أيقونة [WampServer] واختر الخيار [PhpMyAdmin] [2]،
  • في [3]، في النافذة التي فتحت، حدد الرابط [Bases de données
 
  • إلى [4-6]، ثم قم باستيراد ملف SQL،
  • في [7]، يتم تحديد البرنامج النصي SQL وفي [8] يتم تنفيذه،
  • في [9]، تم إنشاء جداول قاعدة البيانات. نتبع أحد الروابط،
 
  • في [10]، محتوى الجدول.

بعد ذلك، لن نعود إلى هذه القاعدة، ولكن ندعو القارئ إلى متابعة تطورها خلال الاختبارات، خاصةً عندما لا يعمل التطبيق.

3.5. خادم الويب / jSON

Image

نحن مهتمون هنا بخادم [1]. لن نقوم بتطويره. فقد تم تفصيله في الوثيقة [Spring MVC et Thymeleaf par l'exemple]. يمكن للقارئ المهتم الرجوع إليها. وقد تم تطويره مثل خادم المثال 15. ويتم توفير شفرة المصدر الخاصة به في الأمثلة. سنستخدم هنا ملفه الثنائي:

  
  • [rdvmedecins-server-all-1.0.jar] هو الملف الثنائي للخادم؛

3.5.1. التنفيذ

في نافذة الأوامر، ننتقل إلى المجلد الذي يحتوي على الملف الثنائي للخادم:


...\rdvmedecins>dir
 Le volume dans le lecteur D s’appelle Données
 Le numéro de série du volume est 7A34-AE5F

 Répertoire de D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins

09/06/2016  10:50    <DIR>          .
09/06/2016  10:50    <DIR>          ..
06/07/2014  16:36             7 631 dbrdvmedecins.sql
08/06/2016  16:31    <DIR>          rdvmedecins-client
08/06/2016  16:22    <DIR>          rdvmedecins-server
08/06/2016  16:23        29 618 709 rdvmedecins-server-all-1.0.jar

ثم لتشغيل الخادم، اكتب الأمر التالي (يجب أن يكون SGBD MySQL قد تم تشغيله بالفعل):


...\rdvmedecins>java -jar rdvmedecins-server-all-1.0.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                  (v1.0)

10:55:48.617 [main] INFO  rdvmedecins.boot.Boot - Starting Boot v1.0 on st-PC (D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins\rdvmedecins-server-all-1.0.jar started by st in D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins)
10:55:48.621 [main] INFO  rdvmedecins.boot.Boot - No active profile set, falling back to default profiles: default
10:55:48.662 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Thu Jun 09 10:55:48 CEST 2016]; root of context hierarchy
10:55:49.948 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardService startInternal
INFOS: Starting service Tomcat
juin 09, 2016 10:55:50 AM org.apache.catalina.core.StandardEngine startInternal
INFOS: Starting Servlet Engine: Apache Tomcat/8.0.33
juin 09, 2016 10:55:50 AM org.apache.catalina.core.ApplicationContext log
INFOS: Initializing Spring embedded WebApplicationContext
10:55:50.255 [localhost-startStop-1] INFO  o.s.web.context.ContextLoader - Root
WebApplicationContext: initialization completed in 1596 ms
...
10:55:55.765 [localhost-startStop-1] INFO  o.s.s.web.DefaultSecurityFilterChain
- Creating filter chain: ...]
10:55:55.785 [localhost-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
10:55:55.791 [localhost-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
...
10:55:56.249 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String)
throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.252 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.255 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws
com.fasterxml.jackson.core.JsonProcessingException
10:55:56.257 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.259 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET],produces=[application/json;charset=UTF-8]}" onto
public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.261 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}"
onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.264 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.266 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.268 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.270 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.273 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.276 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
10:55:56.681 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
10:55:56.686 [main] INFO  rdvmedecins.boot.Boot - Started Boot in 8.231 seconds

يعرض الخادم العديد من السجلات. لم نذكر أعلاه سوى تلك المفيدة للفهم:

  • الأسطر 14-18: يتم تشغيل خادم Tomcat مدمج على المنفذ 8080 للجهاز. هذا الخادم هو الذي يقوم بتشغيل تطبيق الويب لإدارة المواعيد. هذا التطبيق هو في الواقع خدمة ويب / jSON: يتم استدعاؤه عبر URL ويستجيب بإرسال سلسلة jSON؛
  • السطر 24: خدمة الويب مؤمنة باستخدام إطار العمل [Spring Security]. يتم الوصول إلى URL لخدمة الويب عن طريق المصادقة؛
  • الأسطر 29-44: URL المعروضة بواسطة خدمة الويب؛

سنقوم بتفصيل هذه النقاط.

3.5.2. تأمين خدمة الويب

يتم تأمين ملفات URL التي يعرضها خدمة الويب. ينتظر الخادم في طلب HTTP من العميل الرأس التالي:

Authorization: Basic code

الرمز المتوقع هو الترميز base64 [http://fr.wikipedia.org/wiki/Base64] لسلسلة 'مستخدم:كلمة مرور'. لا تقبل خدمة الويب في حالتها الأولية سوى مستخدم 'admin' بكلمة مرور 'admin'. يصبح العنوان أعلاه بالنسبة لهذا المستخدم بالذات السطر التالي:

Authorization: Basic YWRtaW46YWRtaW4=

من أجل إرسال هذا الرأس HTTP، نستخدم العميل HTTP [Advanced Rest Client] وهو مكون إضافي لمتصفح Chrome (انظر الفقرة 6.13). سنقوم باختبار مختلف URL المعروضة من قبل خدمة الويب يدويًا لفهم:

  • المعلمات المتوقعة من قبل URL؛
  • الطبيعة الدقيقة لردها؛

3.5.3. قائمة الأطباء

تسمح URL [/getAllMedecins] بالحصول على قائمة الأطباء:

  • في [1]، الاستعلام URL؛
  • في [2]، الطريقة HTTP المستخدمة لهذا الاستعلام؛
  • في [3]، رأس HTTP لأمان المستخدم (admin، admin
  • في [4]، يتم إرسال الطلب HTTP؛

رد الخادم هو التالي:

  • في [5]، يتم تنسيق استجابة الخادم jSON؛
  • إلى [6]، نفس الرد في صيغته الأولية؛

يتيح الشكل [5] رؤية بنية الرد بشكل أفضل. جميع ردود خدمة الويب هي مثيل للفئة [Response] التالية:


package rdvmedecins.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- الخصائص
    // حالة العملية
    private int status;
    // رسائل الخطأ المحتملة
    private List<String> messages;
    // نص الرد
    private T body;

    // المنشئات
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // مُستردات ومُعيّنات
...
}
  • السطر 9: حالة الرد. القيمة 0 تعني أنه لم تحدث أي أخطاء، وإلا فهذا يعني حدوث خطأ؛
  • السطر 11: قائمة برسائل الخطأ في حالة حدوث خطأ؛
  • السطر 13: الرد الذي ينتظره العميل فعليًا؛

الرد على URL [/getAllMedecins] هو السلسلة jSON لكائن من النوع [Response<List<Medecin>>]. الفئة [Medecin] هي التالية:


package rdvmedecins.android.dao.entities;

public class Medecin extends Personne {

    // منشئ افتراضي
    public Medecin() {
    }

    // منشئ مع معلمات
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }

}

السطر 3، الفئة [Medecin] توسع الفئة [Personne] التالية:


package rdvmedecins.android.dao.entities;

public class Personne extends AbstractEntity {
    // سمات الشخص
    private String titre;
    private String nom;
    private String prenom;

    // منشئ افتراضي
    public Personne() {
    }

    // منشئ مع معلمات
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }

    // مُستردات ومُعيّنات
    ...
}

السطر 3، الفئة [Personne] توسع الفئة [AbstractEntity] التالية:


package rdvmedecins.android.dao.entities;

import java.io.Serializable;

public class AbstractEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    protected Long id;
    protected Long version;

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    // التهيئة
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }

    @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id == other.id;
    }

    // الوصول والضبط
    ...
}

في النهاية، تكون بنية كائن [Medecin] كما يلي:


[Long id; Long version; String titre; String nom; String prenom;]

وهيكل كائن [Response<List<Medecin>>] هو كما يلي:

[int status; List<String> messages; List<Medecin> medecins]

بعد ذلك، سنستخدم هذه التعريفات المختصرة لوصف استجابة الخادم. علاوة على ذلك، لن نعرض لقطات شاشة لفترة من الوقت. يكفي تكرار ما رأيناه للتو. سنعود إلى لقطات الشاشة عندما يتعين إجراء استعلام POST. كما سنقدم مثالاً للتنفيذ بالشكل التالي:

URL

/getAllMedecins
Réponse
{"status":0,"messages":null,"medecins":
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},
{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},
{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]}

3.5.4. قائمة العملاء

URL

/getAllClients
Réponse

Response<List<Client>> :[int status; List<String> messages;
 List<Client> clients]
Client : [Long id;  Long version; String titre;
 String nom; String prenom;]

مثال:

URL

/getAllClients
Réponse
{"status":0,"messages":null,"clients":
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},
{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},
{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]}

3.5.5. قائمة مواعيد الطبيب

URL
/getAllCreneaux/{idMedecin}
Réponse

Response<List<Creneau>>:[int status ; List<String> messages ;
 List<Creneau> creneaux]
Creneau : [int hdebut ; int mdebut ; int hfin ; int mfin ;]
  • [idMedecin]: معرّف الطبيب الذي نريد معرفة مواعيد استشاراته؛
  • [hdebut]: وقت بدء الاستشارة؛
  • [mdebut]: الدقائق التي تبدأ فيها الاستشارة؛
  • [hfin]: ساعة انتهاء الاستشارة؛
  • [mfin]: الدقائق التي تنتهي فيها الاستشارة؛

بالنسبة لفترة زمنية بين الساعة 10:20 و 10:40، سيكون لدينا [hdebut, mdebut, hfin, mfin]=[10, 20, 10, 40].

مثال:

URL
/getAllCreneaux/1
Réponse
{"status":0,"messages":null,"creneaux":
[{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"idMedecin":1},
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"idMedecin":1},
{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"idMedecin":1},
{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"idMedecin":1},
{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"idMedecin":1},
{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"idMedecin":1},
{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"idMedecin":1},
{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"idMedecin":1},
{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"idMedecin":1},
{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"idMedecin":1},
{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"idMedecin":1},
{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"idMedecin":1},
{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"idMedecin":1},
{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"idMedecin":1},
{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"idMedecin":1},
{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"idMedecin":1},
{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"idMedecin":1},
{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"idMedecin":1},
{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"idMedecin":1},
{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"idMedecin":1},
{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"idMedecin":1},
{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"idMedecin":1},
{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"idMedecin":1}]}

3.5.6. قائمة مواعيد الطبيب

URL
/getRvMedecinJour/{idMedecin}/{jour}
Réponse

Response<List<Rv>>:[int status ; List<String> messages ;
 List<Rv> rvs]
Rv : [Date jour ; Client client ; Creneau creneau ;
 long idClient ; long idCreneau]
  • [idMedecin]: معرف الطبيب الذي نريد الحصول على مواعيده؛
  • URL [jour]: يوم المواعيد بالصيغة 'سنة-شهر-يوم'؛
  • الرد [jour]: كما سبق ولكن في صيغة تاريخ Java؛
  • [client]: عميل الموعد. تم وصف هيكله سابقًا؛
  • [idClient]: معرف العميل؛
  • [creneau]: فترة الموعد. تم وصف هيكلها سابقًا؛
  • [idCreneau]: معرف الفترة الزمنية؛

مثال:

URL
/getRvMedecinJour/1/2014-07-08
Réponse
{"status":0,"messages":null,
"rvs":[{"id":45,"version":0,"jour":"2014-07-08","client":
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},"creneau":
{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"idClient":1,"idCreneau":1}]}

3.5.7. جدول أعمال طبيب

URL
/getAgendaMedecinJour/{idMedecin}/{jour}
Réponse

Response<AgendaMedecinJour>:[int status ; List<String> messages ;
 AgendaMedecinJour agenda]
AgendaMedecinJour : [Medecin medecin ;Date jour ; 
CreneauMedecinJour[] creneauxMedecinJour]
CreneauMedecinJour : [Creneau creneau ; Rv rv]
  • [idMedecin]: معرف الطبيب الذي نريد الحصول على مواعيده؛
  • URL [jour]: يوم المواعيد بالصيغة 'سنة-شهر-يوم'؛
  • [agenda]: جدول مواعيد الطبيب؛
  • [medecin]: الطبيب المعني. تم تحديد هيكله مسبقًا؛
  • الرد [jour]: يوم الأجندة في صيغة تاريخ Java؛
  • [creneauxMedecinJour]: مصفوفة من العناصر من النوع [CreneauMedecinJour
  • [creneau]: فترة زمنية. تم وصف هيكلها سابقًا؛
  • [rv]: موعد. تم وصف هيكله سابقًا؛

مثال:

URL
/getAgendaMedecinJour/1/2014-07-08
Réponse

{"status":0,"messages":null,"agenda":{"medecin":
{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},
"jour":1404770400000,"creneauxMedecinJour":[{"creneau":
{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"rv":{"id":45,"version":0,"jour":"2014-07-08","client":
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},
"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"idMedecin":1},
"idClient":1,"idCreneau":1}},{"creneau":
{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":
{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"idMedecin":1},
"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"idMedecin":1},
"rv":null},{"creneau":
{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"idMedecin":1},
"rv":null},{"creneau":
{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"idMedecin":1},
"rv":null}]}}

تم تسليط الضوء على الحالة التي يوجد فيها موعد في الفترة الزمنية المحددة والحالة التي لا يوجد فيها موعد.

3.5.8. الحصول على طبيب من خلال معرّفه

URL
/getMedecinById/{idMedecin}
Réponse

Response<Medecin> :[int status ; List<String> messages ; Medecin medecin]
  • [idMedecin]: رقم تعريف الطبيب؛

مثال 1:

URL
/getMedecinById/1
Réponse
{"status":0,"messages":null,"medecin":
{"id":1,"version":1,"titre":"Mme",
"nom":"PELISSIER","prenom":"Marie"}}

مثال 2:

URL
/getMedecinById/100
Réponse
{"status":2,
"messages":["Médecin [100] inexistant"],"medecin":null}

3.5.9. الحصول على عميل من خلال معرفه

URL
/getClientById/{idClient}
Réponse

Response<Client> :[int status ; List<String> messages ;
 Client client]
  • [idClient]: معرف العميل؛

مثال 1:

URL
/getClientById/1
Réponse
{"status":0,"messages":null,"client":{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}}

مثال 2:

URL
/getClientById/100
Réponse
{"status":2,"messages":["Client [100] inexistant"],"client":null}

3.5.10. الحصول على موعد من خلال معرّفه

URL
/getCreneauById/{idCreneau}
Réponse

Response<Creneau> :[int status ; List<String> messages ; Creneau creneau]
  • [idCreneau]: معرف الموعد؛

مثال 1:

URL
/getCreneauById/10
Réponse
{"status":0,"messages":null,"creneau":
{"id":10,"version":1,"hdebut":11,"mdebut":0,
"hfin":11,"mfin":20,"idMedecin":1}}

يُلاحظ أنه في الرد، لا يوجد اسم الطبيب صاحب الموعد بل يوجد فقط معرّفه.

المثال 2:

URL
/getCreneauById/100
Réponse
{"status":2,"messages":["Créneau [100] inexistant"],
"creneau":null}

3.5.11. الحصول على موعد باستخدام معرف المستخدم

URL
/getRvById/{idRv}
Réponse

Response<Rv> :[int status ; List<String> messages ; Rv rv]
  • [idRv]: معرف الموعد؛

مثال 1:

URL
/getRvById/45
Réponse
{"status":0,"messages":null,"rv":{"id":45,"version":0,
"jour":"2014-07-08","idClient":1,"idCreneau":1}}

يُلاحظ أنه في الرد، لا يوجد اسم العميل ولا موعد اللقاء، بل فقط معرّفاتهما.

مثال 2:

URL
/getCreneauById/455
Réponse
{"status":2,"messages":["Rv [455] inexistant"],"rv":null}

3.5.12. إضافة موعد

تسمح عملية URL [/ajouterRv] بإضافة موعد. يتم إرسال المعلومات اللازمة لإضافة الموعد (اليوم، الفترة الزمنية، والعميل) عبر طلب HTTP POST. نعرض كيفية تنفيذ هذا الطلب باستخدام الأداة [Advanced Rest Client].

Image

  • في [1]، يتم استعلام URL؛
  • في [2]، يتم استعلامها بواسطة POST؛
  • في [3-4]، يتم إعلام الخادم بأن القيم التي يتم إرسالها إليه تكون في شكل سلسلة jSON؛
  • في [4]، رأس HTTP للمصادقة؛
  • في [5]، المعلومات المرسلة بواسطة POST. وهي سلسلة jSON تحتوي على:
    • [jour]: يوم الموعد بالصيغة 'aaaa-mm-jj
    • [idClient]: معرف العميل الذي تم تحديد الموعد له،
    • [idCreneau]: معرف الفترة الزمنية للموعد. وبما أن الفترة الزمنية تخص طبيبًا معينًا، فإنها تشير أيضًا إلى الطبيب؛
  • في [6]، يتم إرسال الطلب؛

السلسلة jSON التي يتم إرسالها هي سلسلة الكائن من النوع [PostAjouterRv] التالي:


public class PostAjouterRv {

  // بيانات المنشور
  private String jour;
  private long idClient;
  private long idCreneau;

  // المنشئات
  public PostAjouterRv() {

  }

  public PostAjouterRv(String jour, long idCreneau, long idClient) {
    this.jour = jour;
    this.idClient = idClient;
    this.idCreneau = idCreneau;
  }

  // الوصول والضبط
  ...
}

رد الخادم هو من النوع [Response<Rv>] [int status; List<String> messages; Rv rv] حيث [rv] هو الموعد المضاف.

رد الخادم على الطلب أعلاه هو التالي:

 

يُلاحظ أعلاه أن بعض المعلومات غير مذكورة في [idClient, idCreneau] ولكنها موجودة في الحقول [client] و [creneau]. المعلومات المهمة هي معرف الموعد المضاف (209). كان بإمكان خدمة الويب الاكتفاء بإرجاع هذه المعلومة وحدها.

3.5.13. حذف موعد

تتم هذه العملية أيضًا عبر POST:

URL
/supprimerRv
POST
{'idRv':idRv}
Réponse

Response<RV> :[int status ; List<String> messages ; Rv rv]

القيمة المنشورة هي السلسلة jSON لكائن من النوع [PostSupprimerRv] التالي:


public class PostSupprimerRv {

  // بيانات المنشور
  private long idRv;

  // المنشئات
  public PostSupprimerRv() {

  }

  public PostSupprimerRv(long idRv) {
    this.idRv = idRv;
  }

  // الوصول إلى البيانات
  ...
}
  • السطر 4، [idRv] هو معرف الموعد المراد حذفه.

مثال 1:

URL
/supprimerRv
POST
{"idRv":209}
Réponse
{"status":0,"messages":null,"rv":null}

تم حذف الموعد رقم 209 بالفعل بسبب [status=0].

مثال 2:

URL
/supprimerRv
POST
{"idRv":650}
Réponse
{"status":2,"messages":["Rv [650] inexistant"],"rv":null}

3.6. عميل Android

Image

الآن بعد أن تم تفصيل الخادم [1] وأصبح جاهزًا للعمل، سنقوم بدراسة عميل Android [2].

3.6.1. بنية مشروع Android Studio

يستند المشروع إلى بنية مشروع [client-android-skel] (انظر الفقرة 1.17). في بنية عميل Android المذكورة أعلاه، يمكن تمييز ثلاث مجموعات:

  • الطبقة [DAO] المسؤولة عن الاتصال بخدمة الويب؛
  • [vues] المسؤولة عن التواصل مع المستخدم؛
  • طبقة [activité] التي تربط بين المجموعتين السابقتين. لا تعرف العروض طبقة [DAO]. فهي تتواصل فقط مع النشاط.

تنعكس هذه البنية في بنية مشروع Android Studio الخاص بعميل Android:

 
  • الحزمة [activity] تنفذ النشاط؛
  • الحزمة [architecture] تتضمن عناصر البنية التي طورناها سابقًا؛
  • الحزمة [dao] تنفذ الطبقة [DAO
  • تقوم الحزمة [fragments] بتنفيذ [vues

3.6.2. تخصيص المشروع

  

يحتوي الملف [architecture / custom] على العناصر القابلة للتخصيص في البنية.

واجهة [IMainActivity] هي كما يلي:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // الوصول إلى الجلسة
  ISession getSession();

  // تغيير العرض
  void navigateToView(int position, ISession.Action action);

  // إدارة الانتظار
  void beginWaiting();

  void cancelWaiting();

  // ثوابت التطبيق -------------------------------------

  // وضع التصحيح
  boolean IS_DEBUG_ENABLED = true;

  // الحد الأقصى لوقت انتظار استجابة الخادم
  int TIMEOUT = 1000;

  // فترة الانتظار قبل تنفيذ طلب العميل
  int DELAY = 000;

  // المصادقة الأساسية
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;

  // تجاور الأجزاء
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // شريط علامات التبويب
  boolean ARE_TABS_NEEDED = false;

  // صورة الانتظار
  boolean IS_WAITING_ICON_NEEDED = true;

  // عدد أجزاء التطبيق
  int FRAGMENTS_COUNT = 4;

  // عدد المشاهدات
  int VUE_CONFIG = 0;
  int VUE_ACCUEIL = 1;
  int VUE_AGENDA = 2;
  int VUE_AJOUT_RV = 3;
}
  • السطران 25 و28: تخصيص الطبقة [DAO
  • السطر 31: يقوم هذا التطبيق بإجراء عمليات وصول مصادق عليها إلى الخادم؛
  • السطر 40: نحتاج إلى صورة انتظار؛
  • السطر 43: يحتوي التطبيق على أربعة أجزاء؛
  • الأسطر 46-49: أرقام الأجزاء الأربعة؛
  • السطر 37: لا توجد علامات تبويب؛

ستكون الفئة الأساسية [CoreState] لحالات الأجزاء كما يلي:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = AccueilFragmentState.class),
  @JsonSubTypes.Type(value = AgendaFragmentState.class),
  @JsonSubTypes.Type(value = AjoutRvFragmentState.class),
  @JsonSubTypes.Type(value = ConfigFragmentState.class)
}
)
public class CoreState {
  // ما إذا تمت زيارة الجزء أم لا
  protected boolean hasBeenVisited = false;
  // حالة القائمة المحتملة للجزء
  protected MenuItemState[] menuOptionsState;

  // أدوات الاسترجاع والتعيين
...
}
  • السطور 15-18: الأجزاء الأربعة لها حالة:
  

وأخيرًا، تحتوي الجلسة على البيانات المشتركة بين الأجزاء:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.fragments.state.AccueilFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AjoutRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;

import java.util.List;

public class Session extends AbstractSession {
  // العناصر التي لا يمكن تسلسلها في jSON يجب أن تحتوي على التعليق التوضيحي @JsonIgnore

  // قائمة الأطباء
  private List<Medecin> médecins;
  // قائمة العملاء
  private List<Client> clients;
  // جدول مواعيد الطبيب ليوم معين
  private AgendaMedecinJour agenda;
  // موقع العنصر الذي تم النقر عليه في جدول المواعيد
  private int position;
  // يوم الموعد بالصيغة الإنجليزية "yyyy-MM-dd"
  private String dayRv;
  // يوم الموعد بالصيغة الفرنسية "dd-MM-yyyy"
  private String jourRv;

  // مُستردات ومُعيّنات
...
}
  • السطور 17-28: الجلسة تخزن ستة معلومات. سنشرح دور هذه المعلومات عند الضرورة.

3.6.3. الطبقة [DAO]

  • في [1]، الكيانات المُغلفة في استجابات الخادم. وقد تم عرضها في الفقرة 3.5؛
  • في [2]، عناصر العميل التي تدير التبادلات مع الخادم؛

لن نعود إلى عناصر [1]. فقد تم عرضها بالفعل. يُرجى من القارئ الرجوع إلى الفقرة 3.5 إذا لزم الأمر. سنقوم بدراسة تنفيذ الحزمة [service]. سيقودنا ذلك إلى الحديث أيضًا عن تنفيذ التبادلات الآمنة بين العميل والخادم.

3.6.3.1. تنفيذ التبادلات بين العميل والخادم

  

الفئة [WebClient] هي مكون AA يصف:

  • URL المعروضة بواسطة خدمة الويب؛
  • معلماتها؛
  • استجاباتها؛

package rdvmedecins.android.dao.service;

import rdvmedecins.android.dao.entities.*;
import org.androidannotations.rest.spring.annotations.*;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  public void setRestTemplate(RestTemplate restTemplate);

  // قائمة الأطباء
  @Get("/getAllMedecins")
  public Response<List<Medecin>> getAllMedecins();

  // قائمة العملاء
  @Get("/getAllClients")
  public Response<List<Client>> getAllClients();

  // قائمة المواعيد المتاحة للطبيب
  @Get("/getAllCreneaux/{idMedecin}")
  public Response<List<Creneau>> getAllCreneaux(@Path long idMedecin);

  // قائمة مواعيد الطبيب
  @Get("/getRvMedecinJour/{idMedecin}/{jour}")
  public Response<List<Rv>> getRvMedecinJour(@Path long idMedecin, @Path String jour);

  // العميل
  @Get("/getClientById/{id}")
  public Response<Client> getClientById(@Path long id);

  // طبيب
  @Get("/getMedecinById/{id}")
  public Response<Medecin> getMedecinById(@Path long id);

  // موعد
  @Get("/getRvById/{id}")
  public Response<Rv> getRvById(@Path long id);

  // الفترة الزمنية
  @Get("/getCreneauById/{id}")
  public Response<Creneau> getCreneauById(@Path long id);

  // إضافة RV
  @Post("/ajouterRv")
  public Response<Rv> ajouterRv(@Body PostAjouterRv post);

  // حذف موعد
  @Post("/supprimerRv")
  public Response<Rv> supprimerRv(@Body PostSupprimerRv post);

  // الحصول على جدول مواعيد الطبيب
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
  public Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

}
  • الأسطر 19-60: نجد جميع مكونات URL التي تمت دراستها في الفقرة 3.5؛
  • السطر 16: المكون [RestTemplate] من [Spring Android] الذي تستند إليه الاتصالات بين العميل والخادم؛

3.6.3.2. واجهة [IDao]

  

الواجهة [IDao] للطبقة [DAO] هي كما يلي:


package rdvmedecins.android.dao.service;

import rdvmedecins.android.dao.entities.*;
import rx.Observable;

import java.util.List;

public interface IDao {
  // عنوان URL لخدمة الويب
  public void setUrlServiceWebJson(String url);

  // المستخدم
  public void setUser(String user, String mdp);

  // مهلة انتظار العميل
  public void setTimeout(int timeout);

  // قائمة العملاء
  public Observable<List<Client>> getAllClients();

  // قائمة الأطباء
  public Observable<List<Medecin>> getAllMedecins();

  // قائمة المواعيد المتاحة للطبيب
  public Observable<List<Creneau>> getAllCreneaux(long idMedecin);

  // قائمة مواعيد الطبيب في يوم معين
  public Observable<List<Rv>> getRvMedecinJour(long idMedecin, String jour);

  // البحث عن عميل باستخدام رقم التعريف الخاص به
  public Observable<Client> getClientById(long id);

  // البحث عن طبيب باستخدام رقم التعريف الخاص به
  public Observable<Medecin> getMedecinById(long id);

  // البحث عن موعد محدد برقمه التعريفي
  public Observable<Rv> getRvById(long id);

  // البحث عن فترة زمنية محددة برقم التعريف الخاص بها
  public Observable<Creneau> getCreneauById(long id);

  // إضافة RV
  public Observable<Rv> ajouterRv(String jour, long idCreneau, long idClient);

  // حذف RV
  public Observable<Rv> supprimerRv(long idRv);

  // المهنة
  public Observable<AgendaMedecinJour> getAgendaMedecinJour(long idMedecin, String jour);

  // وضع التصحيح
  void setDebugMode(boolean isDebugEnabled);
}
  • السطر 10: لتعيين URL لخدمة الويب / jSON؛
  • السطر 13: لتعيين مستخدم اتصال العميل / الخادم. [user] هو معرف المستخدم، و[mdp] هو كلمة المرور الخاصة به؛
  • السطر 16: لتحديد مدة انتظار قصوى لاستجابة الخادم؛
  • الأسطر 18-49: لكل URL تعرضه خدمة الويب، هناك طريقة مقابلة. وهي تتضمن توقيع الطرق التي تحمل نفس الأسماء في المكون AA [WebClient
  • السطر 52: للتحكم في الوضع debug للطبقة [DAO

3.6.3.3. الفئة [Dao]

  

التنفيذ [DAO] للواجهة [IDao] السابقة هو كما يلي:


package client.android.dao.service;

import android.util.Log;
import client.android.dao.entities.*;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // عميل خدمة الويب
  @RestService
  protected WebClient webClient;
  // الأمان
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // مصنع RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    ...
  }

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

  @Override
  public void setUser(String user, String mdp) {
    ...
  }

  @Override
  public void setTimeout(int timeout) {
    ...
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // معترض المصادقة؟
    if (isBasicAuthentificationNeeded) {
      // يتم إضافة معترض المصادقة
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }

  }

  // طرق خاصة -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // تنفيذ الواجهة IDao --------------------------------------------------------------------
  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    // السجل
    log("getAllClients");
    // النتيجة
    return getResponse(new IRequest<Response<List<Client>>>() {
      @Override
      public Response<List<Client>> getResponse() {
        return webClient.getAllClients();
      }
    });
  }

  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    // سجل
    log("getAllMedecins");
    // النتيجة
    return getResponse(new IRequest<Response<List<Medecin>>>() {
      @Override
      public Response<List<Medecin>> getResponse() {
        return webClient.getAllMedecins();
      }
    });
  }

  @Override
  public Observable<Response<List<Creneau>>> getAllCreneaux(final long idMedecin) {
    // سجل
    log("getAllCreneaux");
    // النتيجة
    return getResponse(new IRequest<Response<List<Creneau>>>() {
      @Override
      public Response<List<Creneau>> getResponse() {
        return webClient.getAllCreneaux(idMedecin);
      }
    });
  }

  @Override
  public Observable<Response<List<Rv>>> getRvMedecinJour(final long idMedecin, final String jour) {
    // سجل
    log("getRvMedecinJour");
    // النتيجة
    return getResponse(new IRequest<Response<List<Rv>>>() {
      @Override
      public Response<List<Rv>> getResponse() {
        return webClient.getRvMedecinJour(idMedecin, jour);
      }
    });
  }

  @Override
  public Observable<Response<Client>> getClientById(final long id) {
    // سجل
    log("getClientById");
    // النتيجة
    return getResponse(new IRequest<Response<Client>>() {
      @Override
      public Response<Client> getResponse() {
        return webClient.getClientById(id);
      }
    });
  }

  @Override
  public Observable<Response<Medecin>> getMedecinById(final long id) {
    // سجل
    log("getMedecinById");
    // النتيجة
    return getResponse(new IRequest<Response<Medecin>>() {
      @Override
      public Response<Medecin> getResponse() {
        return webClient.getMedecinById(id);
      }
    });
  }

  @Override
  public Observable<Response<Rv>> getRvById(final long id) {
    // سجل
    log("getRvById");
    // النتيجة
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.getRvById(id);
      }
    });
  }

  @Override
  public Observable<Response<Creneau>> getCreneauById(final long id) {
    // سجل
    log("getCreneauById");
    // النتيجة
    return getResponse(new IRequest<Response<Creneau>>() {
      @Override
      public Response<Creneau> getResponse() {
        return webClient.getCreneauById(id);
      }
    });
  }

  @Override
  public Observable<Response<Rv>> ajouterRv(final String jour, final long idCreneau, final long idClient) {
    // سجل
    log("ajouterRv");
    // النتيجة
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.ajouterRv(new PostAjouterRv(jour, idCreneau, idClient));
      }
    });
  }

  @Override
  public Observable<Response<Rv>> supprimerRv(final long idRv) {
    // سجل
    log("supprimerRv");
    // النتيجة
    return getResponse(new IRequest<Response<Rv>>() {
      @Override
      public Response<Rv> getResponse() {
        return webClient.supprimerRv(new PostSupprimerRv(idRv));
      }
    });
  }

  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // سجل
    log("getAgendaMedecinJour");
    // النتيجة
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
  }

}
  • الأسطر 18-72: هي الأسطر الأساسية الموجودة في الفئة [Dao] للمشروع [client-android-skel
  • الأسطر 74-216: تنفيذ واجهة [IDao]. الطرق التي تستعلم عن URL المعروضة بواسطة خدمة الويب تفوض هذا الاستعلام إلى المكون AA [WebClient] (السطور 22-23)؛
  • السطور 58-63: إذا تمت مصادقة التبادلات بين العميل والخادم بواسطة ترخيص من النوع الأساسي، يتم إضافة معترض إلى المكون [RestTemplate]. سيؤدي ذلك إلى اعتراض أي طلب HTTP صادر عن المكون [RestTemplate] بواسطة الفئة [MyAuthInterceptor] (السطران 25-26)؛

الفئة [MyAuthInterceptor] هي كما يلي:


package rdvmedecins.android.dao.security;

import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // مستخدم
  private String user;
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    HttpHeaders headers = request.getHeaders();
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    headers.setAuthorization(auth);
    return execution.execute(request, body);
  }

  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}
  • السطر 15: الفئة [MyAuthInterceptor] هي مكون AA من النوع [singleton
  • السطر 16: الفئة [MyAuthInterceptor] توسع واجهة Spring [ClientHttpRequestInterceptor]. تحتوي هذه الواجهة على طريقة واحدة، وهي الطريقة [intercept] في السطر 22. يتم توسيع هذه الواجهة لاعتراض أي طلب HTTP من العميل. تتلقى الطريقة [intercept] ثلاثة معلمات؛
    • [HtpRequest request]: الطلب HTTP الذي تم اعتراضه،
    • [byte[] body]: نص الطلب إن وجد (القيم المرسلة على سبيل المثال)،
    • [ClientHttpRequestExecution execution]: مكون Spring الذي ينفذ الطلب؛

نقوم باعتراض جميع طلبات HTTP من عميل Android لإضافة رأس المصادقة HTTP الموضح في الفقرة 3.5.

  • السطر 23: نسترد رؤوس HTTP من الطلب المعترض؛
  • السطر 24: نقوم بإنشاء رأس المصادقة HTTP. يتم توفير طريقة المصادقة المستخدمة (ترميز base64 لسلسلة 'user:mdp') بواسطة فئة Spring [HttpBasicAuthentication
  • السطر 25: يتم إضافة رأس المصادقة الذي أنشأناه للتو إلى الرؤوس الحالية للطلب المعترض؛
  • السطر 26: نواصل تنفيذ الطلب المعترض. باختصار، تم إثراء الطلب المعترض برأس المصادقة؛

يتم تنفيذ طرق واجهة [IDao] جميعها على نفس النموذج. لنأخذ مثالاً على الطريقة [getAgendaMedecinJour]:


  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
    // سجل
    log("getAgendaMedecinJour");
    // النتيجة
    return getResponse(new IRequest<Response<AgendaMedecinJour>>() {
      @Override
      public Response<AgendaMedecinJour> getResponse() {
        return webClient.getAgendaMedecinJour(idMedecin, jour);
      }
    });
}
  • السطر 2: تتوقع الطريقة معلمتين:
    • [idMedecin]: معرف الطبيب الذي نريد جدول مواعيده؛
    • [jour]: اليوم الذي نريد جدول مواعيده؛
  • السطر 6: يتم استدعاء الطريقة [getResponse] من الفئة الأصلية [AbstractDao]. تتوقع هذه الطريقة معلمة من النوع [IRequest<T>] حيث T هو النوع الذي ترجعها الطريقة [getAgendaMedecinJour] في السطر 2، وهنا هو [Response<AgendaMedecinJour>]. تحتوي الواجهة [IRequest] على طريقة واحدة فقط: [getResponse] (السطر 8)؛
  • الأسطر 8-10: تنفيذ الطريقة [IRequest.getResponse]. يجب أن تعرض هذه الطريقة النتيجة المتوقعة من الطريقة [getAgendaMedecinJour] في السطر 2 من النوع [Response<AgendaMedecinJour>]؛
  • السطر 9: يتم إرجاع الاستجابة بواسطة الطريقة [webClient.getAgendaMedecinJour]:

  // الحصول على جدول مواعيد الطبيب
  @Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);

المعلمات المستخدمة في السطر 9 هي تلك التي تم تمريرها إلى الطريقة [getAgendaMedecinJour] في السطر 2. ولهذا السبب، يجب أن يكون لهذه المعلمات السمة final؛

3.6.4. النشاط [MainActivity]

Serveur
  

الفئة [MainActivity] هي كما يلي:


package client.android.activity;

import android.util.Log;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.AccueilFragment_;
import client.android.fragments.behavior.AgendaFragment_;
import client.android.fragments.behavior.AjoutRvFragment_;
import client.android.fragments.behavior.ConfigFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import rx.Observable;

import java.util.List;

@EActivity
public class MainActivity extends AbstractActivity {

  // طبقة [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // فئة الوالد ---------------------------------------
  @Override
  protected void onCreateActivity() {
    // سجل
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    AbstractFragment[] fragments= new AbstractFragment[]{new ConfigFragment_(), new AccueilFragment_(), new AgendaFragment_(), new AjoutRvFragment_()};
    return fragments;
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }

  // واجهة IDao -----------------------------------------------------
...

  @Override
  public Observable<Response<List<Client>>> getAllClients() {
    return dao.getAllClients();
  }

  @Override
  public Observable<Response<List<Medecin>>> getAllMedecins() {
    return dao.getAllMedecins();
  }

  @Override
  public Observable<Response<List<Creneau>>> getAllCreneaux(long idMedecin) {
    return dao.getAllCreneaux(idMedecin);
  }

  @Override
  public Observable<Response<List<Rv>>> getRvMedecinJour(long idMedecin, String jour) {
    return dao.getRvMedecinJour(idMedecin, jour);
  }

  @Override
  public Observable<Response<Client>> getClientById(long id) {
    return dao.getClientById(id);
  }

  @Override
  public Observable<Response<Medecin>> getMedecinById(long id) {
    return dao.getMedecinById(id);
  }

  @Override
  public Observable<Response<Rv>> getRvById(long id) {
    return dao.getRvById(id);
  }

  @Override
  public Observable<Response<Creneau>> getCreneauById(long id) {
    return dao.getCreneauById(id);
  }

  @Override
  public Observable<Response<Rv>> ajouterRv(String jour, long idCreneau, long idClient) {
    return dao.ajouterRv(jour, idCreneau, idClient);
  }

  @Override
  public Observable<Response<Rv>> supprimerRv(long idRv) {
    return dao.supprimerRv(idRv);
  }

  @Override
  public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(long idMedecin, String jour) {
    return dao.getAgendaMedecinJour(idMedecin, jour);
  }
}
  • الأسطر 21-66: هذه الأسطر موجودة بشكل افتراضي في النموذج [client-android-skel
  • الأسطر 66-119: تنفيذ واجهة [IDao]. جميع الطرق تفوض العمل إلى الطبقة [DAO] في السطر 26؛
  • الأسطر 42-46: تعرض الطريقة [getFragments] جدول الأجزاء الأربعة للتطبيق؛
  • الأسطر 58-61: عرض التكوين هو أول عرض يتم عرضه عند بدء تشغيل التطبيق؛

3.6.5. الجلسة

  

تُستخدم الفئة [Session] لتخزين المعلومات التي يجب نقلها بين الأجزاء. وهي كما يلي:


package rdvmedecins.android.architecture;

import rdvmedecins.android.dao.entities.AgendaMedecinJour;
import rdvmedecins.android.dao.entities.Client;
import rdvmedecins.android.dao.entities.Medecin;
import org.androidannotations.annotations.EBean;

import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // قائمة الأطباء
  private List<Medecin> médecins;
  // قائمة العملاء
  private List<Client> clients;
  // التقويم
  private AgendaMedecinJour agenda;
  // موقع العنصر الذي تم النقر عليه في الأجندة
  private int position;
  // تاريخ الموعد بالصيغة الإنجليزية "yyyy-MM-dd"
  private String dayRv;
  // يوم الموعد بالتنسيق الفرنسي "dd-MM-yyyy"
  private String jourRv;


  // مُستردات ومُعيّنات
...
}
  • السطر 10: الفئة [Session] هي مكون AA تم إنشاء مثيل واحد منه؛
  • الأسطر 12-15: سنفترض في هذه الدراسة الحالة أن قوائم الأطباء والعملاء لا تتغير. سيتم طلبها عند بدء تشغيل التطبيق وتخزينها في الجلسة حتى تتمكن الأجزاء من استخدامها؛
  • الأسطر 20-23: اليوم المطلوب للموعد. يتم التعامل معه في شكلين، بالصيغة الفرنسية (السطر 23) داخل تطبيق Android، وبالصيغة الإنجليزية (السطر 21) للتبادل مع الخادم؛
  • السطر 19: موقع العنصر الذي تم النقر عليه (رابط إضافة / حذف) في التقويم؛

3.6.6. إدارة عرض التكوين

3.6.6.1. طريقة العرض

عرض الإعدادات هو العرض الذي يظهر عند بدء تشغيل التطبيق:

Image

عناصر الواجهة المرئية هي التالية:

Type
Nom
1
EditText
edtUrlServiceRest
3
EditText
edtUtilisateur
5
EditText
edtMdp
2
TextView
txtErrorUrlServiceRest
3
TextView
txtErrorUtilisateur

3.6.6.2. الجزء

يتم إدارة عرض التكوين بواسطة الجزء التالي [ConfigFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.Client;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;

import java.net.URI;
import java.util.List;

@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_config)
public class ConfigFragment extends AbstractFragment {

  // عناصر الواجهة المرئية
  @ViewById(R.id.edt_urlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_errorUrlServiceRest)
  protected TextView txtErrorUrlServiceRest;
  @ViewById(R.id.txt_errorUtilisateur)
  protected TextView txtErrorUtilisateur;
  @ViewById(R.id.edt_utilisateur)
  protected EditText edtUtilisateur;
  @ViewById(R.id.edt_mdp)
  protected EditText edtMdp;

  // المدخلات
  private String urlServiceRest;
  private String utilisateur;
  private String mdp;

  // التحقق من صحة الصفحة
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
..
  // تنفيذ طرق الفئة الأم -------------------------------------------
 ...

}
  • السطر 25: يتم ربط المقطع بالقائمة التالية: [menu_config]
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>

</menu>
  • الأسطر 28-38: عناصر الواجهة المرئية؛
  • الأسطر 41-43: الحقول الثلاثة في النموذج؛

يتم إدارة النقر على خيار القائمة [Valider] بواسطة الطريقة [doValider]:


// التحقق من صحة الصفحة
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // نخفي أي رسائل خطأ سابقة
    txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
    txtErrorUtilisateur.setVisibility(View.INVISIBLE);
    // اختبار صحة البيانات المدخلة
    if (!isPageValid()) {
      return;
    }
    // ملء URL لخدمة الويب
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // يتم إعلام المستخدم
    mainActivity.setUser(utilisateur, mdp);
    // بدء الانتظار - سيتم تشغيل مهمتين غير متزامنتين
    beginWaiting(2);
    // الأطباء
    executeInBackground(mainActivity.getAllMedecins(), new Action1<Response<List<Medecin>>>() {
      @Override
      public void call(Response<List<Medecin>> responseMedecins) {
        // يتم استهلاك الرد
        consumeMedecins(responseMedecins);
      }
    });
    // العملاء
    executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
      @Override
      public void call(Response<List<Client>> responseClients) {
        // يتم استهلاك الرد
        consumeClients(responseClients);
      }
    });
  }


  private void consumeMedecins(Response<List<Medecin>> responseMedecins) {
    // السجل
    if (isDebugEnabled) {
      Log.d(className, "consume médecins");
    }
    // خطأ؟
    if (responseMedecins.getStatus() != 0) {
      // رسالة
      showAlert(responseMedecins.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى UI
      return;
    }
    // يتم حفظ الأطباء في الجلسة
    session.setMédecins(responseMedecins.getBody());
  }

  private void consumeClients(Response<List<Client>> responseClients) {
    // سجل
    if (isDebugEnabled) {
      Log.d(className, "consume clients");
    }
    // خطأ؟
    if (responseClients.getStatus() != 0) {
      // رسالة
      showAlert(responseClients.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى UI
      return;
    }
    // يتم حفظ العملاء في الجلسة
    session.setClients(responseClients.getBody());
  }
  • الأسطر 8-10: يتم اختبار صحة المدخلات الثلاثة في النموذج. إذا كان النموذج غير صالح، فلن يتم المضي قدمًا؛
  • الأسطر 11-14: يتم تمرير الإدخالات اللازمة للطبقة [DAO] إلى النشاط؛
  • السطر 16: يتم إخطار الفئة الأم بأننا سنقوم بتشغيل مهمتين غير متزامنتين ويتم التحضير للانتظار؛
  • الأسطر 17-24: يتم طلب قائمة الأطباء؛
  • السطر 18: تتوقع الطريقة [executeInBackground] معلمتين:
    • السطر 18: يتم توفير العملية المراد تنفيذها ومراقبتها بواسطة الطريقة [mainActivity.getAllMedecins()]؛
    • الأسطر 18-24: المعلمة الثانية هي مثيل من النوع [Action1<T>] حيث T هو النوع الذي تعرضه العملية المراقبة، وهنا [Response<List<Medecin>>]
  • السطر 22: عند استلام الرد، يتم تمريره إلى الطريقة [consumeMedecins] في السطر 36؛
  • الأسطر 25-33: بعد إطلاق المهمة غير المتزامنة الأولى، يتم إطلاق مهمة ثانية لطلب قائمة العملاء. وبالتالي، سيكون لدينا مهمتان يتم تنفيذهما بالتوازي؛
  • الأسطر 36-52: تلقينا الرد من مهمة الأطباء. نقوم باستخدامه؛
  • الأسطر 42-49: ننظر أولاً إذا كان الخادم قد أبلغ عن خطأ في الحقل [status] من الرد؛
  • السطر 44: في حالة وجود خطأ، نعرض الرسائل التي وضعها الخادم في الحقل [messages] من الرد؛
  • السطر 46: يتم إلغاء جميع المهام؛
  • السطر 48: نعود إلى واجهة المستخدم؛
  • السطر 51: إذا لم يكن هناك خطأ، يتم تفعيل قائمة الأطباء؛

يتم التحقق من صحة الإدخالات (السطر 8) بالطريقة التالية:


  private boolean isPageValid() {
    // التحقق من صحة البيانات المدخلة
    boolean erreur;
    URI service;
    // صحة URL لخدمة REST
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    try {
      service = new URI(urlServiceRest);
      erreur = service.getHost() == null || service.getPort() == -1;
    } catch (Exception ex) {
      // يتم تسجيل الخطأ
      erreur = true;
    }
    if (erreur) {
      // عرض الخطأ
      txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
    }
    // المستخدم
    utilisateur = edtUtilisateur.getText().toString().trim();
    if (utilisateur.length() == 0) {
      // يتم عرض الخطأ
      txtErrorUtilisateur.setVisibility(View.VISIBLE);
      // يتم تسجيل الخطأ
      erreur = true;
    }
    // كلمة المرور
    mdp = edtMdp.getText().toString().trim();
    // العودة
    return !erreur;
}

الطريقة [beginWaiting] (السطر 16) هي كما يلي:


  // بدء الانتظار
  protected void beginWaiting(int numberOfRunningTasks) {
    // يتم التحضير لبدء المهام
    beginRunningTasks(numberOfRunningTasks);
    // حالة الأزرار والقوائم
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • السطر 4: يتم إخطار المهمة الأم بأننا سنقوم بتشغيل مهام [numberOfRunningTasks
  • السطر 6: يتم إخفاء جميع خيارات القائمة؛
  • السطر 7: ثم نجعل الخيار [Actions/Annuler] مرئيًا؛

يتم إدارة النقر على خيار القائمة [Annuler] بواسطة الطريقة [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // إلغاء المهام غير المتزامنة
    cancelRunningTasks();
}
  • السطر 8: يُطلب من الفئة الأم إلغاء المهام غير المتزامنة؛

3.6.6.3. إدارة دورة حياة الجزء

الجزء له الحالة التالية [ConfigFragmentState]:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // ظهور رسالتي الخطأ
  private boolean txtErrorUrlServiceRestVisible;
  private boolean txtErrorUtilisateurVisible;

  // مُستردات ومُعيّنات
...
}
  • عندما تطلب منه الفئة الأم ذلك، سيحفظ الجزء حالة ظهور رسالتي الخطأ التابعتين له؛

يتم تنفيذ دورة حياة المقطع على النحو التالي:


// تنفيذ طرق الفئة الأصلية -------------------------------------------
  @Override
  public CoreState saveFragment() {
    // حفظ حالة الجزء
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtErrorUrlServiceRestVisible(txtErrorUrlServiceRest.getVisibility() == View.VISIBLE);
    state.setTxtErrorUtilisateurVisible(txtErrorUtilisateur.getVisibility() == View.VISIBLE);
    return state;
  }

  @Override
  protected int getNumView() {
    return     IMainActivity.VUE_CONFIG;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    if (previousState == null) {
      // الزيارة الأولى
      // نخفي رسائل الخطأ
      txtErrorUtilisateur.setVisibility(View.INVISIBLE);
      txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
      // القائمة
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // استعادة عرض رسائل الخطأ
    ConfigFragmentState state = (ConfigFragmentState) previousState;
    // ليست الزيارة الأولى - إعادة عرض رسائل الخطأ
    txtErrorUtilisateur.setVisibility(state.isTxtErrorUtilisateurVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorUrlServiceRest.setVisibility(state.isTxtErrorUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
  }


  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // القائمة
    initMenu();
    // الصفحة التالية؟
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.SUBMIT);
    }
  }

  // طرق خاصة ------------------------------------------------
  private void initMenu(){
    // حالة القائمة
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
  • الأسطر 2-9: عندما تطلب منه الفئة الأم ذلك، يحفظ المقطع حالة رسالتي الخطأ الخاصتين به؛
  • الأسطر 11-14: رقم المقطع هو [IMainActivity.VUE_CONFIG
  • الأسطر 16-19: يتم تنفيذها عند إنشاء المقطع لأول مرة (previousState==null) أو إعادة إنشائه في المرات التالية (previousState !=null). هنا، لا يوجد ما يجب فعله؛
  • الأسطر 21-31: يتم تنفيذها عند إنشاء العرض المرتبط بالجزء لأول مرة (previousState==null) أو إعادة إنشائه في المرات التالية (previousState !=null
    • الأسطر 24-29: في الزيارة الأولى، يتم إخفاء رسائل الخطأ وعرض القائمة بدون الإجراء [Annuler] (الأسطر 62-66)؛
  • الأسطر 33-35: يتم تنفيذها عند الوصول إلى الجزء عن طريق عملية [SUBMIT]. لا يحدث هذا أبدًا هنا؛
  • الأسطر 37-44: يتم تنفيذها عند الوصول إلى الجزء عن طريق عملية [NAVIGATION] أو [RESTORE]. يتم استعادة حالة رسائل الخطأ من الحالة السابقة؛
  • الأسطر 47-49: يتم تنفيذها عند الانتهاء من جميع التحديثات السابقة. لا يوجد شيء آخر للقيام به؛
  • الأسطر 51-59: يتم تنفيذها عند انتهاء جميع المهام غير المتزامنة؛
    • السطور 53-54: يتم إعادة القائمة إلى حالتها الافتراضية؛
    • الأسطر 56-58: إذا انتهت المهام بشكل طبيعي، ننتقل إلى العرض التالي، وإلا نبقى على نفس العرض؛

3.6.7. إدارة شاشة البداية

3.6.7.1. الطريقة

الصفحة الرئيسية هي التالية:

Image

عناصر الواجهة المرئية هي كما يلي:

Type
Nom
1
Spinner
spinnerMedecins
2
DatePicker
edtJourRv

3.6.7.2. الجزء

يتم إدارة الصفحة الرئيسية بواسطة الجزء التالي [AccueilFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.service.Response;
import client.android.fragments.state.AccueilFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;

import java.util.Calendar;
import java.util.List;
import java.util.Locale;

@EFragment(R.layout.accueil)
@OptionsMenu(R.menu.menu_accueil)
public class AccueilFragment extends AbstractFragment {

  // عناصر الواجهة المرئية
  @ViewById(R.id.spinnerMedecins)
  protected Spinner spinnerMedecins;
  @ViewById(R.id.edt_JourRv)
  protected DatePicker edtJourRv;

  // البيانات المحلية
  private List<Medecin> medecins;
  private Calendar calendrier;
  private String[] spinnerMedecinsDataSource;

  // التحقق من صحة الصفحة
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    ...
  }
...

  // تنفيذ طرق الفئة الأصلية -------------------------------------
...
}
  • السطر 26: المرتبطة المرتبطة بالقائمة التالية:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
    </menu>
  </item>
</menu>
  • الأسطر 31-34: عناصر الواجهة المرئية؛
  • السطر 37: قائمة الأطباء؛
  • السطر 38: تقويم؛
  • السطر 39: مصدر بيانات قائمة الأطباء؛

يتم إدارة النقر على الرابط [Valider] بواسطة الطريقة التالية [doValider]:


// التحقق من صحة الصفحة
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // تدوين معرف الطبيب المحدد
    Long idMedecin = medecins.get(spinnerMedecins.getSelectedItemPosition()).getId();
    // يتم حفظ اليوم في الجلسة
    String jourRv = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtJourRv.getDayOfMonth(), edtJourRv.getMonth() + 1, edtJourRv.getYear());
    session.setJourRv(jourRv);
    // التحويل إلى تنسيق التاريخ yyyy-MM-dd
    String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
    session.setDayRv(dayRv);
    // بدء الانتظار - سيتم تشغيل مهمة غير متزامنة واحدة
    beginWaiting(1);
    // يتم طلب جدول مواعيد الطبيب
    executeInBackground(mainActivity.getAgendaMedecinJour(idMedecin, dayRv), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // يتم استهلاك الرد
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // خطأ؟
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // رسالة
      showAlert(responseAgendaMedecinJour.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى UI
      return;
    }
    // يتم وضع الأجندة في الجلسة
    session.setAgenda(responseAgendaMedecinJour.getBody());
  }
  • السطر 5: يتم استرداد معرف الطبيب المحدد؛
  • السطران 7-8: يتم إدخال التاريخ المختار باللغة الفرنسية؛
  • السطران 10-11: يتم إدخال التاريخ المختار، بالصيغة الإنجليزية؛
  • السطر 13: يتم إخطار الفئة الأم بأننا سنقوم بتشغيل مهمة غير متزامنة ونقوم بإعداد الانتظار؛
  • السطور 15-22: يتم طلب جدول مواعيد الطبيب؛
    • السطر 15: تتوقع الطريقة [executeInBackground] معلمتين:
      • السطر 15: يتم توفير العملية المراد تنفيذها ومراقبتها بواسطة الطريقة [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)]؛
      • الأسطر 15-22: المعلمة الثانية هي مثيل من النوع [Action1<T>] حيث T هو النوع الذي تعرضه العملية المراقبة، وهنا [Response<AgendaMedecinJour>]
    • السطر 20: عند استلام الرد، يتم تمريره إلى الطريقة [consumeAgenda] في السطر 25؛
  • الأسطر 25-37: تم استلام جدول مواعيد الطبيب. يتم استخدامه؛
  • الأسطر 27-34: ننظر أولاً إذا كان الخادم قد أبلغ عن خطأ في الحقل [status] في الرد؛
  • السطر 29: في حالة وجود خطأ، يتم عرض الرسائل التي وضعها الخادم في الحقل [messages] في الرد؛
  • السطر 31: يتم إلغاء جميع المهام؛
  • السطر 33: نعود إلى واجهة المستخدم؛
  • السطر 36: إذا لم تكن هناك أخطاء، يتم تشغيل الأجندة؛

الطريقة [beginWaiting] (السطر 13) هي كما يلي:


  // بدء الانتظار
  protected void beginWaiting(int numberOfRunningTasks) {
    // يتم التحضير لبدء المهام
    beginRunningTasks(numberOfRunningTasks);
    // حالة الأزرار والقوائم
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • السطر 4: يتم إخطار المهمة الأم بأننا سنقوم بتشغيل مهام [numberOfRunningTasks
  • السطر 6: يتم إخفاء جميع خيارات القائمة؛
  • السطر 7: ثم إظهار الخيار [Actions/Annuler

يتم إدارة النقر على خيار القائمة [Annuler] بواسطة الطريقة [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // إلغاء المهام غير المتزامنة
    cancelRunningTasks();
}
  • السطر 8: يُطلب من الفئة الأصلية إلغاء المهام غير المتزامنة؛

يتم التعامل مع النقر على خيار القائمة [Retour à la configuration] بالطريقة التالية:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // التنقل إلى عرض التكوين
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • السطر 4: يتم الانتقال إلى عرض التكوين باستخدام الإجراء [NAVIGATION]. وهذا يعني أننا نريد استعادة عرض التكوين في الحالة التي تركناه عليها؛

3.6.7.3. إدارة دورة حياة الجزء

الجزء له الحالة التالية [AccueilFragmentState]:


package client.android.fragments.state;

import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;

public class AccueilFragmentState extends CoreState {

  // حالة الجزء [Accueil]
  // موقع الطبيب المحدد
  private int selectedMedecinPosition;
  // التاريخ المحدد
  private int year;
  private int month;
  private int dayOfMonth;
  // مصدر بيانات قائمة الأطباء
  private String[] spinnerMedecinsDataSource;

  // المنشئون
  public AccueilFragmentState() {

  }

  // مُستردات ومُعيّنات
...
}
  • السطر 11: يسمح باستعادة العنصر المحدد في قائمة الأطباء؛
  • الأسطر 13-15: تسمح باستعادة التاريخ المحدد في التقويم؛
  • السطر 17: يسمح باسترداد مصدر بيانات قائمة الأطباء؛

يتم تنفيذ دورة حياة المقتطف بالطريقة التالية:


// تنفيذ طرق الفئة الأم -------------------------------------
  @Override
  public CoreState saveFragment() {
    // نقوم بحفظ العرض
    AccueilFragmentState state = new AccueilFragmentState();
    state.setSelectedMedecinPosition(spinnerMedecins.getSelectedItemPosition());
    state.setDayOfMonth(edtJourRv.getDayOfMonth());
    state.setMonth(edtJourRv.getMonth());
    state.setYear(edtJourRv.getYear());
    state.setSpinnerMedecinsDataSource(spinnerMedecinsDataSource);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_ACCUEIL;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // استرداد الأطباء في الجلسة
    medecins = session.getMédecins();
    // زيارة أولى؟
    if (previousState == null) {
      // يتم إنشاء الجدول المعروض بواسطة spinner
      spinnerMedecinsDataSource = new String[medecins.size()];
      int i = 0;
      for (Medecin medecin : medecins) {
        spinnerMedecinsDataSource[i] = String.format("%s %s %s", medecin.getTitre(), medecin.getPrenom(), medecin.getNom());
        i++;
      }
    } else {
      // ليست الزيارة الأولى
      AccueilFragmentState state = (AccueilFragmentState) previousState;
      spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
    }
    // التقويم
    calendrier = Calendar.getInstance();
  }

  @Override
  protected void initView(CoreState previousState) {
    // ربط أداة التصفح الخاصة بالأطباء بمصدر نداءاتهم
    ArrayAdapter<String> dataAdapterMedecins = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, spinnerMedecinsDataSource);
    dataAdapterMedecins.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinnerMedecins.setAdapter(dataAdapterMedecins);
    // التاريخ الأدنى للتقويم حتى اليوم
    edtJourRv.setMinDate(calendrier.getTimeInMillis());
    // الزيارة الأولى؟
    if (previousState == null) {
      // القائمة
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // القائمة
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // يتم استعادة الحالة الحالية للجلسة
    AccueilFragmentState state = (AccueilFragmentState) previousState;
    // الاختيار من قائمة الأطباء
    spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
    // التقويم
    edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // يتم استدعاؤها بعد انتهاء جميع المهام أو إلغائها
    // حالة القائمة
    initMenu();
    // العرض التالي؟
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
    }
  }

  // طرق خاصة ------------------------------------------------
  private void initMenu() {
    // حالة القائمة
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • الأسطر 2-9: عندما تطلب منه الفئة الأم، يقوم المقتطف بحفظ حالة العناصر التالية:
    • السطر 6: الموضع المحدد في قائمة الأطباء؛
    • الأسطر 7-9: اليوم والشهر والسنة للتاريخ المحدد في التقويم؛
    • السطر 10: مصدر بيانات عجلة اختيار الأطباء؛
  • الأسطر 14-17: رقم المقطع هو [IMainActivity.VUE_ACCUEIL
  • الأسطر 19-39: يتم تنفيذها عند إنشاء المقطع لأول مرة (previousState==null) أو إعادة إنشائه في المرات التالية (previousState !=null
    • الأسطر 25-31: في حالة الزيارة الأولى، يتم إنشاء مصدر بيانات العجلة الدوارة للأطباء؛
    • الأسطر 33-35: بالنسبة للزيارات الأخرى، يتم استرداد مصدر بيانات قائمة التصفح من الحالة السابقة للجزء؛
  • الأسطر 41-54: يتم تنفيذها عند إنشاء العرض المرتبط بالجزء لأول مرة (previousState==null) أو إعادة إنشائه في المرات التالية (previousState !=null
    • الأسطر 50-53: بالنسبة للزيارة الأولى، يتم عرض القائمة بدون الإجراء [Annuler] (الأسطر 88-92)؛
    • الأسطر 43-48: بالنسبة لجميع الزيارات، سواء كانت الأولى أم لا، يتم ربط قائمة الأطباء بمصدرها (الأسطر 44-46) ويتم تعيين التاريخ الأدنى للتقويم على تاريخ اليوم (السطر 48)؛
  • الأسطر 56-60: يتم تنفيذها عند الوصول إلى الجزء عن طريق عملية [SUBMIT]. نأتي عندئذٍ من العرض [CONFIG]. نضع القائمة في حالتها الأولية؛
  • الأسطر 62-70: يتم تنفيذها عند الوصول إلى المقطع عن طريق عملية [NAVIGATION] أو [RESTORE
    • السطر 67: يتم إعادة وضع مؤشر الأطباء على آخر طبيب تم اختياره؛
    • السطر 69: يتم وضع التقويم على آخر تاريخ تم اختياره؛
  • الأسطر 72-74: يتم تنفيذها عند الانتهاء من جميع التحديثات السابقة. لا يوجد شيء آخر للقيام به؛
  • الأسطر 76-85: يتم تنفيذها عند انتهاء جميع المهام غير المتزامنة؛
    • السطر 80: إعادة القائمة إلى حالتها الافتراضية؛
    • الأسطر 82-84: إذا انتهت المهام بشكل طبيعي، ننتقل إلى العرض التالي، وإلا نبقى على نفس العرض؛

3.6.8. إدارة عرض الأجندة

3.6.8.1. الطريقة

الطريقة التالية:

Image

عناصر الواجهة المرئية هي كما يلي:

Type
Nom
1
TextView
txtTitre2
2
ListView
lstCreneaux

3.6.8.2. الجزء

يتم إدارة عرض الأجندة بواسطة الجزء التالي [AgendaFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.AgendaMedecinJour;
import client.android.dao.entities.CreneauMedecinJour;
import client.android.dao.entities.Medecin;
import client.android.dao.entities.Rv;
import client.android.dao.service.Response;
import client.android.fragments.state.AgendaFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;

@EFragment(R.layout.agenda)
@OptionsMenu(R.menu.menu_agenda)
public class AgendaFragment extends AbstractFragment {

  // عناصر الواجهة المرئية
  @ViewById(R.id.txt_titre2_agenda)
  protected TextView txtTitre2;
  @ViewById(R.id.listViewAgenda)
  protected ListView lstCreneaux;

  // جدول الأعمال المعروض بواسطة الجزء
  private AgendaMedecinJour agenda;
  // معلومات ListView عن المواعيد
  private int firstPosition;
  private int top;
  // الموعد محذوف أم لا
  private boolean rdvSupprimé;
  // رقم الفترة الزمنية المضافة أو المحذوفة
  private int numCréneau;

  // تحديث الأجندة بعد الإضافة / الحذف
  private void updateAgenda() {
  ...
  }

...

  // تنفيذ طرق الفئة الأصلية ------------------------------------------------------
  ...
}
  • السطر 27: المرتبطة المرتبطة بالقائمة التالية:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionAgenda"
        android:title="@string/actionAgenda"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
      <item
        android:id="@+id/navigationToAccueil"
        android:title="@string/navigationToAccueil"/>
    </menu>
  </item>
</menu>
  • الأسطر 32-35: عناصر الواجهة المرئية؛
  • الأسطر 37-45: البيانات العامة للطرق؛

3.6.8.2.1. الطريقة [updateAgenda]

يلزم (إعادة) إنشاء قائمة فترات الجدول الزمني في عدة أماكن من الكود. وقد تم تجميعها في الطريقة الخاصة التالية [updateAgenda]:


  // تحديث التقويم بعد إضافة / حذف
  private void updateAgenda() {
    // (إعادة) إنشاء فترات الجدول
    // يتم تضمين التقويم في الجلسة وتخزينه في حقل من أجزاء
    agenda = session.getAgenda();
    // إعادة إنشاء ListView للفترات الزمنية
    ArrayAdapter<CreneauMedecinJour> adapter = new ListCreneauxAdapter(activity, R.layout.creneau_medecin,
      agenda.getCreneauxMedecinJour(), this);
    lstCreneaux.setAdapter(adapter);
    // نعود إلى المكان الصحيح في ListView
    lstCreneaux.setSelectionFromTop(firstPosition, top);
}
  • السطر 5: يتم أخذ التقويم من الجلسة وتخزينه في الحقل [agenda] من الجزء؛
  • الأسطر 7-9: يتم تعريف محول المكون [ListView]. يحدد هذا المحول في آن واحد مصدر بيانات [ListView] ونموذج عرض كل عنصر منه. سنقدم هذا المحول قريبًا؛
  • السطر 11: نعود إلى الموضع السابق للتقويم. في الواقع، لا نرى سوى جزء من فترات اليوم. إذا أضفنا/حذفنا موعدًا في الفترة الأخيرة، فسيقوم الكود أعلاه بتحديث الصفحة لعرض التقويم الجديد. يؤدي هذا التحديث إلى إعادة وضعنا في الفترة الأولى، وهو أمر غير مرغوب فيه. السطر 5 يعالج هذه المشكلة. يمكن العثور على وصف هذا الحل في URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview

تُستخدم الفئة [ListCreneauxAdapter] لتعريف سطر في [ListView]:

Image

نرى أعلاه أنه اعتمادًا على ما إذا كان الموعد محجوزًا أم لا، فإن العرض يختلف. رمز الفئة [ListCreneauxAdapter] هو التالي:


...

public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {

    // جدول الفترات الزمنية
    private CreneauMedecinJour[] creneauxMedecinJour;
    // سياق التنفيذ
    private Context context;
    // معرف تخطيط عرض سطر من قائمة الفترات الزمنية
    private int layoutResourceId;
    // مستمع النقرات
    private AgendaFragment vue;

    // المنشئ
    public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
            AgendaFragment vue) {
        super(context, layoutResourceId, creneauxMedecinJour);
        // يتم حفظ المعلومات
        this.creneauxMedecinJour = creneauxMedecinJour;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.vue = vue;
        // فرز جدول المواعيد حسب الترتيب الزمني
        Arrays.sort(creneauxMedecinJour, new MyComparator());
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
    ...
}

// فرز جدول الفترات الزمنية
class MyComparator implements Comparator<CreneauMedecinJour> {
...
    }
}
  • السطر 3: يجب أن توسع الفئة [ListCreneauxAdapter] محولًا محددًا مسبقًا لـ [ListView]، وهنا الفئة [ArrayAdapter] التي، كما يشير اسمها، تزود [ListView] بمصفوفة من الكائنات، هنا من النوع [CreneauMedecinJour]. لنتذكر رمز هذه الكيان:

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // الحقول
    private Creneau creneau;
    private Rv rv;
...  
}
  • تحتوي الفئة [CreneauMedecinJour] على فترة زمنية (السطر 5) وموعد محتمل (السطر 6) أو null في حالة عدم وجود موعد؛

العودة إلى كود الفئة [ListCreneauxAdapter]:

  • السطر 15: يتلقى المنشئ أربعة معلمات:
    1. نشاط Android الجاري،
    2. ملف XML الذي يحدد محتوى كل عنصر في [ListView
    3. جدول المواعيد الطبية للطبيب،
    4. الطريقة نفسها؛
  • السطر 24: يتم فرز جدول المواعيد حسب الترتيب التصاعدي للأوقات؛

تتولى الطريقة [getView] إنشاء العرض المطابق لسطر في [ListView]. ويتألف هذا العرض من ثلاثة عناصر:

 
Id
Type
Rôle
1
txtCreneau
TextView
créneau horaire
2
txtClient
TextView
le client
3
btnValider
TextView
lien pour ajouter / supprimer un rendez-vous

رمز الطريقة [getView] هو التالي:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // نحدد الموقع الصحيح
        CreneauMedecinJour creneauMedecin = creneauxMedecinJour[position];
        // يتم إنشاء السطر
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // الفتحة الزمنية
        TextView txtCreneau = (TextView) row.findViewById(R.id.txt_Creneau);
        txtCreneau.setText(String.format("%02d:%02d-%02d:%02d", creneauMedecin.getCreneau().getHdebut(), creneauMedecin
                .getCreneau().getMdebut(), creneauMedecin.getCreneau().getHfin(), creneauMedecin.getCreneau().getMfin()));
        // العميل
        TextView txtClient = (TextView) row.findViewById(R.id.txt_Client);
        String text;
        if (creneauMedecin.getRv() != null) {
            Client client = creneauMedecin.getRv().getClient();
            text = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
        } else {
            text = "";
        }
        txtClient.setText(text);
        // الرابط
        final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
        if (creneauMedecin.getRv() == null) {
            // إضافة
            btnValider.setText(R.string.btn_ajouter);
            btnValider.setTextColor(context.getResources().getColor(R.color.blue));
        } else {
            // حذف
            btnValider.setText(R.string.btn_supprimer);
            btnValider.setTextColor(context.getResources().getColor(R.color.red));
        }
        // مستمع الرابط
        btnValider.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // ننقل المعلومات إلى عرض التقويم
                vue.doValider(position, btnValider.getText().toString());
            }
        });
        // إعادة السطر
        return row;
    }
  • السطر 2: position هو رقم السطر الذي سيتم إنشاؤه في [ListView]. وهو أيضًا رقم الفترة الزمنية في الجدول [creneauxMedecinJour]. يتم تجاهل المعلمتين الأخريين؛
  • السطر 4: يتم استرداد الفترة الزمنية المراد عرضها في سطر [ListView
  • السطر 6: يتم إنشاء السطر بناءً على تعريفه XML
 

رمز [creneau_medecin.xml] هو التالي:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat" >

    <TextView
        android:id="@+id/txt_Creneau"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:text="@string/txt_dummy" />

    <TextView
        android:id="@+id/txt_Client"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Creneau"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_Creneau"
        android:text="@string/txt_dummy" />

    <TextView
        android:id="@+id/btn_Valider"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Client"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_Client"
        android:text="@string/btn_valider"
        android:textColor="@color/blue" />

</RelativeLayout>
 
  • الأسطر 8-10: يتم إنشاء الفترة الزمنية [1]؛
  • الأسطر 12-20: يتم إنشاء هوية العميل [2]؛
  • السطر 23: إذا لم يكن هناك موعد في الفترة الزمنية؛
  • السطور 25-26: يتم إنشاء الرابط [Ajouter] باللون الأزرق؛
  • السطور 29-30: وإلا يتم إنشاء الرابط [Supprimer] باللون الأحمر؛
  • السطور 33-40: بغض النظر عن طبيعة الرابط [Ajouter / Supprimer]، فإن طريقة [doValider] الخاصة بالعرض هي التي ستدير النقر على الرابط. ستتلقى الطريقة حجتين:
    1. رقم الخانة التي تم النقر عليها،
    2. نص الرابط الذي تم النقر عليه؛
  • السطر 42: يتم إرجاع السطر الذي تم إنشاؤه للتو.

تجدر الإشارة إلى أن الطريقة [doValider] الخاصة بالجزء [AgendaFragment] هي التي تدير الروابط. وهي كما يلي:


  // النقر على رابط [Ajouter / Supprimer]
  public void doValider(int numCréneau, String texte) {
    // عملية جارية؟
    if (numberOfRunningTasks != 0) {
      Toast.makeText(activity, "Une opération est en cours. Patientez ou Annulez...", Toast.LENGTH_SHORT).show();
      return;
    }
    // تدوين موضع التمرير للعودة إليه
    // قراءة [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // موضع العنصر الأول المرئي بالكامل أو غير المرئي
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // إزاحة Y لهذا العنصر بالنسبة لأعلى ListView
    // قياس ارتفاع الجزء المخفي إن وجد
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // يتم أيضًا تسجيل رقم المربع الذي تم النقر عليه
    this.numCréneau = numCréneau;
    // وفقًا لنص الرابط، لا يتم القيام بنفس الشيء
    if (texte.equals(getResources().getString(R.string.lnk_ajouter))) {
      doAjouter();
    } else {
      doSupprimer();
    }
}
  • تتلقى الطريقة [doValider] معلومتين:
    • رقم الخانة التي تم النقر عليها؛
    • النص (إضافة / حذف) للرابط الذي تم النقر عليه؛
  • الأسطر 4-7: يتم منع النقر على الروابط [Supprimer / Ajouter] في حالة وجود مهام غير متزامنة قيد التنفيذ. هذا خيار يسهل كتابة الكود. يمكن مناقشته؛
  • الأسطر 11-15: يتم تدوين معلومات (firstPosition، أعلى) الخاصة بـ ListView للفترات الزمنية في حقول الجزء حتى تتمكن الطريقة الخاصة [updateAgenda] من إعادة إنشائها بنفس موضع التمرير؛
  • السطر 17: يتم تدوين رقم الفتحة التي تم النقر عليها؛
  • الأسطر 19-23: وفقًا لنص الرابط الذي تم النقر عليه، يتم إجراء إضافة أو حذف؛

3.6.8.2.2. الطريقة [doSupprimer]

تضمن الطريقة [doSupprimer] حذف الموعد الخاص بالفتحة التي تم النقر عليها:


// حذف موعد
  private void doSupprimer() {
    // الانتظار حتى انتهاء مهمتين
    beginWaiting(2);
    // يتم حذف الموعد في الخلفية
    rdvSupprimé = false;
    // معرف الموعد المراد حذفه
    long idRv = agenda.getCreneauxMedecinJour()[numCréneau].getRv().getId();
    // حذف بواسطة مهمة غير متزامنة
    executeInBackground(mainActivity.supprimerRv(idRv), new Action1<Response<Rv>>() {

      @Override
      public void call(Response<Rv> responseRv) {
        // استهلاك النتيجة
        consumeRv(responseRv);
      }
    });
  }

  // استهلاك إجابة
  private void consumeRv(Response<Rv> responseRv) {
    // خطأ؟
    if (responseRv.getStatus() != 0) {
      // رسالة
      showAlert(responseRv.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى UI
      return;
    }
    // يُلاحظ أن الموعد قد تم حذفه
    rdvSupprimé = true;
    // يُطلب أحدث جدول
    executeInBackground(
      mainActivity.getAgendaMedecinJour(agenda.getMedecin().getId(), session.getDayRv()),
      new Action1<Response<AgendaMedecinJour>>() {

        @Override
        public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
          // يتم استهلاك الرد
          consumeAgenda(responseAgendaMedecinJour);
        }
      });
  }

  // استهلاك جدول
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // خطأ؟
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // رسالة
      showAlert(responseAgendaMedecinJour.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى UI
      return;
    }
    // يتم وضع التقويم في الجلسة
    session.setAgenda(responseAgendaMedecinJour.getBody());
    // يتم تحديث جدول الأعمال الخاص بالعرض
    updateAgenda();
  }
  • السطر 4: يتم إخطار الفئة الأم بأننا سنقوم بتشغيل مهمتين غير متزامنتين ونبدأ في انتظار انتهاء هاتين المهمتين؛
  • السطر 8: يتم استرداد معرف الموعد المراد حذفه. في الواقع، يحتاج الخادم إلى هذه المعلومات؛
  • الأسطر 9-18: نطلب حذف الموعد عبر مهمة غير متزامنة؛
    • السطر 10: تتوقع الطريقة [executeInBackground] معلمتين:
      • السطر 10: يتم توفير العملية المطلوب تنفيذها ومراقبتها بواسطة الطريقة [mainActivity.supprimerRv(idRv)]؛
      • الأسطر 10-17: المعلمة الثانية هي مثيل من النوع [Action1<T>] حيث T هو النوع الذي تعرضه العملية المراقبة، وهنا [Response<Rv>]
    • السطر 15: عند استلام الرد، يتم تمريره إلى الطريقة [consumeRv] في السطر 21؛
  • الأسطر 21-44: تم استلام الرد من المهمة غير المتزامنة. يتم استغلاله؛
  • الأسطر 23-30: ننظر أولاً إذا كان الخادم قد أبلغ عن خطأ في الحقل [status] من الرد؛
    • السطر 25: في حالة وجود خطأ، يتم عرض الرسائل التي وضعها الخادم في الحقل [messages] من الرد؛
    • السطر 27: يتم إلغاء جميع المهام؛
    • السطر 29: نعود إلى واجهة المستخدم؛
  • السطر 32: إذا لم يكن هناك خطأ، يتم تسجيل أن الموعد قد تم حذفه؛
  • الأسطر 34-43: بدلاً من مجرد حذف الموعد من الأجندة المعروضة حاليًا بواسطة الجزء، نطلب الأجندة الجديدة للطبيب. في الواقع، التطبيق متعدد المستخدمين وقد يكون مستخدمون آخرون قد قاموا أيضًا بتعديل أجندة الطبيب. لذا من الأفضل الحصول على الأحدث؛
  • الأسطر 34-43، 47-61: نعيد ما تم في المقتطف [AccueilFragment]، ولكن هذه المرة باستخدام المعلومات المأخوذة من الجلسة؛

الطريقة [beginWaiting] (السطر 4) هي كما يلي:


  // بدء الانتظار
  protected void beginWaiting(int numberOfRunningTasks) {
    // يتم التحضير لبدء المهام
    beginRunningTasks(numberOfRunningTasks);
    // حالة الأزرار والقوائم
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • السطر 4: نُعلم المهمة الأم بأننا سنقوم بتشغيل مهام [numberOfRunningTasks
  • السطر 6: يتم إخفاء جميع خيارات القائمة؛
  • السطر 7: ثم إظهار الخيار [Actions/Annuler

3.6.8.2.3. طريقة [doAnnuler]

يتم إدارة النقر على خيار القائمة [Annuler] بواسطة الطريقة [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // إلغاء المهام غير المتزامنة
    cancelRunningTasks();
}
  • السطر 7: يُطلب من الفئة الأصلية إلغاء المهام غير المتزامنة؛

3.6.8.2.4. خيار القائمة [Retour à la configuration]

يتم التعامل مع النقر على خيار القائمة [Retour à la configuration] بالطريقة التالية:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // التنقل إلى عرض التكوين
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
  • السطر 4: يتم الانتقال إلى عرض التكوين باستخدام الإجراء [NAVIGATION]. وهذا يعني أننا نريد استعادة عرض التكوين بالوضع الذي تركناه عليه؛

3.6.8.2.5. خيار القائمة [Retour à l'accueil]

يتم التعامل مع النقر على خيار القائمة [Retour à l'accueil] بطريقة مماثلة:


  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // التنقل إلى عرض الصفحة الرئيسية
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}

3.6.8.3. إدارة دورة حياة الجزء

يكون للجزء الحالة التالية [AgendaFragmentState]:


package client.android.fragments.state;

import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.CreneauMedecinJour;

public class AgendaFragmentState extends CoreState {

  // عنوان العرض
  private String titre;
  // ListView
  private int firstPosition;
  private int top;

  // المنشئون
  public AgendaFragmentState() {

  }

  public AgendaFragmentState(String titre) {
    this.titre = titre;
  }

  // المُستردات والمُعيّنات
...
}
  • السطر 10: العنوان المعروض في أعلى العرض؛
  • السطران 12-13: يسمحان باستعادة scrolling من ListView لمواعيد الطبيب؛

يتم تنفيذ دورة حياة الجزء على النحو التالي:


// تنفيذ طرق الفئة الأم ------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // حفظ الحالة
    AgendaFragmentState state = new AgendaFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    // تسجيل موضع التمرير للعودة إليه
    // قراءة [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // موضع العنصر الأول المرئي بالكامل أو غير المرئي
    firstPosition = lstCreneaux.getFirstVisiblePosition();
    // إزاحة Y لهذا العنصر بالنسبة لأعلى ListView
    // قياس ارتفاع الجزء المخفي إن وجد
    View v = lstCreneaux.getChildAt(0);
    top = (v == null) ? 0 : v.getTop();
    // يتم حفظ كل ذلك
    state.setTop(top);
    state.setFirstPosition(firstPosition);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AGENDA;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // أول زيارة؟
    if (previousState != null) {
      // ليست الزيارة الأولى
      AgendaFragmentState state = (AgendaFragmentState) previousState;
      // ومعلومات ListView
      firstPosition = state.getFirstPosition();
      top = state.getTop();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // نسترد الأجندة
    agenda = session.getAgenda();
    // يتم إنشاء عنوان الصفحة
    Medecin medecin = agenda.getMedecin();
    txtTitre2.setText(String.format("Rendez-vous de %s %s %s le %s", medecin.getTitre(), medecin.getPrenom(),
      medecin.getNom(), session.getJourRv()));
    // حالة القائمة
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // يتم إعادة إنشاء عنوان الصفحة
    AgendaFragmentState state = (AgendaFragmentState) previousState;
    txtTitre2.setText(state.getTitre());
  }

  @Override
  protected void notifyEndOfUpdates() {
    // إعادة إنشاء قائمة المواعيد
    updateAgenda();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // حالة القائمة
    initMenu();
    // في حالة الإلغاء، ولكن إذا تم حذف الموعد، فيجب تحديث التقويم المحلي
    if (runningTasksHaveBeenCanceled && rdvSupprimé) {
      // يتم حذف الموعد من التقويم المحلي (لم نتمكن من الوصول إلى التقويم العام)
      agenda.getCreneauxMedecinJour()[numCréneau].setRv(null);
      // يتم تحديث الواجهة المرئية
      updateAgenda();
    }
  }


  // طرق خاصة ------------------------------------------------
  private void initMenu() {
    // حالة القائمة
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
  • السطور 2-19: عندما تطلب منه فئته الأم، يحفظ الجزء حالة العناصر التالية:
    • السطر 6: العنوان المعروض في أعلى الشاشة؛
    • الأسطر 7-17: المعلومات (top، firstPosition) التي ستسمح باستعادة scrolling من ListView؛
  • الأسطر 21-24: رقم الجزء هو [IMainActivity.VUE_AGENDA
  • الأسطر 26-35: يتم تنفيذها عند إنشاء الجزء لأول مرة (previousState==null) أو إعادة إنشائه في المرات التالية (previousState !=null
    • الأسطر 30-34: إذا لم تكن هذه هي الزيارة الأولى للجزء، يتم استرداد المعلومات (top، firstPosition) التي ستسمح باستعادة scrolling من ListView؛
  • الأسطر 38-40: يتم تنفيذها عند إنشاء العرض المرتبط بالجزء لأول مرة (previousState==null) أو إعادة إنشائه في المرات التالية (previousState !=null). لا يوجد ما يجب فعله هنا لأن ListView للفترات الزمنية سيتم إنشاؤه بواسطة الطريقة الخاصة [updateAgenda] (الأسطر 61-65)؛
  • الأسطر 42-52: يتم تنفيذها عند الوصول إلى الجزء عن طريق عملية [SUBMIT]. نأتي إذن من العرض [ACCUEIL
    • السطر 45: يتم استرداد الأجندة التي تم تفعيلها بواسطة [AccueilFragment
    • الأسطر 47-49: يتم إنشاء عنوان العرض؛
    • سيتم إنشاء ListView الخاص بالمواعيد من خلال الطريقة الخاصة [updateAgenda] (الأسطر 61-65)؛
  • الأسطر 54-59: يتم تنفيذها عند الوصول إلى الجزء عن طريق عملية [NAVIGATION] أو [RESTORE
    • السطور 57-58: يتم إعادة إنشاء عنوان العرض؛
    • سيتم إنشاء ListView للفترات الزمنية بواسطة الطريقة الخاصة [updateAgenda] (الأسطر 61-65)؛
  • الأسطر 72-74: يتم تنفيذها عند الانتهاء من جميع التحديثات السابقة. يتم تحديث ListView الخاص بالفترات الزمنية لأن هذا التحديث ضروري بغض النظر عن الطريقة التي يتم بها الوصول إلى الجزء؛
  • الأسطر 67-77: يتم تنفيذها عند انتهاء جميع المهام غير المتزامنة؛
    • السطر 70: يتم إعادة القائمة إلى حالتها الافتراضية (الأسطر 82-86)؛
    • السطر 72: كانت هناك مهمتان غير متزامنتان. نتحقق مما إذا كانت المهمة الأولى (حذف الموعد) قد نجحت، على الرغم من الإلغاء؛
    • السطر 74: إذا كان الجواب نعم، يتم حذف الموعد من التقويم المحلي
    • السطر 75: ونقوم بتحديث عرضه؛

3.6.9. إدارة عرض إضافة موعد

3.6.9.1. الطريقة

عرض إضافة موعد هو كما يلي:

Image

عناصر الواجهة المرئية هي كما يلي:

Type
Nom
1
TextView
txtTitre2
2
Spinner
spinnerClients

3.6.9.2. الجزء

يتم إدارة عرض إضافة موعد بواسطة الجزء التالي [AjoutRvFragment]:

 

package client.android.fragments.behavior;

import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Response;
import client.android.fragments.state.AjoutRvFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;

import java.util.List;
import java.util.Locale;

@EFragment(R.layout.ajout_rv)
@OptionsMenu(R.menu.menu_ajout_rv)
public class AjoutRvFragment extends AbstractFragment {

  // عناصر الواجهة المرئية
  @ViewById(R.id.spinnerClients)
  protected Spinner spinnerClients;
  @ViewById(R.id.txt_titre2_ajoutRv)
  protected TextView txtTitre2;

  // العملاء
  private List<Client> clients;

  // البيانات المحلية
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;

  // التحقق من صحة الصفحة
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
   ...
  }
...

  // تنفيذ طرق الفئة الأم ----------------------------------
...
}
  • السطر 26: المرتبطة بالجزء التالي من القائمة [menu_ajout_rv]:
  

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity1">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationToConfig"
        android:title="@string/navigationToConfig"/>
      <item
        android:id="@+id/navigationToAccueil"
        android:title="@string/navigationToAccueil"/>
      <item
        android:id="@+id/navigationToAgenda"
        android:title="@string/navigationToAgenda"/>
    </menu>
  </item>
</menu>
  • الأسطر 30-33: عناصر الواجهة المرئية؛
  • السطر 36: قائمة العملاء؛
  • السطر 43: مصدر بيانات عجلة العملاء؛

يتم إدارة النقر على الرابط [Valider] بواسطة الطريقة التالية [doValider]:


  // العملاء
  private List<Client> clients;

  // البيانات المحلية
  private Creneau creneau;
  private Medecin medecin;
  private boolean rdvAjouté;
  private Rv rv;
  private String[] spinnerClientsDataSource;
...
// التحقق من صحة الصفحة
  @OptionsItem(R.id.actionValider)
  protected void doValider() {
    // استرداد العميل المختار
    Client client = clients.get(spinnerClients.getSelectedItemPosition());
    // بدء انتظار مهمتين غير متزامنتين
    beginWaiting(2);
    // إضافة RV
    rdvAjouté = false;
    executeInBackground(
      mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId()),
      new Action1<Response<Rv>>() {

        @Override
        public void call(Response<Rv> responseRv) {
          // استهلاك الرد
          consumeRv(responseRv);
        }
      });
  }

  // استهلاك كائن Response<Rv>
  void consumeRv(Response<Rv> responseRv) {
    // خطأ؟
    if (responseRv.getStatus() != 0) {
      // رسالة
      showAlert(responseRv.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى UI
      return;
    }
    // يُلاحظ أن الموعد قد أُضيف
    rdvAjouté = true;
    // يتم حفظ الموعد
    this.rv = responseRv.getBody();
    // يُطلب الجدول الجديد
    executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {

      @Override
      public void call(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
        // يتم استهلاك الرد
        consumeAgenda(responseAgendaMedecinJour);
      }
    });
  }

  // استهلاك كائن Response<AgendaMedecinJour>
  private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
    // خطأ؟
    if (responseAgendaMedecinJour.getStatus() != 0) {
      // رسالة
      showAlert(responseAgendaMedecinJour.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى UI
      return;
    }
    // يتم وضع الأجندة في الجلسة
    session.setAgenda(responseAgendaMedecinJour.getBody());
}
  • السطر 13: عند بدء طريقة [doValider]، تم تهيئة الحقول 2 و5 و6 و9 خلال دورة حياة الجزء. سنرى كيف؛
  • السطر 15: يتم استرداد الكيان [Client] المطابق للعنصر المحدد في عجلة التمرير الخاصة بالعملاء؛
  • السطر 17: يتم إخطار الفئة الأم بأننا سنقوم بتشغيل مهمتين غير متزامنتين ونقوم بإعداد الانتظار؛
  • السطر 19: في البداية، لم يتم إضافة الموعد بعد إلى جدول مواعيد الطبيب؛
  • الأسطر 20-30: يتم طلب إضافة موعد من الخادم؛
    • السطر 20: تتوقع الطريقة [executeInBackground] معلمتين:
      • السطر 20: يتم توفير العملية المراد تنفيذها ومراقبتها بواسطة الطريقة [mainActivity.ajouterRv(session.getDayRv(), creneau.getId(), client.getId())]؛
      • الأسطر 22-29: المعلمة الثانية هي مثيل من النوع [Action1<T>] حيث T هو النوع الذي تعرضه العملية المراقبة، وهنا [Response<Rv>]
    • السطر 27: عند استلام الرد، يتم تمريره إلى الطريقة [consumeRV] في السطر 33؛
  • الأسطر 33-56: تم استلام الرد من الخادم. يتم استغلاله؛
    • الأسطر 35-42: ننظر أولاً إذا كان الخادم قد أبلغ عن خطأ في الحقل [status] من الرد؛
    • السطر 37: في حالة وجود خطأ، يتم عرض الرسائل التي وضعها الخادم في الحقل [messages] من الرد؛
    • السطر 39: يتم إلغاء جميع المهام؛
    • السطر 41 : نعود إلى واجهة المستخدم؛
    • السطر 44: إذا لم يكن هناك خطأ، يتم تسجيل أن الموعد قد تمت إضافته؛
    • السطر 46: يتم حفظ الموعد المضاف في حقل من أجزاء الصفحة؛
    • السطور 47-55: كما تم عند حذف موعد، بعد إضافة الموعد نطلب من الخادم أحدث جدول مواعيد الطبيب؛
  • الأسطر 47-56، 59-71: لدينا هنا كود سبق أن صادفناه عدة مرات؛

الطريقة [beginWaiting] (السطر 17) هي كما يلي:


  // بدء الانتظار
  protected void beginWaiting(int numberOfRunningTasks) {
    // يتم التحضير لبدء المهام
    beginRunningTasks(numberOfRunningTasks);
    // حالة الأزرار والقوائم
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true),new MenuItemState(R.id.actionAnnuler, true)});

}
  • السطر 4: يُبلغ المهمة الأم بأننا سنقوم بتشغيل مهام [numberOfRunningTasks
  • السطر 6: يتم إخفاء جميع خيارات القائمة؛
  • السطر 7: ثم نجعل الخيار [Actions/Annuler] مرئيًا؛

يتم إدارة النقر على خيار القائمة [Annuler] بواسطة الطريقة [doAnnuler]:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // إلغاء المهام غير المتزامنة
    cancelRunningTasks();
}
  • السطر 7: يُطلب من الفئة الأم إلغاء المهام غير المتزامنة؛

يتم توفير التنقل للخلف بواسطة الطرق الثلاث التالية:


  @OptionsItem(R.id.navigationToConfig)
  protected void navigationToConfig() {
    // التنقل إلى عرض التكوين
    mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAccueil)
  protected void navigationToAccueil() {
    // التنقل إلى عرض التكوين
    mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
  }

  @OptionsItem(R.id.navigationToAgenda)
  protected void navigationToAgenda() {
    // التنقل إلى عرض التقويم
    mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.NAVIGATION);
}

3.6.9.3. إدارة دورة حياة الجزء

يكون للجزء الحالة التالية: [AjoutRvFragmentState]


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

// حالة الجزء AjoutRvFragment
public class AjoutRvFragmentState  extends CoreState {

  // موضع العميل المحدد
  private int selectedClientPosition;
  // عنوان العرض
  private String titre;
  // مصدر بيانات قائمة العملاء
  private String[] spinnerClientsDataSource;

  // مُستردات ومُعيّنات
...
}

يتم تنفيذ دورة حياة الجزء بالطريقة التالية:


// تنفيذ طرق الفئة الأصلية ----------------------------------
  @Override
  public CoreState saveFragment() {
    // حفظ العرض
    AjoutRvFragmentState state = new AjoutRvFragmentState();
    state.setTitre(txtTitre2.getText().toString());
    state.setSelectedClientPosition(spinnerClients.getSelectedItemPosition());
    state.setSpinnerClientsDataSource(spinnerClientsDataSource);
    return state;
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_AJOUT_RV;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // استرداد العملاء في الجلسة
    clients = session.getClients();
    // زيارة أولى؟
    if (previousState == null) {
      // يتم إنشاء الجدول المعروض بواسطة spinner
      spinnerClientsDataSource = new String[clients.size()];
      int i = 0;
      for (Client client : clients) {
        spinnerClientsDataSource[i] = String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom());
        i++;
      }
    } else {
      // ليست الزيارة الأولى
      AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
      spinnerClientsDataSource = state.getSpinnerClientsDataSource();
    }
  }

  @Override
  protected void initView(CoreState previousState) {
    // ربط العجلة الدوارة بمصدر البيانات الخاص بها
    ArrayAdapter<String> dataAdapterClients = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item,
      spinnerClientsDataSource);
    dataAdapterClients.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinnerClients.setAdapter(dataAdapterClients);
    // زيارة أولى؟
    if (previousState == null) {
      // القائمة
      initMenu();
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // يتم استرداد رقم الموعد المراد حجزه في الجلسة
    int position = session.getPosition();
    // يتم استرداد جدول مواعيد الطبيب في الجلسة
    AgendaMedecinJour agenda = session.getAgenda();
    // استرداد اسم الطبيب والموعد الذي سيتم حجزه
    medecin = agenda.getMedecin();
    creneau = agenda.getCreneauxMedecinJour()[position].getCreneau();
    // يتم إنشاء العنوان 2 للصفحة
    String jour = session.getJourRv();
    txtTitre2.setText(String.format(Locale.FRANCE,
      "Prise de rendez-vous de %s %s %s le %s pour le créneau %02d:%02d-%02d:%02d", medecin.getTitre(),
      medecin.getPrenom(), medecin.getNom(), jour, creneau.getHdebut(), creneau.getMdebut(), creneau.getHfin(),
      creneau.getMfin()));
    // اختيار العميل
    spinnerClients.setSelection(0);
    // القائمة
    initMenu();
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // استعادة الحالة السابقة
    AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
    // العنوان
    txtTitre2.setText(state.getTitre());
    // عجلة التمرير
    spinnerClients.setSelection(state.getSelectedClientPosition());
  }

  @Override
  protected void notifyEndOfUpdates() {
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // حالة القائمة
    initMenu();
    // العرض التالي؟
    if (!runningTasksHaveBeenCanceled) {
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
    // تم الإلغاء - هل تمت إضافة الموعد بالفعل؟
    if (rdvAjouté) {
      // يتم تعديل التقويم المحلي (لم يتم الحصول على التقويم العام)
      AgendaMedecinJour agenda = session.getAgenda();
      agenda.getCreneauxMedecinJour()[session.getPosition()].setRv(rv);
      // يتم عرض الأجندة
      mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
      return;
    }
  }

  // طرق خاصة -------------------
  private void initMenu() {
    // حالة القائمة
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  • الأسطر 2-10: عندما تطلب منه الفئة الأم، يحفظ المقطع حالة العناصر التالية:
    • السطر 6: العنوان في أعلى العرض؛
    • السطر 7: موضع العنصر المحدد في عجلة التمرير الخاصة بالعملاء؛
    • السطر 8: مصدر بيانات قائمة العملاء؛
  • الأسطر 12-15: رقم المقطع هو [IMainActivity.VUE_AJOUT_RV
  • الأسطر 17-35: يتم تنفيذها عند إنشاء المقطع لأول مرة (previousState==null) أو إعادة إنشائه في المرات التالية (previousState !=null
    • السطر 20: يتم استرداد قائمة العملاء في الجلسة لوضعها في حقل في المقطع؛
    • الأسطر 22-30: في حالة الزيارة الأولى، يتم إنشاء مصدر بيانات شريط التمرير الخاص بالعملاء؛
    • السطور 32-33: بالنسبة للزيارات الأخرى، يتم استرداد مصدر بيانات قائمة العملاء المتجددة من الحالة السابقة للجزء؛
  • الأسطر 37-49: يتم تنفيذها عند إنشاء العرض المرتبط بالجزء لأول مرة (previousState==null) أو إعادة إنشائه في المرات التالية (previousState !=null
    • الأسطر 40-43: في جميع الحالات، يتم ربط شريط التمرير الخاص بالعملاء بمصدر البيانات الخاص به؛
    • الأسطر 45-48: في الزيارة الأولى، يتم عرض القائمة بدون الإجراء [Annuler] (الأسطر 107-111)؛
  • الأسطر 51-70: يتم تنفيذها عند الوصول إلى الجزء عن طريق عملية [SUBMIT]. نأتي إذن من العرض [AGENDA
    • السطر 54: يتم استرداد رقم الفترة الزمنية التي سيتم وضع موعد فيها؛
    • الأسطر 56-59: يتم استرداد الكيان [Medecin] والكيان [Creneau] اللازمين لإضافة هذا الموعد ووضعهما في حقول الجزء؛
    • السطور 61-65: باستخدام هذه المعلومات، يمكننا إنشاء عنوان العرض؛
    • السطر 67: يتم وضع عجلة التمرير الخاصة بالعملاء على العنصر الأول؛
    • السطر 69: يتم وضع القائمة في حالتها الأولية (بدون الخيار [Annuler])؛
  • الأسطر 72-80: يتم تنفيذها عند الوصول إلى الجزء عن طريق عملية [NAVIGATION] أو [RESTORE
    • السطر 77: يتم إعادة إنشاء عنوان العرض؛
    • السطر 79: إعادة وضع مؤشر العملاء على آخر عميل تم تحديده؛
  • الأسطر 82-84: يتم تنفيذها عند الانتهاء من جميع التحديثات السابقة. هنا لا يوجد شيء آخر للقيام به؛
  • الأسطر 86-104: يتم تنفيذها عند انتهاء جميع المهام غير المتزامنة؛
    • السطر 89: إعادة القائمة إلى حالتها الافتراضية؛
    • الأسطر 91-94: إذا انتهت المهام بشكل طبيعي، فإننا نعود إلى العرض [AGENDA] عبر [SUBMIT] (هنا، كان من الممكن أن تكون أيضًا إجراءً من النوع NAVIGATION
    • الأسطر 96-103: إذا انتهت المهام بإلغاء، فإننا نتحقق مع ذلك مما إذا كان الموعد قد أُضيف (وهذا يعني أن الحصول على الأجندة الجديدة هو الذي فشل)؛
    • السطور 98-99: إذا تمت إضافة الموعد؛
      • السطور 98-99: يتم إضافة الموعد الذي أعاده الخادم إلى التقويم الحالي، أي التقويم الذي هو قيد التشغيل؛
      • السطر 101: نعود إلى العرض [AGENDA] عن طريق [SUBMIT] (هنا، كان من الممكن أن تكون هذه أيضًا إجراءً من النوع NAVIGATION

3.7. Exécution

قم بإجراء الاختبارات التالية:

  • استخدم التطبيق في الظروف العادية وتحقق من أنه يعمل؛
  • قم بتدوير الجهاز لكل عرض وتحقق من استعادة كل عرض بشكل صحيح؛
  • ضع وقت انتظار لبضع ثوانٍ في [IMainActivity
  • ثم قم بإلغاء المهام وتحقق من أن النتيجة التي تم الحصول عليها هي النتيجة المتوقعة؛
  • قم بتدوير الجهاز أثناء فترات الانتظار وتحقق من أن المهام قد تم إلغاؤها بالفعل وأنه لا يوجد أي تعطل؛
  • تغيير تجاور الأجزاء في [IMainActivity] والتحقق من أن التطبيق يستمر في العمل؛