Skip to content

2. هيكل عميل Android يتواصل مع خدمة ويب / JSON

نقدم الآن هيكلاً لتطبيق Android يتواصل مع خدمة ويب واحدة أو أكثر / JSON. هذا هو مشروع [client-android-skel]، والذي يمكن العثور عليه في مجلد [architecture] ضمن الأمثلة:

  

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

  • يتم التعامل مع الاتصال غير المتزامن مع خادم الويب/JSON باستخدام مكتبة RxJava؛
  • تتم إدارة دورة حياة الجزء (التحديث، الحفظ، الاستعادة) بواسطة فئته الأصلية [AbstractFragment]، التي تستدعي طرقًا معينة من فئاته الفرعية في أوقات محددة. وبالتالي، لا يتعين على الفئة الفرعية القلق بشأن مراحل دورة الحياة، بل تحتاج فقط إلى تنفيذ طرق معينة تتطلبها فئتها الأصلية؛
  • تتم إدارة دورة حياة النشاط (حفظ / استعادة) بواسطة فئة مجردة [AbstractActivity]، والتي تتطلب أيضًا من النشاط الفرعي تنفيذ طرق معينة؛
  • الفئة [AbstractActivity] قادرة على إدارة تطبيق مع أو بدون علامات تبويب، مع أو بدون صورة تحميل، ومع أو بدون مصادقة أساسية مقابل خادم الويب/JSON. ويتم تحديد وجود أو عدم وجود هذه العناصر من خلال التكوين؛

تم استخدام هذا الهيكل الأساسي لجميع الأمثلة اللاحقة. ونظرًا لتنوعها، فإن ما نجح في أحد الأمثلة قد لا ينجح في المثال التالي. وبما أن الهيكل الأساسي استُخدم في ما مجموعه سبعة أمثلة، فقد حدثت العديد من التكرارات. إذا كنا سنستخدمه في مثال ثامن، فمن المحتمل أن نجد مرة أخرى أن الطبيعة المحددة لهذا المثال الجديد تولد أخطاء جديدة. ومع ذلك، فإن استخدام هذا الهيكل سيبسط بشكل كبير كتابة الأمثلة المستقبلية. في الواقع، تعد إدارة دورة حياة الجزء (التحديث، الحفظ، الاستعادة) جنبًا إلى جنب مع مفهوم تجاور الأجزاء أمرًا معقدًا بشكل خاص. هنا، يتم إخفاؤه تمامًا داخل فئة [AbstractFragment].

2.1. بنية عميل Android

يعتمد عميل Android المقترح على البنية التالية:

  • تقوم طبقة [DAO] بتنفيذ واجهة [IDao]. وهي مسؤولة عن التواصل مع خادم الويب/JSON؛
  • هناك نشاط واحد فقط يقوم أيضًا بتنفيذ واجهة [IDao]. وتستدعيه طرق العرض للوصول إلى الخادم؛
  • يتم تنفيذ العروض بواسطة الأجزاء؛

يعكس مشروع Android هذه البنية:

  

سنعرض العناصر المختلفة لهذا المشروع واحدة تلو الأخرى.

2.2. تكوين Gradle

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
 
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
 
  // options de packaging nécessaires pour être capable de produire l'APK
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}
 
def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  apt "org.androidannotations:rest-spring:$AAVersion"
  compile "org.androidannotations:rest-spring-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
  compile 'io.reactivex:rxandroid:1.2.0'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}
 
repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}
  • جميع أرقام الإصدارات قابلة للتغيير. ومع ذلك، يمكنك البدء بالأرقام الحالية إذا قمت بتكوين Android Studio للتأكد من وجود هذه الإصدارات من أدوات Android (السطور 15–16، 47–48) (انظر القسم 6.11

2.3. بيان التطبيق

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="client.android">
 
  <uses-permission android:name="android.permission.INTERNET"/>
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".activity.MainActivity_"
      android:label="@string/app_name"
      android:windowSoftInputMode="stateHidden"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
 
</manifest>
  • السطر 3: قم بتغيير حزمة التطبيق؛
  • السطران 10 و 15: سنقوم بتعيين قيمة العنصر [app_name] في ملف [res/values/strings.xml]. في الوقت الحالي، هي كما يلي:

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
  <!-- application name -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. تنظيم كود Java

  
  • [architecture] تضم العناصر الرئيسية لتنظيم الكود؛
  • [النشاط] يحتوي على النشاط الوحيد للتطبيق؛
  • [fragments] يجمع أجزاء التطبيق أو طرق العرض؛
  • [dao] يجمع عناصر الاتصال بخادم الويب / JSON؛

2.5. عناصر النشاط

 

Image

2.5.1. طريقة العرض المرتبطة بالنشاط

عرض [activity_main.xml] المرتبط بالنشاط هو كما يلي:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">
 
  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">
 
    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>
 
  <!-- fragment container -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
  • السطر 29: يتم استخدام حاوية جزء محددة؛

يحتوي النشاط أيضًا على قائمة [res/menu/menu_main.xml] لعرضه:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
</menu>

في الوقت الحالي، هو فارغ. سيقوم المطور بملئه حسب الحاجة.

2.5.2. حاوية الجزء [MyPager]

  

package client.android.architecture;
 
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
 
public class MyPager extends ViewPager {
 
  // swipe control
  private boolean isSwipeEnabled;
  // controls scrolling
  private boolean isScrollingEnabled;
 
  // manufacturers
  public MyPager(Context context) {
    super(context);
  }
 
  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
 
  // methods to be redefined to manage swiping
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe allowed?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // swipe allowed?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }
 
  // scrolling control
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }
 
  // setters
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }
 
  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }
}

توسع هذه الفئة فئة [ViewPager] القياسية في Android فقط للتعامل مع التمرير (السطر 11) والتمرير (السطر 13) بين العروض.

  • الأسطر 26–43: طرق تعمل على تعطيل التمرير إذا تم إيقاف تشغيله؛
  • الأسطر 46–49: إعادة تعريف طريقة [setCurrentItem]، التي تُستخدم لتغيير العرض المعروض. إذا تم تعطيل التمرير، فسيتغير العرض دون التمرير. لاحظ أنه يمكن للمطور تجاوز هذا السلوك باستخدام طريقة [setCurrentItem(int position, boolean smoothScrolling)]، التي تسمح له بتحديد سلوك التمرير المطلوب؛

2.5.3. فئة [CoreState]

  

فئة [CoreState] هي الفئة الأم لحالات الأجزاء المختلفة:


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo: add subclasses of [CoreState] here
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • السطر 16: يحتوي كل جزء على قيمة منطقية [hasBeenVisited] في حالته تشير إلى ما إذا كان قد تمت زيارته بالفعل أم لا. وهذا ضروري لأنه في بعض الأحيان، عند عرض الجزء لأول مرة، هناك إجراءات محددة يجب اتخاذها؛
  • السطر 18: يقوم مشروع [client-android-skel] تلقائيًا بحفظ واستعادة قوائم الأجزاء إذا كانت تحتوي على واحدة. في مصفوفة MenuItemState[] menuOptionsState، نقوم بتخزين الحالة المرئية أو المخفية لجميع خيارات القائمة؛
  • الأسطر 10–13: كما هو الحال في [Example-22]، سيتم حفظ حالة النشاط وأجزائه في الجلسة، والتي سيتم حفظها بدورها كسلسلة JSON. سنرى أن الجلسة تخزن مصفوفة من العناصر من النوع [CoreState]. إذا لم نفعل شيئًا، فسيتم حفظ سلسلة JSON من النوع [CoreState]. ومع ذلك، نريد حفظ حالات الأجزاء، التي تُشتق من [CoreState]. لضمان إنشاء سلسلة JSON من النوع المشتق بدلاً من النوع الأصلي، يجب إعلان الأنواع المشتقة كما هو موضح في الأسطر 10–13. تعد فئة [CoreState] إحدى فئات البنية التي يجب على المطور تعديلها لكل تطبيق جديد (الأسطر 10–13)؛

2.5.4. واجهة [IMainActivity]

  

تحدد واجهة [IMainActivity] ما يمكن أن تطلبه الأجزاء من النشاط في البنية التالية:

Image


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // application constants (to be modified) -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 0;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 0;
 
  // todo add your constants and other methods here
}
  • السطر 6: واجهة [IMainActivity] تمتد واجهة [IDao] الخاصة بطبقة [DAO
  • السطر 9: هذه هي النشاط الذي يوفر الوصول إلى الجلسة في شكل مثيل لواجهة [ISession
  • السطر 12: هذا هو النشاط المستخدم لتبديل العروض. المعلمة الثانية هي الإجراء الذي يؤدي إلى تغيير العرض، وهي إحدى القيم التالية: SUBMIT أو NAVIGATION أو RESTORE؛
  • الأسطر 15–17: هذا هو النشاط الذي يدير شاشة التحميل؛
  • السطر 22: لتصحيح أخطاء التطبيق؛
  • السطر 25: لتجنب الانتظار لفترة طويلة إذا توقف الخادم عن الاستجابة؛
  • السطر 28: أثناء التصحيح، اضبط هذا على بضع ثوانٍ لإتاحة الوقت لإلغاء العملية مع الخادم ومشاهدة ما يحدث؛
  • السطر 31: اضبطه على true إذا كانت خدمة JSON تتطلب مصادقة أساسية؛
  • السطر 34: تجاور الأجزاء؛
  • السطر 37: اضبطه على "صحيح" إذا كان التطبيق يحتوي على علامات تبويب؛
  • السطر 39: اضبطه على "true" إذا كان التطبيق يتواصل مع خادم ويب/JSON وتريد عرض صورة التحميل أثناء التبادل؛
  • السطر 43: عدد الأجزاء التي يديرها التطبيق؛

واجهة [IMainActivity] هي العنصر الثاني في البنية الذي يجب على المطور تنفيذه (السطر 45).

2.5.5. واجهة [IDao]

توسع واجهة [IMainActivity] واجهة [IDao] التالية:

  

package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // todo: declare your interface here
}
  • السطر 24: سيقوم المطور بإكمال الواجهة هنا؛

2.5.6. الجلسة

  

تغلف فئة [Session] العناصر المشتركة بين النشاط والأجزاء. وهي تنفذ واجهة [ISession] التالية:


package client.android.architecture.core;
 
import client.android.architecture.custom.CoreState;
 
public interface ISession {
 
  // number of last view displayed
  int getPreviousView();
 
  void setPreviousView(int numView);
 
  // last state of a view
  CoreState getCoreState(int numView);
 
  void setCoreState(int numView, CoreState coreState);
 
  // action in progress
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }
 
  Action getAction();
 
  void setAction(Action action);
 
  // status of all views -
  // not used by code but required for serialization / deserialization jSON
  CoreState[] getCoreStates();
 
  void setCoreStates(CoreState[] coreStates);
 
  // last selected tab number
  int getPreviousTab();
 
  void setPreviousTab(int position);
 
  // tab selection navigation
  boolean isNavigationOnTabSelectionNeeded();
 
  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

نقدم واجهة [ISession] لفرض وجود طرق معينة في الجلسة:

  • الأسطر 7–10: رقم آخر عرض (جزء) تم عرضه؛
  • الأسطر 12–15: حالة عرض معين؛
  • الأسطر 17–24: نقدم مفهوم الإجراء الجاري. هناك أربعة (السطر 17):
    • RESTORE: عملية حفظ/استعادة جارية. لا يوجد تغيير في العرض؛
    • NAVIGATION: عملية التنقل جارية. هنا، نُعرّف التنقل على أنه تغيير في العرض حيث يمكن استعادة العرض الجديد من آخر حالة تم حفظها خلال الجلسة؛
    • SUBMIT: نخصص النوع [SUBMIT] لعملية معلقة عندما يكون هناك تغيير في العرض ويعتمد العرض الجديد على الحالة العامة للنشاط، وليس فقط على حالته الخاصة. في بعض الأحيان، يصعب التمييز بين NAVIGATION و SUBMIT. في مثل هذه الحالات، سنستخدم الحالة الأكثر عمومية وهي SUBMIT؛
    • NONE: قيمة الإجراء عندما لم يتلقَ قيمته الأولى بعد؛
  • الأسطر 26-30: سيتم تخزين حالات النشاط والأجزاء في مصفوفة CoreState[]. لضمان معالجة ذلك بشكل صحيح أثناء تسلسل/إلغاء تسلسل JSON، يجب أن يكون لها محدد قيمة ومحدد تعيين؛
  • الأسطر 32-35: رقم علامة التبويب المحددة الأخيرة. تُستخدم خلال دورة الحفظ/الاستعادة لإعادة تحديد علامة التبويب التي تم تحديدها قبل تدوير الجهاز؛
  • الأسطر 37–40: تدير قيمة منطقية تشير إلى ما إذا كان اختيار علامة تبويب يجب أن يكون مصحوبًا بتغيير في الجزء؛

يتم تنفيذ واجهة [ISession] بواسطة الفئة المجردة التالية [AbstractSession]:


package client.android.architecture.core;
 
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;
 
public class AbstractSession implements ISession {
  // previous view no
  private int preViousView;
 
  // view status
  private CoreState[] coreStates = new CoreState[0];
 
  // action in progress
  private Action action = Action.NONE;
 
  // previously selected tab
  private int previousTab;
 
  // tab selection navigation
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;
 
  // manufacturer
  public AbstractSession() {
    // initialize the fragment status table
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }
 
 
  // interface ISession ---------------------------------------------------------
  @Override
  public int getPreviousView() {
    return preViousView;
  }
 
  @Override
  public void setPreviousView(int numView) {
    this.preViousView = numView;
  }
 
  @Override
  public CoreState getCoreState(int numView) {
    return coreStates[numView];
  }
 
  @Override
  public void setCoreState(int numView, CoreState coreState) {
    coreStates[numView] = coreState;
  }
 
  @Override
  public Action getAction() {
    return action;
  }
 
  @Override
  public void setAction(Action action) {
    this.action = action;
  }
 
  @Override
  public CoreState[] getCoreStates() {
    return coreStates;
  }
 
  @Override
  public void setCoreStates(CoreState[] coreStates) {
    this.coreStates = coreStates;
  }
 
  @Override
  public int getPreviousTab() {
    return previousTab;
  }
 
  @Override
  public void setPreviousTab(int position) {
    this.previousTab = position;
  }
 
  @Override
  public boolean isNavigationOnTabSelectionNeeded() {
    return navigationOnTabSelectionNeeded;
  }
 
  @Override
  public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
    this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
  }
}
  • السطر 9: معرف العرض الذي تم عرضه قبل العرض المعروض حاليًا. هذه المعلومات مفيدة عندما يمكن الوصول إلى عرض من مواقع متعددة. وهذا هو الحال عادةً في التنقل القائم على علامات التبويب. يمكن للعرض المعروض بعد ذلك تحديد العرض الذي تم عرضه سابقًا؛
  • السطر 12: مصفوفة الحالات لجميع الأجزاء المعروضة بواسطة النشاط؛
  • السطر 18: معرف علامة التبويب المحددة سابقًا. يلعب دورًا مشابهًا لدور معرف العرض السابق في السطر 9. هذه المعلومات مفيدة عند تدوير الجهاز وضرورة العودة إلى علامة التبويب التي تم تحديدها قبل التدوير؛
  • السطر 22: قيمة منطقية تشير إلى ما إذا كان اختيار علامة تبويب سيؤدي إلى تغيير في الجزء المعروض. لاحظ أن مشروع [client-android-skel] يدير علامات التبويب والأجزاء بشكل منفصل بحيث يمكن استخدامه في الحالات التي يكون فيها عدد علامات التبويب أقل من عدد الأجزاء. هناك نوعان من الاختيار:
    • الاختيار الذي يقوم به المستخدم عند النقر على علامة تبويب. في هذه الحالة، يجب تغيير الجزء المعروض بشكل عام؛
    • اختيار يتم التحكم فيه بواسطة البرنامج عبر طريقة [Tablayout.Tab.select()]. في هذه الحالة، لا يكون تغيير الجزء المعروض مرغوبًا دائمًا. فيما يلي مثالان:
      • عند تدوير الجهاز، يتم إعادة إنشاء النشاط، وكذلك علامات التبويب. ومع ذلك، عند إنشاء علامة التبويب الأولى، تخضع تلقائيًا لعملية [select] برمجية. لذلك، ليس من المرغوب فيه تغيير الجزء المعروض لأننا في مرحلة إعادة إنشاء النشاط حيث لن يكون الجزء المعروض في النهاية هو بالضرورة الجزء المرتبط بعلامة التبويب الأولى؛
      • نظرًا لأن إدارة علامات التبويب منفصلة عن إدارة الأجزاء، فقد ترغب في تحديث علامات التبويب (حذف، إضافة) دون التدخل في الأجزاء المرتبطة بها. ومع ذلك، يمكن أن تؤدي بعض هذه العمليات مرة أخرى إلى تشغيل عملية برمجية [select] ضمنية على إحدى علامات التبويب. لا يؤدي هذا الاختيار بالضرورة إلى الانتقال إلى الجزء المرتبط؛
  • السطر 21: لا يُقصد بحقل [navigationOnTabSelectionNeeded] أن يتم حفظه أثناء عمليات الحفظ للنشاط وأجزائه. تؤدي التعليقة التوضيحية [@JsonIgnore] إلى تجاهل الحقل أثناء تسلسل/إلغاء تسلسل JSON؛
  • الأسطر 25-31: يقوم المنشئ بتهيئة مصفوفة الحالات لأجزاء [FRAGMENTS_COUNT] من التطبيق. يتم تهيئة عناصر هذه المصفوفة باستخدام الحقل [hasBeenVisited=false]. تُستخدم هذه المعلومات لتحديد ما إذا كانت هذه هي الزيارة الأولى للأجزاء أم لا؛

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


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // data to be shared between fragments themselves and between fragments and activities
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
  // don't forget the getters and setters required for serialization / deserialization jSON
}
  • السطر 5: تمتد فئة [Session] من فئة [AbstractSession] التي رأيناها للتو. سيضع المطور هنا العناصر المراد مشاركتها بين الأجزاء نفسها وبين الأجزاء والنشاط. لاحظ أن فئة [Session] لم تعد مزودة بعلامة [@EBean]. فقد أصبحت فئة عادية؛

