Skip to content

2. اسکلت یک کلاینت اندروید که با یک سرویس وب / jSON ارتباط برقرار می‌کند

اکنون یک اسکلت برنامه اندروید ارائه می‌دهیم که با یک یا چند سرویس وب / jSON ارتباط برقرار می‌کند. این پروژه [client-android-skel] است که در پوشه مثال‌های [architecture] یافت می‌شود:

  

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

  • ارتباط غیرهمزمان با سرور وب / jSON توسط کتابخانه RxJava مدیریت می‌شود؛
  • چرخه عمر یک فرگمنت (به‌روزرسانی، ذخیره، بازیابی) توسط کلاس والد آن، [AbstractFragment]، مدیریت می‌شود که در زمان‌های مشخصی متدهای خاصی را در کلاس‌های فرزند خود فراخوانی می‌کند. بنابراین، کلاس فرزند نیازی ندارد که خود را درگیر مراحل چرخه عمر کند، بلکه تنها باید متدهای خاصی را که توسط کلاس والدش مورد نیاز است، پیاده‌سازی نماید؛
  • چرخه عمر فعالیت (ذخیره / بازیابی) توسط یک کلاس انتزاعی، [AbstractActivity]، مدیریت می‌شود که از فعالیت فرزند نیز می‌خواهد روش‌های خاصی را پیاده‌سازی کند؛
  • کلاس [AbstractActivity] قادر به مدیریت یک برنامه با یا بدون تب‌ها، با یا بدون تصویر بارگذاری، و با یا بدون احراز هویت پایه در برابر سرور وب / jSON است. وجود یا عدم وجود این عناصر توسط پیکربندی تعیین می‌شود؛

از این قالب برای تمام مثال‌های بعدی استفاده شده است. به دلیل تنوع این مثال‌ها، آنچه برای یکی کارساز بود ممکن است برای دیگری کارساز نباشد. از آنجایی که این قالب برای مجموعاً هفت مثال استفاده شد، تکرارهای متعددی صورت گرفت. اگر از آن برای مثال هشتم استفاده می‌شد، ممکن بود ماهیت خاص این مثال جدید بار دیگر به خطاهای جدیدی منجر شود. با این وجود، استفاده از این قالب نوشتن مثال‌های آینده را به طور قابل توجهی ساده خواهد کرد. در واقع، مدیریت چرخه عمر یک فرگمنت (به‌روزرسانی، ذخیره، بازیابی)، همراه با مفهوم مجاورت فرگمنت، به‌ویژه پیچیده است. در اینجا، این موضوع کاملاً در کلاس [AbstractFragment] پنهان شده است.

2.1. معماری کلاینت اندروید

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

  • لایه [DAO] رابط [IDao] را پیاده‌سازی می‌کند. این لایه است که با وب سرور / jSON ارتباط برقرار می‌کند؛
  • فقط یک فعالیت وجود دارد که آن نیز رابط [IDao] را پیاده‌سازی می‌کند. ویوها برای دسترسی به سرور به آن فراخوانی می‌کنند؛
  • ویوها توسط فرگمنت‌ها پیاده‌سازی شده‌اند؛

پروژه اندروید این معماری را منعکس می‌کند:

  

اکنون اجزای مختلف این پروژه را یکی‌یکی معرفی خواهیم کرد.

2.2. پیکربندی Gradle

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // از نسخهٔ 0.11 افزونهٔ Gradle اندروید، باید از 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'
    }
  }

  // گزینه‌های بسته‌بندی مورد نیاز برای تولید 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'
  }
}
  • تمام شماره‌های نسخه ممکن است تغییر کنند. با این حال، اگر اندروید استودیو را طوری پیکربندی کنیم که اطمینان حاصل شود این نسخه‌های ابزارهای اندروید (خطوط ۱۵–۱۶، ۴۷–۴۸) موجود هستند (به بخش ۶.۱۱ مراجعه کنید)، می‌توانیم از اعداد فعلی به عنوان نقطه شروع استفاده کنیم؛

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>
  • خط ۳: بستهٔ برنامه را تغییر دهید؛
  • خطوط ۱۰ و ۱۵: مقدار ورودی [app_name] را در فایل [res / values / strings.xml] تنظیم کنید. فعلاً، به این صورت است:

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- نام برنامه -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. ساختار کد جاوا

  
  • [architecture] عناصر سازماندهی اصلی کد را در کنار هم قرار می‌دهد؛
  • [activity] شامل تنها فعالیت برنامه است؛
  • [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>

  <!-- ظرفیت قطعه -->
  <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>
  • خط ۲۹: از یک کانتینر قطعهٔ مشخص استفاده می‌شود؛

فعالیت همچنین یک منو ([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 {

  // کنترل‌های کشیدن
  private boolean isSwipeEnabled;
  // کنترل پیمایش
  private boolean isScrollingEnabled;

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

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

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

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

  //کنترل پیمایش
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }

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

  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }
}

این کلاس صرفاً برای مدیریت کشیدن (خط 11) و پیمایش (خط 13) بین ویوها، کلاس استاندارد اندروید [ViewPager] را گسترش می‌دهد.

  • خطوط 26–43: متدهایی که در صورتی که لغزاندن غیرفعال شده باشد، آن را غیرفعال می‌کنند؛
  • خطوط ۴۶–۴۹: تعریف مجدد متد [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)
// کار در دست انجام: زیرکلاس‌های [CoreState] را اینجا اضافه کنید
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // بازدید شده بودن قطعه یا نه
  protected boolean hasBeenVisited = false;
  //وضعیت منوی قطعه (در صورت وجود)
  protected MenuItemState[] menuOptionsState;

  // گیرنده‌ها و تنظیم‌کننده‌ها
...
}
  • خط ۱۶: هر قطعه در وضعیت خود یک مقدار بولی [hasBeenVisited] دارد که نشان می‌دهد آیا قبلاً بازدید شده است یا خیر. این امر ضروری است زیرا گاهی اوقات، هنگامی که یک قطعه برای اولین بار نمایش داده می‌شود، اقدامات خاصی باید انجام شود؛
  • خط ۱۸: پروژه [client-android-skel] به‌طور خودکار منوهای قطعات را، در صورت وجود، ذخیره و بازیابی می‌کند. آرایه MenuItemState[] menuOptionsState وضعیت قابل مشاهده یا مخفی بودن تمام گزینه‌های منو را ذخیره می‌کند؛
  • خطوط ۱۰–۱۳: همانطور که در [Exemple-22] انجام شد، وضعیت فعالیت و قطعات آن در جلسه ذخیره خواهد شد که به نوبه خود به صورت یک رشته از نوع jSON ذخیره می‌شود. خواهیم دید که جلسه آرایه‌ای از عناصر از نوع [CoreState] را ذخیره می‌کند. اگر هیچ اقدامی انجام نشود، رشته‌ی jSON از نوع [CoreState] ذخیره خواهد شد. با این حال، ما می‌خواهیم حالت‌های قطعات را که از [CoreState] مشتق شده‌اند، ذخیره کنیم. برای اطمینان از اینکه رشته jSON از نوع مشتق‌شده تولید می‌شود، نه از نوع والد، انواع مشتق‌شده باید همان‌طور که در خطوط ۱۰ تا ۱۳ نشان داده شده است، اعلام شوند. کلاس [CoreState] یکی از کلاس‌های معماری است که توسعه‌دهنده باید برای هر برنامه جدید آن را تغییر دهد (خطوط ۱۰–۱۳)؛

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 {

  // دسترسی به جلسه
  ISession getSession();

  // تغییر نما
  void navigateToView(int position, ISession.Action action);

  // مدیریت صف
  void beginWaiting();

  void cancelWaiting();

  // ثوابت برنامه (برای اصلاح) -------------------------------------

  // حالت اشکال‌زدایی
  boolean IS_DEBUG_ENABLED = true;

  // حداکثر زمان انتظار برای پاسخ سرور
  int TIMEOUT = 1000;

  // زمان انتظار قبل از اجرای درخواست کلاینت
  int DELAY = 0;

  // احراز هویت پایه
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // همجواری قطعه
  int OFF_SCREEN_PAGE_LIMIT = 1;

  //نوار برگه
  boolean ARE_TABS_NEEDED = false;

  // بارگذاری تصویر
  boolean IS_WAITING_ICON_NEEDED = false;

  // تعداد قطعات برنامه
  int FRAGMENTS_COUNT = 0;

  // برای انجام: ثابت‌ها و سایر متدهای خود را اینجا اضافه کنید
}
  • خط ۶: رابط [IMainActivity]، رابط [IDao] از لایه [DAO] را گسترش می‌دهد؛
  • خط ۹: این فعالیت است که دسترسی به جلسه را در قالب یک نمونه از رابط [ISession] فراهم می‌کند؛
  • خط ۱۲: این فعالیت برای تغییر نما استفاده می‌شود. پارامتر دوم، عملی است که این تغییر نما را تحریک می‌کند، که یکی از مقادیر SUBMIT، NAVIGATION یا RESTORE است؛
  • خطوط ۱۵–۱۷: این فعالیتی است که صفحهٔ بارگذاری را مدیریت می‌کند؛
  • خط ۲۲: برای اشکال‌زدایی برنامه؛
  • خط ۲۵: برای جلوگیری از انتظار بیش از حد در صورتی که سرور پاسخ‌دهی را متوقف کند؛
  • خط ۲۸: هنگام اشکال‌زدایی، این مقدار را روی چند ثانیه تنظیم کنید تا زمان کافی برای لغو عملیات با سرور و مشاهده نتایج داشته باشید؛
  • خط ۳۱: به true اگر سرویس jSON به احراز هویت پایه نیاز داشته باشد؛
  • خط ۳۴: مجاورت قطعه؛
  • خط ۳۷: در صورتی که برنامه دارای برگه‌ها باشد، مقدار را روی vrai تنظیم کنید؛
  • خط ۳۹: اگر برنامه با یک وب‌سرور (jSON) ارتباط برقرار می‌کند و می‌خواهید در حین ارتباط یک تصویر در حال بارگذاری نمایش دهید، مقدار را روی vrai تنظیم کنید؛
  • خط ۴۳: تعداد قطعاتی که توسط برنامه مدیریت می‌شوند؛

رابط [IMainActivity] عنصر دوم معماری است که توسعه‌دهنده باید آن را تکمیل کند (خط ۴۵).

2.5.5. رابط [IDao]

رابط [IMainActivity] رابط زیر [IDao] را گسترش می‌دهد:

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL سرویس وب
  void setUrlServiceWebJson(String url);

  // کاربر
  void setUser(String user, String mdp);

  // انقضای زمان مشتری
  void setTimeout(int timeout);

  // احراز هویت پایه
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // حالت اشکال‌زدایی
  void setDebugMode(boolean isDebugEnabled);

  // زمان انتظار کلاینت (به میلی‌ثانیه) قبل از ارسال درخواست
  void setDelay(int delay);

  // برای انجام: رابط خود را اینجا اعلام کنید
}
  • خط ۲۴: توسعه‌دهنده این رابط را در اینجا تکمیل خواهد کرد؛

2.5.6. جلسه

  

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


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // شماره آخرین نمای نمایش‌داده‌شده
  int getPreviousView();

  void setPreviousView(int numView);

  // وضعیت نهایی یک نما
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // اقدام در حال انجام
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // وضعیت‌های تمام نماها -
  // توسط کد استفاده نمی‌شود اما برای سریالی‌سازی/دسریالی‌سازی مورد نیاز است jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // شماره آخرین زبانه انتخاب‌شده
  int getPreviousTab();

  void setPreviousTab(int position);

  // پیمایش هنگام انتخاب زبانه
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

ما رابط [ISession] را معرفی می‌کنیم تا اطمینان حاصل شود که متدهای خاصی در جلسه وجود دارند:

  • خطوط ۷–۱۰: شماره آخرین نما (قطعه) نمایش داده شده؛
  • خطوط ۱۲–۱۵: وضعیت یک نمای خاص؛
  • خطوط 17–24: ما مفهوم یک عمل در حال انجام را معرفی می‌کنیم. چهار مورد از این‌ها وجود دارد (خط 17):
    • RESTORE: یک عملیات ذخیره/بازیابی در حال انجام است. هیچ تغییری در نما رخ نمی‌دهد؛
    • NAVIGATION: ناوبری در حال انجام است. در اینجا، ما ناوبری را به عنوان تغییری در نما تعریف می‌کنیم که در آن نمای جدید می‌تواند از آخرین وضعیت ذخیره‌شده خود در جلسه بازیابی شود؛
    • SUBMIT: ما نوع [SUBMIT] را به یک اقدام در حال انجام اختصاص می‌دهیم زمانی که تغییر نما رخ می‌دهد و نمای جدید به وضعیت کل فعالیت بستگی دارد، نه صرفاً به وضعیت خود آن. گاهی اوقات، تمایز بین NAVIGATION و SUBMIT دشوار است. در چنین مواردی، حالت کلی‌تر SUBMIT باید استفاده شود؛
    • NONE: مقدار عمل زمانی که هنوز اولین مقدار خود را دریافت نکرده است؛
  • خطوط 26–30: وضعیت‌های فعالیت و قطعات در یک آرایه از نوع CoreState[] ذخیره خواهند شد. برای اطمینان از اینکه این موضوع در طول سریالی‌سازی و غیرسریالی‌سازی jSON به درستی مدیریت می‌شود، باید یک گتر و یک ستر داشته باشد؛
  • خطوط ۳۲–۳۵: شماره آخرین زبانهٔ انتخاب‌شده. در چرخهٔ ذخیره/بازیابی استفاده می‌شود تا زبانهٔ انتخاب‌شده قبل از چرخش دستگاه دوباره انتخاب شود؛
  • خطوط ۳۷–۴۰: مقدار بولی را مدیریت می‌کند که نشان می‌دهد آیا انتخاب یک تب باید با تغییر فراگمنت همراه باشد یا خیر؛

رابط [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 {
  // شمارهٔ مشاهدهٔ قبلی
  private int preViousView;

  //وضعیت مشاهده
  private CoreState[] coreStates = new CoreState[0];

  // عمل در حال انجام
  private Action action = Action.NONE;

  // زبانه‌ی قبلاً انتخاب‌شده
  private int previousTab;

  // ناوبری انتخاب زبانه
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // سازنده
  public AbstractSession() {
    // ابتدایی‌سازی آرایه وضعیت قطعات
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // رابط 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: شماره ویوی نمایش‌داده‌شده قبل از ویوی فعلی. این اطلاعات زمانی مفید است که یک ویو از چندین مکان قابل دسترسی باشد. این حالت معمولاً در ناوبری تب‌دار رخ می‌دهد. ویوی نمایش‌داده‌شده می‌تواند تشخیص دهد که ویوی قبلی کدام بوده است؛
  • خط ۱۲: جدول وضعیت برای تمام قطعات نمایش داده شده توسط فعالیت؛
  • خط ۱۸: شماره زبانه انتخاب‌شده قبلی. این اطلاعات کاربرد مشابهی با شماره نمای قبلی در خط ۹ دارد. این اطلاعات زمانی مفید است که دستگاه چرخانده می‌شود و کاربر نیاز دارد به زبانه انتخاب‌شده قبل از چرخش بازگردد؛
  • خط ۲۲: یک بول که نشان می‌دهد آیا انتخاب یک تب باید منجر به تغییر قطعه نمایش داده شده شود یا خیر. باید توجه داشت که پروژه [client-android-skel]، تب‌ها و قطعات را به صورت جداگانه مدیریت می‌کند تا بتوان از آن در مواردی استفاده کرد که تعداد تب‌ها کمتر از تعداد قطعات است. دو نوع انتخاب وجود دارد:
    • انتخابی که کاربر هنگام کلیک بر روی یک تب انجام می‌دهد. در این حالت، تغییر قطعه نمایش‌داده‌شده عموماً مطلوب است؛
    • یک انتخاب مبتنی بر نرم‌افزار از طریق متد [Tablayout.Tab.select()]. در این حالت، تغییر قطعه نمایش‌داده‌شده همیشه مطلوب نیست. در اینجا دو مثال آورده شده است:
      • هنگامی که دستگاه چرخانده می‌شود، فعالیت و همچنین برگه‌ها دوباره ایجاد می‌شوند. با این حال، هنگامی که اولین برگه ایجاد می‌شود، به طور خودکار تحت یک عملیات نرم‌افزاری [select] قرار می‌گیرد. بنابراین، تغییر فرگمنت نمایش‌داده‌شده مطلوب نیست، زیرا فعالیت در حال بازسازی است و فرگمنتی که در نهایت نمایش داده می‌شود لزوماً همان فرگمنت مرتبط با اولین تب نخواهد بود؛
      • از آنجایی که مدیریت تب‌ها از مدیریت فرگمنت‌ها جداست، ممکن است بخواهید تب‌ها را (حذف، اضافه) بدون دخالت در فرگمنت‌های مرتبط با آن‌ها به‌روزرسانی کنید. با این حال، برخی از این عملیات ممکن است بار دیگر، یک عملیات نرم‌افزاری ضمنی [select] را در یکی از برگه‌ها فعال کند. این انتخاب لزوماً به پیمایش به قطعه مرتبط منجر نمی‌شود؛
  • خط ۲۱: فیلد [navigationOnTabSelectionNeeded] قرار نیست در طول عملیات ذخیره کردن فعالیت و قطعات آن ذخیره شود. توضیح [@JsonIgnore] تضمین می‌کند که این فیلد در طول سریالی‌سازی و غیرسریالی‌سازی jSON نادیده گرفته شود؛
  • خطوط ۲۵–۳۱: سازنده، آرایهٔ حالت‌ها را برای قطعات [FRAGMENTS_COUNT] برنامهٔ کاربردی مقداردهی اولیه می‌کند. عناصر این آرایه با فیلد [hasBeeenVisited=false] مقداردهی می‌شوند. این اطلاعات برای تعیین اینکه آیا این اولین بازدید از قطعه است یا خیر، استفاده می‌شود؛

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


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  //داده‌هایی که باید بین خود قطعات و بین قطعات و فعالیت به اشتراک گذاشته شوند
  // عناصری که در jSON قابل سریال‌سازی نیستند باید دارای حاشیه‌نویسی @JsonIgnore باشند
  // گیرنده‌ها و تنظیم‌کننده‌های مورد نیاز برای سریالی‌سازی/دسریالی‌سازی در jSON را فراموش نکنید
}
  • خط ۵: کلاس [Session] از کلاس [AbstractSession] که همین حالا دیدیم، ارث می‌برد. توسعه‌دهنده عناصر مشترک بین خود قطعات و بین قطعات و فعالیت را در این کلاس قرار خواهد داد. توجه داشته باشید که کلاس [Session] دیگر توسط آنوتیشن AA [@EBean] آنوتیت نشده است. این کلاس به یک کلاس عادی تبدیل شده است؛

2.5.7. کلاس انتزاعی [AbstractActivity]

  

2.5.7.1. Squelette

کلاس [AbstractActivity] کلاسی است که بیش از ۳۰۰ خط کد را در بر می‌گیرد. ما آن را گام به گام بررسی خواهیم کرد. ساختار اصلی آن به شرح زیر است:


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 {
  //لایه [DAO]
  private IDao dao;
  // جلسه
  protected Session session;

  // ظرفیت قطعه
  protected MyPager mViewPager;
  //نوار ابزار
  private Toolbar toolbar;
  // تصویر نگه‌دارنده مکان
  private ProgressBar loadingPanel;
  //نوار زبانه
  protected TabLayout tabLayout;

  //مدیر بخش یا قطعه
  private FragmentPagerAdapter mSectionsPagerAdapter;
  //نام کلاس
  protected String className;
  // نقشه‌زن jSON
  private ObjectMapper jsonMapper;

  // سازنده
  public AbstractActivity() {
    // نام کلاس
    className = getClass().getSimpleName();
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // پیاده‌سازی IMainActivity --------------------------------------------------------------------
  ...

  //چرخه عمر – پشتیبان‌گیری/بازیابی فعالیت ------------------------------------
  ...

  // مدیریت صفحهٔ نگهداشتن ---------------------------------
  ...

  // رابط IDao -----------------------------------------------------
  ...

  // مدیر قطعه --------------------------------
  ...

  //زیرکلاس‌ها
  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);
  • مسئول ذخیره و بازیابی فعالیت و قطعات آن هنگام چرخش دستگاه است (خط ۵۸)؛
  • صفحه بارگذاری را در حین ارتباط با سرور وب / jSON (خط ۶۱) مدیریت می‌کند؛
  • رابط IDao لایه [DAO] را پیاده‌سازی می‌کند (خط ۶۴);
  • پیاده‌سازی می‌کند دست‌گردانگر قطعه (خط 67);
  • نیاز دارد کلاس‌های فرزند آن شش متد داشته باشند (خطوط 71–81);

2.5.7.2. رابط [IMainActivity] را پیاده‌سازی کنید

پیاده‌سازی رابط [IMainActivity] (رجوع کنید به بخش 2.5.4) به شرح زیر است:


  // 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));
    }
    // نمایش یک قطعه جدید
    mViewPager.setCurrentItem(position);
    // اقدام فعلی در طول این تغییر نما ثبت می‌شود
    session.setAction(action);
}

2.5.7.3. ذخیره وضعیت فعالیت و قطعات آن

وضعیت فعالیت و قطعات آن کاملاً در محدودهٔ جلسه قرار دارد. بنابراین مسئلهٔ ذخیرهٔ جلسه است. در اینجا آنچه در پروژهٔ [Exemple-22] انجام شد (رجوع کنید به بخش 1.23) تکرار می‌کنیم:


  // مدیریت فعالیت ذخیره/بازیابی ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // پدر
    super.onSaveInstanceState(outState);
    // جلسه را به صورت یک رشته ذخیره می‌کند jSON
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // لاگ
    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. بازیابی وضعیت فعالیت و قطعات آن

هدف بازگرداندن جلسه است. ما همان‌طور که در [Exemple-22] نشان داده شده است، عمل می‌کنیم:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // پدر
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // آیا چیزی برای بازیابی وجود دارد؟
    if (savedInstanceState != null) {
      //بازیابی جلسه
      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 = new Session();
    }
...
  • خطوط ۱۰–۲۶: اگر پارامتر [Bundle savedInstanceState] در خط ۲، null نباشد، آنگاه جلسه بازیابی می‌شود (خطوط ۱۲–۱۷);
  • خطوط 26–29: اگر پارامتر [Bundle savedInstanceState] در خط 2 برابر null باشد، این نشان‌دهنده اولین بار شروع فعالیت است. سپس یک جلسه خالی ایجاد می‌شود؛

2.5.7.5. ابتکاری‌سازی لایه [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // والد
    super.onCreate(savedInstanceState);
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    //لایه [DAO]
    dao = getDao();
    if (dao != null) {
      // پیکربندی لایه [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  //کلاس‌های فرزند
  protected abstract IDao getDao();
....
}
  • خط ۱۱: ارجاعی به لایه [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>

  <!-- ظرفیت قطعه -->
  <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) {
    // والد
    super.onCreate(savedInstanceState);
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // نما مرتبط
    setContentView(R.layout.activity_main);
    // اجزای نما ---------------------
    //نوار ابزار
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // تصویر بارگذاری؟
    if (IS_WAITING_ICON_NEEDED) {
      // افزودن تصویر نگه‌دارنده مکان
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // ایجاد ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      //افزودن ProgressBar به نوار ابزار
      toolbar.addView(loadingPanel);
    }
...
  • خط ۱۱: نمای XML [activity_main] با فعالیت مرتبط است؛
  • خطوط 14–15: نوار ابزار یکپارچه شده و پشتیبانی می‌شود؛
  • خطوط 17–27: یک تصویر بارگذاری می‌تواند اضافه شود اگر متغیر بولی [IS_WAITING_ICON_NEEDED] در رابط [IMainActivity] روی true تنظیم شود؛
  • خط ۲۳: ایجاد تصویر جای‌گذاری از نوع [ProgressBar]، ارجاع‌شده توسط فیلد [loadingPanel];
  • خط ۲۴: در ابتدا این تصویر مخفی است؛
  • خط ۲۶: به نوار ابزار اضافه می‌شود؛

2.5.7.7. مدیریت برگه‌ها

رابط [IMainActivity] ممکن است به یک نوار زبانه نیاز داشته باشد. این نوار به شرح زیر اضافه و مدیریت می‌شود:


//نوار زبانه
  protected TabLayout tabLayout;
...

    //نوار برگه؟
    if (ARE_TABS_NEEDED) {
      //نوار برگه اضافه شده است
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // تا زمانی که یک قطعه نمایش داده نشود، هیچ ناوبری وجود ندارد
      session.setNavigationOnTabSelectionNeeded(false);
      // ایجاد نوار زبانه
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // افزودن نوار برگه به نوار برنامه
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // پردازشگر رویداد نوار زبانه
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          //یک برگه انتخاب شده است
          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()) {
            //موقعیت زبانه
            int position = tab.getPosition();
            // حافظه
            session.setPreviousTab(position);
            // قطعهٔ مرتبط نمایش داده شد؟
            navigateOnTabSelected(position);
          }
        }

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

        }

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

        }
      });
    }