2.5.7. الفئة المجردة [AbstractActivity]

  

2.5.7.1. الهيكل

فئة [AbstractActivity] هي فئة تتكون من أكثر من 300 سطر. سنقوم بفحصها خطوة بخطوة. وهيكلها الأساسي هو كما يلي:


package client.android.architecture;
 
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import java.io.IOException;
 
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // layer [DAO]
  private IDao dao;
  // the session
  protected Session session;
 
  // the fragment container
  protected MyPager mViewPager;
  // the toolbar
  private Toolbar toolbar;
  // the waiting image
  private ProgressBar loadingPanel;
  // tab bar
  protected TabLayout tabLayout;
 
  // the fragment or section manager
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // class name
  protected String className;
  // mapper jSON
  private ObjectMapper jsonMapper;
 
  // manufacturer
  public AbstractActivity() {
    // class name
    className = getClass().getSimpleName();
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }
 
  // implémentation IMainActivity --------------------------------------------------------------------
  ...
 
  // life cycle - backup / restore ------------------------------------
  ...
 
  // hold image management ---------------------------------
  ...
 
  // interface IDao -----------------------------------------------------
  ...
 
  // the fragment manager --------------------------------
  ...
 
  // girls' classes
  protected abstract void onCreateActivity();
 
  protected abstract IDao getDao();
 
  protected abstract AbstractFragment[] getFragments();
 
  protected abstract CharSequence getFragmentTitle(int position);
 
  protected abstract void navigateOnTabSelected(int position);
 
  protected abstract int getFirstView();
 
}

فئة [AbstractActivity]:

  • تنفذ واجهة [IMainActivity] (السطران 21 و55)؛
  • تتولى حفظ واستعادة النشاط وأجزائه عند تدوير الجهاز (السطر 58)؛
  • تتولى شاشة التحميل أثناء الاتصال بخادم الويب / JSON (السطر 61)؛
  • تنفذ واجهة IDao لطبقة [DAO] (السطر 64)؛
  • تنفذ مدير الأجزاء (السطر 67)؛
  • تتطلب من فئاتها الفرعية أن تحتوي على ست طرق (الأسطر 71–81)؛

2.5.7.2. تنفيذ واجهة [IMainActivity]

يتم تنفيذ واجهة [IMainActivity] (انظر القسم 2.5.4) على النحو التالي:


  // implémentation IMainActivity --------------------------------------------------------------------
  @Override
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position, ISession.Action action) {
    if (IS_DEBUG_ENABLED) {
      Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
    }
    // display new fragment
    mViewPager.setCurrentItem(position);
    // we note the action in progress when the view changes
    session.setAction(action);
}

2.5.7.3. حفظ حالة النشاط وأجزائه

يتم احتواء حالة النشاط وأجزائه بالكامل داخل الجلسة. لذلك، نحتاج إلى حفظ الجلسة. هنا، نعيد استخدام ما تم في مشروع [المثال-22] (انظر القسم 1.23):


  // backup / restore management ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // save session as jSON string
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // log
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}

2.5.7.4. استعادة حالة النشاط وأجزائه

يتضمن ذلك استعادة الجلسة. نمضي قدماً كما هو موضح في [المثال-22]:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // something to restore?
    if (savedInstanceState != null) {
      // session recovery
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // log
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    } else {
      // session
      session = new Session();
    }
...
  • الأسطر 10–26: إذا كانت المعلمة [Bundle savedInstanceState] في السطر 2 غير فارغة، يتم استعادة الجلسة (الأسطر 12–17)؛
  • الأسطر 26–29: إذا كانت المعلمة [Bundle savedInstanceState] في السطر 2 فارغة، فهذا يعني أن النشاط يتم تشغيله للمرة الأولى. ثم يتم إنشاء جلسة فارغة؛

2.5.7.5. تهيئة طبقة [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // layer [DAO]
    dao = getDao();
    if (dao != null) {
      // layer configuration [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // girls' classes
  protected abstract IDao getDao();
....
}
  • السطر 11: يُطلب من النشاط الفرعي (السطر 21) توفير مرجع إلى طبقة [DAO
  • الأسطر 14-17: إذا كانت طبقة [DAO] موجودة، يتم تكوينها باستخدام المعلومات الموجودة في واجهة [IMainActivity

2.5.7.6. تهيئة العرض المرتبط بالنشاط

تم عرض العرض المرتبط بالنشاط في القسم 2.5.1:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">
 
  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">
 
    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>
 
  <!-- fragment container -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>

يتم تهيئة هذا العرض باستخدام الكود التالي:


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // associated view
    setContentView(R.layout.activity_main);
    // view components ---------------------
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // waiting image?
    if (IS_WAITING_ICON_NEEDED) {
      // we add the waiting image
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // creation ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // added ProgressBar to toolbar
      toolbar.addView(loadingPanel);
    }
...
  • السطر 11: عرض XML [activity_main] مرتبط بالنشاط؛
  • السطران 14-15: شريط الأدوات مدمج ومدعوم؛
  • الأسطر 17-27: إضافة اختيارية لرمز التحميل: إذا كانت القيمة المنطقية [IS_WAITING_ICON_NEEDED] صحيحة في واجهة [IMainActivity
  • السطر 23: إنشاء صورة التحميل من نوع [ProgressBar] المشار إليها بواسطة الحقل [loadingPanel
  • السطر 24: في البداية، تكون هذه الصورة مخفية؛
  • السطر 26: تتم إضافتها إلى شريط الأدوات؛

2.5.7.7. إدارة علامات التبويب

قد تطلب واجهة [IMainActivity] شريط علامات تبويب. يتم إضافة هذا الشريط وإدارته على النحو التالي:


// tab bar
  protected TabLayout tabLayout;
...
 
    // tab bar?
    if (ARE_TABS_NEEDED) {
      // add the tab bar
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // no selection navigation until a fragment is displayed
      session.setNavigationOnTabSelectionNeeded(false);
      // tab bar creation
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // tab bar added to application bar
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // tab bar event manager
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // a tab has been selected
          if (IS_DEBUG_ENABLED) {
            Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
              tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
          }
          if (session.isNavigationOnTabSelectionNeeded()) {
            // miter position
            int position = tab.getPosition();
            // memory
            session.setPreviousTab(position);
            // associated fragment display?
            navigateOnTabSelected(position);
          }
        }
 
        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
 
        }
 
        @Override
        public void onTabReselected(TabLayout.Tab tab) {
 
        }
      });
    }
 
...
  // girls' classes
  protected abstract void navigateOnTabSelected(int position);
...
  • الأسطر 12–48: إضافة شريط علامات التبويب وإدارته؛
  • السطر 6: تتم إضافة شريط علامات التبويب إذا تم تعيين الثابت [ARE_TABS_NEEDED] على "صحيح" في واجهة [IMainActivity
  • السطر 12: عند إنشاء شريط علامات التبويب، قد تحدث عمليات [Tablayout.Tab.select] ضمنية (لا يتم تشغيلها بواسطة المستخدم). نقوم بتعيين القيمة المنطقية [session.navigationOnTabSelectionNeeded] على false لمنع أي تنقل أثناء عمليات الاختيار الخاطئة هذه. سيكون الأمر متروكًا للمطور لاختيار الجزء المراد عرضه باستخدام طريقة [navigateToView]. سيتم إعادة تعيين القيمة المنطقية [session.navigationOnTabSelectionNeeded] إلى true عند عرض هذا الجزء (انظر فئة AbstractFragment
  • السطر 14: إنشاء شريط علامات تبويب يُشار إليه بواسطة الحقل [tabLayout]. نستخدم شريط علامات تبويب مخصص [CustomTabLayout]، والذي سنناقشه لاحقًا؛
  • السطر 15: نضبط ألوان عناوين علامات التبويب. توجد هذه الألوان في الملف [res/color/tab_txt.xml] التالي:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_selected="true" android:color="#FFFF00" />
  <item android:state_selected="false" android:color="#FFFFFF" />
</selector>
    • السطر (ج): لون عنوان علامة التبويب عند تحديدها؛
    • السطر (د): لون عنوان علامة التبويب عندما لا تكون محددة؛

هذا الملف قابل للتعديل بالطبع. يمكنك العثور على رموز الألوان السداسية العشرية هنا، على سبيل المثال.

  • السطران 17-18: إضافة شريط علامات التبويب هذا إلى شريط التطبيق في عرض XML [activity_main
  • الأسطر 20-47: معالج الأحداث لشريط علامات التبويب؛
  • الأسطر 22-36: يتم معالجة حدث [onTabSelected] فقط. وهو يتوافق مع النقر على [علامة التبويب Tab] التي يتم تمريرها كمعلمة إلى الأسلوب أو إلى عملية برمجية [TabLayout.Tab.select
  • السطر 30: موضع علامة التبويب المحددة؛
  • السطر 32: يتم تخزين هذا الموضع في الجلسة؛
  • السطر 34: يجب الآن عرض الجزء المرتبط بهذه العلامة. لا يمكن إجراء هذا الربط إلا من خلال الفئة الفرعية (السطر 52). لاحظ أننا لا نربط شريط علامات التبويب بحاوية الأجزاء [mViewPager] كما تم في بعض الأمثلة التي درسناها. هنا، نفصل إدارة شريط علامات التبويب تمامًا عن إدارة الأجزاء. ولهذا السبب، عند النقر على علامة تبويب، يجب أن نحدد العرض الذي نريد عرضه؛
  • السطر 28: نميز بين اختيار علامة التبويب مع أو بدون التنقل. بشكل عام، عندما ينقر المستخدم على علامة تبويب، يُتوقع التنقل، بينما لا يُتوقع ذلك أثناء الاختيار البرمجي. يميز المطور بين هاتين الحالتين باستخدام عنصر [session.navigationOnTabSelectionNeeded]. عندما لا يتم إجراء التنقل، لا يتم حفظ رقم آخر علامة تبويب تم اختيارها في الجلسة. الأمر متروك للمطور للقيام بذلك؛

2.5.7.8. مدير علامات التبويب [CustomTabLayout]

  

نستخدم مدير علامات تبويب مخصص لعرض عناوين علامات التبويب بخطوط مختلفة. فيما يلي فئة [CustomTabLayout]:


package client.android.architecture.custom;
 
import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
 
public class CustomTabLayout extends TabLayout {
  private Typeface mTypeface;
 
  public CustomTabLayout(Context context) {
    super(context);
    init();
  }
 
  public CustomTabLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
  }
 
  public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }
 
  private void init() {
    mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
  }
 
  @Override
  public void addTab(Tab tab) {
    super.addTab(tab);
 
    ViewGroup mainView = (ViewGroup) getChildAt(0);
    ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());
 
    int tabChildCount = tabView.getChildCount();
    for (int i = 0; i < tabChildCount; i++) {
      View tabViewChild = tabView.getChildAt(i);
      if (tabViewChild instanceof TextView) {
        ((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
      }
    }
  }
 
}
  • يتم تخصيص خط عناوين علامات التبويب في السطرين 30 و 44؛

مجلد [fonts] كما يلي:

  

المصادر:

2.5.7.9. أحدث عمليات التهيئة


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // fragment manager instantiation
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
    // fragment adjacency
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // display 1st view
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // we hand over to the daughter activity
    onCreateActivity();
  }
...
  // girls' classes
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • الأسطر 10–19: هذا الكود موجود عادةً في الأمثلة التي درسناها؛
  • الأسطر 21–23: عرض أول عرض. هناك بلا شك عدة طرق لتمييز هذه الحالة. هنا، استخدمنا حقيقة أن قيمة الإجراء الذي يؤدي إلى تغيير العرض هي NONE بالنسبة لأول عرض؛
  • السطر 22: لا نضع أي افتراضات بشأن الجزء الأول الذي سيتم عرضه. في أمثلةنا، غالبًا ما كان هذا هو الجزء رقم 0، ولكن ليس دائمًا (انظر المثال 22). لذلك سنطلب من النشاط الفرعي (السطر 30) أن يخبرنا ما هي هذه الواجهة؛
  • السطر 25: قمنا بتحليل كل ما يمكننا تحليله هنا. الآن، لدى الفئة الفرعية عمليات التهيئة الخاصة بها التي يجب تنفيذها (السطر 29)؛

2.5.7.10. معالجة صورة التحميل

في فئة [AbstractActivity]، تتم إدارة الصورة المؤقتة بواسطة الطريقتين التاليتين:


  // hold image management ---------------------------------
  public void cancelWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.INVISIBLE);
    }
  }
 
  public void beginWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.VISIBLE);
    }
}

2.5.7.11. تنفيذ واجهة [IDao]

في فئة [AbstractActivity]، يتم تنفيذ واجهة [IDao] (انظر القسم 2.5.5) على النحو التالي:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // layer [DAO]
  private IDao dao;
...
  // interface IDao -----------------------------------------------------
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    dao.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    dao.setBasicAuthentification(isBasicAuthentificationNeeded);
  }
 
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    dao.setDebugMode(isDebugEnabled);
  }
 
  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
}
  • السطر 3: تذكر أن قيمة هذا الحقل تم توفيرها بواسطة النشاط الفرعي في طريقة [onCreate

2.5.7.12. تنفيذ مدير الأجزاء

في فئة [AbstractActivity]، يتم تنفيذ مدير الأجزاء على النحو التالي:


...
  // the fragment manager --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    private AbstractFragment[] fragments;
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // daughter class fragments
      fragments = getFragments();
    }
 
    // must render fragment no. position
    @Override
    public AbstractFragment getItem(int position) {
      // we return the fragment
      return fragments[position];
    }
 
    // makes the number of fragments to manage
    @Override
    public int getCount() {
      return fragments.length;
    }
 
    // makes the title of fragment no. position
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }
 
  // girls' classes
  protected abstract AbstractFragment[] getFragments();
 
  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • السطر 5: مصفوفة الأجزاء المرتبطة بالنشاط. ستُشتق جميع الأجزاء من فئة [AbstractFragment
  • الأسطر 8-12: هذا هو المنشئ الذي يقوم بتهيئة مصفوفة الأجزاء. ويطلبها من الفئة الفرعية للنشاط (السطر 35)؛
  • الأسطر 28-31: يمكن استخدام عناوين الأجزاء في تطبيق يحتوي على عدد من علامات التبويب يساوي عدد الأجزاء. في هذه الحالة، يمكن إعطاء علامة التبويب عنوان الجزء. هنا، يتم طلب هذه العناوين من الفئة الفرعية (السطر 37)؛

2.5.7.13. طريقة [onResume]

يتم تنفيذ طريقة [onResume] قبل وقت قصير من ظهور العرض المرتبط بالنشاط. يتم استخدامها هنا لاختيار علامة تبويب بعد عملية حفظ/استعادة:


  @Override
  public void onResume() {
    // parent
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // if restore, then restore last selected tab
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • السطر 10: اختيار علامة التبويب التي تم تحديدها قبل عملية الحفظ/الاستعادة. من المهم ملاحظة هنا أنه في طريقة [onCreate] — التي يتم تنفيذها في دورة حياة النشاط قبل طريقة [onResume] — تم تعطيل التنقل عند تحديد علامة تبويب. لذلك، هنا، يتم تحديد علامة تبويب ولكن لا يحدث تغيير في الجزء؛

2.5.7.14. ملخص

ستكون الفئة المجردة [AbstractActivity] هي الفئة الأم للنشاط الوحيد للتطبيق.

يجب أن تنفذ النشاط الفرعي الطرق الست التالية:


  // girls' classes
  protected abstract void onCreateActivity();
 
  protected abstract IDao getDao();
 
  protected abstract AbstractFragment[] getFragments();
 
  protected abstract CharSequence getFragmentTitle(int position);
 
  protected abstract void navigateOnTabSelected(int position);
 
protected abstract int getFirstView();

كما أن النشاط الفرعي لديه حق الوصول إلى العناصر المحمية التالية في فئته الأصلية:


  // the session
  protected ISession session;
  // the fragment container
  protected MyPager mViewPager;
  // tab bar
  protected CustomTabLayout tabLayout;
  // class name
protected String className;

2.5.8. النشاط [MainActivity]

  

يمكن أن يكون لفئة [MainActivity] اسم مختلف. الشرط الوحيد هو أن تنفذ واجهة [IMainActivity]. الفئة الافتراضية المتوفرة هي كما يلي:


package client.android.activity;
 
import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
 
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // todo: we continue the initializations started by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // todo: define fragments here
    return new AbstractFragment[0];
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo: define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // todo: tabbed browsing - define the view to be displayed
  }
 
  @Override
  protected int getFirstView() {
    // todo: tabbed browsing - define the first view to be displayed
    return 0;
  }
}
  • السطر 14: لكي تكون علامة AA [@Bean] في السطر 19 صالحة، يجب أن تحتوي النشاط على علامة AA [@EActivity
  • السطر 15: النشاط مرتبط بقائمة XML [menu_main]. حاليًا، هذه القائمة فارغة. سيحتاج المطور إلى ملئها إذا لزم الأمر؛
  • السطر 16: تمتد الفئة إلى فئة [AbstractActivity
  • السطران 19–20: إشارة إلى طبقة [DAO]. سيتم إنشاء مثيل لها بواسطة مكتبة AA قبل تهيئة هذا الحقل. وهذا يعني أن حبة AA [Dao] يجب أن تكون موجودة. وهذا هو الحال دائمًا مع التطبيق الهيكلي الذي نقدمه. حتى في تطبيق بدون طبقة [DAO]، يمكنك ترك حزمة [dao] في مكانها. وهذا لا يسبب أي تعقيدات؛
  • السطر 22: الجلسة كمثيل من النوع [Session]. توجد الجلسة في الفئة الأصلية [AbstractActivity] ولكن كمثيل للواجهة [ISession] (السطر 32)؛
  • الأسطر 24–63: الطرق الست التي تتطلبها الفئة الأصلية [AbstractActivity
  • الأسطر 36–39: تُرجع الطريقة [getDao] مرجعًا إلى طبقة [DAO]. هنا، لا يكون هذا المرجع أبدًا null. ومع ذلك، في الفئة الأصلية [AbstractActivity]، قمنا بتوفير حالة حيث تُرجع الفئة الفرعية مرجعًا null للإشارة إلى عدم وجود طبقة [DAO]. إذا كنت ترغب في استخدام هذا الخيار (وهو غير مفيد جدًا في رأيي)، فهذا هو المكان الذي يجب أن تضبط فيه المؤشر على null؛

2.6. طبقة [DAO]

Image

  

2.6.1. واجهة IDao

تم تقديمها في القسم 2.5.5:


package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // todo: declare your interface here
}

سيقوم المطور بإضافة الطرق الخاصة بطبقة [DAO] بدءًا من السطر 24.

2.6.2. واجهة [WebClient]

  

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


package client.android.dao.service;
 
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
 
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
 
  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);
 
  // todo : declare here the URL to be reached
}

سيقوم المطور بإضافة الطرق التي تتواصل مع عناوين URL التي يعرضها خادم JSON بدءًا من السطر 17.

2.6.3. معترض المصادقة [MyAuthInterceptor]

  

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


package client.android.dao.service;
 
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
 
import java.io.IOException;
 
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
 
  // user
  private String user;
  // password
  private String mdp;
 
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // headers HTTP of the HTTP request intercepted
    HttpHeaders headers = request.getHeaders();
    // the HTTP basic authentication header
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // added to HTTP headers
    headers.setAuthorization(auth);
    // continue the query life cycle HTTP
    return execution.execute(request, body);
  }
 
  // authentication elements
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

تقوم هذه الفئة بإنشاء رأس مصادقة HTTP التالي:

Authorization: Basic code

حيث [code] هي السلسلة المشفرة بـ Base64 'user:mp'. لا تُستخدم هذه الفئة إلا إذا كان خادم JSON يتوقع هذا النوع من المصادقة. وهناك أنواع أخرى.

ملاحظة: يرد توضيح لاستخدام هذه الفئة في القسم 3.6.3.1.

2.6.4. فئة [AbstractDao]

  

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


package client.android.dao.service;
 
import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
 
public abstract class AbstractDao {
 
  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
  // debug mode
  protected boolean isDebugEnabled;
  // class name
  protected String className;
  // timeout before request execution
  private int delay;
 
  // manufacturer
  public AbstractDao() {
    // class name
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }
 
  // méthodes protégées ----------------------------------------------------------
  // generic interface
  protected interface IRequest<T> {
    T getResponse();
  }
 
  // generic request to a web service / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // service execution - a single response is expected
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // service execution
        try {
          // waiting?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // execute the synchronous request
          T response = request.getResponse();
          // log
          if (isDebugEnabled) {
            String log;
            if (response instanceof String) {
              log = (String) response;
            } else {
              log = mapper.writeValueAsString(response);
            }
            Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
          }
          // the response is sent to the observer
          subscriber.onNext(response);
          // we signal the end of the observable
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // log
          if (isDebugEnabled) {
            try {
              Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
            } catch (JsonProcessingException e1) {
              Log.d(className, String.format("Erreur jSON imprévue"));
            }
          }
          // an exception is issued
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }
 
  // debug mode
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }
 
  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • الأسطر 35–81: تستخدم طريقة [getResponse] مكتبة RxAndroid لإرجاع نوع [Observable<T>]. على عكس بعض الأمثلة التي رأيناها سابقًا، فإنها لا ترجع نوع [Response<T>]—وهو نوع خاص—بل أي نوع T؛
  • السطر 35: تأخذ طريقة [getResponse] كمعلمة مثيلًا من النوع [IRequest<T>] من الأسطر 30–32، حيث تحصل طريقة [IRequest.getResponse()] على النوع T عبر عملية HTTP متزامنة؛
  • الأسطر 48-50: بشكل مصطنع، ننتظر [delay] مللي ثانية. في الإنتاج، سنقوم بتعيين [delay=0]. أثناء التصحيح، سنقوم بتعيين [delay=بضع ثوانٍ] لمنح المستخدم فرصة لإلغاء العملية غير المتزامنة وبالتالي رؤية كيف يتصرف الكود في تلك الحالة؛
  • السطر 52: يتم طلب الاستجابة المتوقعة بطلب متزامن؛
  • السطر 64: بمجرد استلام الاستجابة، يتم تمريرها إلى المراقب؛
  • السطر 66: نشير إلى أنه لن يكون هناك المزيد من الانبعاثات. هذه هي الحالة المحددة لعمل غير متزامن يعرض عنصرًا واحدًا فقط؛
  • الأسطر 67-78: في حالة حدوث استثناء، يتم تمرير الاستثناء إلى المراقب (السطر 77)؛

2.6.5. فئة [Dao]

  

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


package client.android.dao.service;
 
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we build the restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // set the URL of the web service
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    // the user is registered in the interceptor
    authInterceptor.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // factory configuration
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // todo: implementation IDao
}
  • السطران 21-22: حقن حبة AA [WebClient]، التي ستتولى الاتصال بخادم الويب / JSON؛
  • السطور 24-25: حقن مانع المصادقة؛
  • الأسطر 31-42: الطريقة التي يتم تنفيذها بعد حقن الحقول من الأسطر 21-25؛
  • السطر 37: يتم إنشاء كائن [RestTemplate]، الذي يتولى الاتصال بين العميل والخادم، من مصنع. هذا ليس ضروريًا تمامًا، لكن المصنع يسمح لنا بتكوين مهلات الاتصال. لهذا السبب لا نستخدم المنشئ بدون معلمات [RestTemplate()]؛
  • السطر 39: نضيف محول JSON إلى محولات [RestTemplate]. سيكون هذا هو المحول الوحيد. وبالتالي، عندما تتلقى إحدى طرق [WebClient] سلسلة JSON من الخادم، سيتم فك تسلسلها تلقائيًا إلى الكائن الذي من المفترض أن تعيده الطريقة؛
  • السطر 41: يتم تمرير كائن [RestTemplate] الذي تم تكوينه بهذه الطريقة إلى عميل الويب، والذي سيتولى معالجة الاتصال بين العميل والخادم باستخدامه؛
  • الأسطر 44–48: نحدد عنوان URL الجذر لخادم الويب/JSON. جميع عناوين URL المعلنة في فئة [WebClient] نسبية بالنسبة إلى عنوان URL الجذر هذا؛
  • الأسطر 50-54: تتيح لك هذه الطريقة تحديد مالك الاتصال عندما يتم التحكم في الاتصال بواسطة المصادقة الأساسية (انظر القسم 2.6.3
  • الأسطر 56-64: قم بتعيين مهلة انتظار التبادلات بين العميل والخادم. يتم ذلك عبر مصنع كائن [RestTemplate]، الذي يتحكم في التبادلات؛
  • الأسطر 66-78: تحدد هذه الطريقة أن الخادم محمي بالمصادقة الأساسية؛
  • الأسطر 72-77: إذا كانت المصادقة الأساسية مطلوبة، يتم إضافة مانع المصادقة الذي تم إدراجه في السطر 25 إلى مانعات كائن [RestTemplate]. سيقوم هذا المانع تلقائيًا بإضافة رأس HTTP للمصادقة الأساسية الذي يتوقعه الخادم إلى جميع طلبات عميل الويب؛
  • سيقوم المطور بتنفيذ واجهة [IDao] بدءًا من السطر 87؛

2.7. الأجزاء

  

2.7.1. فئة [MenuItemState]

تغلف فئة [MenuItemState] حالة خيار القائمة:


package client.android.architecture;
 
public class MenuItemState {
 
  // menu option identify
  private int menuItemId;
  // option visibility
  private boolean isVisible;
 
  // manufacturers
  public MenuItemState() {
 
  }
 
  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }
 
  // getters and setters
...
}

2.7.2. فئة [Utils]

تحتوي فئة [Utils] على طرق مساعدة ثابتة:


package client.android.architecture;
 
import java.util.ArrayList;
import java.util.List;
 
public class Utils {
 
  // list of exception messages - version 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // create a list of error msgs from the exception stack
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }
 
  // exception message list - version 2
  static public String getMessageForAlert(Throwable th) {
    // build the text to be displayed
    StringBuilder texte = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // result
    return texte.toString();
  }
 
  // list of exception messages - version 3
  static public String getMessageForAlert(List<String> messages) {
    // build the text to be displayed
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // result
    return texte.toString();
  }
}

2.7.3. الفئة الأم [AbstractFragment]

تحتوي الفئة [AbstractFragment] على العناصر المشتركة بين جميع الأجزاء في التطبيق. وكما هو الحال في الفئة [AbstractActivity]، فإن كودها معقد. سنقوم أيضًا بتحليلها خطوة بخطوة.

2.7.3.1. الهيكل


package client.android.architecture.core;
 
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
 
import java.util.ArrayList;
import java.util.List;
 
public abstract class AbstractFragment extends Fragment {
 
  // données privées ------------------------------------------------------------
  // subscriptions to observables
  private List<Subscription> abonnements = new ArrayList<>();
  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment status
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // asynchronous tasks
  private boolean runningTasksHaveBeenCanceled;
 
  // data accessible to daughter classes ---------------------------------------
  // debug mode
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // class name
  protected String className;
  // asynchronous tasks
  protected int numberOfRunningTasks;
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
  // session
  protected Session session;
 
 
  // update Fragment ----------------------------------------------------------------------------------
 ...
 
  // menu management ------------------------------------------
  ...
 
  // wait management -------------------------------------------------------------
...
 
  // asynchronous operation management --------------------------------------------------------------------
...
 
  // gestion exception -------------------------------------------------------------------
....
 
  // fragment lifecycle management --------------------------------------------------------
...
 
  // classes filles -----------------------------------------------------
  public abstract CoreState saveFragment();
 
  protected abstract int getNumView();
 
  protected abstract void initFragment(CoreState previousState);
 
  protected abstract void initView(CoreState previousState);
 
  protected abstract void updateOnSubmit(CoreState previousState);
 
  protected abstract void updateOnRestore(CoreState previousState);
 
  protected abstract void notifyEndOfUpdates();
 
  protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
 
}
  • الأسطر 28–45: البيانات الخاصة بالفئة؛
  • الأسطر 47–58: البيانات المحمية التي يمكن للفئات الفرعية الوصول إليها؛
  • السطور 61–62: كود يقوم بتحديث الجزء المراد عرضه؛
  • السطور 64-65: كود المساعدة للتعامل مع القائمة، إن وجدت؛
  • السطور 67-68: كود مساعد للتعامل مع الانتظار أثناء عملية غير متزامنة؛
  • السطور 70–71: كود لتسهيل الاتصال بين الجزء وطبقة [DAO
  • السطور 73-74: كود مساعد للتعامل مع أي استثناءات بطريقة قياسية؛
  • السطران 76-77: كود يدير دورة حياة الجزء؛
  • الأسطر 80-94: تفرض الفئة الأم 8 طرق على فئاتها الفرعية؛

2.7.3.2. منشئ

منشئ الفئة هو كما يلي:


  // class name
  protected String className;
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
...
  // constructeur ----------------------
  public AbstractFragment() {
    // init
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // log
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • السطر 9: يتم تسجيل اسم الفئة الفرعية التي يتم إنشاء مثيل لها هنا. ويُستخدم هذا الاسم في جميع سجلات الفئة الأصلية؛
  • السطر 10: نلاحظ أن الجزء قيد الإنشاء. ستُستخدم هذه المعلومات عندما يُطلب من الجزء الفرعي تحديث نفسه؛

2.7.3.3. إدارة القوائم

في بنية تطبيقنا، يجب أن يكون لكل جزء قائمة، حتى لو كانت فارغة. وقد أظهرت السجلات بالفعل أنه عند تنفيذ طريقة [onCreateOptionsMenu] — التي تعمل عندما يكون للجزء قائمة — يكون الجزء قد تم ربطه بالفعل بنشاطه وعرضه وقائمته ويكون على وشك أن يصبح مرئيًا. ولذلك، فإن هذه هي اللحظة التي يمكن فيها تحديث الواجهة المرئية والقائمة. وفي إطار طريقة [onCreateOptionsMenu] هذه، نوجه الجزء الفرعي لتحديث نفسه.

تتضمن إدارة القوائم طرقًا مساعدة تسمح للجزء الفرعي بإظهار عناصر القائمة أو إخفائها:


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // menu management ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // scroll through all menu items
    for (int i = 0; i < menu.size(); i++) {
      // item n° i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // if item n° i is a sub-menu, then start again
      if (menuItem.hasSubMenu()) {
        // recursivity
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }
 
  private void getMenuOptionsStates(Menu menu) {
    // result
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // we retrieve the identifiers of the menu options
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // transfer menu options to a table
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // identify option
      int id = menuOptionsIds.get(i);
      // status option
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // result
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }
 
  // menu option status
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // status
      MenuItemState state = this.menuOptionsStates[i];
      // menu id
      int id = state.getMenuItemId();
      // initialization status
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // result
    return menuOptionsStates;
  }
 
  // display menu options -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // update all menu options
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }
 
  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // update certain menu options
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • الأسطر 6–18: تسترد هذه الطريقة المعرفات الرقمية لجميع خيارات القائمة؛
  • السطر 6: تأخذ طريقة [getMenuOptions] معلمتين:
    • [Menu menu]: قائمة الجزء؛
    • [List<Integer> menuOptionsIds]: قائمة معرفات Android لخيارات القائمة. في البداية، تكون هذه القائمة فارغة. ثم يتم ملؤها عن طريق مسح متكرر (السطر 15) لشجرة القائمة؛
  • الأسطر 20-40: بناءً على القائمة، تُنشأ مصفوفة الحالات (المعرف، الرؤية) لخيارات القائمة. يتم تخزين هذه المصفوفة في السطر 3. تم وصف فئة [MenuItemState] في القسم 2.7.1؛
  • الأسطر 43–55: نسخة معدلة من الطريقة السابقة. تقوم بنفس الشيء، ولكن بدلاً من إعادة حساب المعرفات لجميع خيارات القائمة — وهو ما تم بالفعل — تستخدم المعرفات من مصفوفة الحالات في السطر 3؛
  • الأسطر 58–63: تتيح لك طريقة [setAllMenuOptionsStates] إخفاء أو إظهار جميع خيارات قائمة الجزء؛
  • الأسطر 65-69: تسمح لك الطريقة [setMenuOptionsStates] بإظهار أو إخفاء خيارات معينة من القائمة بشكل انتقائي؛
  • تم إعلان الطريقتين [getMenuOptions، getMenuOptionsStates] على أنهما خاصتان لأنهما تُستخدمان فقط داخل [AbstractFragment]. تم إعلان الطريقتين [setAllMenuOptionsStates] (السطر 58) و[setMenuOptionsStates] (السطر 65) على أنهما محمية بحيث تكونان متاحتين للفئات الفرعية؛

2.7.3.4. التعامل مع انتظار اكتمال مهمة غير متزامنة


   // subscriptions to observables
  private List<Subscription> abonnements = new ArrayList<>();
// asynchronous tasks
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...
 
  // management of waiting for the end of an asynchronous operation -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // the number of tasks to be executed is noted
    this.numberOfRunningTasks = numberOfRunningTasks;
    // we put the image on hold
    mainActivity.beginWaiting();
    // empty the subscription list
    abonnements.clear();
    // no cancellations yet
    runningTasksHaveBeenCanceled = false;
  }
 
  protected void cancelWaitingTasks() {
    // we hide the waiting image
    mainActivity.cancelWaiting();
  }
 
  • الأسطر 9–18: لبدء عملية غير متزامنة واحدة أو أكثر، سيستدعي الجزء الفرعي الطريقة [beginRunningTasks] الخاصة بالجزء الرئيسي. المعلمة لهذه الطريقة هي عدد المهام غير المتزامنة التي سيطلقها الجزء الفرعي؛
  • السطر 11: نقوم بتخزين معلمة الأسلوب؛
  • السطر 13: يتم إظهار شاشة التحميل؛
  • السطر 15: يتم مسح قائمة الاشتراكات في العمليات غير المتزامنة. لم يتم إنشاء هذه الاشتراكات بعد بواسطة الجزء الفرعي؛
  • السطر 17: يتم الاحتفاظ بقيمة منطقية للإشارة إلى أن المهام غير المتزامنة التي طلبها الجزء الفرعي قد تم إلغاؤها. في البداية، تكون القيمة المنطقية هي false؛
  • الأسطر 20-25: يستدعي الجزء الفرعي الطريقة الأصلية [cancelWaitingTasks] للإشارة إلى رغبته في إلغاء المهام التي أطلقها؛
  • السطر 22: يتم إخفاء الصورة المنتظرة؛