...
  //کلاس‌های فرزند
  protected abstract void navigateOnTabSelected(int position);
...
  • خطوط ۱۲–۴۸: افزودن و مدیریت نوار زبانه؛
  • خط ۶: نوار زبانه اضافه می‌شود اگر مقدار ثابت [ARE_TABS_NEEDED] در رابط [IMainActivity] روی vrai تنظیم شده باشد؛
  • خط ۱۲: هنگامی که نوار تب ایجاد می‌شود، عملیات ضمنی [Tablayout.Tab.select] ممکن است رخ دهد (این عملیات توسط کاربر تحریک نمی‌شوند). متغیر بولی [session.navigationOnTabSelectionNeeded] روی faux تنظیم می‌شود تا از هرگونه ناوبری در طول این انتخاب‌های نادرست جلوگیری کند. این وظیفه توسعه‌دهنده است که با استفاده از متد [navigateToView] قطعه نمایش داده شده را انتخاب کند. متغیر بولی [session.navigationOnTabSelectionNeeded] هنگامی که این قطعه نمایش داده می‌شود، به vrai بازنشانی خواهد شد (به کلاس AbstractFragment مراجعه کنید)؛
  • خط ۱۴: ایجاد یک نوار تب که توسط فیلد [tabLayout] مرجع آن است. ما از یک نوار تب سفارشی، [CustomTabLayout]، استفاده می‌کنیم که بعداً به آن باز خواهیم گشت؛
  • خط ۱۵: ما رنگ‌های عنوان تب‌ها را تنظیم می‌کنیم. این رنگ‌ها در فایل زیر، [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>
    • خط (c): رنگ عنوان زبانه زمانی که انتخاب شده است؛
    • خط (d): رنگ عنوان زبانه زمانی که انتخاب نشده است؛

این فایل البته قابل ویرایش است. برای مثال، کدهای رنگ هگزادسیمال را می‌توان در اینجا یافت.

  • خطوط ۱۷–۱۸: افزودن این نوار برگه به نوار برنامه در نما XML [activity_main];
  • خطوط ۲۰–۴۷: رویدادپرداز برای نوار برگه؛
  • خطوط ۲۲–۳۶: تنها رویداد [onTabSelected] پردازش می‌شود. این معادل کلیک بر روی زبانه [Tab tab] است که به عنوان پارامتر به متد ارسال شده، یا یک عملیات نرم‌افزاری [TabLayout.Tab.select
  • خط ۳۰: موقعیت زبانهٔ انتخاب‌شده؛
  • خط ۳۲: این موقعیت برای جلسه ذخیره می‌شود؛
  • خط ۳۴: قطعه مرتبط با این برگه اکنون باید نمایش داده شود. فقط کلاس فرزند (خط ۵۲) می‌تواند این ارتباط را برقرار کند. توجه داشته باشید که نوار برگه‌ها با کانتینر قطعه [mViewPager] مرتبط نیست، همانطور که در برخی از مثال‌های مورد مطالعه انجام شده بود. در اینجا، مدیریت نوار برگه به طور کامل از مدیریت قطعات جدا شده است. به همین دلیل، هنگامی که یک برگه کلیک می‌شود، باید مشخص کنیم که کدام نما را می‌خواهیم نمایش دهیم؛
  • خط ۲۸: تفاوتی بین انتخاب تب با ناوبری و بدون ناوبری قائل شده است. به‌طور کلی، هنگامی که کاربر روی یک برگه کلیک می‌کند، ناوبری لازم است، در حالی که در طول یک انتخاب مبتنی بر نرم‌افزار، اینطور نیست. این توسعه‌دهنده است که با استفاده از عنصر [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);
      }
    }
  }

}
  • سفارشی‌سازی قلم برای عناوین تب در خطوط ۳۰ و ۴۴ انجام می‌شود؛

فایل [fonts] به شرح زیر است:

  

منابع:

  • کد کلاس [CustomTabLayout] در URL و [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout] یافت شد؛
  • فونت‌ها در URL و [https://www.fontsquirrel.com/fonts/roboto] یافت شدند؛

2.5.7.9. آخرین راه‌اندازی‌ها


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // والد
    super.onCreate(savedInstanceState);
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // نمادسازی مدیر قطعه
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // کانترینر قطعات با مدیر قطعات مرتبط است
    //یعنی قطعه شماره i در محفظهٔ قطعات، همان قطعه شماره i است که توسط مدیر قطعات بازگردانده شده است
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // کشیدن بین قطعات غیرفعال است
    mViewPager.setSwipeEnabled(false);
    // همجواری قطعات
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    //نمای اول نمایش داده می‌شود
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    //کنترل به فعالیت فرزند واگذار می‌شود
    onCreateActivity();
  }
...
  // کلاس‌های فرزند
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • خطوط ۱۰–۱۹: این شامل کدی است که اغلب در نمونه‌های مورد مطالعه مشاهده می‌شود؛
  • خطوط 21–23: نمایش اولین نما. بدون شک راه‌های متعددی برای تمایز این مورد وجود دارد. در اینجا، ما از این واقعیت استفاده کرده‌ایم که برای اولین نما، مقدار عملی که تغییر نما را تحریک می‌کند NONE است؛
  • خط ۲۲: ما هیچ فرضی در مورد اولین قطعه نمایش داده شده نمی‌کنیم. در مثال‌های ما، این اغلب قطعه شماره ۰ بوده است، اما همیشه نه (به مثال ۲۲ مراجعه کنید). بنابراین از فعالیت فرزند (خط ۳۰) خواهیم پرسید که این کدام نما است؛
  • خط ۲۵: ما در اینجا هر چیزی را که می‌توانستیم فاکتورگیری کرده‌ایم. اکنون، کلاس فرزند inicializاسیون‌های خود را برای انجام دارد (خط ۲۹);

2.5.7.10. پردازش تصویر جای‌گیر

در کلاس [AbstractActivity]، تصویر نگه‌دارنده (placeholder) توسط دو متد زیر مدیریت می‌شود:


  // پردازش تصویر بارگذاری ---------------------------------
  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 {
  //لایه [DAO]
  private IDao dao;
...
  //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);
}
  • خط ۳: توجه داشته باشید که مقدار این فیلد توسط فعالیت فرزند در متد [onCreate] ارائه شده است؛

2.5.7.12. پیاده‌سازی فرگمنت هندلر

در کلاس [AbstractActivity]، فرگمنت هندلر به شرح زیر پیاده‌سازی شده است:


...
  // مدیر قطعه --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // سازنده
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // قطعات کلاس فرزند
      fragments = getFragments();
    }

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

    // تعداد قطعاتی را که باید پردازش شوند بازمی‌گرداند
    @Override
    public int getCount() {
      return fragments.length;
    }

    // عنوان قطعه در موقعیت را بازمی‌گرداند
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  //کلاس‌های فرزند
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • خط ۵: آرایهٔ قطعات مرتبط با فعالیت. تمام قطعات از کلاس [AbstractFragment] مشتق خواهند شد؛
  • خطوط ۸–۱۲: این سازنده است که آرایهٔ قطعات را مقداردهی اولیه می‌کند. این آرایه را از کلاس فرزند فعالیت (خط ۳۵) بازیابی می‌کند؛
  • خطوط ۲۸–۳۱: عناوین قطعات می‌توانند در برنامه‌ای استفاده شوند که تعداد تب‌ها با تعداد قطعات برابر است. در این صورت، می‌توان عنوان قطعه را به تب اختصاص داد. در اینجا، این عناوین از کلاس فرزند درخواست می‌شوند (خط ۳۷);

2.5.7.13. متد [onResume]

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


  @Override
  public void onResume() {
    // والد
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // اگر در حال بازیابی باشد، آخرین زبانهٔ انتخاب‌شده باید بازیابی شود
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • خط ۱۰: انتخاب زبانی که قبل از فرآیند ذخیره/بازیابی انتخاب شده بود. در اینجا باید توجه داشت که در متد [onCreate] – که در چرخه عمر فعالیت، قبل از متد [onResume] اجرا می‌شود – ناوبری هنگام انتخاب یک تب غیرفعال شده است. بنابراین، در این مورد، یک تب انتخاب می‌شود اما هیچ تغییری در فرگمنت رخ نمی‌دهد؛

2.5.7.14. Résumé

کلاس انتزاعی [AbstractActivity] کلاس والد یک فعالیت واحد برنامه خواهد بود.

فعالیت فرزند باید شش متد زیر را پیاده‌سازی کند:


  //کلاس‌های فرزند
  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();

فعالیت فرزند همچنین به اعضای محافظت‌شده زیر از کلاس والد خود دسترسی دارد:


  // جلسه
  protected ISession session;
  // ظرفیت قطعه
  protected MyPager mViewPager;
  //نوار زبانه
  protected CustomTabLayout tabLayout;
  // نام کلاس
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 {

  //لایه [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // جلسه
  private Session session;

  // متدهای کلاس والد -----------------------
  @Override
  protected void onCreateActivity() {
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // جلسه
    this.session = (Session) super.session;
    // کار در دست: ادامهٔ инициалиزه‌سازی‌های آغازشده توسط کلاس والد
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // کار در دست: عناوین قطعات را اینجا تعریف کنید
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // to-do: عناوین فرگمنت‌ها را اینجا تعریف کنید
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // to-do: ناوبری تب‌دار – نمای نمایش‌دادنی را تعریف کنید
  }

  @Override
  protected int getFirstView() {
    // کار در دست: ناوبری تب‌دار – تعریف اولین نمای نمایشی
    return 0;
  }
}
  • خط ۱۴: برای اینکه نشانه‌گذاری AA [@Bean] در خط ۱۹ معتبر باشد، فعالیت باید دارای نشانه‌گذاری AA [@EActivity] باشد؛
  • خط ۱۵: فعالیت با منوی XML [menu_main] مرتبط است. این منو در حال حاضر خالی است. توسعه‌دهنده در صورت لزوم باید آن را پر کند؛
  • خط ۱۶: کلاس از کلاس [AbstractActivity] ارث می‌برد؛
  • خطوط ۱۹–۲۰: اشاره‌ای به لایه [DAO]. این لایه قبل از مقداردهی اولیه این فیلد توسط کتابخانه AA نمونه سازی خواهد شد. این بدان معناست که بین AA [Dao] باید وجود داشته باشد. این امر در مورد اپلیکیشن اسکلت (skeleton) که ما ارائه می‌دهیم، همیشه صادق است. حتی در یک اپلیکیشن بدون لایه [DAO]، می‌توان پکیج [dao] را در جای خود باقی گذاشت. این کار باعث هیچ‌گونه پیچیدگی نمی‌شود؛
  • خط ۲۲: جلسه به‌عنوان نمونه‌ای از نوع [Session]. جلسه در کلاس والد [AbstractActivity] وجود دارد اما به‌عنوان نمونه‌ای از رابط [ISession] (خط ۳۲)؛
  • خطوط ۲۴–۶۳: شش متد مورد نیاز کلاس والد [AbstractActivity
  • خطوط ۳۶–۳۹: متد [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 {
  // آدرس سرویس وب
  void setUrlServiceWebJson(String url);

  // کاربر
  void setUser(String user, String mdp);

  // اتمام زمان مشتری
  void setTimeout(int timeout);

  // احراز هویت پایه
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // حالت اشکال‌زدایی
  void setDebugMode(boolean isDebugEnabled);

  //زمان انتظار کلاینت (به میلی‌ثانیه) قبل از ارسال درخواست
  void setDelay(int delay);

  // برای انجام: رابط خود را اینجا اعلام کنید
}

توسعه‌دهنده متدهای لایه [DAO] خود را از خط ۲۴ به بعد اضافه خواهد کرد.

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);

  // برای انجام: اهداف URL را اینجا اعلام کنید
}

توسعه‌دهنده متدهایی را که با رابط‌های 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 {

  // کاربر
  private String user;
  // رمز عبور
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // سربرگ‌های HTTP از درخواست HTTP رهگیری‌شده
    HttpHeaders headers = request.getHeaders();
    // سربرگ احراز هویت پایه‌ای HTTP
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // افزوده شده به هدرهای HTTP
    headers.setAuthorization(auth);
    //چرخهٔ عمر درخواست HTTP ادامه دارد
    return execution.execute(request, body);
  }

  // عناصر احراز هویت
  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 {

  // jSON مپ‌کننده
  private ObjectMapper mapper = new ObjectMapper();
  // حالت اشکال‌زدایی
  protected boolean isDebugEnabled;
  // نام کلاس
  protected String className;
  // زمان انتظار قبل از اجرای یک درخواست
  private int delay;

  // سازنده
  public AbstractDao() {
    // نام کلاس
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }

  // متدهای محافظت‌شده ----------------------------------------------------------
  //رابط عمومی
  protected interface IRequest<T> {
    T getResponse();
  }

  // درخواست عمومی به یک سرویس وب / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // لاگ
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    //اجرای سرویس – در انتظار یک پاسخ واحد
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        //اجرای سرویس
        try {
          // در حال انتظار؟
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // اجرای درخواست همگام
          T response = request.getResponse();
          // لاگ
          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()));
          }
          //ارسال پاسخ به ناظر
          subscriber.onNext(response);
          // نشان‌دهنده پایان مشاهده‌پذیر
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // ثبت
          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"));
            }
          }
          //یک استثنا پرتاب می‌شود
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // حالت اشکال‌زدایی
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • خطوط ۳۵–۸۱: متد [getResponse] از کتابخانه RxAndroid برای رندر کردن یک نوع [Observable<T>] استفاده می‌کند. برخلاف برخی از مثال‌های قبلی، ما در حال رندر کردن یک نوع [Response<T>]—که یک نوع اختصاصی است—نیستیم، بلکه هر نوع T را رندر می‌کنیم؛
  • خط ۳۵: متد [getResponse] به عنوان پارامتر یک نمونه از نوع [IRequest<T>] را از خطوط ۳۰–۳۲ دریافت می‌کند، که از آن متد [IRequest.getReponse()] نوع T را از طریق یک عملیات همگام HTTP به دست می‌آورد؛
  • خطوط ۴۸–۵۰: ما به‌طور مصنوعی به مدت [delay] میلی‌ثانیه منتظر می‌مانیم. در تولید، این مقدار را روی [delay=0] تنظیم خواهیم کرد. در حین اشکال‌زدایی، ما [delay=qqs secondes] را تنظیم خواهیم کرد تا به کاربر فرصتی برای لغو عملیات غیرهمزمان بدهد و در نتیجه ببیند که کد در این حالت چگونه رفتار می‌کند؛
  • خط ۵۲: پاسخ مورد انتظار با استفاده از یک فراخوانی همزمان درخواست می‌شود؛
  • خط ۶۴: پس از دریافت پاسخ، آن به ناظر ارسال می‌شود؛
  • خط ۶۶: ما نشان می‌دهیم که دیگر هیچ انتشاری وجود نخواهد داشت. این مورد خاص یک عمل غیرهمزمان است که تنها یک عنصر را بازمی‌گرداند؛
  • خطوط ۶۷–۷۸: در صورت وقوع استثنا، استثنا به ناظر منتقل می‌شود (خط ۷۷)؛

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 {

  // کلاینت سرویس وب
  @RestService
  protected WebClient webClient;
  // امنیت
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // کارخانه برای RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // لاگ
    Log.d(className, "afterInject");
    // restTemplate در حال ساخت است
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // مبدل jSON در حال نصب است
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // restTemplate کلاینت وب تنظیم شده است
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    //مقدار URL سرویس وب تنظیم شده است
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // کاربر در 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.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));
    }
    // مداخلهگر احراز هویت؟
    if (isBasicAuthentificationNeeded) {
      // افزودن interceptor احراز هویت
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // متدهای خصوصی -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // کارها: پیاده‌سازی IDao
}
  • خطوط ۲۱–۲۲: تزریق bean با کلاس AA [WebClient]، که ارتباط با سرور وب / jSON را مدیریت خواهد کرد؛
  • خطوط ۲۴–۲۵: تزریق interceptor احراز هویت؛
  • خطوط ۳۱–۴۲: متدی که پس از درج فیلدهای خطوط ۲۱–۲۵ اجرا می‌شود؛
  • خط ۳۷: شیء [RestTemplate] که ارتباط کلاینت/سرور را مدیریت می‌کند، از factory ایجاد می‌شود. این امر ضروری نیست، اما از طریق factory است که می‌توان زمان‌های اتمام ارتباط را پیکربندی کرد. به همین دلیل است که ما از سازنده بدون پارامتر [RestTemplate()] استفاده نمی‌کنیم؛
  • خط ۳۹: ما یک مبدل jSON را به مبدل‌های [RestTemplate] اضافه می‌کنیم. این تنها مبدل خواهد بود. علاوه بر این، هنگامی که یک متد در کلاینت [WebClient] یک رشته jSON را از سرور دریافت می‌کند، این رشته به طور خودکار به ابجکتی که متد ملزم به بازگرداندن آن است، سریالیزه می‌شود؛
  • خط ۴۱: شیء [RestTemplate] که به این شکل پیکربندی شده است، به کلاینت وب ارسال می‌شود که از آن برای مدیریت ارتباط کلاینت/سرور استفاده خواهد کرد؛
  • خطوط ۴۴–۴۸: ریشهٔ URL وب‌سرور / jSON تنظیم می‌شود. تمام URLهای اعلام‌شده در کلاس [WebClientURLهایی هستند که نسبت به این ریشه URL قرار دارند؛
  • خطوط 50–54: این متد برای مشخص کردن مالک اتصال زمانی که اتصال توسط احراز هویت پایه کنترل می‌شود، استفاده می‌شود (به بخش 2.6.3 مراجعه کنید)؛
  • خطوط ۵۶–۶۴: timeouts را برای مبادلات کلاینت/سرور تعریف می‌کند. این کار از طریق factory از شیء [RestTemplate] انجام می‌شود که بر این مبادلات نظارت دارد؛
  • خطوط ۶۶–۷۸: این متد برای نشان دادن اینکه سرور توسط احراز هویت پایه محافظت می‌شود، استفاده می‌شود؛
  • خطوط ۷۲–۷۷: اگر احراز هویت پایه مورد نیاز باشد، مخترع احراز هویت که در خط ۲۵ تزریق شده است به مخترع‌های شیء [RestTemplate] اضافه می‌شود. این میان‌گیر به طور خودکار خط احراز هویت پایه‌ای HTTP را، همان‌طور که سرور انتظار دارد، به تمام درخواست‌های کلاینت وب اضافه می‌کند؛
  • توسعه‌دهنده رابط [IDao] را از خط 87 پیاده‌سازی خواهد کرد؛

2.7. قطعات

  

2.7.1. کلاس [MenuItemState]

کلاس [MenuItemState] وضعیت یک گزینه منو را در بر می‌گیرد:


package client.android.architecture;

public class MenuItemState {

  //شناسه گزینه منو
  private int menuItemId;
  // دید گزینه
  private boolean isVisible;

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

  }

  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }

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