2.7.3.5. معالجة الاستثناءات


  // gestion exception -------------------------------------------------------------------
 
  // exception alert display
  protected void showAlert(Throwable th) {
    // display messages from the Throwable th exception stack
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }
 
  // message list display
  protected void showAlert(List<String> messages) {
    // the message list is displayed
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • الأسطر 4-7: تسمح طريقة [showAlert(Throwable)] لجزء فرعي بعرض الرسائل من مكدس الاستثناءات لـ Throwable الذي تم تمريره كمعلمة في نافذة؛
  • الأسطر 10–13: تسمح الطريقة [showAlert(List<String>)] لجزء فرعي بعرض قائمة الرسائل التي تم تمريرها كمعلمة في نافذة؛
  • تم وصف فئة [Utils] المستخدمة في الأسطر 6 و 12 في القسم 2.7.2؛

2.7.3.6. معالجة العمليات غير المتزامنة


...
  // subscriptions to observables
  private List<Subscription> abonnements = new ArrayList<>();
  // asynchronous tasks
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // asynchronous task execution with RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // process: the observable to be executed/observed
    // consumeResult: the method that uses the response obtained
    // 
    // new subscriptions are only created if they have not been cancelled
    if (!runningTasksHaveBeenCanceled) {
      // execution on I/O thread and observation on Ui thread
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // the observable is executed
      try {
        abonnements.add(process.subscribe(
          // consumption result
          consumeResult,
          // consumption exception
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // end of task
          new Action0() {
 
            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }
 
  private void endOfTask() {
...
  }
 
  // an asynchronous operation has thrown an exception
  // or an exception has occurred during the execution of an asynchronous operation
  private void consumeThrowable(Throwable th) {
...
  }
 
  • الأسطر 9–41: تنفيذ مهمة غير متزامنة؛
  • السطر 9: تتوقع طريقة [executeInBackground] معلمتين:
    • [Observable<T> process]: العملية غير المتزامنة المراد تنفيذها؛
    • [Action1<T> consumeResult]: طريقة الجزء الفرعي التي سيتم استدعاؤها لتمرير العناصر التي أصدرتها العملية إليها. في الأمثلة السابقة، كانت العمليات تصدر دائمًا عنصرًا واحدًا فقط. النوع T لـ [Action1<T>] هو النوع T للنتيجة التي تعيدها العملية المراقبة؛
  • السطر 14: لا يتم تشغيل المهمة غير المتزامنة إلا إذا لم يتم إلغاؤها بالفعل من قبل المستخدم أو البرنامج (بسبب استثناء)؛
  • السطر 16: يتم تكوين العملية للتشغيل على مؤشر ترابط I/O ومراقبتها على مؤشر ترابط واجهة المستخدم؛
  • السطر 16: تعمل عبارة [process.subscribe] على تشغيل العملية في مؤشر ترابط الإدخال/الإخراج. ضمن هذا المؤشر، يتم تنفيذ العمليات بشكل متزامن لأننا نستخدم مكتبة HTTP متزامنة؛
  • السطر 19: تحتوي طريقة [process.subscribe] على ثلاثة معلمات:
    • السطر 21: [consumeResult]: طريقة الجزء الفرعي التي ستستهلك العناصر الصادرة عن العملية؛
    • الأسطر 22-28: الطريقة التي يتم تنفيذها عند حدوث استثناء أثناء معالجة المهمة غير المتزامنة. يتم تفويض المعالجة إلى طريقة [consumeThrowable] في السطر 49؛
    • الأسطر 29–36: الطريقة التي يتم تنفيذها عندما تصدر المهمة إشعار انتهاء الإرسال. يتم تفويض معالجة هذا الأمر إلى الطريقة [endOfTask] في السطر 43؛
  • السطر 19: يتم تسجيل المهمة غير المتزامنة التي تم إطلاقها للتو في حقل [subscriptions]، الذي يتتبع جميع المهام غير المتزامنة التي تم إطلاقها. سيسمح ذلك بإلغائها إذا لزم الأمر؛
  • الأسطر 37-39: الطريقة التي يتم تنفيذها عند حدوث استثناء أثناء معالجة المهمة غير المتزامنة. يتم تفويض المعالجة إلى الطريقة [consumeThrowable] في السطر 49؛

طريقة [endOfTask] هي كما يلي:


  // asynchronous tasks
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // one less job to wait for
    numberOfRunningTasks--;
    // finished?
    if (numberOfRunningTasks == 0) {
      // end waiting
      cancelWaitingTasks();
      // the end of tasks is signalled to the daughter class
      notifyEndOfTasks(false);
    }
  }
...
  // classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • السطر 6: تم للتو إنجاز مهمة غير متزامنة. يتم تقليل عداد المهام النشطة؛
  • السطر 8: إذا لم تعد هناك مهام نشطة، فهذا يعني أن الخيط الفرعي قد تلقى جميع ردوده؛
  • السطر 10: يتم إلغاء الانتظار؛
  • السطر 12: نُعلم الجزء الفرعي بأن جميع المهام التي أطلقها قد انتهت عن طريق استدعاء طريقة [notifyEndOfTasks] الخاصة به. تشير معلمة هذه الطريقة إلى كيفية انتهاء المهام — بشكل طبيعي، أو بسبب الإلغاء من قبل المستخدم أو الكود بسبب حدوث استثناء. في السطر 12، نشير إلى نهاية طبيعية. لاحظ أن الجزء الفرعي لا يحتاج إلى تتبع المهام التي لا تزال نشطة. تقوم فئته الأم بذلك نيابة عنه؛

طريقة [consumeThrowable] هي كما يلي:


  // asynchronous tasks
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // an asynchronous operation has thrown an exception
  // or an exception has occurred during the execution of an asynchronous operation
  private void consumeThrowable(Throwable th) {
    // th: the exception to be dealt with
    // 
    // log
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // cancel tasks already started
    cancelRunningTasks();
    // error messages are displayed
    showAlert(th);
  }
 
  // cancel tasks
  protected void cancelRunningTasks() {
    // log
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // cancel all registered asynchronous tasks
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // we note the cancellation
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // end of wait
    cancelWaitingTasks();
    // the cancellation of tasks is reported to the daughter fragment
    notifyEndOfTasks(true);
}
 
...
  // classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • السطر 3: تلتقط الطريقة [consumeThrowable] الاستثناء الذي حدث؛
  • السطر 15: يتم إلغاء جميع المهام التي لا تزال نشطة؛
  • السطر 17: يتم عرض نص الاستثناء؛
  • الأسطر 21–37: يتم إلغاء جميع المهام؛
  • الأسطر 27-29: يتم إلغاء جميع الاشتراكات؛
  • السطر 31: يتم تدوين ملاحظة تفيد بحدوث إلغاء؛
  • السطر 32: إعادة تعيين عداد المهام إلى الصفر؛
  • السطر 34: يتم إلغاء الانتظار؛
  • السطر 36: يتم إخطار الجزء الفرعي بأن المهام قد انتهت عند الإلغاء؛

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


  // life cycle --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }
 
  @Override
  public void onDestroy() {
    // parent
    super.onDestroy();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroy");
    }
  }
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }
 
  private void saveState() {
...
  }
 
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
...
  }
 
 
  @Override
  public void onSaveInstanceState(final Bundle outState) {
...
}
  • الأسطر 2–20: تم تضمين طريقتي [onDestroyView، onDestroy] لأغراض التسجيل فقط. تسمح هاتان الطريقتان للمطور بفهم دورة حياة الجزء بشكل أفضل؛

يتم التعامل مع حفظ الجزء عند دوران الجهاز من خلال الطرق التالية: [setUserVisibleHint، onSaveInstanceState، saveState]:


  // fragment life cycle
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...
 
@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // backup?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // the fragment will be hidden - save it
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // memory
    this.isVisibleToUser = isVisibleToUser;
  }
 
  private void saveState() {
...
  }
 
  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // parent
    super.onSaveInstanceState(outState);
    // save fragment only if visible
    if (isVisibleToUser) {
      // perhaps the backup has already been made
      if (!saveFragmentDone) {
        saveState();
      }
      // restoration to be carried out in all cases
      session.setAction(ISession.Action.RESTORE);
    }
}
  • الأسطر 6-19: يتم حفظ الجزء إذا انتقل من الحالة المرئية إلى الحالة المخفية (السطر 11). توفر طريقة [setUserVisibleHint] هذه المعلومات؛
  • السطر 14: يتم الحفظ بواسطة الطريقة الخاصة في الأسطر 21-23؛
  • الأسطر 25-41: عند تدوير الجهاز، يتم استدعاء طريقة [onSaveInstanceState]. يتم حفظ الجزء في حالتين:
    • إذا كان مرئيًا (السطر 34)؛
    • لم يتم حفظه بعد (السطر 36). من الممكن ألا يتم تنفيذ كل من طريقتي [setUserVisibleHint] و [onSaveInstanceState] عندما يكون الجزء مرئيًا، وبالتالي فإن إدارة القيمة المنطقية [saveFragmentDone] غير ضرورية. في حالة الشك، اخترت استخدامها؛
  • السطر 40: بعد الحفظ تأتي عملية الاستعادة. لاحظ أنه في المرة التالية التي يحتاج فيها الجزء إلى تحديث نفسه، سيقوم بذلك عبر عملية [RESTORE

لاحظ اللحظتين اللتين يُطلب فيهما حفظ الجزء:

  1. عندما ينتقل من الحالة المرئية إلى الحالة المخفية؛
  2. عندما يدور الجهاز؛

الطريقة الخاصة [saveState] هي كما يلي:


...
  private void saveState() {
    // tasks to cancel?
    if (numberOfRunningTasks != 0) {
      // cancel tasks
      cancelRunningTasks();
    }
    // save fragment state
    CoreState currentState = saveFragment();
    // the fragment has been visited
    currentState.setHasBeenVisited(true);
    // save menu status
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // session setting
    session.setCoreState(getNumView(), currentState);
    // backup done
    saveFragmentDone = true;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }
 
 
...
  // classes filles -----------------------------------------------------
public abstract CoreState saveFragment();
 
protected abstract int getNumView();
  • الأسطر 4-7: قد يحدث دوران للجهاز أثناء تنفيذ العمليات غير المتزامنة. هنا، يتم اتخاذ قرار بإلغاء جميع هذه العمليات. هذا ليس قرارًا جيدًا بالنسبة للمستخدم، الذي سيضطر إلى إرسال طلب جديد قد يستغرق وقتًا طويلاً لمجرد أنه حرك هاتفه أو جهازه اللوحي أو تلقى مكالمة هاتفية. من الممكن الحفاظ على اتصالات الشبكة من خلال دورة حفظ/استعادة. ومع ذلك، فإن الحلول ليست مباشرة، وقد قررت عدم تناولها في هذه الدورة التدريبية للمبتدئين. الطريق إلى الأمام هو إنشاء اتصالات الشبكة هذه عبر جزء لا يحتوي على واجهة مستخدم مرفقة ولا يتم إتلافه أثناء دورة الحفظ/الاستعادة. للقيام بذلك، ما عليك سوى استخدام التعليمات [Fragment.setRetainInstance(true)]؛
  • السطر 9: نطلب من الجزء الفرعي حفظ حالته في نوع مشتق من [CoreState] (السطر 31)؛
  • السطر 11: نلاحظ أن الجزء قد تمت زيارته. هذه المعلومات مفيدة. عندما تتم زيارة جزء لأول مرة، قد يختلف تحديثه عن التحديثات اللاحقة لأنه لا يوجد له حالة سابقة في الجلسة؛
  • السطر 13: نحفظ حالة القائمة، مما سيسمح لنا باستعادتها تلقائيًا؛
  • السطر 15: يتم حفظ هذه الحالة الحالية في الجلسة. في الجلسة، يتم تجميع الحالات حسب العرض/الجزء، ولكل منها حالة. يتم توفير رقم العرض بواسطة الجزء الفرعي (السطر 33)؛
  • السطر 17: نلاحظ أن الجزء قد تم حفظه. وذلك لأن طريقتين قد تستدعيان طريقة [saveState]، وليس من الضروري إجراء عمليتي حفظ؛

يتم إعادة إنشاء العرض المرتبط بالجزء بالطريقة التالية:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // the view must be restored
    viewHasToBeInitialized = true;
}

في دورة الحياة، يتم تنفيذ الطريقة [onActivityCreated] مباشرة بعد الطريقة [onCreateView]. يشير استدعاء الطريقة الأخيرة إلى أنه يجب إعادة بناء العرض المرتبط بالجزء. نلاحظ ذلك ببساطة في السطر 10.

2.7.3.8. تحديث الجزء

يعد تحديث الجزء هو العملية الأخيرة التي يتم إجراؤها على الجزء قبل أن يصبح مرئيًا وينتظر إدخال المستخدم. يتم التعامل مع ذلك بواسطة الكود التالي:


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
  // update Fragment ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // memory
    this.menu = menu;
    // retrieve # menu options if not already done
    if (fragmentHasToBeInitialized) {
      // retrieve the # menu options
      getMenuOptionsStates(menu);
      // activity
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
    previousState = session.getCoreState(getNumView());
    // multi-stage update of the daughter fragment
    // step 1 - is this your 1st visit?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // this is not the 1st visit
      // step 2: does the fragment need to be initialized?
      ...
      // step 3: should the view be initialized?
      ...
    }
    // step 4: a submit, a browse, a restore?
    ...
 
    // step 5: terminal updates ----------------------
...
  }