2.7.2. کلاس [Utils]

کلاس [Utils] شامل متدهای کمکی ایستا است:


package client.android.architecture;

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

public class Utils {

  //فهرست پیام‌ها برای یک استثنا – نسخهٔ ۱
  static public List<String> getMessagesFromException(Throwable ex) {
    //فهرستی از پیام‌های خطا را از روی استک استثنا ایجاد می‌کند
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // فهرست پیام‌ها برای یک استثنا – نسخهٔ ۲
  static public String getMessageForAlert(Throwable th) {
    // متن قابل نمایش را ساختاربندی می‌کند
    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--;
    }
    // نتیجه
    return texte.toString();
  }

  // فهرست پیام‌ها برای یک استثنا – نسخهٔ ۳
  static public String getMessageForAlert(List<String> messages) {
    // ساخت متن قابل نمایش
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // نتیجه
    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 {

  // داده‌های خصوصی ------------------------------------------------------------
  // اشتراک‌ها در قابل‌مشاهده‌ها
  private List<Subscription> abonnements = new ArrayList<>();
  // منوی قطعه
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  //چرخهٔ عمر قطعه
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  //وضعیت قطعه
  private CoreState previousState;
  // jSON نگاشت‌کننده
  private ObjectMapper jsonMapper = new ObjectMapper();
  //چرخه عمر قطعه
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // وظایف ناهمزمان
  private boolean runningTasksHaveBeenCanceled;

  // داده‌ها قابل دسترسی برای کلاس‌های فرزند ---------------------------------------
  // حالت اشکال‌زدایی
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // نام کلاس
  protected String className;
  // وظایف ناهمزمان
  protected int numberOfRunningTasks;
  // فعالیت
  protected IMainActivity mainActivity;
  protected Activity activity;
  // جلسه
  protected Session session;


  // به‌روزرسانی قطعه ----------------------------------------------------------------------------------
 ...

  // مدیریت منو ------------------------------------------
  ...

  // مدیریت صف -------------------------------------------------------------
...

  //مدیریت عملیات ناهمزمان --------------------------------------------------------------------
...

  // مدیریت استثنا -------------------------------------------------------------------
....

  // مدیریت چرخه عمر قطعه --------------------------------------------------------
...

  //زیرکلاس‌ها -----------------------------------------------------
  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);

}
  • خطوط ۲۸–۴۵: داده‌های خصوصی کلاس؛
  • خطوط ۴۷–۵۸: داده‌های محافظت‌شده که برای کلاس‌های فرزند قابل دسترسی هستند؛
  • خطوط ۶۱–۶۲: کدی که قطعه نمایش داده شده را به‌روزرسانی می‌کند؛
  • خطوط ۶۴–۶۵: کد کمکی برای مدیریت هر منو؛
  • خطوط ۶۷–۶۸: کد کمکی برای مدیریت حالت انتظار در طول یک عملیات ناهمزمان؛
  • خطوط ۷۰–۷۱: کدی برای تسهیل ارتباط بین فرگمنت و لایه [DAO
  • خطوط ۷۳–۷۴: کد کمکی برای رسیدگی به هرگونه استثنا به روش استاندارد؛
  • خطوط ۷۶–۷۷: کدی که چرخه عمر فرگمنت را مدیریت می‌کند؛
  • خطوط ۸۰–۹۴: کلاس والد ۸ متد را بر کلاس‌های فرزند خود تحمیل می‌کند؛

2.7.3.2. سازنده

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


  // نام کلاس
  protected String className;
  // چرخه عمر قطعه
  private boolean fragmentHasToBeInitialized = false;
...
  // سازنده ----------------------
  public AbstractFragment() {
    // ابتدا
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // لاگ
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • خط ۹: نام کلاس فرزند که در اینجا نمونه سازی می‌شود، ثبت می‌گردد. این نام در تمام لاگ‌های کلاس والد استفاده می‌شود؛
  • خط ۱۰: ذکر شده است که قطعه در حال ساخت است. این اطلاعات زمانی استفاده خواهد شد که از قطعهٔ فرزند خواسته شود خود را به‌روزرسانی کند؛

2.7.3.3. مدیریت منو

در معماری ما، هر قطعه (fragment) باید یک منو داشته باشد، حتی اگر خالی باشد. لاگ‌ها در واقع نشان داده‌اند که وقتی متد [onCreateOptionsMenu]—که زمانی اجرا می‌شود که قطعه منو داشته باشد—اجرا می‌شود، آن قطعه قبلاً با فعالیت، نما (view) و منوی خود مرتبط شده و در آستانه نمایش قرار دارد. بنابراین، این همان نقطه‌ای است که رابط کاربری بصری و منو می‌توانند به‌روزرسانی شوند. در همین متد، [onCreateOptionsMenu]، است که به فرگمنت فرزند دستور می‌دهیم خود را به‌روزرسانی کند.

مدیریت منو شامل متدهای کمکی است که به فرگمنت فرزند اجازه می‌دهند موارد منو را نمایش دهد یا پنهان کند:


  // منوی قطعه
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // مدیریت منو ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // حلقه‌ی مرور تمام آیتم‌های منو
    for (int i = 0; i < menu.size(); i++) {
      // مورد شمارهٔ i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // اگر آیتم شماره i یک زیرمنو باشد، تکرار کن
      if (menuItem.hasSubMenu()) {
        //بازگشتی
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // نتیجه
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // استخراج شناسه‌های گزینه‌های منو
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // ما گزینه‌های منو را به یک آرایه منتقل می‌کنیم
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      //شناسه گزینه
      int id = menuOptionsIds.get(i);
      //وضعیت گزینه
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // نتیجه
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  //وضعیت‌های گزینه‌های منو
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      //وضعیت
      MenuItemState state = this.menuOptionsStates[i];
      //شناسهٔ منو
      int id = state.getMenuItemId();
      // راه‌اندازی گزارش
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // نتیجه
    return menuOptionsStates;
  }

  // نمایش گزینه‌های منو -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // تمام گزینه‌های منو به‌روزرسانی شده‌اند
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // به‌روزرسانی برخی گزینه‌های منو
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • خطوط ۶–۱۸: این متد شناسه‌های عددی تمام گزینه‌های منو را بازیابی می‌کند؛
  • خط ۶: متد [getMenuOptions] دو پارامتر می‌گیرد:
    • [Menu menu]: منوی فرگمنت؛
    • [List<Integer> menuOptionsIds]: فهرست شناسه‌های اندروید برای گزینه‌های منو. در ابتدا، این فهرست خالی است. سپس با پیمایش بازگشتی (خط 15) درخت منو پر می‌شود؛
  • خطوط ۲۰–۴۰: بر اساس منو، آرایه وضعیت‌ها (شناسه، دید) را برای گزینه‌های منو ایجاد می‌کند. این آرایه در خط ۳ ذخیره می‌شود. کلاس [MenuItemState] در بخش ۲.۷.۱ توصیف شده است؛
  • خطوط ۴۳–۵۵: گونه‌ای از روش قبلی. این روش همان کار را انجام می‌دهد، اما به جای محاسبه مجدد شناسه‌ها برای تمام گزینه‌های منو — که قبلاً انجام شده است — از شناسه‌های آرایه وضعیت در خط ۳ استفاده می‌کند؛
  • خطوط ۵۸–۶۳: متد [setAllMenuOptionsStates] امکان پنهان یا نمایش دادن تمام گزینه‌های منوی قطعه را فراهم می‌کند؛
  • خطوط ۶۵–۶۹: متد [setMenuOptionsStates] به شما امکان می‌دهد گزینه‌های منوی خاصی را به‌صورت انتخابی نمایش دهید یا پنهان کنید؛
  • متدهای [getMenuOptions, getMenuOptionsStates] به‌صورت خصوصی تعریف شده‌اند زیرا تنها در داخل [AbstractFragment] استفاده می‌شوند. متدهای [setAllMenuOptionsStates] (خط 58) و [setMenuOptionsStates] (خط 65) به‌عنوان محافظت‌شده (protected) اعلام شده‌اند تا برای کلاس‌های فرزند در دسترس باشند؛

2.7.3.4. مدیریت انتظار برای تکمیل یک وظیفه ناهمزمان


   // اشتراک‌گذاری در مشاهده‌پذیرها
  private List<Subscription> abonnements = new ArrayList<>();
// وظایف ناهمزمان
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // مدیریت انتظار برای تکمیل یک عملیات ناهمزمان -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // تعداد وظایفی که باید اجرا شوند ثبت می‌شود
    this.numberOfRunningTasks = numberOfRunningTasks;
    // پرچم انتظار را تنظیم کنید
    mainActivity.beginWaiting();
    // پاک کردن فهرست اشتراک‌ها
    abonnements.clear();
    // هنوز لغو نشده است
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // پنهان کردن تصویر بارگذاری
    mainActivity.cancelWaiting();
  }

  • خطوط ۹–۱۸: برای آغاز یک یا چند عملیات ناهمزمان، قطعهٔ فرزند متد والد [beginRunningTasks] را فراخوانی می‌کند. پارامتر این متد تعداد وظایف ناهمزمانی است که قطعهٔ فرزند راه‌اندازی خواهد کرد؛
  • خط ۱۱: پارامتر متد ذخیره می‌شود؛
  • خط ۱۳: تصویر بارگذاری قابل مشاهده می‌شود؛
  • خط ۱۵: لیست اشتراک‌ها در عملیات غیرهم‌زمان پاک می‌شود. این اشتراک‌ها هنوز توسط قطعه فرزند ایجاد نشده‌اند؛
  • خط ۱۷: یک متغیر بولی برای نشان دادن لغو شدن وظایف غیرهمزمان درخواست‌شده توسط قطعه فرزند نگهداری می‌شود. در ابتدا، این متغیر بولی مقدار false را دارد؛
  • خطوط ۲۰–۲۵: فرگمنت فرزند متد والد [cancelWaitingTasks] را فراخوانی می‌کند تا اعلام کند که می‌خواهد وظایفی را که آغاز کرده است لغو کند؛
  • خط ۲۲: تصویر در حال انتظار پنهان می‌شود؛

2.7.3.5. پردازش استثنا


  // پردازش استثنا -------------------------------------------------------------------

  // نمایش هشدار استثنا
  protected void showAlert(Throwable th) {
    //نمایش پیام‌ها از استک استثنای نوع Throwable
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  //نمایش لیست پیام‌ها
  protected void showAlert(List<String> messages) {
    // نمایش لیست پیام‌ها
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • خطوط ۴–۷: متد [showAlert(Throwable)] به یک قطعه فرزند اجازه می‌دهد تا پیام‌های موجود در استک استثنای Throwable را که به‌عنوان پارامتر ارسال شده است، در یک پنجره نمایش دهد؛
  • خطوط ۱۰–۱۳: متد [showAlert(List<String>] به یک قطعه فرعی امکان می‌دهد تا لیست پیام‌های ارسال‌شده به‌عنوان پارامتر را در یک پنجره نمایش دهد؛
  • کلاس [Utils] که در خطوط ۶ و ۱۲ استفاده شده است، در بخش ۲.۷.۲ توضیح داده شده است؛

2.7.3.6. پردازش عملیات ناهمزمان


...
  // اشتراک‌ها در قابل‌مشاهده‌ها
  private List<Subscription> abonnements = new ArrayList<>();
  // وظایف ناهمزمان
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // اجرای یک وظیفه ناهمزمان با RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // فرآیند: مشاهده‌پذیر قابل اجرا / قابل مشاهده
    // consumeResult: متدی که پاسخ دریافتی را پردازش می‌کند
    // 
    // اشتراک‌های جدید تنها در صورتی ایجاد می‌شوند که هیچ لغوی صورت نگرفته باشد
    if (!runningTasksHaveBeenCanceled) {
      // اجرا روی نخ I/O و مشاهده روی نخ UI
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // مشاهده‌پذیر اجرا می‌شود
      try {
        abonnements.add(process.subscribe(
          // نتیجه مصرف می‌شود
          consumeResult,
          // مصرف استثنا
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // وظیفه کامل شد
          new Action0() {

            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }

  private void endOfTask() {
...
  }

  //یک عملیات ناهمزمان خطا پرتاب کرده است
  //یا یک استثنا در حین اجرای یک عملیات ناهمزمان رخ داده است
  private void consumeThrowable(Throwable th) {
...
  }

  • خطوط ۹–۴۱: اجرای یک وظیفه ناهمزمان؛
  • خط 9: متد [executeInBackground] دو پارامتر انتظار دارد:
    • [Observable<T> process]: فرآیند غیرهم‌زمان که باید اجرا شود؛
    • [Action1<T> consumeResult]: متد فرگمنت فرزند که باید فراخوانی شود تا عناصر صادر شده توسط فرآیند را به آن منتقل کند. در مثال‌های قبلی ما، فرآیندها همیشه فقط یک عنصر صادر کرده‌اند. نوع T از [Action1<T>] همان نوع T نتیجه بازگردانده شده توسط فرآیند مشاهده‌شده است؛
  • خط ۱۴: وظیفهٔ ناهمزمان تنها در صورتی اجرا می‌شود که قبلاً توسط کاربر یا برنامه (به دلیل یک استثنا) لغو نشده باشد؛
  • خط 16: فرآیند برای اجرا روی یک نخ I/O پیکربندی شده و روی نخ UI مشاهده می‌شود؛
  • خط ۱۶: دستور [process.subscribe] اجرای فرآیند را در نخ I/O آغاز می‌کند. در این نخ، عملیات به صورت همزمان اجرا می‌شوند زیرا ما از کتابخانه‌ای به نام HTTP که همزمان است، استفاده می‌کنیم؛
  • خط ۱۹: متد [process.subscribe] دارای سه پارامتر است:
    • خط ۲۱: [consumeResult]: متد قطعه فرزند که عناصر تولیدشده توسط فرآیند را مصرف خواهد کرد؛
    • خطوط ۲۲–۲۸: متدی که هنگام رخ دادن یک استثنا در حین پردازش وظیفه ناهمزمان اجرا می‌شود. پردازش در خط ۴۹ به متد [consumeThrowable] واگذار می‌شود؛
    • خطوط 29–36: متدی که هنگام ارسال اعلان «پایان انتقال» توسط وظیفه اجرا می‌شود. پردازش به متد [endOfTask] در خط 43 واگذار می‌شود؛
  • خط ۱۹: وظیفه غیرهمزمان که به تازگی اجرا شده است، در فیلد [abonnements] ثبت می‌شود، که تمام وظایف غیرهمزمان راه‌اندازی‌شده را ثبت می‌کند. این کار امکان لغو آن‌ها را در صورت لزوم فراهم می‌کند؛
  • خطوط ۳۷–۳۹: متدی که هنگام رخ دادن یک استثنا در حین پردازش وظیفه ناهمزمان اجرا می‌شود. پردازش به متد [consumeThrowable] در خط ۴۹ واگذار می‌شود؛

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


  // وظایف ناهمزمان
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    //یک وظیفه کمتر برای انتظار
    numberOfRunningTasks--;
    //انجام شد؟
    if (numberOfRunningTasks == 0) {
      // انتظار کامل
      cancelWaitingTasks();
      // تکمیل وظایف به کلاس فرزند سیگنال داده می‌شود
      notifyEndOfTasks(false);
    }
  }
...
  //کلاس‌های فرزند -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • خط ۶: یک وظیفهٔ ناهمزمان به‌تازگی تکمیل شده است. شمارندهٔ وظایف فعال یک واحد کاهش می‌یابد؛
  • خط ۸: اگر دیگر هیچ وظیفه فعالی وجود نداشته باشد، آنگاه قطعه فرعی تمام پاسخ‌های خود را دریافت کرده است؛
  • خط ۱۰: انتظار لغو می‌شود؛
  • خط ۱۲: به قطعهٔ فرزند اطلاع داده می‌شود که تمام وظایفی که راه‌اندازی کرده بود، با فراخوانی متد [notifyEndOfTasks] به پایان رسیده‌اند. پارامتر این متد نشان می‌دهد که وظایف چگونه پایان یافته‌اند: به طور عادی، یا به دلیل لغو توسط کاربر یا کد به دلیل رخ دادن یک استثنا. در خط ۱۲، خاتمه‌ای عادی سیگنال داده می‌شود. توجه داشته باشید که قطعه فرزند نیازی به پیگیری وظایفی که هنوز فعال هستند ندارد. کلاس والد آن این کار را برایش انجام می‌دهد؛

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


  // وظایف ناهمزمان
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    //یک عملیات ناهمزمان استثناء پرتاب کرده است
  // یا یک استثنا در حین اجرای یک عملیات ناهمزمان رخ داده است
  private void consumeThrowable(Throwable th) {
    // th: استثنایی که باید رسیدگی شود
    // 
    // لاگ
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // وظایفی که قبلاً آغاز شده‌اند لغو می‌شوند
    cancelRunningTasks();
    // پیام‌های خطا نمایش داده می‌شوند
    showAlert(th);
  }

  //لغو وظایف
  protected void cancelRunningTasks() {
    // لاگ
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // تمام وظایف ناهمزمان ثبت‌شده را لغو می‌کند
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    //لغو ثبت می‌شود
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    //پایان انتظار
    cancelWaitingTasks();
    // اطلاع‌رسانی به قطعهٔ فرزند در مورد لغو وظیفه
    notifyEndOfTasks(true);
}

...
  // کلاس‌های فرزند -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • خط ۳: متد [consumeThrowable] استثناء رخ داده را دریافت می‌کند؛
  • خط ۱۵: تمام وظایفی که هنوز فعال هستند لغو می‌شوند؛
  • خط ۱۷: متن استثنا نمایش داده می‌شود؛
  • خطوط ۲۱–۳۷: تمام وظایف لغو می‌شوند؛
  • خطوط ۲۷–۲۹: تمام اشتراک‌ها لغو می‌شوند؛
  • خط ۳۱: یادداشتی ثبت می‌شود که لغو صورت گرفته است؛
  • خط ۳۲: شمارنده وظایف روی صفر تنظیم می‌شود؛
  • خط ۳۴: انتظار لغو می‌شود؛
  • خط ۳۶: به فرگمنت فرزند اطلاع داده می‌شود که وظایف در پی لغو پایان یافته‌اند؛

2.7.3.7. مدیریت چرخهٔ عمر قطعه


  //چرخه زندگی --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // والد
    super.onDestroyView();
    // لاگ
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // والد
    super.onDestroy();
    // لاگ
    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) {
...
}
  • خطوط ۲–۲۰: متدهای [onDestroyView, onDestroy] صرفاً برای اهداف ثبت (لاگ) گنجانده شده‌اند. این متدها به توسعه‌دهنده امکان می‌دهند چرخه عمر فرگمنت را بهتر درک کند؛

قطعه هنگام چرخش دستگاه با استفاده از روش‌های زیر [setUserVisibleHint, onSaveInstanceState, saveState] ذخیره می‌شود:


  //چرخهٔ عمر قطعه
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // پدر
    super.setUserVisibleHint(isVisibleToUser);
    // ذخیره؟
    if (this.isVisibleToUser && !isVisibleToUser) {
      // قطعه در آستانه پنهان شدن است – در حال ذخیره آن هستیم
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // حافظه
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // لاگ
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // والد
    super.onSaveInstanceState(outState);
    // قطعه را فقط در صورتی ذخیره کن که قابل مشاهده باشد
    if (isVisibleToUser) {
      // شاید قطعه قبلاً ذخیره شده باشد
      if (!saveFragmentDone) {
        saveState();
      }
      //بازیابی باید در همه موارد انجام شود
      session.setAction(ISession.Action.RESTORE);
    }
}
  • خطوط ۶–۱۹: اگر فرگمنت از حالت نمایش داده شده به حالت پنهان تغییر کند (خط ۱۱)، ذخیره می‌شود. این متد [setUserVisibleHint] است که این اطلاعات را فراهم می‌کند؛
  • خط ۱۴: ذخیره توسط متد خصوصی در خطوط ۲۱–۲۳ انجام می‌شود؛
  • خطوط ۲۵–۴۱: هنگامی که دستگاه چرخانده می‌شود، متد [onSaveInstanceState] فراخوانی می‌شود. قطعه تحت دو شرط ذخیره می‌شود:
    • قابل مشاهده باشد (خط ۳۴)؛
    • هنوز ذخیره نشده باشد (خط ۳۶). ممکن است هر دو متد [setUserVisibleHint, onSaveInstanceState] نتوانند همزمان زمانی که قطعه قابل مشاهده است اجرا شوند، و بنابراین مدیریت بولین [saveFragmentDone] غیرضروری است. در صورت تردید، من ترجیح دادم از این یکی استفاده کنم؛
  • خط ۴۰: پس از ذخیره، بازیابی قرار دارد. توجه داشته باشید که دفعه بعد که قطعه نیاز به به‌روزرسانی خود داشته باشد، باید این کار را از طریق یک عملیات [RESTORE] انجام دهد؛

به دو مورد اشاره کنید که در آن ذخیرهٔ قطعه درخواست می‌شود:

  1. وقتی از حالت قابل مشاهده به حالت پنهان تغییر می‌کند؛
  2. هنگامی که دستگاه چرخانده می‌شود؛

متد خصوصی [saveState] به شرح زیر است:


...
  private void saveState() {
    // وظایف باید لغو شوند؟
    if (numberOfRunningTasks != 0) {
      // وظایف لغو شده‌اند
      cancelRunningTasks();
    }
    // ذخیره وضعیت قطعه
    CoreState currentState = saveFragment();
    // قطعه بازدید شده است
    currentState.setHasBeenVisited(true);
    // ذخیره وضعیت منو
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // وارد شده
    session.setCoreState(getNumView(), currentState);
    // ذخیره کامل شد
    saveFragmentDone = true;
    // لاگ
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  //کلاس‌های فرزند -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • خطوط ۴–۷: دستگاه ممکن است در حالی که عملیات ناهمزمان در حال اجرا هستند بچرخد. در اینجا تصمیم گرفته می‌شود که همهٔ آن‌ها لغو شوند. این تصمیم برای کاربر مناسب نیست، زیرا او مجبور است صرفاً به دلیل جابه‌جایی تلفن یا تبلت خود یا دریافت یک تماس تلفنی، درخواستی که ممکن است زمان‌بر باشد را دوباره ارسال کند. می‌توان ارتباطات شبکه را در طول چرخه پشتیبان‌گیری/بازیابی حفظ کرد. با این حال، راه‌حل‌ها ساده نیستند و من تصمیم گرفته‌ام در این دوره مقدماتی به آن‌ها نپردازم. راه پیش رو این است که این اتصالات شبکه را از طریق یک قطعه (fragment) بدون رابط کاربری (visual interface) مرتبط برقرار کنیم، قطعه‌ای که در طول چرخه پشتیبان‌گیری/بازیابی از بین نمی‌رود. برای این کار، کافی است از دستور [Fragment.setRetainInstance(true)] استفاده کنید؛
  • خط ۹: به فرگمنت فرزند دستور داده می‌شود تا وضعیت خود را در نوعی که از [CoreState] (خط ۳۱) مشتق شده است ذخیره کند؛
  • خط ۱۱: ما ثبت می‌کنیم که از این قطعه بازدید شده است. این اطلاعات مفید است. هنگامی که یک قطعه برای اولین بار بازدید می‌شود، به‌روزرسانی آن ممکن است با بازدیدهای بعدی متفاوت باشد زیرا هیچ وضعیت قبلی در جلسه ندارد؛
  • خط ۱۳: وضعیت منو ذخیره می‌شود که به ما امکان می‌دهد آن را به طور خودکار بازیابی کنیم؛
  • خط ۱۵: این وضعیت فعلی در جلسه ذخیره می‌شود. در داخل جلسه، وضعیت‌ها بر اساس نما/قطعه گروه‌بندی می‌شوند و هر کدام وضعیت خاص خود را دارند. شماره نما توسط قطعه فرزند (خط ۳۳) ارائه می‌شود؛
  • خط ۱۷: ما ثبت می‌کنیم که فرگمنت ذخیره شده است. این به این دلیل است که ممکن است دو متد متد [saveState] را فراخوانی کنند، و نیازی به انجام دو بار ذخیره نیست؛

نما مرتبط با قطعه با روش زیر بازتولید می‌شود:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // والد
    super.onActivityCreated(savedInstanceState);
    // لاگ
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // نمایش باید بازیابی شود
    viewHasToBeInitialized = true;
}

در چرخه عمر، متد [onActivityCreated] بلافاصله پس از متد [onCreateView] اجرا می‌شود. فراخوانی متد دوم نشان می‌دهد که نمای مرتبط با قطعه باید دوباره ساخته شود. ما صرفاً این موضوع را در خط ۱۰ ثبت می‌کنیم.

2.7.3.8. به‌روزرسانی قطعه

به‌روزرسانی قطعه، آخرین عملیاتی است که روی قطعه انجام می‌شود، پیش از آنکه قابل مشاهده شود و منتظر ورودی کاربر بماند. این کار توسط کد زیر انجام می‌شود:


  // منوی قطعه
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  //چرخهٔ عمر قطعه
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // حالت‌های قطعه
  private CoreState previousState;
  // jSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  //چرخه عمر قطعه
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // به‌روزرسانی قطعه ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // لاگ
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // حافظه
    this.menu = menu;
    // دریافت گزینه‌های منوی # اگر این کار قبلاً انجام نشده باشد
    if (fragmentHasToBeInitialized) {
      //بازیابی گزینه‌های منوی #
      getMenuOptionsStates(menu);
      // فعالیت
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    //بازیابی وضعیت قبلی قطعه (در اولین بار، تنها مقدار بولی hasBeenVisited معنادار است)
    previousState = session.getCoreState(getNumView());
    //به‌روزرسانی قطعهٔ فرعی در چند مرحله
    // مرحلهٔ ۱ – آیا این اولین بازدید است؟
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      //این اولین بازدید نیست
      // مرحله ۲: آیا باید قطعه را اولیه کنیم؟
      ...
      // مرحله ۳: آیا باید نما اولیه شود؟
      ...
    }
    // مرحله ۴: یک ارسال، یک ناوبری، یک بازیابی؟
    ...

    // مرحله ۵: به‌روزرسانی‌های ترمینال ----------------------
...
  }
...
  //زیرکلاس‌ها -----------------------------------------------------
  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();
  • خط ۱۹: متد [onCreateOptionsMenu] برای به‌روزرسانی قطعه استفاده می‌شود. به همین دلیل، قطعه باید یک منو داشته باشد که در صورت لزوم می‌تواند خالی باشد. هنگامی که این متد اجرا می‌شود، قطعه با نما و فعالیت خود مرتبط شده و همچنین قابل مشاهده است؛
  • خط ۲۵: منویی که به‌عنوان پارامتر (خط ۲۲) به متد ارسال شده است، ذخیره می‌شود؛
  • خطوط ۲۷–۳۴: اگر نیاز باشد قطعه راه‌اندازی شود:
    • خط ۲۹: وضعیت گزینه‌های منو در آرایه [menuOptionsStates] از خط ۳ ذخیره می‌شود؛
    • خط ۳۱: اکتیویتی به عنوان یک نمونه از نوع اندروید [Activity] ذخیره می‌شود؛
    • خط ۳۲: اکتیویتی به عنوان نمونه‌ای از رابط [IMainActivity] ذخیره می‌شود؛
    • خط ۳۳: جلسه ذخیره می‌شود. تبدیل نوع ضروری است زیرا متد [mainActivity.getSession()] نوع [ISession] را برمی‌گرداند؛
  • خط ۳۶: وضعیت قبلی قطعه از جلسه بازیابی می‌شود. اگر این اولین بازدید از قطعه باشد، تنها بولین [previousState.hasBeenVisited] مرتبط است؛
  • خطوط ۳۹–۴۴: کدی که هنگام اولین بازدید از قطعه اجرا می‌شود. در این حالت، وضعیت قبلی آن بی‌ربط است؛
  • خطوط ۴۴–۵۰: کدی که زمانی اجرا می‌شود که این اولین بازدید از قطعه نیست؛
  • خطوط ۴۶–۴۷: کدی که در صورت فراخوانی سازنده (constructor) قطعه اجرا می‌شود (fragmentHasToBeInitialized==true);
  • خطوط ۴۸–۴۹: کدی که در صورت بازسازی شدن نمای مرتبط با قطعه اجرا می‌شود (viewHasToBeInitialized==true);
  • خطوط ۵۱–۵۲: کدی که بسته به اقدام فعلی اجرا می‌شود (SUBMIT, NAVIGATION, RESTORE);
  • خطوط ۵۴–۵۵: کد همچنان در حال اجرا است؛

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

مرحله ۱


  // منوی قطعه
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  //چرخهٔ عمر قطعه
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // حالت‌های قطعه
  private CoreState previousState;
  // jSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  //چرخه عمر قطعه
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // وضعیت قبلی قطعه بازیابی می‌شود (در اولین بار، تنها مقدار بولی hasBeenVisited معنا دارد)
    previousState = session.getCoreState(getNumView());
    //به‌روزرسانی قطعهٔ فرزند در چند مرحله
    // مرحله ۱ – آیا این اولین بازدید است؟
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // ابتدایی‌سازی قطعه و نما
      initFragment(null);
      initView(null);
      // ریست previousState برای مرحله بعدی
      previousState = null;
    } else {
      // این اولین بازدید شما نیست
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • خط ۱۹: وضعیت قبلی قطعه از جلسه بازیابی می‌شود؛
  • خطوط 22–31: کد در صورتی اجرا می‌شود که قطعه هرگز بازدید نشده باشد؛
  • خط ۲۷: از کلاس فرزند خواسته می‌شود که قطعه را مقداردهی اولیه کند. پارامتر متد [initFragment] در خط ۳۵، وضعیت قبلی قطعه است. در اینجا، null ارسال می‌شود تا به قطعه فرزند نشان دهد که این اولین بازدید است؛
  • خط ۲۸: به کلاس فرزند دستور داده می‌شود که نمای مرتبط با قطعه را مقداردهی اولیه کند. پارامتر متد [initView] در خط ۳۷، وضعیت قبلی قطعه است. در اینجا، null ارسال می‌شود تا به قطعه فرزند نشان دهد که این اولین بازدید است؛
  • خط ۳۰: وضعیت قبلی برای مراحل بعدی روی null تنظیم می‌شود؛

مراحل ۲ و ۳


// منوی قطعه
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  //چرخهٔ عمر قطعه
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  //حالات قطعه
  private CoreState previousState;
  // jSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  //چرخهٔ عمر قطعه
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    //وضعیت قبلی قطعه بازیابی می‌شود (در اولین بار، تنها بولی hasBeenVisited معنایی دارد)
    previousState = session.getCoreState(getNumView());
    //به‌روزرسانی قطعهٔ فرزند در چند مرحله
    // مرحلهٔ ۱ – آیا این اولین بازدید است؟
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      //این اولین بازدید نیست
      // مرحله ۲: آیا باید قطعه را اولیه کرد؟
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        //قطعهٔ فرزند
        initFragment(previousState);
      }
      // مرحله ۳: آیا باید نما اولیه شود؟
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // فرگمنت فرزند
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • خطوط ۲۴–۴۲: زمانی اجرا می‌شود که این اولین بازدید از قطعه نیست؛
  • خطوط ۲۷–۳۳: اگر قطعه به تازگی بازسازی شده باشد، با فراخوانی متد [initFragment] از کلاس فرزند، ریست می‌شود (خطوط ۳۲، ۴۶). وضعیت قبلی قطعه به آن ارسال می‌شود؛
  • خطوط ۳۵–۵۱: اگر نمای مرتبط با قطعه نیاز به راه‌اندازی اولیه یا ریست داشته باشد، از قطعهٔ فرزند خواسته می‌شود این کار را انجام دهد (خطوط ۴۰، ۴۸). در اینجا نیز، آخرین وضعیت شناخته‌شدهٔ قطعه به آن ارسال می‌شود؛

مرحله ۴


//منوی قطعه
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  //چرخهٔ عمر قطعه
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // حالت‌های قطعه
  private CoreState previousState;
  // jSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  //چرخهٔ عمر قطعه
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    //وضعیت قبلی قطعه بازیابی می‌شود (در اولین بار، تنها بولی hasBeenVisited معنایی دارد)
    previousState = session.getCoreState(getNumView());
    //به‌روزرسانی قطعهٔ فرزند در چند مرحله
 ...

    // مرحله ۴: یک ارسال، یک ناوبری، یک بازیابی؟
    // لاگ
    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();
      }
    }
    // عمل در حال انجام
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // قطعهٔ فرزند
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // بازیابی منو
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // قطعهٔ کودک
          updateOnRestore(previousState);
        } else {
          //این اولین بازدید است – نیازی به اقدام نیست
        }
        break;
      case RESTORE:
        // بازیابی
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // بازیابی منو (previousState نمی‌تواند null باشد)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // قطعهٔ دختر
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • خطوط ۳۴–۶۶: اقدام فعلی پردازش می‌شود که ممکن است یکی از سه مورد زیر باشد:
    • RESTORE: قطعه پس از چرخش دستگاه بازیابی می‌شود؛
    • NAVIGATION: ما به قطعه بازمی‌گردیم، با نیت بازیابی آن در وضعیتی که آخرین بار آن را رها کردیم؛
    • SUBMIT: سایر موارد؛
  • خط ۳۴: اقدام فعلی بازیابی می‌شود؛
  • خطوط ۳۶–۴۲: برای عملی از نوع SUBMIT، متد [updateOnSubmit] از قطعه فرعی فراخوانی می‌شود (خطوط ۴۱، ۶۸)، و آخرین وضعیت شناخته‌شده قطعه به آن پاس داده می‌شود؛
  • خطوط ۴۳–۵۵: برای عملی از نوع NAVIGATION؛
  • خطوط ۴۷–۵۴: ما می‌خواهیم قطعه را به آخرین وضعیت شناخته‌شده خود بازگردانیم. عملیات NAVIGATION ممکن است با اولین بازدید همزمان باشد. این حالت، برای مثال، در یک برنامه تب‌بندی‌شده رخ می‌دهد: اگر از تب ۱ به تب ۴ سوئیچ کنم:
    • اگر این اولین بازدید باشد، باید قطعه را برای زبانهٔ ۴ مقداردهی اولیه کنم؛
    • اگر این اولین بازدید نباشد، قطعه را برای برگه ۴ به وضعیت قبلی‌اش بازگردانید؛
  • خطوط ۵۲–۵۴: اگر این اولین بازدید باشد، کاری انجام ندهید. انجام این inicialization بر عهده متد فرزند [initView(CoreState previousState)] خواهد بود. اولین بازدید توسط شرط [previousState==null] شناسایی می‌شود؛
  • خط ۴۹: اگر این اولین بازدید از قطعه نباشد، منوی آن بازیابی می‌شود؛
  • خط ۵۱: به کلاس فرزند دستور داده می‌شود که با فراخوانی متد در خط ۷۰، خود را به‌روزرسانی کند. وضعیت قبلی قطعه به آن پاس داده می‌شود تا بتواند وظیفه‌اش را انجام دهد؛
  • خطوط ۵۶–۶۶: در صورت عملیات بازسازی قطعه، همانند مورد ناوبری خارج از اولین بازدید عمل می‌کنیم؛

مرحله ۵


// منوی قطعه
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  //چرخهٔ عمر قطعه
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // وضعیت‌های قطعه
  private CoreState previousState;
  // jSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  //چرخه عمر قطعه
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // مرحله ۵: به‌روزرسانی ترمینال ----------------------
    // ما نما را تغییر دادیم
    session.setPreviousView(getNumView());
    // اقدام بیشتری در جریان نیست
    session.setAction(ISession.Action.NONE);
    // هنگامی که از این قطعه خارج می‌شویم، باید ذخیره شود
    saveFragmentDone = false;
    //تا زمانی که قطعه بازسازی نشود، نیازی به инициалиزه‌سازی آن نیست
    fragmentHasToBeInitialized = false;
    //تا زمانی که نما بازسازی شود، نیازی به inicialization ندارد
    viewHasToBeInitialized = false;
    // انتخاب زبانه به عملکرد عادی بازمی‌گردد
    session.setNavigationOnTabSelectionNeeded(true);

    // قطعه مطلع می‌شود که نما آماده است
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • خطوط ۱۸–۳۰: وقتی به این نقطه می‌رسیم، قطعه اولیه شده و آماده نمایش است. سپس تمام پرچم‌های استفاده شده در مدیریت چرخه عمر قطعه را به وضعیت اولیه خود بازمی‌گردانیم؛
  • خط ۲۰: ما نماها را تغییر داده‌ایم: این را در جلسه ثبت می‌کنیم؛
  • خط ۲۲: دیگر هیچ اقدامی در حال اجرا نیست؛
  • خط ۲۴: هنگامی که از قطعهٔ فعلی روی صفحه خارج می‌شویم، باید هنگام خروج ذخیره شود؛
  • خط ۲۶: دیگر نیازی به بازسازی قطعه نیست. این پرچم هنگام اجرای مجدد سازنده قطعه به vrai بازنشانی خواهد شد؛
  • خط ۲۸: نمای مرتبط با قطعه دیگر نیازی به مقداردهی اولیه ندارد. این پرچم هنگام اجرای مجدد متد [onActivityCreated] به مقدار vrai بازنشانی خواهد شد؛
  • خط ۳۰: ممکن است قطعه در یک برنامهٔ تب‌دار نمایش داده شود. در این صورت، هنگامی که کاربر روی یکی از تب‌ها کلیک می‌کند، باید یک تغییر قطعه رخ دهد؛
  • خط ۳۶: کلاس فرزند مطلع می‌شود که قطعه آماده است. این کلاس می‌تواند در متد [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 {

  //فیلدهایی که از کلاس والد به ارث رسیده‌اند -------------------------------------------------------

  // حالت اشکال‌زدایی
  //-- نهایی محافظت‌شده بولین isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // نام کلاس
  //-- رشته محافظت‌شده className;
  // وظایف ناهمزمان
  //-- int محافظت‌شده numberOfRunningTasks;
  //فعالیت
  //-- محافظت‌شده IMainActivity mainActivity;
  //-- Activity محافظت‌شده activity;
  //جلسه
  //-- محافظت‌شده Session session;

  // متدهای ارث‌بری‌شده از کلاس والد -------------------------------------------------------

  // نمایش گزینه‌های منو
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // پردازش انتظار برای تکمیل مجموعه‌ای از وظایف ناهمزمان
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // اجرای یک وظیفه ناهمزمان با RxAndroid
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // لغو وظیفه
  //-- protected void cancelRunningTasks() {
  // نمایش هشدار استثنا
  //-- protected void showAlert(Throwable th) {
  // نمایش لیست پیام‌ها
  //-- protected void showAlert(List<String> messages) {

  // متدهای تحمیل‌شده توسط کلاس والد -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // قطعه باید ذخیره شود
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // اگرچیزی برای ذخیره وجود ندارد، [return new CoreState();] را فراخوانی کنید و کلاس [DummyFragmentState] را حذف کنید
  }

  @Override
  protected int getNumView() {
    // شماره قطعه باید به جدول قطعات مدیریت‌شده توسط فعالیت بازگردانده شود (رجوع کنید به MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    //قطعه نمایان می‌شود و در این مرحله یا مرحله‌ی قبلی رندر شده است
    // این زمانی رخ می‌دهد که برنامه اجرا می‌شود و هر بار که دستگاه اندروید چرخانده می‌شود
    // حتماً با اجرای [initView] دنبال می‌شود
    // میدان‌های قطعه بازسازی‌شده باید مقداردهی اولیه شوند
    //previousState جدیدترین ذخیرهٔ قطعه است – اگر این اولین بازدید از قطعه باشد، مقدار آن null است
  }

  @Override
  protected void initView(CoreState previousState) {
    //قطعه قابل مشاهده می‌شود و نمای مرتبط در این مرحله یا مرحله قبلی بازسازی شده است
    //این هر بار که [initFragment] اجرا می‌شود و هر بار که قطعه از محدوده قطعه نمایش‌داده‌شده خارج می‌شود رخ می‌دهد
    // اجزای نمایی که بازسازی شده است باید مقداردهی اولیه شوند
    //previousState جدیدترین ذخیرهٔ قطعه است – اگر این اولین بازدید از قطعه باشد، null است

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // پس از [initFragment, initView] اجرا می‌شود اگر این متدها اجرا شوند
    //نما پس از عملیاتی از نوع SUBMIT نمایش داده می‌شود
    // قطعه و نمای مرتبط عموماً باید از جلسه اولیه شوند
    // previousState جدیدترین ذخیرهٔ قطعه است – اگر این اولین بازدید از قطعه باشد، null است
    // اگر نتوان از طریق عملیات SUBMIT به قطعه دسترسی داشت، کاری برای انجام دادن وجود ندارد
    //اگر بتوان از طریق عملیات SUBMIT از قطعات مختلف به قطعه دسترسی یافت، می‌توان نمای قبلی را با استفاده از [session.getPreviousView] تعیین کرد
    //اگر بتوان از طریق چندین عملیات SUBMIT از همان قطعه به قطعه دسترسی یافت، باید یک پرچم جلسه تنظیم شود تا بین انواع مختلف SUBMIT که از آن قطعه منشأ می‌گیرند تمایز قائل شد.
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    //پس از [initFragment, initView] اجرا می‌شود اگر این متدها اجرا شوند
    //نما پس از عملیاتی از نوع RESTORE یا NAVIGATION نمایش داده می‌شود
    //previousState جدیدترین ذخیرهٔ قطعه است – هرگز null نیست
    //نما باید به وضعیت قبلی خود بازگردانده شود

  }

  @Override
  protected void notifyEndOfUpdates() {
    // پس از متدهای [updateOnSubmit, updateOnRestore] فراخوانی می‌شود
    // تا زمانی که به این نقطه می‌رسیم، نما ساخته و مقداردهی اولیه شده است
    // اغلب در اینجا کاری برای انجام دادن وجود ندارد، اما اقداماتی که باید صرف‌نظر از نحوه دسترسی به این نما انجام شوند نیز می‌توانند در اینجا لحاظ گردند
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // زمانی فراخوانی می‌شود که وظایف ناهمزمان راه‌اندازی‌شده توسط قطعه یا تکمیل شده باشند یا لغو شده باشند
    // این دو حالت را می‌توان با استفاده از پارامتر runningTasksHaveBeenCanceled از یکدیگر متمایز کرد
    // نما عموماً نیاز دارد که به حالتی متفاوت از حالتی که در حین انتظار برای پاسخ‌های وظایف ناهمزمان در آن بود، بازنشانی شود

  }
}

کلاس [DummyFragment] ممکن است حالت نداشته باشد. در اینجا، ما یک حالت را برای نشان دادن آنچه در داخل آن انتظار می‌رود، گنجانده‌ایم:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  //وضعیت قطعه [DummyFragment]
  // فقط فیلدهای قابل سریالی‌سازی را در jSON بگنجانید
  // توضیحات @JsonIgnore را به موارد دیگر اضافه کنید، اما مشخص نیست چه کاربردی دارند
  //فراموش نکنید گترها و سترها – آنها برای سریالی‌سازی و دِسریالی‌سازی استفاده می‌شوند
}

برای نشان دادن نحوه استفاده از پروژه [client-android-skel]، ابتدا از چند مثال ساده استفاده می‌کنیم و سپس به یک مطالعه موردی جامع‌تر می‌پردازیم.

2.8. تمرین‌های نمایشی

ما با بازسازی برخی از مثال‌های موجود شروع خواهیم کرد.

2.8.1. مثال 17B

ما مثال ۱۷ را که در بخش ۱.۱۸ مورد بحث قرار گرفته است، مجدداً بررسی خواهیم کرد. این یک برنامه با یک قطعه واحد است که شامل هیچ وظیفه غیرهمزمان و هیچ زبانه نیست. ما آن را بررسی خواهیم کرد تا ببینیم هنگام چرخش دستگاه چگونه رفتار می‌کند. ما موارد زیر را وارد خواهیم کرد:

Image

سپس، در [1]، دستگاه را دو بار می‌چرخانیم. نمای جدید به شرح زیر است:

Image

اگر نماها را با هم مقایسه کنیم، همه چیز حفظ شده است به جز لیست [2] که اکنون خالی است.

علاوه بر این، اگر روی دکمه [Valider] کلیک کنید، یک کادر محاوره‌ای ظاهر می‌شود که ورودی‌های انجام‌شده در فرم را نشان می‌دهد. اگر در این مرحله دستگاه را بچرخانید، کادر محاوره‌ای ناپدید می‌شود.

بنابراین، هنگام چرخاندن دستگاه، باید موارد زیر را مجدداً تولید کنیم:

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

2.8.1.1. پروژه [Exemple-17B]

ما پروژه [client-android-skel] را در examples/Example-17B کپی می‌کنیم. سپس پروژه جدید [1] را بارگذاری می‌کنیم:

  • به داخل [2-3]، در پوشه [behavior]، قطعه [Vue1Fragment] را از پروژه [Exemple-17] می‌چسبانیم؛
  • در [4-5]، درون پوشه [layout] از [Exemple-17B]، نمای [vue1.xml] را از [Exemple-17] می‌چسبانیم. این نما با قطعه مرتبط است؛
  • در [6]، پوشه [values] از [Exemple-17B] با پوشه [values] از [Exemple-17] جایگزین می‌شود؛

حاشیه بالایی نمای [vue1.xml] به 80 dp تغییر خواهد یافت:


    <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"/>

در این مرحله، می‌توانیم یک کامپایل اولیه را برای بررسی خطاها اجرا کنیم. اولین خطاهای گزارش‌شده مربوط به imports است که به دلیل جابجایی پکیج‌ها ایجاد شده است. ما این خطاها را برطرف می‌کنیم (Ctrl-Shift-O). خطاهای دیگری به وجود می‌آیند زیرا ویو [Vue1Fragment] تمام متدهای مورد نیاز کلاس والد خود [AbstractParent] را پیاده‌سازی نکرده است:

Image

متدهای از دست رفته را تولید کنید (Alt-Enter).

یک خطای کامپایل دیگر گزارش‌شده به شرح زیر است:

Image

این مشکل در فایل [build.gradle] ماژول (خط ۲۰ زیر) اصلاح شده است:

 

در این مرحله می‌توانید دوباره کامپایل کنید تا هرگونه خطای باقی‌مانده را ببینید. تنها خطای گزارش‌شده در متد [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 {

  //دسترسی به جلسه
  ISession getSession();

  // تغییر نما
  void navigateToView(int position, ISession.Action action);

  // پردازش انتظار
  void beginWaiting();

  void cancelWaiting();

  // ثوابت برنامه -------------------------------------

  // حالت اشکال‌زدایی
  boolean IS_DEBUG_ENABLED = true;

  // حداکثر زمان انتظار برای پاسخ سرور
  int TIMEOUT = 1000;

  // زمان انتظار قبل از اجرای درخواست کلاینت
  int DELAY = 0;

  // احراز هویت پایه
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // همجواری قطعه
  int OFF_SCREEN_PAGE_LIMIT = 1;

  //نوار زبانه
  boolean ARE_TABS_NEEDED = false;

  // بارگذاری تصویر
  boolean IS_WAITING_ICON_NEEDED = false;

  // تعداد قطعات برنامه
  int FRAGMENTS_COUNT = 1;

}
  • خطوط ۲۴–۳۱: برنامه در اینجا از لایه [DAO] خود استفاده نمی‌کند. این ثابت‌ها استفاده نخواهند شد؛
  • خط ۳۴: مجاورت قطعه ۱، که مقدار پیش‌فرض است. از آنجا که برنامه تنها یک قطعه دارد (خط ۴۳)، این مقدار بی‌ربط است؛
  • خطوط ۳۹–۴۰: از آنجایی که هیچ عملیاتی شامل لایه [DAO] وجود ندارد، نیازی به تصویر جای‌گذاری (placeholder) نیست؛
  • خط ۳۷: این یک برنامهٔ تب‌دار نیست؛
  • خط ۴۳: تنها یک قطعه وجود دارد؛

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


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // عناصری که نمی‌توانند در jSON سریالی‌سازی شوند باید دارای نشانه‌گذاری @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 {
  // اینکه آیا قطعه بازدید شده است یا خیر
  protected boolean hasBeenVisited = false;
  //وضعیت منوی قطعه (در صورت وجود)
  protected MenuItemState[] menuOptionsState;

  // گیرنده و تنظیم‌کننده
...
}
  • خطوط ۱۱–۱۳: ما باید تمام کلاس‌های مشتق‌شده از [CoreState] را که وضعیت قطعات مختلف را ذخیره می‌کنند، شامل کنیم. در اینجا تنها یک مورد وجود دارد (خط ۱۲)؛

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 {

  // [DAO] لایه
  @Bean(Dao.class)
  protected IDao dao;
  // جلسه
  private Session session;

  // متدهای کلاس والد -----------------------
  @Override
  protected void onCreateActivity() {
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // جلسه
    this.session = (Session) super.session;
    // کار در دست: ادامهٔ инициалиزه‌سازی‌های آغازشده توسط کلاس والد
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // کار در دست: عناوین قطعات را اینجا تعریف کنید
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // کار در دست: عناوین قطعات را اینجا تعریف کنید
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // کار در دست: ناوبری برگه‌ای – مشخص کنید چه نمایی هنگام انتخاب برگه شماره [position] نمایش داده شود
  }

  @Override
  protected int getFirstView() {
    // کار در دست: تعداد نما (قطعه) اولی که باید نمایش داده شود را مشخص کنید
    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 {

  //لایه [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // جلسه
  private Session session;

  // متدهای کلاس والد -----------------------
  @Override
  protected void onCreateActivity() {
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // جلسه
    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;
  }
}

فقط متد در خطوط ۴۱–۴۴ نیاز به تغییر دارد. این متد باید آرایهٔ قطعات برنامه را بازگرداند. در خط ۴۳ فراموش نکنید که زیرخط (_) را پس از نام قطعه اضافه کنید.

2.8.1.5. وضعیت قطعه [FragmentState]

پس از انجام آزمون‌های چرخش روی پروژه [Exemple-17]، تصمیم گرفته شده است که عناصر زیر از قطعه را ذخیره کنیم:

  • فهرست مقادیر در لیست کشویی؛
  • موقعیت آیتم انتخاب‌شده در این لیست؛
  • پیامی که توسط کادر محاوره‌ای نمایش داده می‌شود، در صورتی که در زمان چرخش موجود باشد؛

کلاس [Vue1FragmentState] به شرح زیر خواهد بود:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

  // مقادیر لیست کشویی
  private List<String> list;
  // مورد انتخاب‌شده از فهرست کشویی
  private int listSelectedPosition;
  // پیامی که در کادر محاوره‌ای نمایش داده می‌شود
  private String message;

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

2.8.1.6. قطعه [AbstractFragment]

در حال حاضر، چرخهٔ عمر قطعه توسط دو متد (خطوط ۶ و ۳۲) مدیریت می‌شود:


// لیست کشویی
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // اولین دکمه تیک خورده است
    radioButton1.setChecked(true);
    // تقویم
    datePicker1.setCalendarViewShown(false);
    // 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));
      }
    });
    // لیست کشویی
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // ابتدایی‌سازی آداپتور لیست کشویی
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }

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