...
  // classes filles -----------------------------------------------------
  protected abstract void initFragment(CoreState previousState);
 
  protected abstract void initView(CoreState previousState);
 
  protected abstract void updateOnSubmit(CoreState previousState);
 
  protected abstract void updateOnRestore(CoreState previousState);
 
  protected abstract void notifyEndOfUpdates();
  • السطر 19: تُستخدم طريقة [onCreateOptionsMenu] لتحديث الجزء. ولهذا السبب، يجب أن يحتوي الجزء على قائمة، حتى لو كانت فارغة. عند تنفيذ هذه الطريقة، يكون الجزء قد تم ربطه بعرضه ونشاطه ويكون مرئيًا أيضًا؛
  • السطر 25: يتم تخزين القائمة التي تم تمريرها كمعلمة (السطر 22) إلى الطريقة؛
  • الأسطر 27-34: إذا كان الجزء بحاجة إلى التهيئة:
    • السطر 29: يتم تخزين حالات خيارات القائمة في المصفوفة [menuOptionsStates] من السطر 3؛
    • السطر 31: يتم تخزين النشاط كمثيل لنوع Android [Activity
    • السطر 32: يتم تخزين النشاط كمثيل لواجهة [IMainActivity
    • السطر 33: يتم تخزين الجلسة. يعد تحويل النوع ضروريًا لأن الطريقة [mainActivity.getSession()] تُرجع نوع [ISession
  • السطر 36: يتم استرداد الحالة السابقة للجزء من الجلسة. إذا كانت هذه هي الزيارة الأولى للجزء، فإن القيمة المنطقية [previousState.hasBeenVisited] هي الوحيدة ذات الصلة؛
  • الأسطر 39-44: يتم تنفيذ الكود عندما تكون هذه هي الزيارة الأولى للجزء. في هذه الحالة، لا تكون حالته السابقة ذات صلة؛
  • الأسطر 44-50: يتم تنفيذ الكود عندما لا تكون هذه هي الزيارة الأولى للجزء؛
  • السطور 46-47: يتم تنفيذ الكود إذا تم استدعاء منشئ الجزء (fragmentHasToBeInitialized == true
  • السطور 48-49: يتم تنفيذ الكود إذا تمت إعادة بناء العرض المرتبط بالجزء (viewHasToBeInitialized==true
  • السطور 51-52: يتم تنفيذ الكود اعتمادًا على الإجراء الحالي (SUBMIT، NAVIGATION، RESTORE
  • السطران 54-55: يتم تنفيذ الكود دائمًا؛

الخطوات الخمس للتحديث هي كما يلي:

الخطوة 1


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
    previousState = session.getCoreState(getNumView());
    // multi-stage update of the daughter fragment
    // step 1 - is this your 1st visit?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // fragment and view initialization
      initFragment(null);
      initView(null);
      // raz previousState for the suite
      previousState = null;
    } else {
      // this is not the 1st visit
...
 
  protected abstract void initFragment(CoreState previousState);
 
protected abstract void initView(CoreState previousState);
  • السطر 19: يتم استرداد الحالة السابقة للجزء من الجلسة؛
  • الأسطر 22–31: يتم تنفيذ الكود إذا لم يتم زيارة الجزء من قبل؛
  • السطر 27: يُطلب من الفئة الفرعية تهيئة الجزء. المعلمة الخاصة بالطريقة [initFragment] في السطر 35 هي الحالة السابقة للجزء. هنا، يتم تمرير القيمة null للإشارة إلى الجزء الفرعي بأن هذه هي الزيارة الأولى؛
  • السطر 28: يُطلب من الفئة الفرعية تهيئة العرض المرتبط بالجزء. المعلمة الخاصة بالطريقة [initView] في السطر 37 هي الحالة السابقة للجزء. هنا، يتم تمرير القيمة null للإشارة إلى الجزء الفرعي بأن هذه هي الزيارة الأولى؛
  • السطر 30: نضبط الحالة السابقة على null للخطوات التالية؛

الخطوتان 2 و 3


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
     // we retrieve the previous fragment state (the very 1st time, only the boolean hasBeenVisited represents anything)
    previousState = session.getCoreState(getNumView());
    // multi-stage update of the daughter fragment
    // step 1 - is this your 1st visit?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // ce n'is not the 1st visit
      // step 2: does the fragment need to be initialized?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // girl fragment
        initFragment(previousState);
      }
      // step 3: should the view be initialized?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // girl fragment
        initView(previousState);
      }
    }
 
...
 
  protected abstract void initFragment(CoreState previousState);
 
protected abstract void initView(CoreState previousState);
  • الأسطر 24–42: يتم تنفيذها عندما لا تكون هذه هي الزيارة الأولى للجزء؛
  • الأسطر 27–33: إذا تم إعادة بناء الجزء للتو، يتم إعادة تهيئته عن طريق استدعاء طريقة [initFragment] للفئة الفرعية (الأسطر 32، 46). يتم تمرير الحالة السابقة للجزء إليه؛
  • الأسطر 35–51: إذا كانت العرض المرتبط بالجزء بحاجة إلى التهيئة أو إعادة الضبط، يُطلب من الجزء الفرعي القيام بذلك (الأسطر 40، 48). ومرة أخرى، يتم تمرير آخر حالة معروفة للجزء إليه؛

الخطوة 4


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
    previousState = session.getCoreState(getNumView());
    // multi-stage update of the daughter fragment
 ...
 
    // step 4: a submit, a browse, a restore?
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
        Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // action in progress
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // girl fragment
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // catering menu
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // girl fragment
          updateOnRestore(previousState);
        } else {
          // 1st visit - nothing to do
        }
        break;
      case RESTORE:
        // restoration
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // menu restoration (previousState cannot be null)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // girl fragment
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);
 
protected abstract void updateOnRestore(CoreState previousState);
  • الأسطر 34–66: نقوم بمعالجة الإجراء الحالي، والذي يمكن أن يكون أحد الإجراءات الثلاثة التالية:
    • RESTORE: نقوم باستعادة الجزء بعد تدوير الجهاز؛
    • NAVIGATION: نعود إلى الجزء، بهدف العثور عليه في الحالة التي تركناه عليها في آخر مرة استخدمناه فيها؛
    • SUBMIT: جميع الحالات الأخرى؛
  • السطر 34: استرداد الإجراء الحالي؛
  • الأسطر 36-42: بالنسبة للإجراء من نوع SUBMIT، نستدعي طريقة [updateOnSubmit] للجزء الفرعي (الأسطر 41 و68)، ونمرر إليها آخر حالة معروفة للجزء؛
  • الأسطر 43–55: بالنسبة لإجراء من نوع NAVIGATION؛
  • الأسطر 47–54: نريد استعادة الجزء إلى آخر حالة معروفة له. قد تتزامن عملية NAVIGATION مع الزيارة الأولى. قد يكون هذا هو الحال، على سبيل المثال، في تطبيق ذي علامات تبويب: إذا قمت بالتبديل من علامة التبويب 1 إلى علامة التبويب 4:
    • يجب عليّ تهيئة الجزء الخاص بعلامة التبويب 4 إذا كانت هذه هي الزيارة الأولى؛
    • استعادة الجزء الخاص بعلامة التبويب 4 إلى حالته السابقة إذا لم تكن هذه هي الزيارة الأولى؛
  • الأسطر 52-54: لا تفعل شيئًا إذا كانت هذه هي الزيارة الأولى. ستتولى الطريقة الفرعية [initView(CoreState previousState)] عملية التهيئة هذه. يتم تحديد الزيارة الأولى بواسطة الشرط [previousState == null
  • السطر 49: إذا لم تكن هذه هي الزيارة الأولى للجزء، فاستعد قائمته؛
  • السطر 51: نطلب من الفئة الفرعية تحديث نفسها عن طريق استدعاء الطريقة في السطر 70. نمرر لها الحالة السابقة للجزء حتى تتمكن من أداء مهمتها؛
  • الأسطر 56–66: في حالة عملية استعادة الجزء، نقوم بنفس الشيء كما في حالة التنقل خارج الزيارة الأولى؛

الخطوة 5


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment life cycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment life cycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // step 5: terminal updates ----------------------
    // we've changed our view
    session.setPreviousView(getNumView());
    // more action in progress
    session.setAction(ISession.Action.NONE);
    // when leaving this fragment, it must be saved
    saveFragmentDone = false;
    // as long as the fragment has not been rebuilt, it does not need to be initialized
    fragmentHasToBeInitialized = false;
    // as long as the view has not been rebuilt, it does not need to be initialized
    viewHasToBeInitialized = false;
    // returns to normal tab selection operation
    session.setNavigationOnTabSelectionNeeded(true);
 
    // the fragment is notified that the view is ready
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • الأسطر 18–30: عندما نصل إلى هذه النقطة، يكون الجزء قد تم تهيئته ويكون جاهزًا للعرض. ثم نعيد تعيين جميع المؤشرات المستخدمة في إدارة دورة حياة الجزء إلى حالتها الأولية؛
  • السطر 20: لقد تغير العرض؛ ويتم تسجيل ذلك في الجلسة؛
  • السطر 22: لم تعد هناك أي إجراءات قيد التنفيذ؛
  • السطر 24: عند الخروج من الجزء المعروض حاليًا، سنحتاج إلى حفظه عند الخروج؛
  • السطر 26: لم يعد هناك حاجة لإعادة بناء الجزء. سيتم تعيين هذه العلامة على "صحيح" عند تنفيذ منشئ الجزء مرة أخرى؛
  • السطر 28: لم يعد من الضروري تهيئة العرض المرتبط بالجزء. سيتم تعيين هذه العلامة على "صحيح" مرة أخرى عند تنفيذ طريقة [onActivityCreated] مرة أخرى؛
  • السطر 30: قد يتم عرض الجزء في تطبيق مبوب. في هذه الحالة، عندما ينقر المستخدم على إحدى علامات التبويب، يجب أن يحدث تغيير في الجزء؛
  • السطر 36: يتم إخطار الفئة الفرعية بأن الجزء جاهز. ويمكنها استخدام طريقة [notifyEndOfUpdates] لإجراء التحديثات التي يجب إجراؤها في أي حال، أو تشغيل عملية غير متزامنة لجلب بيانات جديدة، وما إلى ذلك.

2.7.4. مثال على جزء

  

لقد أدرجنا مثالاً لجزء في مشروع [client-android-skel] لتوضيح البنية النموذجية لجزء في تطبيق يستند إلى هذا المشروع.

فيما يلي فئة [DummyFragment]:


package client.android.fragments.behavior;
 
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
 
public class DummyFragment extends AbstractFragment {
 
  // fields inherited from parent class -------------------------------------------------------
 
  // debug mode
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // class name
  //-- protected String className;
  // asynchronous tasks
  //-- protected int numberOfRunningTasks;
  // activity
  //-- protected IMainActivity mainActivity;
  //-- protected Activity activity;
  // session
  //-- protected Session session;
 
  // methods inherited from the parent class -------------------------------------------------------
 
  // display menu options
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // management of waiting for the end of a series of asynchronous tasks
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // asynchronous task execution with RxAndroid
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // cancel tasks
  //-- protected void cancelRunningTasks() {
  // exception alert display
  //-- protected void showAlert(Throwable th) {
  // message list display
  //-- protected void showAlert(List<String> messages) {
 
  // methods imposed by the parent class -------------------------------------------------------
 
  @Override
  public CoreState saveFragment() {
    // save the fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
  }
 
  @Override
  protected int getNumView() {
    // return the fragment number in the table of fragments managed by the activity (cf MainActivity)
    return 0;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // the fragment becomes visible and has undergone construction in this or a previous stage
    // this happens on application startup and every time the Android device is rotated
    // is necessarily followed by the execution of [initView]
    // the fields of the fragment that has been rebuilt must be initialized
    // previousState is the fragment's last save - is null if this is the fragment's 1st visit
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // the fragment becomes visible and the associated view has been reconstructed in this or a previous step
    // this happens every time [initFragment] is executed and every time the fragment leaves the adjacency of the displayed fragment
    // initialize the components of the view that has been rebuilt
    // previousState is the fragment's last save - is null if it's the fragment's 1st visit
 
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // is executed after [initFragment, initView] if these methods are executed
    // the view will be displayed after an operation of type SUBMIT
    // the fragment and the associated view usually have to be initialized from the session
    // previousState is the fragment's last save - is null if it's the fragment's 1st visit
    // there's nothing to be done if the fragment can't be reached by a SUBMIT operation
    // if the fragment can be reached by SUBMIT operations from different fragments, the previous view can be known by [session.getPreviousView]
    // if the fragment can be reached by several SUBMIT operations from the same fragment, then a flag must be set to differentiate between the different types of SUBMIT from this fragment
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // is executed after [initFragment, initView] if these methods are executed
    // the view will be displayed after an operation of type RESTORE or NAVIGATION
    // previousState is the fragment's last backup - never null
    // restore the view to its previous state
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
    // comes after methods [updateOnSubmit, updateOnRestore]
    // when we're there, the view has been built and initialized
    // there's often nothing to do here, but you can also factor in actions that need to be done no matter how you arrive at this view
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // called when asynchronous tasks launched by the fragment are either completed or cancelled
    // these two cases can be differentiated using parameter runningTasksHaveBeenCanceled
    // the view generally needs to be reset to a different state from the one it had while waiting for responses from asynchronous tasks
 
  }
}

قد لا تحتوي فئة [DummyFragment] على حالة. وقد أدرجنا هنا حالة لتذكيرنا بما يُتوقع داخلها:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class DummyFragmentState extends CoreState {
  // fragment status [DummyFragment]
  // set only serializable fields to jSON
  // put the annotation @JsonIgnore on the others, but it's hard to see what use they could be
  // don't forget the getters/setters - they are used for serialization/deserialization
}

لتوضيح استخدام مشروع [client-android-skel]، سنستخدم أولاً أمثلة بسيطة قبل الانتقال إلى دراسة حالة أكثر شمولاً.

2.8. تمارين توضيحية

سنبدأ بإعادة هيكلة الأمثلة التي تمت كتابتها بالفعل.

2.8.1. المثال-17B

سنعيد النظر في المثال 17 من القسم 1.18. هذا تطبيق يحتوي على جزء واحد، ولا يحتوي على مهام غير متزامنة، ولا يحتوي على علامات تبويب. سنقوم بفحصه لنرى كيف يتصرف عند تدوير الجهاز. سنقوم بإدخال ما يلي:

Image

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

Image

إذا قارنا العروض، نجد أن كل شيء قد تم الحفاظ عليه باستثناء القائمة [2]، التي أصبحت فارغة الآن.

علاوة على ذلك، إذا نقرت على زر [إرسال]، يظهر مربع حوار يعرض الإدخالات التي تمت في النموذج. إذا قمت بتدوير الجهاز في تلك اللحظة، يختفي مربع الحوار.

لذلك، أثناء الدوران، سنحتاج إلى إعادة إنشاء:

  • القائمة المنسدلة والعنصر المحدد فيها؛
  • مربع الحوار إذا كان معروضًا أثناء الدوران؛

2.8.1.1. مشروع [Example-17B]

نقوم بنسخ مشروع [client-android-skel] إلى examples/Example-17B. ثم نقوم بتحميل المشروع الجديد [1]:

  • في [2-3]، في المجلد [behavior]، نلصق الجزء [Vue1Fragment] من مشروع [Example-17
  • في [4-5]، في مجلد [layout] الخاص بـ [Example-17B]، نلصق العرض [vue1.xml] من [Example-17]. هذا هو العرض المرتبط بالجزء؛
  • في [6]، يتم استبدال مجلد [values] من [Example-17B] بمجلد [values] من [Example-17

سنقوم بتغيير الهامش العلوي لعرض [vue1.xml] إلى 80 نقطة:


    <TextView
      android:id="@+id/textViewFormulaireTitre"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="10dp"
      android:layout_marginTop="80dp"
      android:text="@string/titre_vue1"
android:textSize="30sp"/>

في هذه المرحلة، يمكننا إجراء تجميع أولي للتحقق من وجود أخطاء. تنشأ الأخطاء الأولى المبلغ عنها من عمليات استيراد الحزم التي تم نقلها. نقوم بإصلاحها (Ctrl-Shift-O). تنشأ أخطاء أخرى، مثل ، لأن العرض [Vue1Fragment] لا ينفذ جميع الطرق المطلوبة من قبل فئته الأصلية [AbstractParent]:

Image

قم بإنشاء الطرق المفقودة (Alt-Enter).

هناك خطأ تجميع آخر تم الإبلاغ عنه وهو كما يلي:

Image

نقوم بإصلاح هذا في ملف [build.gradle] الخاص بالوحدة النمطية (السطر 20 أدناه):

 

في هذه المرحلة، يمكننا إعادة التجميع لمعرفة الأخطاء المتبقية. الخطأ الوحيد الذي تم الإبلاغ عنه هو في طريقة [Vue1Fragment.updateFragment]:

 

يجب إزالة التعليق التوضيحي [@Override] من السطر 135. لم يعد هناك أخطاء الآن. سنستخدم هذا كنقطة انطلاق لتعديل المشروع.

2.8.1.2. حالة جزء [Vue1Fragment]

يحتاج الجزء [Vue1Fragment] إلى حفظ المعلومات عند تدوير الجهاز حتى يمكن استعادته بالكامل. ننشئ فئة [Vue1FragmentState] لهذا الغرض:

  

في الوقت الحالي، هذه الفئة فارغة:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class Vue1FragmentState extends CoreState {
 
}

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

  

يحتوي المجلد [custom] على عناصر بنية يمكن للمطور تخصيصها.

ستكون الثوابت الخاصة بواجهة [IMainActivity] كما يلي:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 0;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • الأسطر 24–31: لا يستخدم التطبيق طبقة [DAO] الخاصة به هنا. لن يتم استخدام هذه الثوابت؛
  • السطر 34: تجاور جزء يساوي 1، وهو القيمة الافتراضية. وبما أن التطبيق يحتوي على جزء واحد فقط (السطر 43)، فإن هذه القيمة غير ذات صلة؛
  • السطور 39-40: نظرًا لعدم وجود عمليات مع طبقة [DAO]، فلا حاجة لصورة بديلة؛
  • السطر 37: هذا ليس تطبيقًا يعمل بنظام علامات التبويب؛
  • السطر 43: يوجد جزء واحد فقط؛

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


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
 
}

إنه فارغ. وبالفعل، نظرًا لوجود جزء واحد فقط، فلا داعي لتوفير اتصال بين الأجزاء باستخدام جلسة عمل.

وأخيرًا، فإن فئة [CoreState] هي كما يلي:


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • الأسطر 11–13: علينا إدراج جميع الفئات المشتقة من [CoreState] التي تخزن حالة الأجزاء المختلفة. هنا، لا يوجد سوى فئة واحدة (السطر 12)؛

2.8.1.4. [MainActivity]

تبدو نشاط [MainActivity] حاليًا كما يلي:


package client.android.activity;
 
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
 
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // todo: we continue the initializations started by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // todo: define fragments here
    return new AbstractFragment[0];
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo: define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // todo: tabbed navigation - define the view to be displayed when tab no. [position] is selected
  }
 
  @Override
  protected int getFirstView() {
    // todo: define the number of the first view (fragment) to be displayed
    return 0;
  }
}

تشير التعليقات [//todo] إلى ما يتعين على المطور القيام به. تتطور فئة [MainActivity] على النحو التالي:


package client.android.activity;
 
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
 
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new Vue1Fragment_()};
  }
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
 
  }
 
  @Override
  protected int getFirstView() {
    return 0;
  }
}

يجب تعديل الطريقة الموجودة في الأسطر 41–44 فقط. يجب أن تُرجع مصفوفة أجزاء التطبيق. في السطر 43، لا تنسَ إضافة شرطة سفلية بعد اسم الجزء.

2.8.1.5. حالة الجزء [FragmentState]

بعد اختبارات الدوران التي أجريت على مشروع [Example-17]، قررنا تخزين العناصر التالية من الجزء:

  • قائمة القيم الموجودة في القائمة المنسدلة؛
  • موضع العنصر المحدد في هذه القائمة؛
  • الرسالة التي يعرضها مربع الحوار إذا كان موجودًا في وقت الدوران؛

ستكون فئة [Vue1FragmentState] كما يلي:

  

package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
import java.util.List;
 
public class Vue1FragmentState extends CoreState {
 
  // drop-down list values
  private List<String> list;
  // the item selected from the drop-down list
  private int listSelectedPosition;
  // the message displayed in the
  private String message;
 
  // getters and setters
...
}

2.8.1.6. الجزء [AbstractFragment]

حاليًا، تتم إدارة دورة حياة الجزء بواسطة طريقتين (السطران 6 و 32):


// drop-down list
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;
 
  @AfterViews
  void afterViews() {
    // check the first button
    radioButton1.setChecked(true);
    // the calendar
    datePicker1.setCalendarViewShown(false);
    // on seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
 
      public void onStopTrackingTouch(SeekBar seekBar) {
      }
 
      public void onStartTrackingTouch(SeekBar seekBar) {
      }
 
      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // the drop-down list
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // initialize drop-down list adapter
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }

سيتم نقل كود هاتين الطريقتين إلى الطرق المحددة بواسطة فئة [AbstractFragment] على النحو التالي:


// fragment lifecycle management ---------------------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    Vue1FragmentState state = new Vue1FragmentState();
    state.setList(list);
    state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
    state.setMessage(message);
    return state;
  }
 
  @Override
  protected int getNumView() {
    return 0;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // 1st visit?
    if (previousState == null) {
      // create drop-down list values
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // returns values from the drop-down list
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // and the
      message = state.getMessage();
    }
    // initialize drop-down list adapter
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // the calendar
    datePicker1.setCalendarViewShown(false);
    // on seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
 
      public void onStopTrackingTouch(SeekBar seekBar) {
      }
 
      public void onStartTrackingTouch(SeekBar seekBar) {
      }
 
      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // initialize drop-down list adapter
    dropDownList.setAdapter(dataAdapter);
    // 1st visit?
    if (previousState == null) {
      // check the first button
      radioButton1.setChecked(true);
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // seekbar value
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // item selected from drop-down list
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // visible dialogue?
    if (message != null) {
      // we display it
      showMessage();
    }
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
 
}
  • الأسطر 2–9: يجب أن تضع طريقة [saveFragment] عناصر الجزء المراد حفظها في فئة مشتقة من [CoreState] وتُرجع مثيلًا لتلك الفئة؛
  • الأسطر 11–14: يجب أن تُرجع الطريقة [getNumView] رقم الجزء. هنا، يوجد جزء واحد فقط، رقمه هو 0؛
  • الأسطر 16–34: يجب أن تقوم طريقة [initFragment] بتهيئة حقول الجزء. وهي تتلقى الحالة السابقة للجزء. إذا كانت [previousState] فارغة، فهذا يعني أن هذه هي الزيارة الأولى؛
  • الأسطر 19-25: في الزيارة الأولى، يتم إنشاء قيم القائمة المنسدلة؛
  • الأسطر 26-30: إذا لم تكن هذه هي الزيارة الأولى، يتم استعادة حقول [list، message] الخاصة بالجزء من الحالة السابقة؛
  • السطور 33-34: تهيئة حقل [dataAdapter] الخاص بالجزء. هذا هو مصدر البيانات للقائمة المنسدلة؛
  • الأسطر 37–62: تُستخدم طريقة [initView] لتهيئة مكونات الواجهة المرئية. وتتلقى الحالة السابقة [previousState] كمعلمة. إذا كانت [previousState == null]، فهذا يعني أن هذه هي الزيارة الأولى؛
  • هنا، نرى ما كان موجودًا سابقًا في طريقة [@AfterViews
  • الأسطر 57-61: في الزيارة الأولى، نتأكد من تحديد زر الاختيار الأول؛
  • الأسطر 64-67: يتم تنفيذ طريقة [updateOnSubmit] عندما يكون الإجراء الحالي هو [SUBMIT]. هنا، لا يوجد تنقل بين الأجزاء وبالتالي لا يوجد إجراء حالي؛
  • الأسطر 69–81: يتم تنفيذ طريقة [updateOnRestore] عندما يكون الإجراء الحالي هو [NAVIGATION] أو [RESTORE]. هنا، لا يوجد تنقل بين الأجزاء وبالتالي لا يوجد إجراء [NAVIGATION] ممكن؛
  • السطر 72: نعيد حساب (وليس استعادة) قيمة TextView seekBarValue. وذلك لأن قيمتها كانت تضيع أحيانًا أثناء عمليات الدوران؛
  • الأسطر 74-75: يتم وضع القائمة على العنصر الذي تم تحديده قبل الدوران. بدون ذلك، ستعود القائمة افتراضيًا إلى العنصر الأول؛
  • الأسطر 76-80: يتم عرض مربع الحوار مرة أخرى إذا كانت الرسالة من الحالة السابقة غير فارغة. سنعود إلى طريقة [showMessage] (السطر 79)؛
  • الأسطر 83–86: طريقة [notifyEndOfUpdates] هي آخر طريقة تستدعيها الفئة الأم قبل ترك الجزء الفرعي بمفرده. هنا، لا يوجد ما يجب فعله؛
  • الأسطر 88-91: تشير طريقة [notifyEndOfTasks] إلى نهاية المهام غير المتزامنة التي أطلقها الجزء. هنا، لا توجد أي مهام؛

يتم استعادة مربع الحوار على النحو التالي:


  // dialog box message
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // list of messages to display
    List<String> messages = new ArrayList<>();
    ...
    // display
    doAfficher(messages);
  }
 
  private void doAfficher(final List<String> messages) {
    // poster text is created
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // the message is memorized
    message = texte.toString();
    // we display it
    showMessage();
  }
 
  private void showMessage() {
    // we display it
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // message reset
        message = null;
      }
    }).show();
}

عندما يرسل المستخدم النموذج، تقوم الدالة [doValider] (السطر 5) بإنشاء قائمة بالرسائل، ثم تعرضها (السطر 10) في مربع الحوار.

  • الأسطر 14–20: يتم ربط قائمة الرسائل في رسالة واحدة، يتم تخزينها في السطر 2؛
  • الأسطر 25-33: هذه هي الرسالة التي يعرضها مربع الحوار، وهي نفس الرسالة التي تعرضها طريقة [updateOnRestore
  • السطر 27: المعلمة الثانية لطريقة [setNeutralButton] هي الطريقة التي يتم تنفيذها عندما ينقر المستخدم على زر [Close] في مربع الحوار؛
  • السطر 31: عند إغلاق مربع الحوار، يتم تعيين الرسالة إلى null للإشارة إلى أن مربع الحوار لم يعد موجودًا؛

2.8.1.7. الاختبارات

ندعو القراء إلى اختبار هذا المشروع والتحقق من أن الجزء لا يزال موجودًا بعد دورة واحدة أو أكثر من الدورات المتتالية.

2.8.2. مثال-23: عميل الطقس

تقدم بعض مواقع الويب معلومات الطقس في شكل سلاسل JSON. وإليك مثال على ذلك:

Image

عنوان URL هو على الشكل: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} حيث:

  • city: المدينة التي تريد معرفة الطقس فيها، وهي هنا أنجيه؛
  • country: البلد الذي تقع فيه المدينة، في هذه الحالة فرنسا (fr)؛
  • APPID: مفتاح يتم الحصول عليه من خلال التسجيل في الموقع [https://home.openweathermap.org/users/sign_up

2.8.2.1. المشروع

  

تم إنشاء المشروع استنادًا إلى مشروع [client-android-skel]. ويتميز بالميزات التالية:

  • يحتوي على جزء واحد فقط لا تحتاج حالته إلى الصيانة؛
  • يقوم بإجراء طلبات غير متزامنة؛

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

  

تسمح لك واجهة [IMainActivity] بتحديد خصائص معينة للمشروع:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 5000;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • السطور 25 و28 و31 و40: خصائص طبقة [DAO]. السطر 31: المصادقة الأساسية غير مطلوبة؛
  • السطر 34: تجاور الأجزاء. هنا، هذا الثابت غير ذي صلة نظرًا لوجود جزء واحد فقط؛
  • السطر 37: هذا ليس تطبيقًا مبوبًا؛
  • السطر 43: يوجد جزء واحد فقط؛

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


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo: add subclasses of [CoreState] here
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • الأسطر 10–13: لا يوجد ما يُعلن عنه لأن هذا التطبيق يحتوي على جزء واحد فقط لا يتم حفظ حالته؛

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


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}

إنها فارغة لأنه لا يوجد اتصال بين الأجزاء في هذا التطبيق.

2.8.2.3. طبقة [DAO]

  

في طبقة [DAO]، يجب تخصيص ثلاث فئات:

  • واجهة IDao؛
  • تنفيذ Dao الخاص بها؛
  • واجهة WebClient للتواصل مع خادم الويب / JSON؛

وستكون واجهة [WebClient] على النحو التالي:


package client.android.dao.service;
 
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
 
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
 
  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);
 
  // weather service
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • السطران 18-19: عنوان URL لخدمة الطقس. لاحظ أن هذا العنوان نسبي بالنسبة لعنوان URL الجذر للعميل (RestClientRootUrl، السطر 12). هنا، سيكون عنوان URL الجذر هذا هو [http://api.openweathermap.org/

وستكون واجهة [IDao] كما يلي:


package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  //  weather service
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • لاحظ أن الطرق في الأسطر 6–22 مضمنة افتراضيًا في واجهة IDao لمشروع [client-android-skel
  • السطر 25: تسترد الطريقة [getWeatherForecast] سلسلة JSON الخاصة بالطقس في المدينة [city] بالبلد [country]. المعلمة الثالثة هي المفتاح الذي تم الحصول عليه من موقع الويب [https://home.openweathermap.org/users/sign_up

يتم تنفيذ واجهة [IDao] بواسطة فئة [Dao] التالية:


package client.android.dao.service;
 
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // timeout
  private int timeout;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we build the restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // set the URL of the web service
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    // the user is registered in the interceptor
    authInterceptor.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // memory
    this.timeout = timeout;
    // factory configuration
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
 
  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // service météo ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
    }
    // result
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • لاحظ أن الأسطر 17–90 مضمنة افتراضيًا في فئة [Dao] لمشروع [client-android-skel]. ما عليك سوى إضافة طرق التنفيذ لواجهة [IDao]، الخاصة بالتطبيق (السطر 92)؛
  • الأسطر 93–105: تنفيذ طريقة [getWeatherForecast]. هذا بسيط للغاية ويشغل 6 أسطر، الأسطر 100–105؛
  • السطر 100: طريقة [getResponse] هي طريقة للفئة الأصلية [AbstractDao]. وتتوقع معلمة من النوع [IRequest<T>]، حيث T هو نوع الاستجابة المتوقعة من الخادم؛ وهنا، يكون String لأننا نتوقع سلسلة JSON. يجب أن يكون النوع T لـ [IRequest<T>] هو النوع T لطريقة [Observable<T> getWeatherForecast
  • تحتوي واجهة [IRequest<T>] على طريقة واحدة فقط: getResponse. وتتمثل مهمتها في توفير الاستجابة من النوع T التي يجب أن تعيدها طريقة [Observable<T> getWeatherForecast
  • السطر 103: واجهة [WebClient] هي التي توفر هذا الرد. نمرر إليها المعلمات الثلاثة التي تم استلامها في السطر 94. ولهذا السبب، يجب أن تحتوي هذه المعلمات على السمة final؛

2.8.2.4. [MainActivity]

  

نشاط [MainActivity] هو كما يلي:


package client.android.activity;
 
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
 
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new MeteoFragment_()};
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
  }
 
  @Override
  protected int getFirstView() {
    return 0;
  }
 
  // interface IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • يرجى ملاحظة أن الأسطر من 15 إلى 55 مضمنة افتراضيًا في مشروع [client-android-skel]. ما عليك سوى تخصيصها؛
  • الأسطر 37–40: مصفوفة الأجزاء. يوجد جزء واحد فقط هنا؛
  • الأسطر 43–46: لا يلزم وجود عناوين للأجزاء؛
  • الأسطر 48-50: لا توجد علامات تبويب هنا؛
  • الأسطر 52-55: العرض الأول الذي سيتم عرضه هو العرض رقم 0، وهو عرض [MeteoFragment
  • الأسطر 58–61: تنفيذ واجهة [IDao]. هنا، لا يوجد ما يمكن فعله سوى تفويض المهمة إلى طبقة [DAO] في السطر 21؛

2.8.2.5. الجزء [MeteoFragment]

  

تستعلم [MeteoFragment] عن خدمة الويب الخاصة بالطقس / JSON. وهيكلها الأساسي هو كما يلي:


package client.android.fragments;
 
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;
 
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
  • السطر 14: العرض [res/layout/meteo_fragment.xml] هو كما يلي:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
 
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Construisez votre interface visuelle"
    android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
    android:layout_marginTop="120dp"/>
</RelativeLayout>

تعرض طريقة العرض النص من السطر 10 فقط؛

  • السطر 15: القائمة [res / menu / menu_meteo.xml] هي كما يلي:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionMeteo"
        android:title="@string/actionMeteo"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionTerminer"
        android:title="@string/actionTerminer"/>
    </menu>
  </item>
</menu>
  • الأسطر 10-12: يُستخدم خيار القائمة هذا لطلب معلومات الطقس لمدينة ما؛
  • السطور 14-15: يُستخدم خيار القائمة هذا لإلغاء الطلب إذا كان قيد التنفيذ؛
  • الأسطر 16-18: يغلق خيار القائمة هذا التطبيق؛

فيما يلي الكود الكامل للجزء:


package client.android.fragments.behavior;
 
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;
 
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {
 
  // local data
  private int nbReponsesRecues;
 
  // gestion des événements ---------------------------------------------------------------------------------------
  // cities whose weather we want
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
 
  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // his country
    String country = "fr";
    // get a API login by creating an account [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL web service / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // start waiting for [paysDeLoire.length] asynchronous tasks
    beginWaiting(paysDeLoire.length);
    // number of responses received
    nbReponsesRecues = 0;
    // asynchronous calls are made in parallel
    for (String city : paysDeLoire) {
      // weather
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // exploiting the answer
          consumeResponse(response);
          // a + response
          nbReponsesRecues++;
        }
      });
    }
  }
 
  // exploitation server response
  private void consumeResponse(String response) {
    // log
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }
 
  // start of waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // parent
    beginRunningTasks(numberOfRunningTasks);
    // the [Cancel] option is displayed
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // displaying results
    String message;
    switch (nbReponsesRecues) {
      case 0:
        message = "Aucune réponse n'a été reçue";
        break;
      case 1:
        message = "Une réponse a été reçue. Consultez vos logs...";
        break;
      default:
        message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
        break;
    }
    Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
  }
 
  // private methods -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }
 
  // life cycle management ---------------------------------------------------------------------------------------