// مدیریت چرخهٔ عمر قطعه ---------------------------------------------------------------------
  @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) {
    // اولین بازدید؟
    if (previousState == null) {
      // ایجاد مقادیر لیست کشویی
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // مقادیر لیست کشویی بازیابی می‌شوند
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      //و پیام کادر محاوره‌ای
      message = state.getMessage();
    }
    // ابتدایی‌سازی تطبیق‌دهندهٔ فهرست کشویی
    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) {
    // تقویم
    datePicker1.setCalendarViewShown(false);
    // 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));
      }
    });
    // ابتکاری‌سازی تطبیق‌دهنده فهرست کشویی
    dropDownList.setAdapter(dataAdapter);
    // اولین بازدید؟
    if (previousState == null) {
      // دکمهٔ اول تیک خورده است
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    //مقدار نوار جستجو
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // آیتم انتخاب‌شده در لیست کشویی
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    //آیا کادر گفتگو قابل مشاهده است؟
    if (message != null) {
      // نمایش آن
      showMessage();
    }
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

}
  • خطوط ۲–۹: متد [saveFragment] باید عناصر قطعه‌ای را که باید ذخیره شوند، در کلاسی مشتق‌شده از [CoreState] قرار دهد و یک نمونه از آن کلاس را بازگرداند؛
  • خطوط ۱۱–۱۴: متد [getNumView] باید شماره قطعه را بازگرداند. در اینجا تنها یک قطعه وجود دارد که شماره‌اش ۰ است؛
  • خطوط ۱۶–۳۴: متد [initFragment] باید فیلدهای قطعه را مقداردهی اولیه کند. این متد وضعیت قبلی قطعه را دریافت می‌کند. اگر [previousState] برابر null باشد، در این صورت این اولین بازدید است؛
  • خطوط ۱۹–۲۵: در اولین بازدید، مقادیر لیست کشویی ایجاد می‌شوند؛
  • خطوط ۲۶–۳۰: اگر این اولین بازدید نباشد، فیلدهای [list, message] قطعه از وضعیت قبلی بازیابی می‌شوند؛
  • خطوط ۳۳–۳۴: مقداردهی اولیه فیلد [dataAdapter] قطعه. این منبع داده برای لیست کشویی است؛
  • خطوط ۳۷–۶۲: متد [initView] برای راه‌اندازی اجزای رابط کاربری بصری استفاده می‌شود. این متد وضعیت قبلی، [previousState]، را به عنوان پارامتر می‌پذیرد. اگر [previousState==null] باشد، این اولین بازدید است؛
  • این بخش حاوی کدی است که قبلاً در متد [@AfterViews] یافت می‌شد؛
  • خطوط ۵۷–۶۱: در اولین بازدید، اطمینان حاصل می‌کنیم که اولین دکمه رادیویی انتخاب شده باشد؛
  • خطوط 64–67: متد [updateOnSubmit] زمانی اجرا می‌شود که اقدام فعلی [SUBMIT] باشد. در اینجا، هیچ ناوبری بین-قطعه وجود ندارد و در نتیجه هیچ اقدام فعلی وجود ندارد؛
  • خطوط ۶۹–۸۱: متد [updateOnRestore] زمانی اجرا می‌شود که اقدام فعلی [NAVIGATION] یا [RESTORE] باشد. در اینجا هیچ ناوبری بین-قطعه وجود ندارد و بنابراین هیچ اقدام [NAVIGATION] ممکن نیست؛
  • خط ۷۲: مقدار TextView seekBarValue مجدداً محاسبه می‌شود (احیا نمی‌شود). این به این دلیل است که در طول چرخش‌ها، مقدار آن گاهی از دست می‌رفت؛
  • خطوط ۷۴–۷۵: لیست روی موردی که قبل از چرخش انتخاب شده بود تنظیم می‌شود. بدون این، لیست به طور پیش‌فرض روی اولین مورد خود قرار می‌گرفت؛
  • خطوط ۷۶–۸۰: اگر پیام از وضعیت قبلی null نباشد، کادر گفتگو مجدداً نمایش داده می‌شود. ما به متد [showMessage] (خط ۷۹) باز خواهیم گشت؛
  • خطوط ۸۳–۸۶: متد [notifyEndOfUpdates] آخرین متدی است که توسط کلاس والد قبل از رها کردن قطعه فرزند بدون دستکاری فراخوانی می‌شود. در اینجا کاری برای انجام دادن وجود ندارد؛
  • خطوط 88–91: متد [notifyEndOfTasks] پایان هرگونه وظیفه ناهمزمان را که توسط قطعه راه‌اندازی شده است، اعلام می‌کند. در این مورد، هیچ وظیفه‌ای وجود ندارد؛

کادر محاوره‌ای به شرح زیر بازیابی می‌شود:


  // پیام در کادر محاوره‌ای
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // فهرست پیام‌ها برای نمایش
    List<String> messages = new ArrayList<>();
    ...
    //نمایش
    doAfficher(messages);
  }

  private void doAfficher(final List<String> messages) {
    // ساختار متن قابل نمایش را بسازید
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // پیام را ذخیره کنید
    message = texte.toString();
    // نمایش آن
    showMessage();
  }

  private void showMessage() {
    // نمایش
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // پیام را ریست کنید
        message = null;
      }
    }).show();
}

هنگامی که کاربر فرم را ارسال می‌کند، متد [doValider] (خط ۵) یک لیست پیام‌ها را ایجاد می‌کند و سپس آن را (خط ۱۰) در کادر گفتگو نمایش می‌دهد.

  • خطوط ۱۴–۲۰: لیست پیام‌ها در یک پیام واحد ادغام شده و در خط ۲ ذخیره می‌شود؛
  • خطوط ۲۵–۳۳: این پیامی است که در کادر محاوره‌ای نمایش داده می‌شود و همان پیامی است که متد [updateOnRestore] نمایش می‌دهد؛
  • خط ۲۷: پارامتر دوم متد [setNeutralButton]، متدی است که هنگام کلیک کاربر روی دکمه [Fermer] در کادر گفتگو اجرا می‌شود؛
  • خط ۳۱: هنگامی که کادر محاوره بسته می‌شود، پیام به null بازنشانی می‌شود تا نشان دهد که کادر محاوره دیگر وجود ندارد؛