...
}
  • الأسطر 25-50: معالجة النقر على خيار القائمة [Weather
  • السطر 32: إنشاء عنوان URL لخدمة الويب / JSON لخدمة الطقس. ثم يتم تمرير هذا إلى طبقة [DAO] عبر النشاط؛
  • السطر 34: نبدأ الانتظار. نمرر عدد المهام المطلوب تشغيلها حتى تتمكن الفئة الأصلية من إخطارنا عند اكتمالها. هنا، هناك خمس مهام لأننا نطلب الطقس للمدن الخمس المدرجة في السطر 23؛
  • السطر 16: نحسب عدد الردود المستلمة حتى نتمكن من عرضها؛
  • الأسطر 38-50: نقوم بتكرار الدوران عبر المدن التي نريد معرفة حالة الطقس فيها؛
  • السطر 40: سنقوم بإجراء 5 طلبات HTTP بالتوازي؛
  • السطر 40: نطلب من الفئة الأصلية [AbstractParent] الاستعلام عن خدمة الويب / JSON؛
  • الأسطر 40-48: تتوقع طريقة [executeInBackground] معلمتين:
    • السطر 40: يتم توفير العملية المراد مراقبة وتنفيذها بواسطة طريقة [mainActivity.getWeatherForecast
    • الأسطر 40-48: مثيل [Action1] الذي سيتم تنفيذه عند استلام الرد من الخدمة غير المتزامنة. يجب أن يكون النوع T لـ [Action1<T>] هو النوع T لنتيجة طريقة [getWeatherForecast
  • السطر 44: تم استلام استجابة. يتم تمريرها إلى طريقة [consumeResponse] في السطر 53؛
  • السطر 46: يتم زيادة عداد الردود المستلمة؛
  • الأسطر 53-56: استهلاك استجابة JSON من خدمة الطقس؛
  • السطر 55: نقوم ببساطة بتسجيل سلسلة JSON؛
  • الأسطر 59-72: يتم تنفيذ الكود قبل بدء المهام غير المتزامنة؛
  • السطر 65: نمرر عدد المهام المطلوب تنفيذها إلى الفئة الأصلية [AbstractParent]. وهذا يسمح لها بإخطارنا عند انتهاء جميع المهام؛
  • الأسطر 67-70: إعداد القائمة للانتظار. نحتفظ فقط بخيار [Actions/Cancel]، الذي سيسمح للمستخدم بإلغاء المهام التي تم تشغيلها؛
  • الأسطر 74–92: يتم تنفيذ الكود عندما تُعلمنا الفئة الأصلية بأن جميع المهام التي تم تشغيلها قد اكتملت؛
  • السطر 77: نعيد تعيين القائمة إلى حالتها الأولية. تعرض طريقة [initMenu] (السطور 95-102) القائمة مع جميع خياراتها باستثناء خيار [Actions/Cancel]، الذي يتم إخفاؤه؛
  • الأسطر 80–91: يتم عرض عدد الردود المستلمة؛

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


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

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


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // we stop everything
    System.exit(0);
}

تتم إدارة دورة حياة الجزء من خلال الطرق التالية:


  // life cycle management ---------------------------------------------------------------------------------------
 
  @Override
  public CoreState saveFragment() {
    return new CoreState();
  }
 
  @Override
  protected int getNumView() {
    return 0;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // 1st visit?
    if (previousState == null) {
      initMenu();
    }
  }
 
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
}
  • الأسطر 3-6: تُستخدم لتخزين حالة الجزء في فئة مشتقة من [CoreState]. إذا لم يكن للجزء حالة لتخزينها، كما في هذه الحالة، فإننا نُرجع ببساطة مثيلًا من [CoreState]. لا تُرجع null، لأن هذا سيؤدي في النهاية إلى تعطل؛
  • الأسطر 8-11: يجب إرجاع معرف العرض. هنا، [MeteoFragment] له المعرف 0؛
  • الأسطر 13–16: تُستخدم لتهيئة الجزء بمجرد إنشائه (previousState == null) أو إعادة إنشائه (previousState != null). هنا، لا يوجد ما يمكن فعله. الحقل الوحيد الذي يمكن تهيئته هو التالي:

  // villes dont on veut la météo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

ولكنها تقوم بتهيئة نفسها؛

  • الأسطر 18–24: تُستخدم لتهيئة العرض المرتبط بالجزء بمجرد إنشائه (previousState == null) أو إعادة إنشائه (previousState != null
  • الأسطر 21-23: إذا كانت هذه هي الزيارة الأولى للجزء، يتم تهيئة قائمته لإخفاء خيار [Cancel
  • الأسطر 27–30: يتم استدعاؤها إذا تضمن التنقل إلى الجزء إجراء [إرسال]. هنا، لا يوجد تنقل بين الأجزاء نظرًا لوجود جزء واحد فقط؛
  • الأسطر 32-35: يتم استدعاؤها أثناء دورة الحفظ/الاستعادة بسبب دوران الجهاز أو لأي سبب آخر. هنا، نظرًا لعدم حفظ أي حالة، فلا يوجد ما يجب فعله؛
  • الأسطر 37–40: يتم استدعاؤها عند اكتمال جميع التحديثات السابقة. هنا، لا يوجد ما يجب القيام به؛

2.8.2.6. الاختبارات

نقوم الآن بتشغيل المثال:

Image

Image

السجلات هي كما يلي:


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

الآن، نرسل الطلب باستخدام معرّف API غير صحيح:


    String APIID = "";

Image

وتكون السجلات كما يلي:


07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
  • الأسطر 3-6، 10: تسببت المكالمات الخمس عبر HTTP في 5 استثناءات؛
  • السطر 7: يتلقى الجزء [MeteoFragment] الاستثناء الأول. ثم يقوم بإلغاء جميع المهام؛

الآن دعونا نضبط مهلة انتظار مدتها 5 ثوانٍ [IMainActivity.DELAY] ونلغي العملية. تكون السجلات عندئذٍ كما يلي:


07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
  • السطر 3: طلب الإلغاء؛
  • السطر 4: تم إلغاء الانتظار بسبب حدوث إلغاء؛
  • الأسطر 6–10: يؤدي إلغاء المهام إلى حدوث استثناء في كل من خيوط المهام الخمسة. يعتمد نوع الاستثناء على التطبيقات. الاستثناء هنا هو [java.lang.InterruptedException] لأن المهام تمت مقاطعتها أثناء تنفيذ تعليمات [Thread.sleep(delay)]، مما يجعلها تنتظر بشكل مصطنع لمدة [delay] مللي ثانية؛

2.8.3. المثال 16B

هنا نقوم بإعادة هيكلة المثال 16 من القسم 1.17. وهو يعرض جزءًا يقوم بإجراء مكالمات غير متزامنة إلى خادم أرقام عشوائية. دعونا نرى كيف يتصرف أثناء دوران الجهاز:

Image

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

Image

يمكننا أن نرى أننا فقدنا جميع رسائل الخطأ. سنحاول تحسين هذا.

2.8.3.1. مشروع المثال 16B

نقوم بنسخ مشروع [client-android-skel] إلى مشروع [examples/Example-16B]، ثم نقوم بتحميل المشروع الجديد:

  

من المشروع الأولي [مثال-16]، ننسخ العناصر التالية إلى [مثال-16B]:

  • الملف [res/layout/vue1.xml]، والمجلد [res/values]:
  

سنقوم بتغيير الهامش العلوي لعرض [vue1.xml] إلى 80 نقطة:


  <TextView
    android:id="@+id/txt_Titre2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="80dp"
    android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
  • الجزء [View1Fragment]:
  
  • الفئة [DAO / service / Response]:
  

في هذه المرحلة، يمكننا محاولة إجراء تجميع أولي:

  • النوع الأول من الأخطاء يتعلق بالاستيرادات. تم نقل بعض الفئات إلى حزم مختلفة أثناء الترحيل إلى [Example-16B]. نبدأ بإصلاح هذه الأخطاء؛
  • تم الإبلاغ عن نوع ثانٍ من الأخطاء في فئة [Vue1Fragment] لأنها لا تنفذ الطرق المطلوبة من قبل الفئة الأم [AbstractParent]. نقوم بإنشاء هذه الطرق تلقائيًا؛

نحاول إجراء تجميع ثانٍ:

  • تتركز الآن جميع الأخطاء المتبقية في فئة [Vue1Fragment]، وهي الفئة التي ستخضع لأكبر قدر من التغييرات؛

2.8.3.2. إنشاء حالة لجزء [Vue1Fragment]

لقد رأينا أن بعض المعلومات من الجزء ستحتاج إلى الحفظ أثناء الدوران من أجل إعادة الجزء إلى حالته قبل الدوران. لذلك نقوم بإنشاء حالة [Vue1FragmentState]، وهي فارغة في الوقت الحالي:

  

package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class Vue1FragmentState extends CoreState {
 
}

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

  

تسمح لك واجهة [IMainActivity] بتحديد خصائص معينة للمشروع:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 5000;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • السطور 25 و28 و31 و40: خصائص طبقة [DAO]. المصادقة الأساسية غير مطلوبة؛
  • السطر 34: تجاور الأجزاء. هنا، هذا الثابت غير ذي صلة نظرًا لوجود جزء واحد فقط؛
  • السطر 37: هذا ليس تطبيقًا مبوبًا؛
  • السطر 43: لا يوجد سوى جزء واحد؛

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


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • السطر 12: نعلن فئة حالة الجزء [Vue1Fragment

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


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}

إنها فارغة لأنه لا يوجد اتصال بين الأجزاء في هذا التطبيق.

2.8.3.4. طبقة [DAO]

  

في طبقة [DAO]، يجب تخصيص ثلاث فئات:

  • واجهة IDao؛
  • تنفيذ Dao الخاص بها؛
  • واجهة WebClient للتواصل مع خادم الويب / JSON؛

تأتي فئة [Response] من مشروع [Example-16]، الذي يستخدمها:


package client.android.dao.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

ستكون واجهة [WebClient] كما يلي:


package client.android.dao.service;
 
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
 
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
 
  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);
 
  // 1 random number in the range [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
 
}
  • السطران 18–19: عنوان URL لخدمة الأرقام العشوائية. لاحظ أن عنوان URL هذا نسبي بالنسبة لعنوان URL الجذر للعميل (RestClientRootUrl، السطر 12). هنا، عنوان URL الجذر هو [http://localhost:8080

وستكون واجهة [IDao] كما يلي:


package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // random number service
  Observable<Response<Integer>> getAlea(int a, int b);
 
}
  • لاحظ أن الطرق الموجودة في الأسطر 6–22 موجودة بشكل افتراضي في واجهة IDao لمشروع [client-android-skel
  • السطر 25: تُرجع الطريقة [getAlea] رقمًا عشوائيًا في النطاق [a,b]. يتم إرجاع هذا الرقم في استجابة [Response<Integer>]، حيث يوجد الرقم العشوائي في حقل [body] من هذا النوع؛

يتم تنفيذ واجهة [IDao] بواسطة فئة [Dao] التالية:


package client.android.dao.service;
 
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we build the restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // set the URL of the web service
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    // the user is registered in the interceptor
    authInterceptor.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // factory configuration
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // random number service
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // web client execution
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }
 
}
  • لاحظ أن الأسطر 17–85 مضمنة افتراضيًا في فئة [Dao] لمشروع [client-android-skel]. ما عليك سوى إضافة الطرق لتنفيذ واجهة [IDao
  • الأسطر 88–97: تنفيذ طريقة [getAlea]. هذا بسيط للغاية ويشغل 6 أسطر، الأسطر 91–96؛
  • السطر 91: الطريقة [getResponse] هي طريقة للفئة الأصلية [AbstractDao]. وهي تتوقع معلمة من النوع [IRequest<T>]، حيث T هو نوع الاستجابة المتوقعة، وفي هذه الحالة هو نوع Response<Integer>. يجب أن يكون النوع T لـ [IRequest<T>] (السطر 91) هو النوع T للطريقة [Observable<T> getAlea] (السطر 89)؛
  • تحتوي واجهة [IRequest<T>] على طريقة واحدة فقط: getResponse. وتتمثل مهمتها في توفير الاستجابة من النوع T التي يجب أن تعيدها الطريقة [Observable<T> getAlea
  • السطر 94: واجهة [WebClient] هي التي توفر هذا الرد. يتم تمرير المعلمتين المستلمتين في السطر 89 إليها. ولهذا السبب، يجب أن يكون لهما السمة final؛

2.8.3.5. [MainActivity]

  

نشاط [MainActivity] هو كما يلي:


package client.android.activity;
 
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
 
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continue the initializations begun by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // define fragments here
    return new AbstractFragment[]{new Vue1Fragment_()};
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // tabbed browsing - define the view to be displayed
  }
 
  @Override
  protected int getFirstView() {
    return 0;
  }
 
  // interface IDao ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }
 
}
  • يرجى ملاحظة أن الأسطر من 15 إلى 61 مضمنة بشكل افتراضي في مشروع [client-android-skel]. ما عليك سوى تخصيصها؛
  • الأسطر 40–44: مصفوفة الأجزاء. يوجد جزء واحد فقط هنا؛
  • الأسطر 47–51: لا حاجة لعناوين الأجزاء؛
  • الأسطر 53–56: لا توجد علامات تبويب هنا؛
  • الأسطر 58-61: العرض الأول الذي سيتم عرضه هو العرض رقم 0، وهو [Vue1Fragment
  • الأسطر 64-67: تنفيذ واجهة [IDao]. هنا، لا يوجد شيء للقيام به سوى تفويض المهمة إلى طبقة [DAO] في السطر 23؛

2.8.3.6. حالة جزء [Vue1Fragment]

  

ستكون فئة [Vue1FragmentState] كما يلي:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
import java.util.ArrayList;
import java.util.List;
 
public class Vue1FragmentState extends CoreState {
 
  // fragment status ------------------------
  // list of answers
  private List<String> reponses = new ArrayList<>();
  // condition view ------------------------
  // error msg on the number of random numbers requested
  private boolean txtErrorAleasVisible = false;
  // error msg on generation interval [a,b]
  private boolean txtErrorIntervalleVisible = false;
  // error msg on the URL of the web service
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // waiting time error msg
  private boolean textViewErreurDelayVisible = false;
  // whether or not the Execute button is visible
  private boolean btnExecuterVisible = true;
 
  // getters and setters
...
}

لتحديد ما يجب حفظه في الجزء، قمنا بتدوير الجهاز في حالات مختلفة ولاحظنا ما فقد عند الاستعادة. وخلصنا إلى أن المعلومات الموجودة في الأسطر 10–23 يجب حفظها.

2.8.3.7. الجزء [View1Fragment]

  

في الوقت الحالي، تحتوي طريقة العرض [Vue1Fragment] على أخطاء متنوعة بسبب تغيير الفئة الأصلية [AbstractFragment] التي تنحدر منها. وبدلاً من وصف التغييرات المطلوب إجراؤها واحدة تلو الأخرى، سنعلق مباشرةً على النسخة النهائية.

هيكل العنصر الفرعي كما يلي:


package client.android.fragments.behavior;
 
import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
 
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {
 
...
}
  • السطر 26: لاحظ أن كل جزء يجب أن يحتوي على قائمة، حتى لو كانت فارغة. وهذا هو الحال هنا.

2.8.3.7.1. معالجة النقر على زر [Execute]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // check the data entered
    if (!isPageValid()) {
      return;
    }
    // delete previous answers
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // reset the response counter to 0
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // activity initialization
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // prepare the random task
    beginWaiting(1);
    // we ask for the random numbers
    getAleasInBackground(nbAleas, a, b);
  }
 
  void getAleasInBackground(int nbAleas, int a, int b) {
    // create the process to be observed
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // we ask for the random numbers
    executeInBackground(process, new Action1<Response<Integer>>() {
 
      @Override
      public void call(Response<Integer> response) {
        // we consume the answer
        consumeAleaResponse(response);
      }
    });
  }
 
  protected void consumeAleaResponse(Response<Integer> response) {
    // log
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // a + response
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // we analyze the response
    // mistake?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // cancellation
      doAnnuler();
      // back to Ui
      return;
    }
    // we add the information to the list of answers
    reponses.add(0, String.valueOf(response.getBody()));
    // refreshing the answers
    dataAdapterReponses.notifyDataSetChanged();
  }
 
  // cancellation ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
 
  private void beginWaiting(int nbRunningTasks) {
    // we set the hourglass
    beginRunningTasks(nbRunningTasks);
    // the [Cancel] button replaces the [Execute] button
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • الأسطر 4-6: أولاً، نتحقق من صحة الإدخالات. قد تظهر بعد ذلك رسائل خطأ؛
  • السطور 8-9: يتم مسح قائمة الردود. ينعكس هذا التغيير في ListView التي تعرضها؛
  • السطور 11-12: يتم إعادة تعيين عدد الردود المستلمة إلى صفر؛
  • السطر 14: نحدد عنوان URL لخدمة الأرقام العشوائية. سيتم تمرير هذه المعلومات إلى طبقة [DAO
  • السطر 15: يتم تعيين فترة المهلة قبل إرسال الطلب إلى خدمة الأرقام العشوائية. سيتم تمرير هذه المعلومات إلى طبقة [DAO
  • السطر 17: نستعد لتشغيل مهمة واحدة غير متزامنة (وليس N؛ سنرى السبب لاحقًا)؛
  • الأسطر 24-27: ندمج المهام غير المتزامنة N في تسلسل واحد من العمليات [دمج]؛
  • الأسطر 29-36: نطلب من الفئة الأصلية [AbstractParent] الاستعلام عن خدمة الويب للأرقام العشوائية / JSON؛
  • الأسطر 29-36: تتوقع طريقة [executeInBackground] معلمتين:
    • السطر 29: العملية التي سيتم ملاحظتها وتنفيذها هي تلك التي تم حسابها في الأسطر السابقة؛
    • الأسطر 29–36: مثيل [Action1] الذي سيتم تنفيذه عند استلام الاستجابة من الخدمة غير المتزامنة. يجب أن يكون النوع T لـ [Action1<T>] هو النوع T لنتيجة طريقة [getAlea]، أي نوع [Response<Integer>]؛
  • السطر 34: عند وصول استجابة (رقم عشوائي)، يتم استهلاكها في الطريقة في السطر 39؛
  • السطور 49-50: نسجل ونشير إلى أنه تم استلام استجابة جديدة؛
  • الأسطر 53-60: يحتوي النوع [Response<T>] على حقل [status] وهو رمز خطأ. إذا كان هذا الرمز غير صفر، فهذا يعني أن الخادم واجه مشكلة؛
  • السطر 55: يتم عرض رسالة خطأ. تنتمي الطريقة [showAlert] إلى الفئة الأصلية؛
  • السطر 57: يتم استدعاء الطريقة الموجودة في الأسطر 68-75. ستقوم بإلغاء أي مهام لا تزال نشطة (السطر 74)؛
  • السطر 62: تتم إضافة الاستجابة إلى قائمة الاستجابات، والتي تمثل مصدر البيانات لـ ListView؛
  • السطر 64: يتم تحديث ListView؛
  • الأسطر 77-83: تقوم الطريقة [beginWaiting(int nbRunningTasks)] بإعداد العرض للانتظار (الأسطر 81-82) وإخطار الفئة الأصلية بأن [nbRunningTasks] المهام ستُنفذ قريبًا (السطر 79)؛

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

تتم إدارة دورة حياة الجزء من خلال الطرق التالية:


  // local data
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // life cycle management ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // current view status
    Vue1FragmentState state = new Vue1FragmentState();
    state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
    state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
    state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
    state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
    state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
    state.setReponses(reponses);
    return state;
  }
 
  @Override
  protected int getNumView() {
    return 0;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // 1st visit?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // listView data source
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // nre of responses
    nbReponses = reponses.size();
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // listview / adapter link
    listReponses.setAdapter(dataAdapterReponses);
    // 1st visit?
    if (previousState == null) {
      // hide error messages
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // buttons
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // previous view status
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // show / hide error msg
    txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
    textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
    // buttons
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // no. of responses
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // the [Execute] button replaces the [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
 
}
  • الأسطر 7–18: تضمن حفظ الجزء عندما تطلبه الفئة الأم؛
  • السطر 11: يعرض رسالة الخطأ المتعلقة بانتهاء المهلة؛
  • السطر 12: إظهار رسالة الخطأ المتعلقة بعدد الأرقام العشوائية المطلوبة؛
  • السطر 13: ظهور رسالة الخطأ المتعلقة بعنوان URL لخدمة الويب / JSON؛
  • السطر 14: يعرض رسالة الخطأ المتعلقة بنطاق [a,b] لتوليد الأرقام العشوائية؛
  • السطر 15: ظهور زر [تشغيل]؛
  • السطر 16: قائمة الردود المستلمة؛
  • الأسطر 20-23: يجب إرجاع معرف العرض. معرف الجزء هنا هو 0 نظرًا لوجود جزء واحد فقط؛
  • الأسطر 25-38: تهيئة حقول الجزء، إما عند الزيارة الأولى (previousState == null) أو عند زيارة لاحقة؛
    • السطور 29-30: إذا لم تكن هذه هي الزيارة الأولى، يتم استعادة حقل [reponses] من الحالة السابقة للجزء؛
    • الأسطر 31-33: إذا كانت هذه هي الزيارة الأولى، يتم تهيئة حقل [reponses] بقائمة فارغة؛
    • الأسطر 34-37: باستخدام حقل [reponses]، يمكننا إنشاء مصدر البيانات لـ ListView الخاص بالجزء (السطر 35) بالإضافة إلى عدد الردود (السطر 37)؛
  • الأسطر 40-55: يتم تنفيذها لتهيئة العرض المرتبط بالجزء، سواء في الزيارة الأولى (previousState == null) أو في زيارة لاحقة؛
    • السطر 43: يتم ربط ListView الخاص بالجزء بمصدر البيانات الذي تم إنشاؤه للتو في طريقة [initFragment
    • الأسطر 45-54: إذا كانت هذه هي الزيارة الأولى، يتم تجهيز العرض لعرضه لأول مرة؛
  • الأسطر 57-60: يتم تنفيذها أثناء التنقل بين الأجزاء المرتبط بعملية [SUBMIT]. هنا، يوجد جزء واحد فقط وبالتالي لا يوجد تنقل بين الأجزاء؛
  • الأسطر 63-76: يتم تنفيذها أثناء التنقل بين الأجزاء المرتبط بعملية [NAVIGATION] أو أثناء دورة الحفظ/الاستعادة بسبب دوران الجهاز أو لأي سبب آخر. هنا، يمكن أن تحدث الحالة الأخيرة فقط. تذكر أنه هنا، في جميع الحالات، يكون [previousState] دائمًا غير فارغ؛
  • السطر 65: يتم تحويل الحالة السابقة إلى نوع حالة الجزء؛
  • الأسطر 66-75: يتم استخدام محتويات الحالة السابقة لاستعادة العرض؛
  • الأسطر 78-81: يتم استدعاؤها عند اكتمال جميع التحديثات السابقة. هنا، لا يوجد ما يجب فعله؛
  • الأسطر 83-89: يتم تنفيذها عند اكتمال جميع المهام غير المتزامنة. هنا، يتم إخفاء زر [Cancel] واستبداله بزر [Execute

2.8.3.8. الاختبارات

ندعو القارئ إلى إجراء الاختبارات التالية:

  • إنشاء أخطاء وتشغيل الجهاز: يجب أن تظل رسائل الخطأ معروضة؛
  • إنشاء أرقام عشوائية وتشغيل الجهاز: يجب أن تظل الأرقام العشوائية التي تم إنشاؤها معروضة؛
  • تعيين فترة انتظار لعدة ثوانٍ وتشغيل الجهاز أثناء الانتظار: يجب أن تكون المهام قد ألغيت (يمكن رؤية ذلك في السجلات)؛

2.8.4. المثال-22B

هنا نعيد النظر في المثال 22 لإعادة هيكلته وفقًا لنموذج مشروع [client-android-skel]. تذكر أن مشروع [المثال-22] يتعامل بشكل صحيح مع دورة حفظ/استعادة الأجزاء أثناء الدوران وأنه كان بمثابة الأساس لمشروع [client-android-skel].

نقوم بنسخ مشروع [client-android-skel] إلى [examples/Example-22B] ونقوم بتحميل المشروع الأخير:

  

ثم ننسخ عناصر مختلفة من مشروع [Example-22] إلى مشروع [Example-22B].

أولاً، ننسخ العناصر من المجلد [res]:

  • [layout/fragment_main.xml، layout/view1.xml، menu/menu_fragment.xml، menu/menu_main.xml، مجلد [values
  

سنقوم بتغيير الهامش العلوي لكلا العرضين إلى 120 نقطة:

[view1.xml]:


  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginTop="120dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>

[fragment_main]:


  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>

بعد ذلك، ننسخ العناصر [View1Fragment، PlaceHolderFragment، PlaceHolderFragmentState]:

 

في هذه المرحلة، يمكننا محاولة إجراء التجميع الأول. يظهر نوع أول من الأخطاء: استيرادات غير صحيحة لأن الفئات قد غيرت الحزم. نقوم بتصحيح هذه الاستيرادات. النوع الثاني من الأخطاء يرجع إلى حقيقة أن الأجزاء لا تنفذ جميع أساليب فئتها الأم [AbstractFragment]. نقوم بتصحيح هذا بالضغط على (Alt+Enter).

تنبع الأخطاء المتبقية من الاختلافات بين الفئات القديمة والجديدة [AbstractFragment]. في الوقت الحالي، نتجاهلها.

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

  

يحتوي المجلد [custom] على عناصر بنية يمكن للمطور تخصيصها.

تسمح لك واجهة [IMainActivity] بتحديد خصائص معينة للمشروع:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 0;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = true;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of fragments
  int FRAGMENTS_COUNT = 5;
 
}
  • السطور 23 و26 و29 و38: خصائص طبقة [DAO]. لا توجد أي خصائص هنا؛
  • السطر 41: توجد خمسة أجزاء هنا؛
  • السطر 32: تجاور الأجزاء. يمكن أن تأخذ هذه الثابتة قيمة بين [1،4] هنا. يُنصح القارئ بتغيير هذه القيمة لمعرفة ما إذا كان التطبيق سيستمر في العمل؛
  • السطر 35: هذا تطبيق يعمل بنظام علامات التبويب؛

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


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • السطر 12: نعلن فئة حالة الجزء [PlaceHolderFragment]. الجزء [Vue1Fragment] نفسه ليس له حالة؛

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


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // data to be shared between fragments themselves and between fragments and activities
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
  // don't forget the getters and setters required for serialization / deserialization jSON
 
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in second tab
  private int numFragment = -1;
 
  // getters and setters
...
}

هذه هي الجلسة الخاصة بمشروع [Example-22].

2.8.4.2. [MainActivity]

  

نشاط [MainActivity] هو كما يلي:


package client.android.activity;
 
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
 
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // menu management-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }
 
  private void showFragment(int i) {
...
  }
 
  // implementation of parent class methods ---------------------------------------------------
  ...
}

هنا، تكون فئة [MainActivity] أكبر من الفئة في الأمثلة السابقة لسببين:

  • هناك علامات تبويب يجب إدارتها؛
  • هناك قائمة يجب إدارتها؛

2.8.4.2.1. تنفيذ أساليب الفئة الأم

// methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continue the initializations begun by the parent class
    // session
    this.session = (Session) super.session;
    ...
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // fragment no
    final String ARG_SECTION_NUMBER = "section_number";
    // initialization of fragment table
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // create a fragment
      fragments[i] = new PlaceholderFragment_();
      // you can pass arguments to the
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // a fragment of +
    fragments[i] = new Vue1Fragment_();
    // result
    return fragments;
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // no titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
...
  }
 
  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • الأسطر 2–12: يتم استدعاء الأسلوب [onCreateActivity] بواسطة الفئة الأصلية [AbstractActivity] عند إنشاء النشاط لأول مرة أو إعادة إنشائه أثناء دورة الحفظ/الاستعادة. عند استدعاء هذا الأسلوب، تكون الفئة الأصلية قد استعادت الجلسة بالفعل؛
  • السطر 10: يتم استرداد مرجع محلي للجلسة. يعد تحويل النوع ضروريًا لأن جلسة الفئة الأصلية من النوع [AbstractSession
  • الأسطر 19-38: يجب أن تعيد طريقة [getFragments] إلى الفئة الأصلية مصفوفة الأجزاء التي يديرها التطبيق. يوجد هنا [FRAGMENTS_COUNT] جزء، وهو رقم محدد في [IMainActivity]. الأجزاء [FRAGMENTS_COUNT-1] الأولى من النوع [PlaceHolderFragment] والأخير من النوع [Vue1Fragment
  • الأسطر 41–45: يجب أن تُرجع الطريقة [getFragmentTitle] عناوين الأجزاء عندما تكون هذه المعلومات مفيدة. وهذا ليس هو الحال هنا؛
  • الأسطر 47-50: يتم استدعاء هذه الطريقة من قبل الفئة الأم عندما ينقر المستخدم على علامة تبويب. سنعود إلى هذا في القسم التالي؛
  • الأسطر 52–55: تُرجع رقم العرض الأول الذي سيُعرض عند بدء تشغيل التطبيق. هنا، يجب عرض جزء [Vue1Fragment] أولاً. يمكن استبدال طريقة [getFirstView] بثابت في [IMainActivity

2.8.4.2.2. إدارة علامات التبويب

تتم إدارة علامات التبويب باستخدام الطرق التالية:


@Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continue the initializations begun by the parent class
    // session
    this.session = (Session) super.session;
    // 1st tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2nd tab ?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // fragment number to display
    int numFragment;
    switch (position) {
      case 0:
        // fragment no. [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // fragment no. [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // fragment display
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • السطور 1–20: يتم استدعاء الطريقة [onCreateActivity] بواسطة الفئة الأصلية [AbstractActivity] عند إنشاء النشاط لأول مرة أو إعادة إنشائه أثناء دورة الحفظ/الاستعادة. عند استدعاء هذه الطريقة، تكون الفئة الأصلية قد استعادت الجلسة بالفعل؛
  • السطر 9: يتم استرداد مرجع محلي للجلسة. يعد تحويل النوع ضروريًا لأن جلسة الفئة الأصلية من النوع [AbstractSession
  • الأسطر 11-13: يتم إنشاء علامة التبويب الأولى؛
  • الأسطر 15-20: يتم إنشاء علامة التبويب الثانية إذا تم تخزين معرف جزء في الجلسة (السطر 15). يتم تعيين هذا المعرف مبدئيًا على -1 عند إنشاء النشاط لأول مرة؛
  • الأسطر 23-39: يتم استدعاء هذه الطريقة بواسطة الفئة الأصلية عندما ينقر المستخدم على علامة تبويب؛
  • الأسطر 28-31: إذا تم النقر على علامة التبويب 0، فيجب عرض [Vue1Fragment]. نعلم أن هذه هي أول طريقة عرض تم عرضها عند بدء تشغيل التطبيق؛
  • الأسطر 32-35: إذا تم النقر على علامة التبويب 1، فيجب عرض الجزء الذي تم تخزين رقمه في الجلسة؛
  • الأسطر 37–39: ننتقل إلى الجزء المحدد. الإجراء المرتبط هو [SUBMIT]. هل كان من الممكن أن يكون [NAVIGATION]؟ في هذا المستند، نستخدم [NAVIGATION] فقط عندما يتطلب عرض الجزء الجديد معرفة حالته السابقة فقط. هنا، ليس هذا هو الحال، حيث يجب أن يتغير عرض الجزء عن حالته السابقة لإظهار زيارة أخرى؛

2.8.4.2.3. إدارة القائمة

يرتبط النشاط بالقائمة التالية [menu_main.xml]:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment1"
        android:title="@string/fragment1"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment2"
        android:title="@string/fragment2"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment3"
        android:title="@string/fragment3"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment4"
        android:title="@string/fragment4"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>

الذي يعرض ما يلي:

  

تتم إدارة القائمة بالطرق التالية:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // processing menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d(className, "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
    }
    // item processed
    return true;
  }
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // no navigation on software tab selection
      session.setNavigationOnTabSelectionNeeded(false);
      // we recreate the two tabs for a title font issue
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // the fragment number to be displayed is set in session
      session.setNumFragment(i);
      // select tab 2 with navigation
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • الأسطر 16-31: معالجة النقر على خيار قائمة [Fragmenti
  • الأسطر 37–50: عرض الجزء #i (هذه أجزاء من نوع PlaceHolderFragment) في علامة التبويب #1 (علامة التبويب الثانية)؛
  • الأسطر 42-44: قررنا إزالة علامات التبويب الموجودة لإنشاء علامتي تبويب جديدتين. تم اتخاذ هذا القرار للتغلب على المشكلة التالية: عندما نعرض المقتطف ببساطة في علامة التبويب 1 الموجودة (دون حذفها)، يظهر عنوانه (الخط والحجم) بشكل غريب مختلفًا عن عنوان علامة التبويب 0؛
  • السطور 43–44: يتم إنشاء علامتي التبويب ولكن لا يتم تحديدهما (تم تعيين المعلمة الأخيرة على false
  • السطر 40: قد تؤدي العمليات في الأسطر 42-44 إلى تشغيل عمليات [select] على علامات التبويب، مما سيؤدي إلى استدعاء معالج [onTabSelected]. إذا لم يتم اتخاذ أي إجراء، فسيؤدي ذلك إلى الانتقال إلى جزء. نمنع ذلك عن طريق تعيين القيمة المنطقية [navigationOnTabSelectionNeeded] على false في الجلسة. يتم إعادة تعيين هذه القيمة المنطقية تلقائيًا إلى true بواسطة فئة [AbstractFragment] عندما يصبح الجزء مرئيًا؛
  • السطر 46: نقوم بتخزين رقم الجزء المراد عرضه في الجلسة؛
  • الأسطر 48-50: حدد علامة التبويب رقم 2 مع التنقل (السطر 48). سيؤدي ذلك إلى تشغيل الإجراء [onTabSelected]، والذي سيقوم بما يلي:
    • يعرض الجزء الذي تم تخزين رقمه في الجلسة؛
    • تخزين رقم علامة التبويب المحددة في الجلسة؛

2.8.4.3. الجزء [Vue1Fragment]

إليك النسخة النهائية من الجزء:


package client.android.fragments.behavior;
 
import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  // fragment life cycle -----------------------------------------------
  private void initFragment() {
    // nothing to do
  }
 
  // save fragment status
  @Override
  public CoreState saveFragment() {
    // view status - nothing to save
    return new CoreState();
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // nothing to do
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // 1st visit?
    if (previousState == null) {
      // the visit number is displayed
      showNumVisit();
    }
 
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // the visit number is displayed
    showNumVisit();
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
 
  }
 
  // private methods -------------------------------------
  // display visit no
  private void showNumVisit() {
    // increment visit no
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // the visit number is displayed
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

الفصل شبه فارغ.

  • الأسطر 35-39: يتم استدعاؤها من قبل الفئة الأم عندما تحتاج القطعة إلى حفظ حالتها. لا تحتوي قطعة [Vue1Fragment] على حالة لحفظها. نحن ببساطة نُرجع مثيلًا من الفئة الأساسية [CoreState] (تذكير: يجب ألا نُرجع null
  • الأسطر 41-44: يجب إرجاع معرف الجزء. حسب التصميم، يمتلك الجزء [Vue1Fragment] المعرف [FRAGMENTS_COUNT-1
  • الأسطر 51-59: يتم استدعاؤها بواسطة الفئة الأصلية عند إنشاء الجزء لأول مرة (previousState == null) أو في الزيارات اللاحقة (previousState != null
    • الأسطر 54-57: إذا كانت هذه هي الزيارة الأولى، فقم بزيادة عدد الزيارات وعرضه (الأسطر 85-92)؛
  • الأسطر 61-65: يتم استدعاؤها عندما يكون الجزء على وشك العرض بالاقتران مع إجراء [SUBMIT]. يتم زيادة عدد الزيارات وعرضه. هنا، لا يمكن زيادة عدد الزيارات مرتين خلال دورة الحياة. في الواقع، تحدث الزيارة الأولى للجزء [Vue1Fragment] عند بدء تشغيل التطبيق عندما يتم تعيين الإجراء على [NONE] حسب التصميم في الجلسة. وهذا يضمن عدم استدعاء طريقة [updateOnSubmit]. بعد ذلك، لن تكون هذه الزيارة هي الزيارة الأولى مرة أخرى، ولن تقوم طريقة [initView] بأي شيء؛
  • الأسطر 68-71: يتم استدعاؤها أثناء دورة الحفظ/الاستعادة. نظرًا لأن الجزء ليس له حالة، فلا يوجد شيء لاستعادته هنا؛
  • الأسطر 73-76: يتم استدعاؤها عند اكتمال جميع التحديثات السابقة. هنا، لم يتبق شيء للقيام به؛
  • الأسطر 78-81: يتم استدعاؤها عند اكتمال جميع المهام غير المتزامنة التي تم تشغيلها. هنا، لا توجد مهام غير متزامنة؛

2.8.4.4. حالة [PlaceHolderFragmentState]

ستكون حالة الجزء [PlaceHolderFragment] كما يلي:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class PlaceHolderFragmentState extends CoreState {
  // text
  private String text;
 
  // manufacturers
  public PlaceHolderFragmentState() {
 
  }
 
  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }
 
  // getters and setters
 ...
}
  • عندما نحتاج إلى حفظ حالة الجزء، سنحفظ النص الذي كان يعرضه (السطر 7)؛

2.8.4.5. الجزء [PlaceHolderFragment]

سيكون المقطع [PlaceHolderFragment] كما يلي:


package client.android.fragments.behavior;
 
import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
 
  // visual interface components
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;
 
  // data
  private String text;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // implementation of parent class methods ----------------------------
  @Override
  public CoreState saveFragment() {
    // save fragment state
    PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
    placeHolderFragmentState.setText(textViewInfo.getText().toString());
    return placeHolderFragmentState;
  }
 
  @Override
  protected int getNumView() {
    return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // original text
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }
 
  @Override
  protected void initView(CoreState previousState) {
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // update the text displayed
    // increment visit no
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
    }
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restore displayed text
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
 
  }
 
}
  • الأسطر 30–36: عندما تطلب الفئة الأم من الجزء حفظ حالته، يتم حفظ النص المعروض بواسطة الجزء (السطر 34)؛
  • الأسطر 38–41: إرجاع معرف الجزء. يعتمد هذا على معرف القسم الذي تم تمريره كحجة عند إنشاء الجزء؛
  • الأسطر 43-47: يتم استدعاؤها أثناء الإنشاء الأول للجزء (previousState == null) أو أثناء عمليات الإنشاء اللاحقة (previousState != null
    • السطر 46: هنا، لا يتم استخدام الحالة السابقة. يتم إعادة حساب النص الأولي [text] (السطر 24) المعروض عند الزيارة الأولى في كل مرة. هذا أمر قابل للنقاش. كان بإمكاننا اختيار تضمين هذه المعلومات أيضًا في حالة الجزء؛
  • الأسطر 49–51: يتم استدعاؤها أثناء العرض الأول للطريقة المرتبطة بالجزء (previousState == null) أو أثناء عمليات العرض اللاحقة (previousState != null). لا يوجد ما يجب فعله؛
  • الأسطر 53-56: يتم استدعاؤها عندما يكون الجزء على وشك العرض بالارتباط مع إجراء [SUBMIT]. هذا هو الحال دائمًا باستثناء دورة الحفظ/الاستعادة، حيث يكون الإجراء هو [RESTORE]. لذلك نقوم بزيادة رقم الزيارة وعرضه؛
  • الأسطر 68–74: يتم استدعاؤها أثناء دورة الحفظ/الاستعادة. نقوم باستعادة النص الذي تم حفظه في حالة الجزء؛
  • الأسطر 76–79: يتم استدعاؤها عند اكتمال جميع التحديثات السابقة. هنا، لا يوجد شيء آخر يمكن القيام به؛
  • السطور 82-83: يتم استدعاؤها عند اكتمال جميع المهام غير المتزامنة التي تم تشغيلها. هنا، لا توجد مهام غير متزامنة؛

2.8.4.6. الاختبارات

ندعو القارئ إلى اختبار التطبيق عن طريق تدوير الجهاز للتحقق من أن الجزء المعروض لا يفقد حالته. سنقوم أيضًا بفحص السجلات.

2.9. الخلاصة

في نهاية هذا الفصل، لدينا مشروع نموذجي [client-android-skel] لعميل Android يتواصل مع خدمة ويب / JSON مع الميزات التالية:

  • يتم التعامل مع الاتصال غير المتزامن مع خادم الويب/JSON باستخدام مكتبة RxJava؛
  • يتم إدارة دورة حياة الجزء (التحديث، الحفظ، الاستعادة) بواسطة فئته الأم [AbstractFragment]، التي تستدعي طرقًا محددة من فئاته الفرعية في لحظات محددة. وبالتالي، لا يحتاج الجزء الفرعي إلى الاهتمام بمراحل دورة الحياة، بل فقط بتنفيذ طرق معينة تتطلبها فئته الأم؛
  • يتم إدارة دورة حياة النشاط (الحفظ / الاستعادة) بواسطة فئة مجردة [AbstractActivity]، والتي تتطلب أيضًا من النشاط الفرعي تنفيذ طرق معينة؛
  • يمكن لفئة [AbstractActivity] التعامل مع تطبيق مع أو بدون علامات تبويب، مع أو بدون صورة تحميل، ومع أو بدون مصادقة أساسية مقابل خادم الويب / JSON. ويتم تحديد وجود أو عدم وجود هذه العناصر من خلال التكوين؛

سنقدم الآن دراسة حالة أكثر تعقيدًا من الأمثلة السابقة. سيستند التطبيق الجديد إلى مشروع القالب [client-android-skel].