2.8.1.7. Tests

از خوانندگان دعوت می‌شود این پروژه را آزمایش کرده و بررسی کنند که آیا قطعه پس از یک یا چند چرخش متوالی حفظ می‌شود یا خیر.

2.8.2. مثال ۲۳: مشتری آب‌وهوا

برخی وب‌سایت‌ها اطلاعات آب و هوا را در قالب رشته‌هایی مانند jSON ارائه می‌دهند. در اینجا یک مثال آمده است:

Image

URL در قالب زیر است: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} که در آن:

  • city: شهری که برای آن می‌خواهید پیش‌بینی آب‌وهوا را داشته باشید، در این مورد آنژ؛
  • کشور: کشور شهر، در این مورد فرانسه (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 {

  // دسترسی به جلسه
  ISession getSession();

  // تغییر نما
  void navigateToView(int position, ISession.Action action);

  // مدیریت صف
  void beginWaiting();

  void cancelWaiting();

  // ثوابت برنامه -------------------------------------

  // حالت اشکال‌زدایی
  boolean IS_DEBUG_ENABLED = true;

  // حداکثر زمان انتظار برای پاسخ سرور
  int TIMEOUT = 1000;

  // زمان انتظار قبل از اجرای درخواست کلاینت
  int DELAY = 5000;

  // احراز هویت پایه
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // همجواری قطعه
  int OFF_SCREEN_PAGE_LIMIT = 1;

  //نوار برگه
  boolean ARE_TABS_NEEDED = false;

  // بارگذاری تصویر
  boolean IS_WAITING_ICON_NEEDED = true;

  // تعداد قطعات برنامه
  int FRAGMENTS_COUNT = 1;

}
  • خطوط ۲۵، ۲۸، ۳۱، ۴۰: ویژگی‌های لایه [DAO]. خط ۳۱: احراز هویت پایه لازم نیست؛
  • خط ۳۴: مجاورت قطعه. در اینجا این ثابت بی‌ربط است زیرا تنها یک قطعه وجود دارد؛
  • خط ۳۷: این یک برنامهٔ تب‌دار نیست؛
  • خط ۴۳: تنها یک قطعه وجود دارد؛

کلاس [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)
// کار در دست انجام: افزودن زیرکلاس‌های [CoreState] در اینجا
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // بازدید شده یا نشده
  protected boolean hasBeenVisited = false;
  //وضعیت منوی قطعه (در صورت وجود)
  protected MenuItemState[] menuOptionsState;

  // گیرنده و تنظیم‌کننده
...
}
  • خطوط ۱۰–۱۳: چیزی برای اعلام وجود ندارد، زیرا این برنامه تنها یک قطعه دارد که وضعیت آن حفظ نمی‌شود؛

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


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // عناصری که نمی‌توانند در jSON سریالیزه شوند باید دارای حاشیه‌نویسی @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);

  // سرویس آب و هوا
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • خطوط ۱۸–۱۹: رابط URL برای سرویس آب و هوا. توجه داشته باشید که این مربوط به ریشهٔ مشتری URL (RestClientRootUrl، خط ۱۲) است. در اینجا، این ریشهٔ URL برابر با [http://api.openweathermap.org/] خواهد بود؛

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


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // آدرس URL سرویس وب
  void setUrlServiceWebJson(String url);

  // کاربر
  void setUser(String user, String mdp);

  // اتمام زمان مشتری
  void setTimeout(int timeout);

  // احراز هویت پایه
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // حالت اشکال‌زدایی
  void setDebugMode(boolean isDebugEnabled);

  // زمان انتظار کلاینت (به میلی‌ثانیه) قبل از ارسال درخواست
  void setDelay(int delay);

  // سرویس آب و هوا
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • توجه داشته باشید که متدهای موجود در خطوط ۶ تا ۲۲ به طور پیش‌فرض در رابط IDao پروژه [client-android-skel] وجود دارند؛
  • خط ۲۵: متد [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 {

  // کلاینت سرویس وب
  @RestService
  protected WebClient webClient;
  // امنیت
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // کارخانه برای RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // timeout
  private int timeout;

  @AfterInject
  public void afterInject() {
    // لاگ
    Log.d(className, "afterInject");
    // مبدل jSON ساخته شده است
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    //مبدل jSON تنظیم شده است
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // restTemplate کلاینت وب تنظیم شده است
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    //URL سرویس وب تنظیم شده است
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    //کاربر در تلهٔ میان‌گیر ثبت شده است
    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));
    }
    //حافظه
    this.timeout = timeout;
    //کارخانه پیکربندی
    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));
    }
    // مداخلهگر احراز هویت؟
    if (isBasicAuthentificationNeeded) {
      // افزودن interceptor احراز هویت
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


  // متدهای خصوصی -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // سرویس آب و هوا ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // لاگ
    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));
    }
    // نتیجه
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • توجه داشته باشید که خطوط 17 تا 90 به طور پیش‌فرض در کلاس [Dao] پروژه [client-android-skel] موجود هستند. شما فقط باید متدهای خاص برنامه را که رابط [IDao] را پیاده‌سازی می‌کنند (خط 92) اضافه کنید؛
  • خطوط ۹۳–۱۰۵: پیاده‌سازی متد [getWeatherForecast]. این بسیار ساده است و تنها ۶ خط، خطوط ۱۰۰–۱۰۵، را در بر می‌گیرد؛
  • خط 100: متد [getResponse] یک متد از کلاس والد [AbstractDao] است. این متد یک پارامتر از نوع [IRequest<T>] را انتظار دارد، که در آن T نوع پاسخ مورد انتظار از سرور است؛ در این مورد، یک String، زیرا یک رشته jSON انتظار می‌رود. نوع T در `[IRequest<T>]` باید همان نوع Tِ متد `[Observable<T> getWeatherForecast]` باشد؛
  • رابط [IRequest<T>] تنها یک متد دارد: getResponse. هدف این متد، ارائه پاسخ از نوع T است که متد [Observable<T> getWeatherForecast] باید بازگرداند؛
  • خط ۱۰۳: این رابط [WebClient] است که این پاسخ را فراهم می‌کند. سه پارامتر دریافت‌شده در خط ۹۴ به آن پاس داده می‌شود. به همین دلیل، این پارامترها باید دارای ویژگی 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 {

  //لایه [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // متدهای کلاس والد -----------------------
  @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;
  }

  // رابط IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • لطفاً توجه داشته باشید که خطوط ۱۵ تا ۵۵ به طور پیش‌فرض در پروژه [client-android-skel] گنجانده شده‌اند. شما فقط باید آن‌ها را سفارشی‌سازی کنید؛
  • خطوط ۳۷–۴۰: جدول قطعه. در اینجا تنها یک مورد وجود دارد؛
  • خطوط ۴۳–۴۶: نیازی به عنوان‌بندی قطعات نیست؛
  • خطوط ۴۸–۵۰: هیچ زبانی در اینجا وجود ندارد؛
  • خطوط ۵۲–۵۵: اولین نمای نمایش داده شده، نمای شماره ۰، یعنی [MeteoFragment] است؛
  • خطوط ۵۸–۶۱: پیاده‌سازی رابط [IDao]. در اینجا کاری جز واگذاری کار به لایه [DAO] در خط ۲۱ انجام نمی‌شود؛

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 {
...
}
  • خط ۱۴: نمای [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>

این نما فقط متن خط ۱۰ را نمایش می‌دهد؛

  • خط ۱۵: منوی [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>
  • خطوط ۱۰–۱۲: این گزینه منو برای درخواست وضعیت آب و هوا برای یک شهر استفاده می‌شود؛
  • خطوط ۱۴–۱۵: این گزینه منو برای لغو این درخواست در صورت در حال اجرا بودن استفاده می‌شود؛
  • خطوط ۱۶–۱۸: این گزینه منو برنامه را می‌بندد؛

کد کامل این قطعه به شرح زیر است:


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 {

  // داده‌های محلی
  private int nbReponsesRecues;

  // مدیریت رویداد ---------------------------------------------------------------------------------------
  //شهرهایی که برای آن‌ها پیش‌بینی هوا لازم است
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // کشور آنها
    String country = "fr";
    // با ایجاد یک حساب کاربری، شناسه API را دریافت کنید: [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL از سرویس وب / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // شروع صف برای [paysDeLoire.length] وظایف غیرهمزمان
    beginWaiting(paysDeLoire.length);
    // تعداد پاسخ‌های دریافتی
    nbReponsesRecues = 0;
    // فراخوانی‌های ناهمزمان به‌صورت موازی انجام می‌شوند
    for (String city : paysDeLoire) {
      // وضعیت آب و هوا
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // پردازش پاسخ
          consumeResponse(response);
          // یک پاسخ مثبت
          nbReponsesRecues++;
        }
      });
    }
  }

  // پردازش پاسخ سرور
  private void consumeResponse(String response) {
    // لاگ
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // شروع انتظار
  protected void beginWaiting(int numberOfRunningTasks) {
    // لاگ
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // والد
    beginRunningTasks(numberOfRunningTasks);
    // گزینه نمایش داده می‌شود [Annuler]
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // منو
    initMenu();
    // نمایش نتایج
    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 void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // منو
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // مدیریت چرخه عمر ---------------------------------------------------------------------------------------
...
}
  • خطوط ۲۵–۵۰: رسیدگی به کلیک بر روی گزینه منوی [Météo
  • خط ۳۲: ساخت سرویس وب URL / سرویس آب و هوا jSON. این سپس از طریق فعالیت به لایه [DAO] ارسال می‌شود؛
  • خط ۳۴: منتظر می‌مانیم. تعداد وظایفی را که باید اجرا شوند، ارسال می‌کنیم تا کلاس والد بتواند هنگام اتمام آن‌ها به ما اطلاع دهد. در اینجا، پنج وظیفه وجود دارد زیرا قصد داریم آب و هوای پنج شهری را که در خط ۲۳ فهرست شده‌اند، درخواست کنیم؛
  • خط ۱۶: ما تعداد پاسخ‌های دریافتی را می‌شماریم تا بتوانیم آن را نمایش دهیم؛
  • خطوط ۳۸–۵۰: ما برای شهرهایی که می‌خواهیم آب و هوای آن‌ها را دریافت کنیم، یک حلقه می‌زنیم؛
  • خط ۴۰: ما ۵ درخواست HTTP را به‌طور موازی انجام می‌دهیم؛
  • خط ۴۰: ما به کلاس والد [AbstractParent] دستور می‌دهیم تا از سرویس وب /jSON استعلام بگیرد؛
  • خطوط ۴۰–۴۸: متد [executeInBackground] دو پارامتر را انتظار دارد:
    • خط ۴۰: فرآیند مورد نظارت و اجرا توسط متد [mainActivity.getWeatherForecast] فراهم می‌شود؛
    • خطوط ۴۰–۴۸: نمونه [Action1] که پس از دریافت پاسخ از سرویس غیرهمزمان باید اجرا شود. نوع T از [Action1<T>] باید همان نوع T از نتیجه متد [getWeatherForecast] باشد؛
  • خط ۴۴: یک پاسخ دریافت شده است. این پاسخ در خط ۵۳ به متد [consumeResponse] ارسال می‌شود؛
  • خط ۴۶: شمارنده پاسخ‌های دریافتی افزایش می‌یابد؛
  • خطوط ۵۳–۵۶: یک پاسخ jSON از سرویس هواشناسی پردازش می‌شود؛
  • خط ۵۵: صرفاً رشته jSON را ثبت کنید؛
  • خطوط ۵۹–۷۲: کدی که قبل از راه‌اندازی وظایف غیرهمزمان اجرا می‌شود؛
  • خط ۶۵: تعداد وظایفی که باید اجرا شوند به کلاس والد [AbstractParent] ارسال می‌شود. این امر به کلاس والد امکان می‌دهد تا زمانی که همه آن‌ها تکمیل شدند، به ما اطلاع دهد؛
  • خطوط ۶۷–۷۰: آماده‌سازی منو برای انتظار. ما فقط گزینه [Actions/Annuler] را حفظ می‌کنیم که به کاربر اجازه می‌دهد وظایفی را که آغاز شده‌اند لغو کند؛
  • خطوط ۷۴–۹۲: کدی که زمانی اجرا می‌شود که کلاس والد به ما اطلاع می‌دهد که تمام وظایف راه‌اندازی‌شده به پایان رسیده‌اند؛
  • خط ۷۷: منو به وضعیت اولیه خود بازنشانی می‌شود. متد [initMenu] (خطوط ۹۵–۱۰۲) منو را با تمام گزینه‌هایش به نمایش می‌گذارد، به جز گزینه [Actions/Annuler] که مخفی است؛
  • خطوط ۸۰–۹۱: تعداد پاسخ‌های دریافتی نمایش داده می‌شود؛

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


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    //لغو وظایف ناهمزمان
    cancelRunningTasks();
}
  • خط ۷: به کلاس والد دستور داده می‌شود که هر وظیفه‌ای را که هنوز فعال است لغو کند؛

کلیک بر روی گزینه منوی [Terminer] توسط کد زیر مدیریت می‌شود:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // خاموش کردن همه چیز
    System.exit(0);
}

چرخهٔ عمر قطعه توسط روش‌های زیر مدیریت می‌شود:


  // مدیریت چرخه عمر ---------------------------------------------------------------------------------------

  @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) {
    // اولین بازدید؟
    if (previousState == null) {
      initMenu();
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

}
  • خطوط ۳–۶: برای ذخیره وضعیت فرگمنت در کلاسی مشتق‌شده از [CoreState] استفاده می‌شوند. اگر فرگمنت هیچ وضعیتی برای ذخیره نداشته باشد، مانند این مورد، ما به سادگی یک نمونه از [CoreState] را بازمی‌گردانیم. نباید null بازگردانده شود، زیرا این امر به دنباله منجر به کرش (crash) خواهد شد؛
  • خطوط ۸–۱۱: باید شناسه نما را بازگردانند. در اینجا، قطعه [MeteoFragment] شناسه ۰ دارد؛
  • خطوط ۱۳–۱۶: برای مقداردهی اولیه قطعه پس از ساخته شدن (previousState==null) یا بازسازی شدن (previousState!=null) استفاده می‌شوند. در اینجا کاری برای انجام دادن وجود ندارد. تنها فیلدی که ممکن است نیاز به مقداردهی اولیه داشته باشد، عبارت زیر است:

  // شهرهایی که می‌خواهید آب‌وهوای آن‌ها را ببینید
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

اما خودِ آن مقداردهی اولیه می‌شود؛

  • خطوط ۱۸–۲۴: برای مقداردهی اولیه نمای مرتبط با قطعه پس از ساخت (previousState==null) یا بازسازی (previousState!=null) استفاده می‌شوند؛
  • خطوط ۲۱–۲۳: اگر این اولین بازدید از قطعه باشد، منوی آن برای پنهان کردن گزینه [Annuler] inicialize می‌شود؛
  • خطوط ۲۷–۳۰: در صورتی فراخوانی می‌شود که برای رسیدن به قطعه از ناوبری شامل عملی از نوع [SUBMIT] استفاده شده باشد. در اینجا، از آنجا که تنها یک قطعه وجود دارد، ناوبری بین‌قطعه وجود ندارد؛
  • خطوط ۳۲–۳۵: در طول یک چرخه ذخیره/بازیابی به دلیل چرخش دستگاه یا به دلیل دیگری فراخوانی می‌شود. در اینجا، از آنجایی که هیچ حالتی ذخیره نشده است، کاری برای انجام دادن وجود ندارد؛
  • خطوط ۳۷–۴۰: هنگام تکمیل تمام به‌روزرسانی‌های قبلی فراخوانده می‌شود. در اینجا کاری برای انجام دادن وجود ندارد؛

2.8.2.6. Tests

اکنون مثال را اجرا می‌کنیم:

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
  • خطوط ۳۲–۳۶: پاسخ‌های jSON در نخ‌های I/O دریافت می‌شوند
  • خطوط ۳۷–۴۱: قطعه پنج پاسخ را از نخ رابط کاربری (UI) بازیابی می‌کند؛

اکنون، ما درخواست را با شناسه‌ی نادرست 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"]]
  • خطوط ۳–۶، ۱۰: ۵ فراخوانی به HTTP، ۵ استثنا تولید کردند؛
  • خط ۷: قطعه [MeteoFragment] اولین استثنا را دریافت می‌کند. سپس تمام وظایف را لغو خواهد کرد؛

اکنون یک تایم‌اوت ۵ ثانیه‌ای برای [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]]
  • خط ۳: درخواست لغو؛
  • خط ۴: انتظار لغو می‌شود زیرا لغو انجام شده است؛
  • خطوط ۶–۱۰: لغو وظایف در هر یک از پنج نخ وظیفه، یک استثنا را فعال می‌کند. نوع استثنا به برنامه‌ها بستگی دارد. استثناء در اینجا [java.lang.InterruptedException] است زیرا وظایف هنگام اجرای دستور [Thread.sleep(delay)] متوقف شدند، که باعث می‌شود آن‌ها به‌طور مصنوعی برای میلی‌ثانیه‌های [delay] منتظر بمانند؛

2.8.3. مثال 16B

در اینجا ما در حال بازسازی مثال ۱۶ از بخش ۱.۱۷ هستیم. این بخش شامل قطعه‌ای است که فراخوانی‌های ناهمزمان به یک سرور اعداد تصادفی انجام می‌دهد. بیایید ببینیم وقتی دستگاه چرخانده می‌شود چگونه رفتار می‌کند:

Image

  • در [1]، دستگاه دو بار چرخانده می‌شود؛

Image

می‌توانیم ببینیم که تمام پیام‌های خطا از بین رفته‌اند. سعی می‌کنیم این مشکل را بهبود بخشیم.

2.8.3.1. پروژه Example-16B

ما پروژه [client-android-skel] را در پروژه [exemples/Exemple-16B] کپی می‌کنیم، سپس پروژه جدید را بارگذاری می‌کنیم:

  

از پروژه اصلی [Exemple-16]، موارد زیر را به [Exemple-16B] کپی می‌کنیم:

  • فایل [res/layout/vue1.xml]، پوشه [res/values]:
  

ما حاشیه بالایی نما [vue1.xml] را به ۸۰ dp تغییر خواهیم داد:


  <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" />
  • قطعه [Vue1Fragment]:
  
  • کلاس [dao / service / Response]:
  

در این مرحله، می‌توانیم یک کامپایل اولیه را امتحان کنیم:

  • اولین نوع خطا مربوط به imports است. برخی کلاس‌ها در حین مهاجرت به [Exemple-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 {

  //ورود
  ISession getSession();

  // تغییر نما
  void navigateToView(int position, ISession.Action action);

  // مدیریت صف
  void beginWaiting();

  void cancelWaiting();

  // ثوابت برنامه -------------------------------------

  // حالت اشکال‌زدایی
  boolean IS_DEBUG_ENABLED = true;

  // حداکثر زمان انتظار برای پاسخ سرور
  int TIMEOUT = 1000;

  // زمان انتظار قبل از اجرای درخواست کلاینت
  int DELAY = 5000;

  // احراز هویت پایه
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // همجواری قطعه
  int OFF_SCREEN_PAGE_LIMIT = 1;

  //نوار زبانه
  boolean ARE_TABS_NEEDED = false;

  // بارگذاری تصویر
  boolean IS_WAITING_ICON_NEEDED = true;

  // تعداد قطعات برنامه
  int FRAGMENTS_COUNT = 1;

}
  • خطوط ۲۵، ۲۸، ۳۱، ۴۰: ویژگی‌های لایه [DAO]. احراز هویت پایه لازم نیست؛
  • خط ۳۴: مجاورت قطعه. در اینجا این ثابت بی‌ربط است زیرا تنها یک قطعه وجود دارد؛
  • خط ۳۷: این یک برنامهٔ تب‌دار نیست؛
  • خط ۴۳: تنها یک قطعه وجود دارد؛

کلاس [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 {
  // اینکه آیا قطعه بازدید شده است یا خیر
  protected boolean hasBeenVisited = false;
  //وضعیت منوی قطعه (در صورت وجود)
  protected MenuItemState[] menuOptionsState;

  // گیرنده و تنظیم‌کننده
...
}
  • خط ۱۲: ما کلاس وضعیت قطعه [Vue1Fragment] را اعلام می‌کنیم؛

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


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // عناصری که نمی‌توانند در jSON سریالیزه شوند باید دارای انوتیشن @JsonIgnore باشند
}

این کلاس خالی است زیرا در این برنامه هیچ ارتباطی بین قطعات وجود ندارد.

2.8.3.4. لایه [DAO]

  

در لایه [DAO]، سه کلاس نیاز به سفارشی‌سازی دارند:

  • رابط IDao؛
  • پیاده‌سازی DAO آن؛
  • رابط WebClient برای ارتباط با سرور وب / jSON؛

کلاس [Response] از پروژه [Exemple-16] می‌آید که از آن استفاده می‌کند:


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- ویژگی‌ها
    //وضعیت عملیات
    private int status;
    // هر پیام خطا
    private List<String> messages;
    // متن پاسخ
    private T body;

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

    }

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

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

رابط [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);

  // ۱ عدد تصادفی در بازه [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • خطوط ۱۸–۱۹: URL از سرویس اعداد تصادفی. توجه داشته باشید که این مربوط به ریشه کلاینت URL (RestClientRootUrl، خط ۱۲) است. در اینجا، این ریشه URL برابر با [http://localhost:8080] خواهد بود؛

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


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL سرویس وب
  void setUrlServiceWebJson(String url);

  // کاربر
  void setUser(String user, String mdp);

  // اتمام زمان مشتری
  void setTimeout(int timeout);

  // احراز هویت پایه
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // حالت اشکال‌زدایی
  void setDebugMode(boolean isDebugEnabled);

  // زمان انتظار کلاینت (به میلی‌ثانیه) قبل از ارسال درخواست
  void setDelay(int delay);

  // سرویس عدد تصادفی
  Observable<Response<Integer>> getAlea(int a, int b);

}
  • توجه داشته باشید که متدهای موجود در خطوط ۶ تا ۲۲ به طور پیش‌فرض در رابط IDao پروژه [client-android-skel] وجود دارند؛
  • خط ۲۵: متد [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 {

  // کلاینت سرویس وب
  @RestService
  protected WebClient webClient;
  // امنیت
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  //کارخانه برای RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // لاگ
    Log.d(className, "afterInject");
    // restTemplate در حال ساخت است
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // مبدل jSON در حال نصب است
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // restTemplate کلاینت وب تنظیم شده است
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    //URL سرویس وب تنظیم شده است
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // کاربر در 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.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));
    }
    // مداخلهگر احراز هویت؟
    if (isBasicAuthentificationNeeded) {
      // افزودن interceptor احراز هویت
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // متدهای خصوصی -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // خدمات عدد تصادفی
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    //اجرای کلاینت وب
    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، می‌باشد؛
  • خط ۹۱: متد [getResponse] یک متد از کلاس والد [AbstractDao] است. این متد یک پارامتر از نوع [IRequest<T>] را انتظار دارد، که در آن T نوع پاسخ مورد انتظار است، در این مورد نوع Response<Integer>. نوع T در [IRequest<T>] (خط 91) باید با نوع T در متد [Observable<T> getAlea] (خط 89) یکسان باشد؛
  • رابط [IRequest<T>] تنها یک متد دارد: getResponse. هدف این متد فراهم کردن پاسخ از نوع T است که متد [Observable<T> getAlea] باید بازگرداند؛
  • خط ۹۴: این رابط [WebClient] است که این پاسخ را فراهم می‌کند. دو پارامتر دریافت‌شده در خط ۸۹ به آن ارسال می‌شود. به همین دلیل، این پارامترها باید دارای ویژگی 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 {

  // [DAO] لایه
  @Bean(Dao.class)
  protected IDao dao;

  // متدهای کلاس والد -----------------------
  @Override
  protected void onCreateActivity() {
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    //ادامهٔ инициалиزه‌سازی‌های آغازشده توسط کلاس والد
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // عناوین قطعات را اینجا تعریف کنید
    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;
  }

  // IDao رابط ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }

}
  • لطفاً توجه داشته باشید که خطوط ۱۵ تا ۶۱ به طور پیش‌فرض در پروژه [client-android-skel] گنجانده شده‌اند. شما فقط باید آن‌ها را سفارشی‌سازی کنید؛
  • خطوط ۴۰–۴۴: جدول قطعه. در اینجا تنها یک مورد وجود دارد؛
  • خطوط ۴۷–۵۱: نیازی به عنوان‌بندی قطعات نیست؛
  • خطوط ۵۳–۵۶: هیچ زبانه ای در اینجا وجود ندارد؛
  • خطوط ۵۸–۶۱: اولین نمای نمایش داده شده، نمای شماره ۰، یعنی [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 {

  //وضعیت قطعه ------------------------
  // فهرست پاسخ‌ها
  private List<String> reponses = new ArrayList<>();
  //وضعیت نما ------------------------
  // پیام خطا در مورد تعداد اعداد تصادفی درخواستی
  private boolean txtErrorAleasVisible = false;
  // پیام خطا در مورد فاصله زمانی تولید [a,b]
  private boolean txtErrorIntervalleVisible = false;
  //پیام خطا در مورد سرویس وب URL
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // پیام خطا در مورد دوره زمانی انتظار
  private boolean textViewErreurDelayVisible = false;
  //وضعیت دیده‌شدن دکمه «اجرا»
  private boolean btnExecuterVisible = true;

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

برای تعیین اینکه چه اطلاعاتی باید در قطعه ذخیره شود، دستگاه را در موقعیت‌های مختلف چرخاندیم و مشاهده کردیم که در هنگام بازیابی چه اطلاعاتی از دست رفته بود. نتیجه گرفتیم که اطلاعات خطوط ۱۰ تا ۲۳ باید ذخیره شوند.

2.8.3.7. قطعه [Vue1Fragment]

  

در حال حاضر، ویو [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 {

...
}
  • خط ۲۶: توجه داشته باشید که هر قطعه باید یک منو داشته باشد، حتی اگر خالی باشد. در اینجا نیز همین‌طور است.

2.8.3.7.1. پردازش کلیک روی دکمه [Exécuter]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    //بررسی داده‌های واردشده
    if (!isPageValid()) {
      return;
    }
    // پاسخ‌های قبلی پاک می‌شوند
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    //شمارنده پاسخ را روی 0 تنظیم مجدد کنید
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // ابتدایی‌سازی فعالیت
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // آماده‌سازی وظیفه تصادفی
    beginWaiting(1);
    // درخواست اعداد تصادفی
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    //ایجاد فرآیند مورد مشاهده
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // درخواست اعداد تصادفی
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // پاسخ پردازش می‌شود
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // لاگ
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // پاسخی مثبت
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // پاسخ را تحلیل کنید
    // خطا؟
    if (response.getStatus() != 0) {
      //نمایش
      showAlert(response.getMessages());
      //لغو
      doAnnuler();
      //بازگشت به رابط کاربری
      return;
    }
    // افزودن اطلاعات به فهرست پاسخ‌ها
    reponses.add(0, String.valueOf(response.getBody()));
    // تازه کردن پاسخ‌ها
    dataAdapterReponses.notifyDataSetChanged();
  }

  //لغو ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    //لغو وظایف ناهمزمان
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // نماد ساعت شنی نمایش داده می‌شود
    beginRunningTasks(nbRunningTasks);
    // دکمه [Annuler] جایگزین دکمه [Exécuter] می‌شود
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • خطوط ۴–۶: ابتدا بررسی می‌کنیم که ورودی‌ها معتبر هستند. سپس ممکن است پیام‌های خطا ظاهر شوند؛
  • خطوط ۸–۹: فهرست پاسخ‌ها پاک می‌شود. این تغییر در ListView که آن‌ها را نمایش می‌دهد، منعکس می‌شود؛
  • خطوط ۱۱–۱۲: تعداد پاسخ‌های دریافتی روی صفر تنظیم می‌شود؛
  • خط ۱۴: URL برای سرویس عدد تصادفی تنظیم می‌شود. این اطلاعات به لایه [DAO] ارسال خواهد شد؛
  • خط ۱۵: زمان انتظار قبل از ارسال درخواست به سرویس عدد تصادفی تنظیم می‌شود. این اطلاعات به لایه [DAO] ارسال خواهد شد؛
  • خط ۱۷: آمادگی‌ها برای راه‌اندازی ۱ وظیفه ناهمزمان فراهم می‌شود (به جای N؛ دلیل آن را خواهیم دید)؛
  • خطوط ۲۴–۲۷: از میان N وظیفهٔ ناهمزمان، آن‌ها را به یک توالی عملیات [merge] تبدیل می‌کنیم؛
  • خطوط ۲۹–۳۶: به کلاس والد [AbstractParent] دستور داده می‌شود تا برای اعداد تصادفی از سرویس وب /jSON استعلام بگیرد؛
  • خطوط ۲۹–۳۶: متد [executeInBackground] دو پارامتر انتظار دارد:
    • خط ۲۹: فرآیند مورد مشاهده و اجرا، همان فرآیندی است که در خطوط قبلی محاسبه شده است؛
    • خطوط ۲۹–۳۶: نمونه [Action1] که پس از دریافت پاسخ از سرویس ناهمزمان باید اجرا شود. نوع T متد [Action1<T>] باید همان نوع T نتیجه متد [getAlea] باشد، یعنی نوع [Response<Integer>]؛
  • خط ۳۴: هنگامی که یک پاسخ (یک عدد تصادفی) می‌رسد، در متد خط ۳۹ پردازش می‌شود؛
  • خطوط ۴۹–۵۰: ثبت و گزارش می‌شود که یک پاسخ جدید دریافت شده است؛
  • خطوط ۵۳–۶۰: نوع [Response<T>] دارای یک فیلد [status] است که یک کد خطا است. اگر این کد غیرصفر باشد، سرور با مشکلی مواجه شده است؛
  • خط ۵۵: یک پیام خطا نمایش داده می‌شود. متد [showAlert] به کلاس والد تعلق دارد؛
  • خط ۵۷: متد در خطوط ۶۸–۷۵ فراخوانی می‌شود. این متد هر وظیفه‌ای را که هنوز فعال است لغو خواهد کرد (خط ۷۴);
  • خط ۶۲: پاسخ به فهرست پاسخ‌ها اضافه می‌شود که منبع داده برای ListView است؛
  • خط ۶۴: ListView تازه می‌شود؛
  • خطوط ۷۷–۸۳: متد [beginWaiting(int nbRunningTasks)] نما را برای انتظار آماده می‌کند (خطوط ۸۱–۸۲) و به کلاس والد اطلاع می‌دهد که وظایف [nbRunningTasks] در شرف اجرا هستند (خط ۷۹);

2.8.3.7.2. چرخهٔ عمر قطعه

چرخهٔ عمر این قطعه توسط متدهای زیر مدیریت می‌شود:


  // داده‌های محلی
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // مدیریت چرخه عمر ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    //وضعیت نمای فعلی
    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) {
    // اولین بازدید؟
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    //منبع داده برای listView
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // تعداد پاسخ‌ها
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // نمایه فهرست / پیوند اداپتور
    listReponses.setAdapter(dataAdapterReponses);
    // اولین بازدید؟
    if (previousState == null) {
      // پیام‌های خطا پنهان هستند
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // دکمه‌ها
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // وضعیت نمای قبلی
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // نمایش/پنهان کردن پیام‌های خطا
    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);
    // دکمه‌ها
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // تعداد پاسخ‌ها
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // دکمه [Exécuter] جایگزین دکمه [Annuler] می‌شود
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • سطور ۷–۱۸: اطمینان از ذخیره شدن قطعه زمانی که کلاس والد آن را درخواست می‌کند؛
  • خط ۱۱: نمایش پیام خطا در مورد زمان‌بندی خاتمه (time-out);
  • خط ۱۲: نمایش پیام خطا در مورد تعداد اعداد تصادفی درخواستی؛
  • خط ۱۳: نمایش پیام خطا در مورد URL سرویس وب / jSON;
  • خط ۱۴: نمایش پیام خطا در مورد فاصله زمانی تولید اعداد تصادفی [a,b
  • خط ۱۵: نمایش دکمه [Exécuter
  • خط ۱۶: فهرست پاسخ‌های دریافتی؛
  • خطوط ۲۰–۲۳: باید شناسه نما را بازگرداند. شناسه قطعه در اینجا ۰ است زیرا تنها یک قطعه وجود دارد؛
  • خطوط ۲۵–۳۸: inicialization فیلدهای قطعه، یا در اولین بازدید (previousState==null) یا در بازدیدهای بعدی؛
    • خطوط ۲۹–۳۰: اگر این اولین بازدید نباشد، فیلد [reponses] از وضعیت قبلی خود بازیابی می‌شود؛
    • خطوط ۳۱–۳۳: اگر این اولین بازدید باشد، آنگاه فیلد [reponses] با یک لیست خالی مقداردهی اولیه می‌شود؛
    • خطوط ۳۴–۳۷: با استفاده از فیلد [reponses]، می‌توانیم منبع داده را برای ListView (خط ۳۵) و همچنین تعداد پاسخ‌ها (خط ۳۷) بسازیم؛
  • خطوط 40–55: برای راه‌اندازی نمای مرتبط با قطعه، یا در اولین بازدید (previousState==null) یا در بازدیدهای بعدی اجرا می‌شود؛
    • خط ۴۳: قطعه ListView با منبع داده‌ای که به تازگی در متد [initFragment] ساخته شده، مرتبط می‌شود؛
    • خطوط ۴۵–۵۴: اگر این اولین بازدید باشد، نما برای اولین نمایش خود آماده می‌شود؛
  • خطوط ۵۷–۶۰: در حین ناوبری بین-قطعه مرتبط با عملی از نوع [SUBMIT] اجرا می‌شود. در اینجا، تنها یک قطعه وجود دارد و بنابراین ناوبری بین-قطعه صورت نمی‌گیرد؛
  • خطوط ۶۳–۷۶: در حین ناوبری بین-قطعه مرتبط با عملی از نوع [NAVIGATION]، یا در طول یک چرخه ذخیره/بازیابی ناشی از چرخش دستگاه یا دلیل دیگری اجرا می‌شود. در اینجا، فقط حالت دوم می‌تواند رخ دهد. باید توجه داشت که در تمام موارد، [previousState] همیشه null نیست؛
  • خط ۶۵: وضعیت قبلی به نوع وضعیت قطعه (fragment state) تبدیل می‌شود؛
  • خطوط ۶۶–۷۵: محتوای وضعیت قبلی برای بازگرداندن نما استفاده می‌شود؛
  • خطوط ۷۸–۸۱: زمانی که همه به‌روزرسانی‌های قبلی تکمیل شده‌اند، فراخوانی می‌شود. در اینجا کاری برای انجام دادن وجود ندارد؛
  • خطوط ۸۳–۸۹: زمانی که همه وظایف غیرهمزمان تکمیل شوند، اجرا می‌شود. در اینجا، دکمه [Annuler] پنهان شده و با دکمه [Exécuter] جایگزین می‌شود؛

2.8.3.8. آزمایش‌ها

از خواننده دعوت می‌شود تا آزمایش‌های زیر را انجام دهد:

  • ایجاد خطاها و اجرای دستگاه: پیام‌های خطا باید روی صفحه باقی بمانند؛
  • اعداد تصادفی تولید کرده و دستگاه را اجرا کنید: اعداد تصادفی تولید شده باید روی صفحه باقی بمانند؛
  • چند ثانیه تأخیر ایجاد کرده و در حین انتظار دستگاه را اجرا کنید: وظایف باید لغو شده باشند (این موضوع را می‌توان در لاگ‌ها مشاهده کرد)؛

2.8.4. مثال ۲۲B

در اینجا مثال ۲۲ را مجدداً بررسی می‌کنیم تا آن را مطابق مدل پروژه [client-android-skel] بازسازی کنیم. شایان ذکر است که پروژه [Exemple-22] چرخه ذخیره/بازیابی قطعات را در حین چرخش به درستی مدیریت می‌کند و این پروژه مبنای پروژه [client-android-skel] بوده است.

ما پروژه [client-android-skel] را در پروژه [exemples/Exemple-22B] کپی کرده و پروژه دوم را بارگذاری می‌کنیم:

  

سپس عناصر مختلفی را از پروژه [Exemple-22] به پروژه [Exemple-22B] کپی می‌کنیم.

ابتدا، ما عناصر را از پوشه [res] کپی می‌کنیم:

  • [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, پوشه [values];
  

ما حاشیه بالایی هر دو نما را به ۱۲۰ dp تغییر می‌دهیم:

[vue1.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"/>

سپس، ما عناصر را کپی خواهیم کرد [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

در این مرحله، می‌توانیم یک کامپایل اولیه را امتحان کنیم. یک نوع اول از خطا ظاهر می‌شود: عناصر نادرست imports به دلیل تغییر پکیج کلاس‌ها. ما این عناصر imports را اصلاح می‌کنیم. نوع دوم خطا به این دلیل است که قطعات، تمام متدهای کلاس والد خود [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 {

  //دسترسی به جلسه
  ISession getSession();

  // تغییر نما
  void navigateToView(int position, ISession.Action action);

  //مدیریت صف
  void beginWaiting();

  void cancelWaiting();

  // حالت اشکال‌زدایی
  boolean IS_DEBUG_ENABLED = true;

  //حداکثر زمان انتظار برای پاسخ سرور
  int TIMEOUT = 1000;

  // زمان انتظار قبل از اجرای درخواست کلاینت
  int DELAY = 0;

  // احراز هویت پایه
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // همجواری قطعات
  int OFF_SCREEN_PAGE_LIMIT = 1;

  //نوار زبانه
  boolean ARE_TABS_NEEDED = true;

  // بارگذاری تصویر
  boolean IS_WAITING_ICON_NEEDED = false;

  // تعداد قطعات
  int FRAGMENTS_COUNT = 5;

}
  • خطوط ۲۳، ۲۶، ۲۹، ۳۸: ویژگی‌های لایه [DAO]. در اینجا هیچ‌کدام وجود ندارد؛
  • خط ۴۱: در اینجا پنج قطعه وجود دارد؛
  • خط ۳۲: مجاورت قطعات. این مقدار ثابت می‌تواند از [1,4] در اینجا گرفته شود. خواننده تشویق می‌شود تا این مقدار را تغییر دهد تا ببیند آیا برنامه همچنان کار می‌کند یا خیر؛
  • خط ۳۵: این یک برنامهٔ تب‌دار است؛

کلاس [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 {
  // آیا قطعه بازدید شده است
  protected boolean hasBeenVisited = false;
  //وضعیت منوی قطعه (در صورت وجود)
  protected MenuItemState[] menuOptionsState;

  // گیرنده و تنظیم‌کننده
...
}
  • خط ۱۲: ما کلاس وضعیت قطعه [PlaceHolderFragment] را اعلام می‌کنیم. با این حال، قطعه [Vue1Fragment] هیچ وضعیتی ندارد؛

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


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // داده‌هایی که باید بین خود قطعات و بین قطعات و فعالیت به اشتراک گذاشته شوند
  // عناصری که نمی‌توانند در jSON سریالیزه شوند باید دارای حاشیه‌نویسی @JsonIgnore باشند
  // گیرنده‌ها و تنظیم‌کننده‌های مورد نیاز برای سریالی‌سازی/دسریالی‌سازی در jSON را فراموش نکنید

  // تعداد قطعات بازدید شده
  private int numVisit;
  //شناسه قطعه از نوع [PlaceholderFragment] نمایش داده شده در زبانه دوم
  private int numFragment = -1;

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

این جلسه مربوط به پروژه [Exemple-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 {

  // [DAO] لایه
  @Bean(Dao.class)
  protected IDao dao;
  // جلسه
  private Session session;

  // مدیریت منو-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

  private void showFragment(int i) {
...
  }

  //پیاده‌سازی متدهای کلاس والد ---------------------------------------------------
  ...
}

در اینجا کلاس [MainActivity] به دو دلیل گسترده‌تر از نمونه‌های قبلی است:

  • زبانه‌هایی برای مدیریت وجود دارد؛
  • یک منو برای مدیریت وجود دارد؛

2.8.4.2.1. پیاده‌سازی متدهای کلاس والد

// متدهای کلاس والد -----------------------
  @Override
  protected void onCreateActivity() {
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    //ادامهٔ инициалиزه‌سازی‌های آغازشده توسط کلاس والد
    // جلسه
    this.session = (Session) super.session;
    ...
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // شماره قطعه
    final String ARG_SECTION_NUMBER = "section_number";
    // ابتدایی‌سازی آرایهٔ قطعه
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // ایجاد یک قطعه
      fragments[i] = new PlaceholderFragment_();
      // می‌توان آرگومان‌ها را به قطعه ارسال کرد
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // یک قطعه از +
    fragments[i] = new Vue1Fragment_();
    // نتیجه
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // در اینجا هیچ سربرگی وجود ندارد
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
...
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • خطوط ۲–۱۲: متد [onCreateActivity] توسط کلاس والد [AbstractActivity] زمانی که فعالیت برای اولین بار ایجاد می‌شود یا در طول چرخه ذخیره/بازیابی دوباره ایجاد می‌شود، فراخوانی می‌شود. هنگامی که این متد فراخوانی می‌شود، کلاس والد قبلاً جلسه را بازیابی کرده است؛
  • خط ۱۰: یک مرجع محلی به جلسه بازیابی می‌شود. تغییر نوع به این دلیل است که جلسه کلاس والد از نوع [AbstractSession] است؛
  • خطوط ۱۹–۳۸: متد [getFragments] باید آرایهٔ قطعاتی را که توسط برنامه مدیریت می‌شوند، به کلاس والد بازگرداند. در اینجا [FRAGMENTS_COUNT] از آنها وجود دارد، عددی که در [IMainActivity] تعریف شده است. اولین [FRAGMENTS_COUNT-1] از نوع [PlaceHolderFragment] و آخرین آن از نوع [Vue1Fragment] است؛
  • خطوط ۴۱–۴۵: متد [getFragmentTitle] باید عناوین قطعات را بازگرداند که این اطلاعات مفید باشد. در اینجا چنین نیست؛
  • خطوط ۴۷–۵۰: این متد توسط کلاس والد زمانی که کاربر روی یک تب کلیک می‌کند فراخوانی می‌شود. در بخش بعدی به این موضوع باز خواهیم گشت؛
  • خطوط ۵۲–۵۵: تعداد اولین نمایی را که هنگام راه‌اندازی برنامه نمایش داده می‌شود، برمی‌گرداند. در اینجا، قطعه [Vue1Fragment] باید ابتدا نمایش داده شود. روش [getFirstView] می‌توانست به‌طور مفیدی با یک مقدار ثابت در [IMainActivity] جایگزین شود؛

2.8.4.2.2. مدیریت تب‌ها

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


@Override
  protected void onCreateActivity() {
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // ما ادامهٔ инициалиزاسيون‌هایی را که توسط کلاس والد آغاز شده بودند، انجام می‌دهیم
    // جلسه
    this.session = (Session) super.session;
    // زبانه‌ی اول
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(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) {
    // شماره قطعه برای نمایش
    int numFragment;
    switch (position) {
      case 0:
        // شماره قطعه [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // شماره قطعه [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // نمایش قطعه
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • خطوط ۱–۲۰: متد [onCreateActivity] توسط کلاس والد [AbstractActivity] زمانی که فعالیت برای اولین بار ایجاد می‌شود یا در طول چرخه ذخیره/بازیابی دوباره ایجاد می‌شود، فراخوانی می‌شود. هنگامی که این متد فراخوانی می‌شود، کلاس والد قبلاً جلسه را بازیابی کرده است؛
  • خط ۹: یک مرجع محلی به جلسه بازیابی می‌شود. تغییر نوع به این دلیل است که جلسه کلاس والد از نوع [AbstractSession] است؛
  • خطوط ۱۱–۱۳: اولین تب ایجاد می‌شود؛
  • خطوط ۱۵–۲۰: اگر شماره قطعه (fragment) در جلسه ذخیره شده باشد (خط ۱۵)، زبانه دوم ایجاد می‌شود. این شماره در ابتدا هنگام ساخت اولیه فعالیت روی -1 تنظیم می‌شود؛
  • خطوط ۲۳–۳۹: این متد توسط کلاس والد زمانی که کاربر روی یک تب کلیک می‌کند، فراخوانی می‌شود؛
  • خطوط ۲۸–۳۱: اگر تب ۰ کلیک شود، آنگاه [Vue1Fragment] باید نمایش داده شود. می‌دانیم که این اولین نمایی است که هنگام شروع برنامه نمایش داده شد؛
  • خطوط ۳۲–۳۵: اگر تب ۱ کلیک شود، آنگاه باید قطعه با شماره ذخیره‌شده در جلسه نمایش داده شود؛
  • خطوط ۳۷–۳۹: ما به قطعهٔ انتخاب‌شده ناوبری می‌کنیم. اقدام مرتبط [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) {
    // لاگ
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    //پردازش گزینه‌های منو
    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;
      }
    }
    // آیتم پردازش شده
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // بدون ناوبری هنگام انتخاب یک برگه از طریق نرم‌افزار
      session.setNavigationOnTabSelectionNeeded(false);
      //ما دو تب را به دلیل مشکل در فونت استفاده شده برای عناوین مجدداً ایجاد می‌کنیم
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // شمارهٔ قطعهٔ قابل نمایش برای جلسه تعیین شده است
      session.setNumFragment(i);
      // زبانه‌ی شمارهٔ ۲ با استفاده از ناوبری انتخاب شده است
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • خطوط ۱۶–۳۱: رسیدگی به کلیک بر روی یک گزینه منو از نوع [Fragmenti];
  • خطوط ۳۷–۵۰: نمایش قطعه شماره i (اینها قطعاتی از نوع PlaceHolderFragment هستند) در برگه شماره ۱ (برگه دوم)؛
  • خطوط ۴۲–۴۴: تصمیمی برای حذف برگه‌های موجود و ایجاد دو برگه جدید گرفته می‌شود. این تصمیم برای حل مشکل زیر اتخاذ شد: هنگامی که قطعه به سادگی در زبانهٔ موجود ۱ نمایش داده می‌شود (بدون حذف آن)، به طرز جالبی عنوان آن از نظر قلم و اندازه با عنوان زبانهٔ ۰ متفاوت به نظر می‌رسد؛
  • خطوط ۴۳–۴۴: دو زبانه ایجاد می‌شوند اما انتخاب نمی‌شوند (پارامتر آخر در false);
  • خط ۴۰: عملیات در خطوط ۴۲–۴۴ ممکن است عملیات [select] را روی تب‌ها تحریک کند که این امر، هندلر [onTabSelected] را فراخوانی می‌کند. اگر اقدامی صورت نگیرد، در این صورت ناوبری به یک فرگمنت انجام خواهد شد. این امر با تنظیم مقدار بولی [navigationOnTabSelectionNeeded] به faux در جلسه جلوگیری می‌شود. این مقدار بولی به طور خودکار توسط کلاس [AbstractFragment] هنگامی که یک قطعه قابل مشاهده می‌شود، به vrai بازنشانی می‌شود؛
  • خط ۴۶: شماره قطعه نمایش داده شده در جلسه ثبت می‌شود؛
  • خطوط ۴۸–۵۰: برگه شمارهٔ ۲ با ناوبری انتخاب می‌شود (خط ۴۸). این امر رویهٔ [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 {

  // عناصر رابط کاربری بصری
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // پردازشگر رویداد
  @Click(R.id.buttonValider)
  protected void doValider() {
    // نام وارد شده نمایش داده می‌شود
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  //چرخه عمر قطعه -----------------------------------------------
  private void initFragment() {
    // کاری برای انجام دادن ندارد
  }

  // ذخیره وضعیت قطعه
  @Override
  public CoreState saveFragment() {
    // وضعیت نما – چیزی برای ذخیره نیست
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // کاری برای انجام دادن نیست
  }

  @Override
  protected void initView(CoreState previousState) {
    // اولین بازدید؟
    if (previousState == null) {
      //نمایش شماره بازدید
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // نمایش شماره بازدید
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // متدهای خصوصی -------------------------------------
  // نمایش شماره بازدید
  private void showNumVisit() {
    // افزایش شمارش بازدید
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // شماره بازدید را نمایش می‌دهد
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

کلاس تقریباً خالی است.

  • خطوط ۳۵–۳۹: توسط کلاس والد فراخوانی می‌شود وقتی که قطعه نیاز به ذخیره وضعیت خود دارد. قطعه [Vue1Fragment] هیچ حالتی برای ذخیره ندارد. ما به سادگی یک نمونه از کلاس پایه [CoreState] را بازمی‌گردانیم (یادآوری: نباید null را بازگردانیم
  • خطوط ۴۱–۴۴: باید شناسهٔ قطعه را بازگردانیم. طبق طراحی، قطعهٔ [Vue1Fragment] دارای شناسهٔ [FRAGMENTS_COUNT-1] است؛
  • خطوط ۵۱–۵۹: توسط کلاس والد هنگام ساخت اولین بار (previousState==null) یا در دفعات بعدی (previousState!=null) فراخوانی می‌شود؛
    • خطوط ۵۴–۵۷: اگر این اولین بازدید باشد، شمارش بازدید افزایش یافته و نمایش داده می‌شود (خطوط ۸۵–۹۲);
  • خطوط ۶۱–۶۵: زمانی فراخوانی می‌شود که قطعه قرار است در ارتباط با یک اقدام [SUBMIT] نمایش داده شود. شماره بازدید افزایش یافته و نمایش داده می‌شود. در اینجا امکان افزایش دوباره‌ی شماره بازدید در طول چرخه عمر وجود ندارد. این به این دلیل است که اولین بازدید از قطعه [Vue1Fragment] هنگام شروع برنامه رخ می‌دهد، که در این نقطه، طبق طراحی، اقدام در طول جلسه روی [NONE] تنظیم می‌شود. این امر تضمین می‌کند که متد [updateOnSubmit] فراخوانی نخواهد شد. پس از آن، دیگر هرگز اولین بازدید نخواهد بود و متد [initView] کاری انجام نخواهد داد؛
  • خطوط ۶۸–۷۱: در طول یک چرخهٔ ذخیره/بازیابی فراخوانی می‌شود. از آنجایی که این قطعه هیچ حالتی ندارد، چیزی برای بازیابی در اینجا وجود ندارد؛
  • خطوط ۷۳–۷۶: زمانی فراخوانی می‌شود که تمام به‌روزرسانی‌های قبلی انجام شده باشند. در اینجا، کار دیگری باقی نمانده است؛
  • خطوط ۷۸–۸۱: زمانی فراخوانی می‌شود که تمام وظایف ناهمزمان راه‌اندازی‌شده به پایان برسند. در اینجا، هیچ وظیفه ناهمزمان وجود ندارد؛

2.8.4.4. وضعیت [PlaceHolderFragmentState]

وضعیت قطعه [PlaceHolderFragment] به شرح زیر خواهد بود:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class PlaceHolderFragmentState extends CoreState {
  // متن
  private String text;

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

  }

  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }

  // گیرنده و تنظیم‌کننده
 ...
}
  • وقتی نیاز به ذخیره وضعیت قطعه باشد، متنی که نمایش می‌داد (خط ۷) ذخیره خواهد شد؛

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 {

  // اجزای رابط کاربری بصری
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;

  // داده‌ها
  private String text;

  // شماره قطعه
  private static final String ARG_SECTION_NUMBER = "section_number";

  //پیاده‌سازی متدهای کلاس والد ----------------------------
  @Override
  public CoreState saveFragment() {
    // وضعیت قطعه ذخیره می‌شود
    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) {
    // متن اصلی
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // به‌روزرسانی متن نمایش‌داده‌شده
    // افزایش شمارش بازدید
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // متن اصلاح‌شده
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // لاگ
    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) {
    // بازیابی متن نمایش‌داده‌شده
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • خطوط ۳۰–۳۶: هنگامی که کلاس والد از فرگمنت می‌خواهد وضعیت خود را ذخیره کند، متنی که توسط فرگمنت نمایش داده می‌شود ذخیره می‌شود (خط ۳۴);
  • خطوط ۳۸–۴۱: شماره قطعه را بازمی‌گردانند. این بستگی به شماره بخشی دارد که هنگام ایجاد به عنوان آرگومان به آن پاس شده است؛
  • خطوط ۴۳–۴۷: در طول اولین ساخت‌وساز قطعه (previousState==null) یا در طول ساخت‌وسازهای بعدی (previousState !=null) فراخوانی می‌شود؛
    • خط ۴۶: در اینجا، وضعیت قبلی مورد استفاده قرار نمی‌گیرد. متن اولیه [text] (خط ۲۴)، که در اولین بازدید نمایش داده می‌شود، هر بار مجدداً محاسبه می‌شود. این موضوع قابل بحث است. می‌توانست این اطلاعات را نیز در وضعیت قطعه گنجاند؛
  • خطوط ۴۹–۵۱: زمانی فراخوانی می‌شود که نمای مرتبط با قطعه برای اولین بار ساخته می‌شود (previousState==null) یا در دفعات بعدی (previousState!=null). کاری برای انجام دادن وجود ندارد؛
  • خطوط ۵۳–۵۶: زمانی فراخوانی می‌شود که قطعه قرار است در ارتباط با یک اقدام [SUBMIT] نمایش داده شود. این حالت همیشه صادق است به جز در طول چرخه ذخیره/بازیابی، که در آن اقدام [RESTORE] است. بنابراین ما شمارشگر بازدید را افزایش داده و آن را نمایش می‌دهیم؛
  • خطوط ۶۸–۷۴: در طول چرخهٔ ذخیره/بازیابی فراخوانده می‌شود. متنی که در وضعیت قطعه ذخیره شده بود، بازیابی می‌شود؛
  • خطوط ۷۶–۷۹: هنگام انجام تمام به‌روزرسانی‌های قبلی فراخوانده می‌شود. در اینجا، کار دیگری باقی نمانده است؛
  • خطوط ۸۲–۸۳: زمانی که تمام وظایف ناهمزمان راه‌اندازی‌شده به پایان رسیده‌اند، فراخوانی می‌شود. در اینجا، هیچ وظیفه ناهمزمان وجود ندارد؛

2.8.4.6. Tests

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

2.9. Conclusion

در پایان این فصل، یک پروژه نمونه، [client-android-skel]، برای یک کلاینت اندروید که با یک سرویس وب / jSON ارتباط برقرار می‌کند، ارائه شده است که دارای ویژگی‌های زیر است:

  • ارتباط غیرهمزمان با سرور وب / jSON با استفاده از کتابخانه RxJava مدیریت می‌شود؛
  • چرخه عمر یک فرگمنت (به‌روزرسانی، ذخیره، بازیابی) توسط کلاس والد آن، [AbstractFragment]، مدیریت می‌شود که در زمان‌های مشخصی متدهای خاصی از کلاس‌های فرزند خود را فراخوانی می‌کند. بنابراین، فرگمنت فرزند نیازی ندارد که به مراحل چرخه عمر رسیدگی کند، بلکه تنها باید متدهای خاصی را که کلاس والدش می‌طلبد پیاده‌سازی نماید؛
  • چرخه عمر فعالیت (ذخیره / بازیابی) توسط یک کلاس انتزاعی، [AbstractActivity]، مدیریت می‌شود که آن نیز از فعالیت فرزند می‌خواهد روش‌های خاصی را پیاده‌سازی کند؛
  • کلاس [AbstractActivity] قادر به مدیریت یک برنامه با یا بدون تب‌ها، با یا بدون تصویر بارگذاری، و با یا بدون احراز هویت پایه در برابر سرور وب / jSON است. وجود یا عدم وجود این عناصر توسط پیکربندی تعیین می‌شود؛

اکنون یک مطالعه موردی ارائه می‌دهیم که پیچیده‌تر از مثال‌های قبلی است. برنامه جدید بر اساس پروژهٔ الگوی [client-android-skel] ساخته خواهد شد.