1. یادگیری برنامهنویسی اندروید
نسخه PDF سند در |ICI| موجود است.
نمونههای موجود در سند در |ICI| در دسترس هستند.
1.1. Introduction
1.1.1. فهرست مطالب
این سند بازنویسی چندین سند موجود است:
- مقدمهای بر برنامهنویسی تبلت اندروید از طریق مثالها؛
- کنترل آردوینو با تبلت اندروید؛
- مقدمهای بر برنامهنویسی تبلت اندروید از طریق مثالها – نسخهٔ ۲
و ویژگیهای جدید زیر را معرفی میکند:
- سند ۱ معماریای به نام AVAT (فعالیت-نماها-اقدامات-وظایف) را برای تسهیل برنامهنویسی غیرهمزمان در یک برنامه اندروید ارائه داد. در این سند، کتابخانه استاندارد RxJava برای مدیریت اقدامات غیرهمزمان استفاده میشود؛
- سند ۲ از Eclipse با پلاگین Android به نام IDE استفاده میکرد. این سند از Android Studio استفاده میکند؛
- سند ۳ همانگونه که هست بازتولید شده است؛
- سند ۴ از کتابخانه [Android Annotations] (AA) با نسخه Community Edition از IntelliJ IDE IDEA استفاده کرد. این سند کل سند ۴ را با تفاوتهای زیر بازتولید میکند:
- IDE اکنون Android Studio است؛
- سیستم ساخت برای همه پروژههای کلاینت و سرور Gradle است (در سند ۴ گاهی از Maven استفاده میشد)؛
- برنامهنویسی غیرهمزمان با استفاده از کتابخانه RxJava پیادهسازی شده است (در سند ۴، از کتابخانه AA استفاده میشد)؛
- این سند حوزههایی را بررسی میکند که در اسناد قبلی پوشش داده نشدهاند یا تنها به طور مختصر پوشش داده شدهاند:
- مفهوم مجاورت فرگمنت؛
- ذخیره و بازیابی فعالیت و قطعات آن؛
- چرخه عمر فرگمنتها؛
در نهایت، چارچوبی برای یک کلاینت اندروید که با یک سرویس وب / jSON ارتباط برقرار میکند، ارائه میشود که در آن تعداد زیادی از عناصر رایج در این نوع کلاینت انتزاع شدهاند. این چارچوب در تمام مثالها از فصل ۲ به بعد استفاده میشود. این بخش واقعاً نوآورانه سند است.
مثالهای زیر ارائه شدهاند:
طبیعت | |
وارد کردن یک پروژه اندروید موجود | |
یک پروژهٔ پایهٔ اندروید | |
یک پروژهٔ پایهٔ [Android Annotations] | |
نماها و رویدادها | |
پیمایش بین نماها | |
پیمایش برگهها | |
استفاده از کتابخانه [Android Annotations] با Gradle | |
مدیریت قطعات در یک برنامه اندروید | |
نگاهی نو به ناوبری بین ویوها | |
معماری دو لایه | |
معماری کلاینت/سرور | |
مدیریت ناهمزمانی با RxJava | |
کامپوننتهای ورود داده | |
استفاده از قالب نما | |
کامپوننت ListView | |
استفاده از منو | |
استفاده از کلاس والد برای قطعات | |
ذخیره و بازیابی وضعیت فعالیت و قطعات | |
کلاینت آب و هوا | |
اسکلت یک کلاینت اندروید در حال ارتباط با یک سرویس وب / jSON. این اسکلت، تعداد زیادی از عناصری را که معمولاً در این نوع کلاینتهای اندروید یافت میشوند، استخراج میکند. | |
مدیریت قرار ملاقات برای یک مطب پزشکی | |
تمرین عملی – مدیریت پایه حقوق و دستمزد | |
تمرین عملی – کنترل بردهای آردوینو |
این سند در سال آخر دانشکده مهندسی IstiA در دانشگاه آنژ [istia.univ-angers.fr] مورد استفاده قرار گرفته است. این موضوع توضیح میدهد که چرا لحن متن گاهی اوقات کمی غیرمعمول است. این دو تمرین عملی از TP گرفته شدهاند که در آن تنها خطوط کلی راهحل ارائه شده است. بر عهده خواننده است که راهحل را بیابد.
کد منبع مثالها در |ICI| موجود است. برای اجرای این مثالها، باید رویههای بخش 6.12 را دنبال کنید.
این سند یک راهنمای مقدماتی برای برنامهنویسی اندروید است. هدف آن جامع بودن نیست. این سند عمدتاً برای مبتدیان تهیه شده است.
وبسایت مرجع برای برنامهنویسی اندروید در URL [http://developer.android.com/guide/components/index.html] قرار دارد. این همان جایی است که باید برای کسب دید کلی از برنامهنویسی اندروید به آن مراجعه کنید.
1.1.2. پیشنیازها
برای بهرهبرداری حداکثری از این سند، باید تسلط خوبی بر زبان جاوا داشته باشید.
1.1.3. ابزارهای مورد استفاده
مثالهای زیر در محیط زیر آزمایش شدهاند:
- ماشین ویندوز 10 پرو 64 بیتی؛
- JDK 1.8;
- Android SDK API 23;
- Android Studio، نسخهٔ 2.1؛
- شبیهساز Genymotion، نسخه 2.6.0;
برای دنبال کردن این سند، باید نصب کنید:
- یک JDK (به بخش 6.8 مراجعه کنید);
- مدیر شبیهساز اندروید Genymotion (بخش 6.9 را ببینید)؛
- مدیر وابستگی Maven (بخش 6.10 را ببینید)؛
- IDE و [Android Studio] (به بخش 6.11 مراجعه کنید);
1.2. مثال-۰۱: وارد کردن یک مثال اندروید
1.2.1. ایجاد پروژه
بیایید اولین پروژه اندروید خود را با استفاده از اندروید استودیو ایجاد کنیم. ابتدا یک پوشه خالی به نام [exemples] ایجاد کنیم که تمام پروژههای ما در آن ذخیره خواهند شد:
![]() |
سپس با استفاده از Android Studio یک پروژه ایجاد کنید. ابتدا یکی از مثالهای همراه با IDE [1-5] را وارد میکنیم:
![]() |

![]() | ![]() |
وارد کردن پروژه ممکن است به دلیل عدم تطابق بین محیطی که هنگام ایجاد پروژه استفاده شده و محیطی که اکنون برای اجرای آن به کار میرود، با خطاهایی همراه باشد. این فرصتی است تا ببینیم چگونه این نوع خطا را برطرف کنیم. در اینجا با خطای زیر مواجه هستیم:
![]() | ![]() |
پروژه واردشده توسط فایل زیر پیکربندی شده است: [build.gradle] [2]:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
}
dependencies {
compile "com.android.support:support-v4:23.3.0"
compile "com.android.support:support-v13:23.3.0"
compile "com.android.support:cardview-v7:23.3.0"
}
// ساخت نمونه از چندین پوشه برای
// نگهداشتن کدهای تکراری و مشترک جدا از
// کد نمونهٔ اصلی.
List<String> dirs = [
'main', // کد نمونهٔ اصلی؛ برای مطالب جالب اینجا را ببینید.
'common', // کامپوننتهایی که توسط چندین نمونه مجدداً استفاده میشوند
'template'] // کد boilerplate تولیدشده توسط فرآیند قالب نمونه
android {
compileSdkVersion 21
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 21
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
main {
dirs.each { dir ->
java.srcDirs "src/${dir}/java"
res.srcDirs "src/${dir}/res"
}
}
androidTest.setRoot('tests')
androidTest.java.srcDirs = ['tests/src']
}
aaptOptions {
noCompress "pdf"
}
}
- خطای گزارششده مربوط به خطوط ۳۱ و ۳۴–۳۵ است: ما نسخه ۲۱ از SDK را نداریم. ما این نسخه را با نسخه ۲۳ که در اختیار داریم جایگزین میکنیم.
در فایل [build.gradle]، اندروید استودیو پیشنهادهایی را همانطور که در زیر نشان داده شده ارائه میدهد:
![]() |
برای پذیرش پیشنهادها، روی [alt-entrée] در پیشنهاد کلیک کنید:
![]() |
ممکن است با خطایی مرتبط با نسخه Gradle مواجه شوید:
![]() |
این خطا به دلیل عدم تطابق بین نسخه Gradle مورد نیاز فایل [build.gradle] پروژه (نسخه 2.10، خط 6 در زیر) ایجاد میشود:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
و نسخهای که در فایل [<projet>/gradle/wrapper/gradle-wrapper.properties] مشخص شده است:
#چهارشنبه ۱۰ آوریل ۲۰۱۵ ۱۵:۲۷:۱۰ PDT ۲۰۱۳
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
در خط ۶ بالا، عدد ۲.۸ باید با ۲.۱۰ جایگزین شود.
برای دسترسی به فایل [<projet>/gradle/wrapper/gradle-wrapper.properties]، از نمای پروژه برای پروژه استفاده کنید:
![]() | ![]() | ![]() |
پس از اصلاح این مورد، میتوانید اپلیکیشن [1] را کامپایل کرده، شبیهساز Genymotion [2] را راهاندازی کرده و سپس پروژه [3] را اجرا کنید:
![]() | ![]() | ![]() |
![]() |

بیایید برنامه را متوقف کنیم:
![]() |
اکنون میتوانیم پروژه را ببندیم. یک پروژه جدید ایجاد خواهیم کرد.
![]() |
1.2.2. چند نکته در مورد IDE
1.2.2.1. نماها
Android Studio (AS) نماهای مختلفی برای کار روی یک پروژه ارائه میدهد. ما عمدتاً از دو مورد از آنها استفاده خواهیم کرد:
- منظر [Android] [1]:
- منظر [Project] [4]؛
![]() | ![]() |
![]() |
در بیشتر مواقع با دیدگاه [Android] کار خواهیم کرد. وقتی یک پروژه را در پروژهای دیگر کپی میکنیم، به دیدگاه [Project] نیاز خواهیم داشت.
1.2.2.2. مدیریت اجرا
چندین روش برای اجرای، توقف یا اجرای مجدد پروژه AS وجود دارد. اولاً، دکمههایی در نوار ابزار وجود دارند:
![]() | ![]() | ![]() |
دکمههای [Rerun] و [3] اجرای پروژه [2] را متوقف کرده و سپس آن را مجدداً راهاندازی میکنند ([1]).
1.2.2.3. مدیریت کش
Android Studio یک کش از پروژههایی که مدیریت میکند نگه میدارد تا IDE تا حد امکان پاسخگو باشد. با انتشار Android 2.1 (مه 2016)، این کش اغلب تغییرات کدی را که تازه اعمال شده بودند در نظر نمیگرفت. در این صورت، باید کش را باطل کنید:
![]() | ![]() |
با اندروید ۲.۱ (مه ۲۰۱۶)، عملیات فوق باید چندین بار انجام میشد و گاهی این کار برای رفع ناهنجاری شناساییشده کافی نبود. راهحل غیرفعال کردن فناوری [Instant Run] بود:
![]() | ![]() |
- در [3-4]، همه چیز غیرفعال شده بود؛
در تمام عملیاتهای بعدی، با این پیکربندی کش کار کردیم و هیچ مشکلی پیش نیامد.
1.2.2.4. مدیریت لاگها
هنگام اجرای پروژه، لاگها در مانیتور اندروید نمایش داده میشوند:
![]() | ![]() |
در زبانه [Android Monitor] [1]، لاگها در زبانه [logcat] [2] نمایش داده میشوند. دکمه [3] به شما امکان میدهد لاگها را پاک کنید. این دکمه زمانی مفید است که بخواهید لاگهای یک اقدام خاص را مشاهده کنید:
- پاک کردن لاگها؛
- روی دستگاه اندروید، عملی را که میخواهید گزارشهای آن را مشاهده کنید، انجام دهید؛
- گزارشهایی که سپس ظاهر میشوند مربوط به اقدام انجامشده هستند؛
چندین سطح لاگ وجود دارد: [4]. به طور پیشفرض، حالت [Verbose] انتخاب شده است. این بدان معناست که لاگهای تمام سطوح نمایش داده میشوند. با [4]، میتوانید یک سطح خاص را انتخاب کنید.
لاگها برای شناسایی اینکه در چه نقاطی از اجرای یک پروژه، متدهای خاصی فراخوانی میشوند، بسیار مفید هستند. ما اغلب از آنها استفاده خواهیم کرد. بیایید کد را از کلاس [MainActivity] در پروژه [Exemple-01] در نظر بگیریم:
![]() |
package com.example.android.pdfrendererbasic;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_real);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.container, new PdfRendererBasicFragment(),
FRAGMENT_PDF_RENDERER_BASIC)
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_info:
new AlertDialog.Builder(this)
.setMessage(R.string.intro_message)
.setPositiveButton(android.R.string.ok, null)
.show();
return true;
}
return super.onOptionsItemSelected(item);
}
}
در مثال بالا، متدهای [onCreate, ligne 14] و [onCreateOptionsMenu, ligne 26] متدهای کلاس والد [Activity] (خط ۹) هستند. این متدها در نقاط مختلف چرخه عمر برنامه فراخوانی میشوند. گاهی اوقات چندین بار اجرا میشوند. حتی هنگام مطالعه مستندات، گاهی دشوار است تشخیص دهیم که آیا چنین متدی از چرخه عمر قبل یا بعد از متدی که خودمان نوشتهایم اجرا میشود. با این حال، دانستن این اطلاعات اغلب مهم است. بنابراین میتوانیم لاگها را همانطور که در زیر نشان داده شده اضافه کنیم:
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
...
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("MainActivity","onCreateOptionsMenu");
getMenuInflater().inflate(R.menu.main, menu);
...
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("MainActivity","onOptionsItemSelected");
switch (item.getItemId()) {
...
}
}
- در خطوط ۷، ۱۴ و ۲۱، از کلاس `[Log]` استفاده شده است. این کلاس امکان نوشتن لاگها را در کنسول اندروید از طریق `[logcat]` فراهم میکند. لاگها به سطوح مختلفی (info، warning، debug، verbose، error) دستهبندی میشوند. کلاس [Log.d]، لاگهای سطح [debug] را نمایش میدهد. آرگومان اول آن منبع پیام لاگ است. در واقع، منابع مختلفی میتوانند پیامهایی را به کنسول لاگ ارسال کنند. این آرگومان اول برای تمایز قائل شدن بین آنها استفاده میشود. آرگومان دوم، پیامی است که باید در کنسول لاگ نوشته شود؛
اگر پروژه [Exemple-01] را دوباره اجرا کنیم، لاگهای زیر را دریافت میکنیم:
05-28 08:37:12.709 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreate
05-28 08:37:12.778 23881-23923/com.example.android.pdfrendererbasic D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
[ 05-28 08:37:12.781 23881:23881 D/ ]
HostConnection::get() New Host ...
05-28 08:37:12.967 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreateOptionsMenu
بنابراین میتوانیم ببینیم که متد [onCreate]، که اکتیویتی اندروید را ایجاد میکند، قبل از متد [onCreateOptionsMenu]، که منوی برنامه را ایجاد میکند، اجرا میشود.
اکنون، اگر روی گزینه منو در شبیهساز اندروید ([1]) کلیک کنیم:
![]() |
ورودی لاگ زیر به کنسول لاگ اضافه میشود:
05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected
در بخشهای بعدی، ما مرتباً دستورات لاگگیری را به کد اندروید اضافه خواهیم کرد. در بیشتر موارد، آنها را کامنت نمیکنیم. آنها صرفاً برای تشویق خواننده به بررسی کنسول لاگ جهت درک تدریجی چرخه عمر یک برنامه اندروید قرار داده شدهاند.
1.2.2.5. مدیریت شبیهساز [Genymotion]
گاهی اوقات، شبیهساز Genymotion از کار میافتد و قابل راهاندازی مجدد نیست. این به این دلیل است که فرآیندهای VirtualBox در مدیر وظیفه (Task Manager) فعال باقی ماندهاند. مدیر وظیفه (Task Manager) را باز کرده و تمام فرآیندهای VirtualBox را حذف کنید:
![]() | ![]() |
پس از انجام این کار، شبیهساز Genymotion را از اندروید استودیو مجدداً راهاندازی کنید.
1.2.2.6. مدیریت باینری ایجادشده APK
کامپایل پروژه، یک فایل باینری با پسوند .apk تولید میکند:
![]() | ![]() | ![]() |
دو نسخه وجود دارد: یکی با نام [debug] و دیگری با نام [debug-unaligned]. شما باید از نسخه اول استفاده کنید؛ نسخه دیگر یک نسخه واسطه است. فایل باینری .pak تولیدشده توسط [4] را میتوان مستقیماً به یک شبیهساز یا دستگاه اندروید منتقل کرد. برای انتقال آن به شبیهساز، کافی است آن را با ماوس به داخل شبیهساز بکشید و رها کنید.
1.3. مثال ۰۲: یک پروژهٔ پایهای اندروید
بیایید یک پروژه اندروید جدید به نام [1-12] را با استفاده از Android Studio ایجاد کنیم:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() | ![]() | ![]() |
در [13]، برنامه اجرا میشود. سپس صفحهای که در [14] نشان داده شده است، روی شبیهساز Genymotion نمایش داده میشود.
1.3.1. پیکربندی Gradle
پروژه ایجاد شده توسط فایل زیر پیکربندی میشود: [build.gradle]:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
}
این فایل توسط IDE با استفاده از تنظیمات پیکربندی آن تولید شده است. این یک فایل حداقلی است که به تدریج آن را گسترش خواهیم داد.
- خطوط ۳–۱۲: ویژگیهای اپلیکیشن اندروید؛
- خطوط ۲۲–۲۵: وابستگیهای آن. اینجاست که ما عمدتاً بر اساس نمونههای مطالعه شده تغییرات ایجاد خواهیم کرد؛
1.3.2. بیانیهٔ برنامه
![]() |
فایل [AndroidManifest.xml] [1] ویژگیهای باینری اپلیکیشن اندروید را تعریف میکند. محتویات آن به شرح زیر است:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- خط ۳: نام بسته پروژه اندروید؛
- خط ۱۰: نام فعالیت؛
این دو مورد اطلاعات از جزئیات وارد شده هنگام ایجاد پروژه به دست میآیند:
![]() |
- خط ۳ فایل manifest (پکیج) از ورودی [4] بالا گرفته شده است. تعدادی کلاس به طور خودکار در این پکیج ایجاد میشوند؛
![]() |
- خط ۱۰ منیفست (نام فعالیت) از ورودی [1] بالا گرفته شده است؛
بیایید به مانیفست بازگردیم:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
![]() | ![]() | ![]() |
- خط ۱۰: فعالیت اصلی برنامه. این به کلاس [1] در بالا ارجاع میدهد؛
- خط ۶: آیکون برنامه ([2]). این قابل تغییر است؛
- خط ۷: برچسب برنامه. این در فایل [strings.xml] [3] یافت میشود:
<resources>
<string name="app_name">Exemple-02</string>
</resources>
فایل [strings.xml] حاوی رشتههای متنی مورد استفادهٔ برنامه است. خط ۲: نام برنامه از ورودی ثبتشده هنگام ایجاد پروژه در [4] میآید:
![]() |
- خط ۱۰: یک تگ اکتیویتی. یک اپلیکیشن اندروید میتواند چندین اکتیویتی داشته باشد؛
- خط ۱۲: این فعالیت بهعنوان فعالیت اصلی تعیین شده است؛
- خط ۱۳: و باید در لیست برنامههایی که میتوانند روی دستگاه اندروید اجرا شوند، ظاهر شود.
1.3.3. فعالیت اصلی
![]() | ![]() |
یک برنامه اندروید بر پایهٔ یک یا چند فعالیت است. در اینجا، یک فعالیت به نام [1] ایجاد شده است: [MainActivity]. یک فعالیت میتواند بسته به نوع خود یک یا چند نما را نمایش دهد. کلاس تولیدشده [MainActivity] به شرح زیر است:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- خط ۶: کلاس [MyActivity] از کلاس اندروید [AppCompatActivity] ارث میبرد. این امر برای تمام فعالیتهای آینده صادق خواهد بود؛
- خط ۹: متد [onCreate] هنگام ایجاد فعالیت اجرا میشود. این اتفاق قبل از نمایش نمای مرتبط با فعالیت رخ میدهد؛
- خط ۱۰: متد [onCreate] کلاس والد فراخوانی میشود. این کار باید همیشه انجام شود؛
- خط ۱۱: فایل [activity_main.xml] [2] نمای مرتبط با فعالیت است. تعریف XML این نما به شرح زیر است:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity">
<TextView
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- خطوط b–k: مدیر چیدمان. انتخاب پیشفرض نوع [RelativeLayout] است. در این نوع کانتینر، کامپوننتها نسبت به یکدیگر (سمت راست، سمت چپ، پایین، بالا) قرار میگیرند؛
- خطوط m–p: یک کامپوننت از نوع [TextView] که برای نمایش متن استفاده میشود؛
- خط n: متن نمایش داده شده. توصیه نمیشود که متن را مستقیماً در ویوها (views) کدگذاری سخت (hard-code) کنید. بهتر است این متن به فایل [res/values/strings.xml] [3] منتقل شود:
بنابراین متن نمایش داده شده [Hello World!] خواهد بود. کجا نمایش داده خواهد شد؟ کانتینر [RelativeLayout] صفحه را پر خواهد کرد. [TextView] که تنها عنصر آن است، در بالای سمت چپ این کانتینر و در نتیجه در بالای سمت چپ صفحه نمایش داده خواهد شد؛
[R.layout.activity_main] در خط ۱۱ به چه معناست؟ به هر منبع اندروید (ویوها، فراگمنتها، کامپوننتها و غیره) یک شناسهی منحصربهفرد اختصاص داده میشود. بنابراین، یک ویو با نام [V.xml] که در پوشه [res / layout] قرار دارد، با شناسه [R.layout.V] شناسایی خواهد شد. R کلاسی است که در پوشه [app / build / generated] [1-3] تولید شده است:
![]() |
کلاس [R] به شرح زیر است:
...............
public static final class string {
public static final int abc_action_bar_home_description=0x7f060000;
public static final int abc_action_bar_home_description_format=0x7f060001;
public static final int abc_action_bar_home_subtitle_description_format=0x7f060002;
...
public static final int app_name=0x7f060014;
}
public static final class layout {
public static final int abc_action_bar_title_item=0x7f040000;
public static final int abc_action_bar_up_container=0x7f040001;
...
public static final int activity_main=0x7f040019;
...
}
public static final class mipmap {
public static final int ic_launcher=0x7f030000;
}
- خط 14: ویژگی [R.layout.activity_main] شناسه نما [res / layout / activity_main.xml] است؛
- خط ۷: ویژگی [R.string.app_name] شناسه رشته [app_name] در فایل [res / values / string.xml] است:
- خط ۱۹: صفت [R.mipmap.ic_launcher] شناسهٔ تصویر [res / mipmap / ic_launcher] است؛
بنابراین مهم است به خاطر داشته باشیم که وقتی در کد به [R.layout.activity_main] ارجاع میدهیم، در واقع به یک ویژگی از کلاس [R] اشاره میکنیم. IDE به ما کمک میکند تا عناصر مختلف این کلاس را شناسایی کنیم:
![]() | ![]() |
1.3.4. اجرای برنامه
برای اجرای یک برنامه اندروید، باید یک پیکربندی اجرا ایجاد کنیم:
![]() | ![]() | ![]() |
- در [1]، [Edit Configurations] را انتخاب کنید؛
- پروژه با یک پیکربندی به نام [app] ایجاد شده است که ما آن را برای ایجاد مجدد، حذف خواهیم کرد ([2]);
- در [3]، یک پیکربندی اجرای جدید ایجاد کنید؛
![]() |
- در [4]، [Android Application] را انتخاب کنید؛

- در [5]، از لیست کشویی، ماژول [app] را انتخاب کنید؛
- برای [6-8]، مقادیر پیشفرض را حفظ کنید؛
- در [7]، فعالیت پیشفرض همان است که در فایل [AndroidManifest.xml] (خط ۱ زیر) تعریف شده است:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- در [8]، [Show Chooser Dialog] را انتخاب کنید که به شما امکان میدهد دستگاهی را که برنامه روی آن اجرا میشود (شبیهساز، تبلت) انتخاب کنید؛
- در [9]، مشخص کنید که این انتخاب باید ذخیره شود؛
- پیکربندی را تأیید کنید؛
![]() |
- در [11]، مدیر شبیهساز [Genymotion] را راهاندازی کنید (به بخش 6.9 مراجعه کنید)؛
![]() |
- در [12]، یک شبیهساز تبلت را انتخاب کرده و [13] را راهاندازی کنید؛
![]() | ![]() |
- به [14]، پیکربندی اجرای [app] را اجرا کنید؛
- در [15]، فرم انتخاب دستگاه اجرای نمایش داده میشود. در اینجا تنها یک مورد در دسترس است: شبیهساز [Genymotion] که قبلاً راهاندازی شده است؛
پس از مدتی کوتاه، شبیهساز نرمافزاری نمای زیر را نمایش میدهد:

1.3.5. چرخه عمر یک فعالیت
بیایید به کد فعالیت [MainActivity] بازگردیم:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
متد [onCreate] در خطوط ۸–۱۲ یکی از متدهایی است که میتواند در طول چرخهٔ عمر یک فعالیت فراخوانی شود. مستندات اندروید این موارد را به شرح زیر فهرست میکند:
![]() |
- [1]: متد [onCreate] زمانی که فعالیت شروع میشود، فراخوانی میشود. در همین متد است که فعالیت با یک نما (view) مرتبط شده و ارجاعات به اجزای آن بازیابی میشوند؛
- [2-3]: سپس متدهای [onStart, onResume] فراخوانی میشوند. میتوانیم ببینیم که متد [onResume] آخرین متدی است که قبل از رسیدن به وضعیت [4] از فعالیت در حال اجرا، اجرا میشود؛
1.4. مثال-۰۳: بازنویسی پروژه [Exemple-02] با استفاده از کتابخانه [Android Annotations]
اکنون کتابخانه [Android Annotations] را معرفی میکنیم که نوشتن برنامههای اندروید را آسانتر میکند. برای این کار، با دنبال کردن رویه در [1-16]، مثال [Exemple-02] را به [Exemple-03] کپی میکنیم.
![]() | ![]() |
- به [1]؛ برای مشاهده کل پروژه اندروید، به نمای [Project] بروید؛
![]() | ![]() |
![]() | ![]() |
![]() | ![]() | ![]() | ![]() |
توجه: بین [14] و [15]، ما از دیدگاه [Android] به دیدگاه [Project] تغییر کردهایم (به بخش 1.2.2.1 مراجعه کنید).
سپس فایل [res / values / strings.xml] [17] را اصلاح میکنیم:
![]() |
فایل [strings.xml] به شرح زیر اصلاح میشود:
<resources>
<string name="app_name">Exemple-03</string>
</resources>
اکنون، ما برنامه جدید را اجرا میکنیم که کل پیکربندی را از [Exemple-02] به ارث برده است:
![]() | ![]() |
در [19]، همان نتیجه را مانند [Exemple-02] اما با نامی جدید به دست میآوریم.
اکنون کتابخانه [Android Annotations] را معرفی میکنیم که برای سهولت، آن را AA مینامیم. این کتابخانه کلاسهای جدیدی را برای حاشیهنویسی کد منبع اندروید معرفی میکند. این حاشیهنویسیها توسط پردازندهای استفاده میشوند که کلاسهای جدید جاوا را درون ماژول ایجاد خواهد کرد؛ این کلاسها به همان شیوهای که کلاسهای نوشته شده توسط توسعهدهنده در کامپایل ماژول گنجانده میشوند، در کامپایل ماژول گنجانده خواهند شد. این به ما زنجیره کامپایل زیر را میدهد:
![]() |
ابتدا، وابستگیها به کامپایلر آنوتیشن AA (پردازندهی مذکور در بالا) را به فایل [build.gradle] اضافه خواهیم کرد:
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
}
- خطوط ۴–۵ دو وابستگی را که کتابخانه AA را تشکیل میدهند اضافه میکنند؛
فایل [build.gradle] بار دیگر اصلاح میشود تا از پلاگینی به نام [android-apt] استفاده کند، که فرآیند کامپایل را به دو مرحله تقسیم میکند:
- پردازش حاشیهنویسیهای اندروید که منجر به ایجاد کلاسهای جدید میشود؛
- کامپایل شدن تمام کلاسهای پروژه؛
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-apt] که از مخزن مرکزی Maven (خط ۳) بازیابی خواهد شد؛
- خط ۱۳: فعالسازی این پلاگین؛
در این مرحله، بررسی کنید که پیکربندی اجرای [app] هنوز کار میکند.
اکنون اولین حاشیهنویسی را از کتابخانه AA به کلاس [MainActivity] معرفی میکنیم:
![]() |
کلاس [MainActivity] در حال حاضر به شکل زیر است:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
ما قبلاً این کد را در بخش 1.3.3 توضیح دادهایم. ما آن را به شرح زیر تغییر میدهیم:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- خط ۷: حاشیهنویسی [@EActivity] یک حاشیهنویسی از AA (خط ۳) است. پارامتر آن نمای مرتبط با فعالیت است؛
این حاشیهنویسی یک کلاس [MainActivity_] مشتقشده از کلاس [MainActivity] ایجاد خواهد کرد و این کلاس همان فعالیت واقعی است. بنابراین باید مانیفست پروژه [AndroidManifest.xml] را به شرح زیر اصلاح کنیم:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity_">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- خط ۱۱: فعالیت جدید؛
پس از انجام این کار، میتوانیم پروژه [1] را کامپایل کنیم:
![]() | ![]() |
- در [2]، میتوانیم کلاس [MainActivity_] را که در پوشه [app / build / generated / source / apt / debug] تولید شده است، ببینیم؛
کلاس تولیدشده [MainActivity_] به شرح زیر است:
//
//DO NOT EDIT THIS FILE.
//تولید شده با استفاده از AndroidAnnotations 4.0.0.
//
// شما میتوانید اثر بزرگتری ایجاد کنید که این فایل را در بر گیرد و آن اثر را تحت شرایط دلخواه خود توزیع کنید.
//
package exemples.android;
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
public final class MainActivity_
extends MainActivity
implements HasViews
{
private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
@Override
public void onCreate(Bundle savedInstanceState) {
OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
init_(savedInstanceState);
super.onCreate(savedInstanceState);
OnViewChangedNotifier.replaceNotifier(previousNotifier);
setContentView(R.layout.activity_main);
}
...
- خطوط ۲۴–۲۵: کلاس [MainActivity_] از کلاس [MainActivity] ارث میبرد؛
ما تلاش نمیکنیم کد کلاسهای تولید شده توسط AA را توضیح دهیم. این کلاسها پیچیدگیای را که حاشیهنویسیها (annotations) در پی پنهان کردن آن هستند، مدیریت میکنند. با این حال، گاهی اوقات بررسی این کدها زمانی که میخواهیم بفهمیم حاشیهنویسیهایی که استفاده میکنیم چگونه «ترجمه» میشوند، میتواند مفید باشد.
اکنون میتوانیم پیکربندی [app] را دوباره اجرا کنیم. نتیجه همانند قبل است. اکنون از این پروژه بهعنوان نقطه شروع استفاده خواهیم کرد؛ آن را کپی میکنیم تا مفاهیم کلیدی برنامهنویسی اندروید را معرفی کنیم.
1.5. مثال-۰۴: ویوها و رویدادها
1.5.1. ایجاد پروژه
ما رویه توصیفشده برای کپیبرداری از [Exemple-02] به [Exemple-03] را در بخش 1.4 دنبال خواهیم کرد:
ما خواهیم:
- پروژه [Exemple-03] را در [Exemple-04] کپی کنید (پس از حذف پوشه [app / build] از [Exemple-03]);
- پروژه [Exemple-04] را بارگذاری کنید؛
- پروژه را در فایل [app / res / values / strings.xml] (نمای اندروید) تغییر نام دهید؛
- پروژه را حذف کنید (نمای پروژه)؛
- کامپایل کرده و سپس پروژه را اجرا کنید؛
![]() | ![]() |
1.5.2. ساخت یک نما
اکنون از ویرایشگر گرافیکی برای تغییر نمای نمایشدادهشده توسط پروژه [Exemple-04] استفاده خواهیم کرد:
![]() | ![]() |
- در [1-4]، یک نمای جدید XML ایجاد کنید؛
- در [5]، به نما نام دهید؛
- در [6]، تگ ریشه برای نما را مشخص کنید. در اینجا، یک کانتینر به نام [RelativeLayout] را انتخاب میکنیم. در داخل این کانتینر کامپوننت، کامپوننتها نسبت به یکدیگر قرار میگیرند: «سمت راست»، «سمت چپ»، «زیر»، «بالا»؛
![]() |
فایل [vue1.xml] که از [7] تولید شده است، به شرح زیر است:
<?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">
</RelativeLayout>
- خط ۲: یک کانتینر خالی [RelativeLayout] که تمام عرض تبلت (خط ۳) و تمام ارتفاع آن (خط ۴) را در بر میگیرد؛
![]() | ![]() |
- در [1]، زبانه [Design] را در نمای نمایشدادهشده [vue1.xml] انتخاب کنید؛
- در [2-4]، به حالت تبلت سوئیچ کنید؛
![]() | ![]() | ![]() |
- در [5]، تبلت را روی مقیاس ۱ تنظیم کنید؛
- در [6]، حالت «افقی» را برای تبلت انتخاب کنید؛
- اسکرینشات [7] گزینههای انتخابشده را خلاصه میکند.
![]() | ![]() | ![]() |
- در [1]، یک [Large Text] را انتخاب کرده و آن را به روی نمای [2] بکشید؛
- در [3]، روی مؤلفه دوبار کلیک کنید؛
- در [4]، متن نمایشدادهشده را ویرایش کنید. به جای کدنویسی سخت در نمای XML، آن را به فایل [res / values / string.xml] منتقل خواهیم کرد.
![]() | ![]() | ![]() |
- در [5]، مقدار جدیدی به فایل [strings.xml] اضافه میشود؛
- در [8]، یک شناسگر به رشته اختصاص داده میشود؛
- در [9]، مقدار رشته ارائه میشود؛
- در [10]، نمای جدید پس از اعتبارسنجی مرحلهٔ قبلی؛
![]() | ![]() | ![]() |
- پس از دوبار کلیک روی کامپوننت، شناسهی آن را به [11] تغییر دهید؛
- به [12]؛ در ویژگیهای کامپوننت، اندازه قلم از [50sp] تغییر داده میشود؛
- به [13]؛ نمای جدید؛
فایل [vue1.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="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
- تغییراتی که در رابط کاربری گرافیکی اعمال شده در خطوط ۱۰، ۱۱ و ۱۴ است. سایر ویژگیهای [TextView] یا مقادیر پیشفرض هستند یا توسط موقعیتدهی کامپوننت در نما تعیین میشوند؛
- خطوط ۷–۸: ارتفاع و عرض کامپوننت برابر با ارتفاع و عرض متنی است که در خود جای داده (wrap_content);
- خط ۱۳: بالای کامپوننت با بالای ویو (خط ۱۳) همتراز است، ۵۰ پیکسل پایینتر از آن (خط ۱۳)؛
- خط ۱۲: لبهٔ چپ کامپوننت با لبهٔ چپ نما (خط ۱۳) همتراز شده و ۲۱۳ پیکسل به سمت راست جابجا شده است (خط ۱۲);
بهطور کلی، اندازههای دقیق حاشیههای چپ، راست، بالا و پایین مستقیماً در XML تنظیم خواهند شد.
با دنبال کردن همین رویه، نمای زیر را ایجاد کنید [1]:
![]() |
اجزاء به شرح زیر هستند:
موقعیتیابی اجزا نسبت به یکدیگر میتواند کاری کلافهکننده باشد، زیرا رفتار ویرایشگر گرافیکی گاهی غیرقابلپیشبینی است. ممکن است ترجیح داده شود از ویژگیهای اجزا استفاده شود:
قطعه [textView1] باید ۵۰ پیکسل پایینتر از عنوان و ۵۰ پیکسل دورتر از لبه چپ کانتینر قرار گیرد:
![]() | ![]() | ![]() |
- در [1]، لبه بالایی کامپوننت با لبه پایینی کامپوننت [textViewTitreVue1] در فاصله ۵۰ پیکسلی از [3] (بالایی) همتراز شده است؛
- در [2]، لبه چپ مؤلفه با لبه چپ کانتینر همتراز شده و در فاصله ۵۰ پیکسلی از [3] (سمت چپ) قرار دارد؛
کامپوننت [editTextNom] باید ۶۰ پیکسل به سمت راست کامپوننت [textView1] قرار گیرد و در پایین با همان کامپوننت همتراز شود؛
![]() | ![]() |
- در [1]، لبه چپ این کامپوننت با لبه راست کامپوننت [textView1] در فاصله ۶۰ پیکسلی از [2] (سمت چپ) همتراز شده است. این با لبهٔ پایین (bottom:bottom) کامپوننت [textView1] [1] همتراز است؛
کامپوننت [buttonValider] باید ۶۰ پیکسل به سمت راست کامپوننت [editTextNom] قرار گیرد و در پایین با همان کامپوننت همتراز شود؛
![]() | ![]() |
- در [1]، لبه چپ این کامپوننت با لبه راست کامپوننت [editTextNom] در فاصله ۶۰ پیکسلی از [2] (سمت چپ) همتراز شده است. این با لبهٔ پایینِ مؤلفه (bottom:bottom) [editTextNom] [1] همتراز است؛
کامپوننت [buttonVue2] باید ۵۰ پیکسل پایینتر از کامپوننت [textView1] قرار گیرد و در سمت چپ همان کامپوننت تراز شود؛
![]() | ![]() |
- در [1]، لبه چپ کامپوننت با لبه چپ کامپوننت [textView1] تراز شده و در پایین (بالا:پایین) در فاصله ۵۰ پیکسلی از [2] (بالا) قرار گرفته است؛
فایل تولیدشده 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="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="49dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_nom"
android:id="@+id/textView1"
android:layout_below="@+id/textViewTitreVue1"
android:layout_alignParentLeft="true"
android:layout_marginLeft="50dp"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
</RelativeLayout>
این فایل حاوی تمام کارهایی است که به صورت گرافیکی انجام شده است. بنابراین، راه دیگری برای ایجاد یک نما، ویرایش مستقیم این فایل است. هنگامی که به آن عادت کنید، این کار میتواند سریعتر از استفاده از ویرایشگر گرافیکی باشد.
- در خط ۳۸، اطلاعاتی وجود دارد که ما نشان ندادهایم. این اطلاعات از طریق ویژگیهای کامپوننتهای [editTextNom] و [1] ارائه میشود:
![]() | ![]() |
تمام متن از فایل زیر آمده است: [strings.xml] [2]:
<resources>
<string name="app_name">Exemple-04</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
اکنون، بیایید فعالیت [MainActivity] را طوری تغییر دهیم که این نما هنگام شروع برنامه نمایش داده شود:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- خط ۷: فعالیت اکنون نمای [vue1.xml] را نمایش میدهد؛
فایل [AndroidManifest.xml] را به شرح زیر ویرایش کنید:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- خط ۱۲: این خط پیکربندی مانع از ظاهر شدن صفحهکلید به محض نمایش ویو [vue1] میشود. دلیل این امر آن است که ویو حاوی یک فیلد ورودی است که هنگام نمایش ویو، فوکوس روی آن قرار میگیرد. به طور پیشفرض، این فوکوس باعث ظاهر شدن صفحهکلید روی صفحه میشود؛
اپلیکیشن را اجرا کنید و بررسی کنید که نمای [vue1.xml] واقعاً نمایش داده میشود:

1.5.3. پردازش رویداد
اکنون کلیک روی دکمه [Valider] در نمای [Vue1] را مدیریت کنیم:

کد برای [MainActivity] به شرح زیر تغییر میکند:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
// عناصر رابط کاربری بصری
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews(){
Log.d("MainActivity","afterViews");
}
// مدیر رویداد
@Click(R.id.buttonValider)
protected void doValider() {
// نام واردشده نمایش داده میشود
Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- خطوط 17–18: فیلد [protected EditText editTextNom] با شناسهی component [R.id.editTextNom] از رابط کاربری بصری مرتبط است. میدانی که با این کامپوننت مرتبط است باید در کلاس مشتق [MainActivity_] قابل دسترسی باشد و به همین دلیل نمیتواند دامنه [private] را داشته باشد. میدانی که توسط [R.id.editTextNom] شناسایی شده است، از نمای [vue1.xml] منشأ میگیرد:
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
توجه: از کاراکترهای دارای نشانه در شناسههای [id] استفاده نکنید. AA آنها را به درستی پردازش نمیکند.
- خط ۳۲: حاشیهنویسی [@Click(R.id.buttonValider)] به متدی اشاره دارد که رویداد «Click» را روی دکمهای با شناسهی [R.id.buttonValider] مدیریت میکند. این شناسه همچنین از نمای [vue1.xml] میآید:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
- خط ۳۵: نام وارد شده را نمایش میدهد:
- Toast.makeText(...).show(): متن را روی صفحه نمایش میدهد،
- پارامتر اول makeText فعالیت است،
- پارامتر دوم متنی است که باید در کادر نمایش داده شود و توسط makeText نمایش داده میشود،
- پارامتر سوم عمر کادر نمایش داده شده است: Toast.LENGTH_LONG یا Toast.LENGTH_SHORT;
- در خط ۲۶، حاشیهنویسی [@AfterViews] متدی را مشخص میکند که پس از مقداردهی اولیه تمام فیلدهایی که توسط [@ViewById] حاشیهنویسی شدهاند، اجرا شود. دانستن اینکه این فیلدها چه زمانی مقداردهی اولیه میشوند، مهم است. برای مثال، آیا میتوان ارجاع خط ۱۸ را در داخل متد [onCreate] استفاده کرد؟ برای پاسخ به این سؤال، ما چند لاگ اضافه کردهایم؛
پروژه [Exemple-04] را اجرا کنید و بررسی کنید که با کلیک روی دکمه [Valider] اتفاقی رخ میدهد. لاگهای زیر را دریافت میکنیم:
میتوان نتیجه گرفت که هنگامی که متد [onCreate] اجرا میشود، فیلدهایی که توسط [@ViewById] برچسبگذاری شدهاند هنوز مقداردهی اولیه نشدهاند. بار دیگر، خوانندگان تازهکار تشویق میشوند که این نوع لاگ را در متدهایی که چرخه عمر برنامه را مدیریت میکنند، بگنجانند.
1.6. مثال-۰۵: ناوبری بین ویوها
در پروژه قبلی، دکمه [Vue n° 2] مورد استفاده قرار نگرفت. ما پیشنهاد میکنیم با ایجاد یک نمای دوم و نشان دادن نحوه پیمایش بین نماها از آن استفاده کنیم. راههای مختلفی برای حل این مشکل وجود دارد. رویکرد پیشنهادی در اینجا این است که هر نما را با یک فعالیت مرتبط سازیم. روش دیگر این است که یک فعالیت واحد از نوع [AppCompatActivity] داشته باشیم که نماهایی از نوع [Fragment] را نمایش دهد. این روشی است که در برنامههای آینده استفاده خواهد شد.
1.6.1. ایجاد پروژه
پروژه [Exemple-04] را در [Exemple-05] کپی کنید. برای انجام این کار، رویه توصیفشده برای کپیبرداری [Exemple-02] به [Exemple-03] در بخش 1.4 را دنبال کنید که در بخش 1.5 بازتولید شده است.
![]() | ![]() |
1.6.2. افزودن یک فعالیت دوم
برای مدیریت نمای دوم، یک فعالیت دوم ایجاد خواهیم کرد. این فعالیت، نمای شمارهٔ ۲ را مدیریت میکند. در اینجا از مدل «یک نما = یک فعالیت» پیروی میکنیم. مدلهای دیگری نیز امکانپذیر است.
123

- در [1-4]، یک فعالیت جدید ایجاد میکنیم؛

- در [5]، نام کلاسی که تولید خواهد شد؛
- در [6]، نام نما (vue2.xml) مرتبط با فعالیت جدید؛
![]() |
- [7-8]، فایلهای تحت تأثیر پیکربندی قبلی؛
فعالیت [SecondActivity] به شرح زیر است:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.vue2);
}
}
- خط ۱۱: فعالیت با نما [vue2.xml] مرتبط است؛
نما [vue2.xml] به شرح زیر است:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
</RelativeLayout>
در حال حاضر این یک نمای خالی است با مدیر چیدمان از نوع [RelativeLayout] (خط ۲). در خط ۱۱ میبینیم که با فعالیت جدید مرتبط شده است.
فایل مانیفست ماژول اندروید [AndroidManifest.xml] به شرح زیر تغییر کرده است:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity">
</activity>
</application>
</manifest>
در خط ۲۰، یک فعالیت دوم ثبت شده است.
1.6.3. ناوبری از نما شماره ۱ به نما شماره ۲
بیایید به کد کلاس [MainActivity] که نما شمارهٔ ۱ را نمایش میدهد بازگردیم. ناوبری به نما شمارهٔ ۲ در حال حاضر پشتیبانی نمیشود:
![]() |
ما این را به شرح زیر مدیریت میکنیم:
// حرکت به نما شمارهٔ ۲
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// به نما شمارهٔ ۲ پیمایش میکند و نام واردشده در نما شمارهٔ ۱ را به آن ارسال میکند
// یک Intent ایجاد میکند
Intent intent = new Intent();
// این Intent را با یک فعالیت مرتبط میسازد
intent.setClass(this, SecondActivity.class);
// ما اطلاعات را با این Intent مرتبط میکنیم
intent.putExtra("NOM", editTextNom.getText().toString().trim());
// فعالیت از نوع [SecondActivity] را با ارسال Intent به آن راهاندازی میکند
startActivity(intent);
}
- خطوط ۲–۳: متد [navigateToView2] کلیک روی دکمهای را که توسط [R.id.buttonVue2] شناسایی شده است، مدیریت میکند، که در نمای [vue1.xml] تعریف شده است:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
توضیحات، مراحل لازم برای تغییر نما را توصیف میکنند:
- خط ۶: یک شیء از نوع [Intent] ایجاد کنید. این شیء برای مشخص کردن هم فعالیتی که باید راهاندازی شود و هم اطلاعاتی که باید به آن ارسال شود، استفاده خواهد شد؛
- خط ۸: Intent را با یک فعالیت مرتبط کنید، در این مورد یک فعالیت از نوع [SecondActivity] که مسئول نمایش نمای شماره ۲ خواهد بود. به یاد داشته باشید که فعالیت [MainActivity] نمای شماره ۱ را نمایش میدهد. بنابراین ما یک نما = یک فعالیت داریم. ما باید نوع [SecondActivity] را تعریف کنیم؛
- خط ۱۰: اختیاری، اطلاعات را در شیء [Intent] وارد کنید. این اطلاعات برای فعالیت [SecondActivity] که در شرف راهاندازی است، در نظر گرفته شده است. پارامترهای [Intent.putExtra] عبارتند از (Key Object, Value Object). توجه داشته باشید که متد [EditText.getText()]، که متن وارد شده در فیلد ورودی را برمیگرداند، نوع [String] را بازنمیگرداند، بلکه نوع [Editable] را بازمیگرداند. شما باید از متد [toString] برای بازیابی متن وارد شده استفاده کنید؛
- خط ۱۲: فعالیت تعریفشده توسط شیء [Intent] را راهاندازی کنید.
پروژه [Exemple-05] را اجرا کنید و بررسی کنید که واقعاً نمای شمارهٔ ۲ (فعلاً خالی) به شما نمایش داده میشود:
![]() | ![]() |
1.6.4. در حال ساخت نما شمارهٔ ۲
![]() | ![]() |
- در [1-2]، نمای [main.xml] را که دیگر مورد نیاز نیست، حذف میکنیم و سپس نمای [vue2.xml] را به شرح زیر اصلاح میکنیم:
![]() |
اجزاء به شرح زیر هستند:
فایل XML [vue2.xml] به شرح زیر است:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue2"
android:id="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textViewBonjour"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_below="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:layout_marginLeft="50dp"
android:textSize="30sp"
android:text="Bonjour !"
android:textColor="#ffffb91b"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue1"
android:id="@+id/buttonVue1"
android:layout_marginTop="50dp"
android:textSize="30sp"
android:layout_alignLeft="@+id/textViewBonjour"
android:layout_below="@+id/textViewBonjour"/>
</RelativeLayout>
پروژه [Exemple-05] را اجرا کنید و با کلیک روی دکمه [Vue n° 2] بررسی کنید که واقعاً نمای جدید به شما نمایش داده میشود.
1.6.5. فعالیت [SecondActivity]
در [MainActivity]، کد زیر را نوشتهایم:
// به نمای شمارهٔ ۲ پیمایش میکند
protected void navigateToView2() {
// به نما شمارهٔ ۲ هدایت میکند و نام واردشده در نما شمارهٔ ۱ را به آن ارسال میکند
// یک Intent ایجاد میکند
Intent intent = new Intent();
// این Intent را با یک فعالیت مرتبط میسازد
intent.setClass(this, SecondActivity.class);
// ما اطلاعات را با این Intent مرتبط میکنیم
intent.putExtra("NOM", edtNom.getText().toString().trim());
// فعالیت از نوع [SecondActivity] را با ارسال Intent به آن راهاندازی میکند
startActivity(intent);
}
در خط ۹، اطلاعاتی را برای [SecondActivity] درج کردیم که مورد استفاده قرار نگرفته بود. اکنون از آن استفاده میکنیم و این کار در کد [SecondActivity] انجام شده است:
![]() |
کد برای [SecondActivity] به شرح زیر تغییر میکند:
package exemples.android;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
// اجزای رابط کاربری بصری
@ViewById
protected TextView textViewBonjour;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews() {
// در صورتی که intent وجود داشته باشد، آن را بازیابی کنید
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// نام را بازیابی میکند
String nom = extras.getString("NOM");
if (nom != null) {
// آن را نمایش دهید
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
}
}
}
- خط ۱۱: انوتیشن [@EActivity] برای نشان دادن اینکه کلاس [SecondActivity] یک فعالیت مرتبط با ویو [vue2.xml] است، استفاده میشود؛
- خطوط ۱۵–۱۶: مرجعی برای کامپوننت [TextView] که با [R.id.textViewBonjour] شناسایی شده است، بازیابی میشود. در اینجا، [@ViewById(R.id.textViewBonjour)] مشخص نشده است. در این حالت، AA فرض میکند که شناسهی مؤلفه با فیلد حاشیهنویسیشده یکسان است، که در این مورد فیلد [textViewBonjour] است؛
- خط ۲۳: حاشیهنویسی [@AfterViews] روشی را مشخص میکند که باید پس از مقداردهی اولیه فیلدهایی که توسط [@ViewById] حاشیهنویسی شدهاند، اجرا شود. در متد [OnCreate] (خط ۱۹)، این فیلدها قابل استفاده نیستند زیرا هنوز مقداردهی نشدهاند. در پروژه [Exemple-05]، سیستم از یک فعالیت به فعالیت دیگر تغییر میکند و بلافاصله مشخص نبود که آیا متدی که با [@AfterViews] نشانهگذاری شده است، یکبار در هنگام نمونهسازی اولیه فعالیت اجرا میشود یا هر بار که فعالیت شروع میشود. آزمایشها نشان دادند که فرض دوم صحیح بود؛
- خط ۲۶: کلاس [AppCompatActivity] دارای متدی به نام [getIntent] است که شیء [Intent] مرتبط با فعالیت را برمیگرداند؛
- خط ۲۸: متد [Intent.getExtras] یک نوع [Bundle] را بازمیگرداند که نوعی دیکشنری حاوی اطلاعات مرتبط با شیء فعالیت [Intent] است؛
- خط ۳۱: نام ذخیره شده در شی [Intent] فعالیت بازیابی میشود؛
- خط ۳۴: این نمایش داده میشود.
یادآوری: فیلدهایی که با برچسب [@ViewById] علامتگذاری شدهاند نباید حاوی کاراکترهای دارای علامت باشند.
بیایید به کلاس [SecondActivity] بازگردیم. زیرا ما نوشتهایم:
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
AA کلاسی به نام [SecondActivity_] مشتقشده از [SecondActivity] تولید خواهد کرد، و این کلاس همان فعالیت واقعی خواهد بود. این ما را به انجام تغییراتی در موارد زیر وا میدارد:
[MainActivity]
// به نمای شمارهٔ ۲ هدایت میکند
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// این Intent را با یک فعالیت مرتبط میسازد
intent.setClass(this, SecondActivity_.class);
...
}
- در خط ۶، باید [SecondActivity] را با [SecondActivity_] جایگزین کنیم؛
[AndroidManifest.xml]
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity_">
</activity>
</application>
</manifest>
- در خط ۲۰، [SecondActivity] را با [SecondActivity_] جایگزین کنید؛
این نسخهٔ جدید را آزمایش کنید. در نما شمارهٔ ۱ یک نام وارد کنید و بررسی کنید که نما شمارهٔ ۲ آن را بهدرستی نمایش میدهد.
![]() | ![]() |
1.6.6. ناوبری از نما شمارهٔ ۲ به نما شمارهٔ ۱
برای ناوبری از نما شماره ۲ به نما شماره ۱، رویهٔ مشاهدهشدهٔ قبلی را دنبال میکنیم:
- کد ناوبری را در فعالیت [SecondActivity] که نما شماره 2 را نمایش میدهد، قرار دهید؛
- متد [@AfterViews] را در فعالیت [MainActivity] که نمای شماره ۱ را نمایش میدهد، بنویسید؛
کد برای [SecondActivity] به شرح زیر تغییر میکند:
@Click(R.id.buttonVue1)
protected void navigateToView1() {
// یک Intent برای فعالیت ایجاد میکند [MainActivity]
Intent intent1 = new Intent();
intent1.setClass(this, MainActivity_.class);
//بازیابی Intent برای فعالیت فعلی [SecondActivity]
Intent intent2 = getIntent();
if (intent2 != null) {
Bundle extras2 = intent2.getExtras();
if (extras2 != null) {
//نام را در Intent برای [MainActivity] تنظیم میکند
intent1.putExtra("NOM", extras2.getString("NOM"));
}
// راهاندازی [MainActivity]
startActivity(intent1);
}
}
- خطوط ۱–۲: متد [navigateToView1] با کلیک روی دکمه [btn_vue1] مرتبط است؛
- خط ۴: یک [Intent] جدید ایجاد میشود؛
- خط ۵: مرتبط با فعالیت [MainActivity_]؛
- خط ۷: Intent مرتبط با [SecondActivity] را بازیابی کنید؛
- خط ۹: بازیابی اطلاعات برای این Intent؛
- خط ۱۲: کلید [NOM] از [intent2] بازیابی میشود تا با همان مقدار مرتبط در [intent1] قرار گیرد؛
- خط ۱۵: فعالیت [MainActivity_] راهاندازی میشود.
در کد [MainActivity]، متد زیر [@AfterViews] اضافه میشود:
@AfterViews
protected void afterViews() {
// در صورتی که نیت وجود داشته باشد، آن را بازیابی کنید
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// نام را بازیابی میکند
String nom = extras.getString("NOM");
if (nom != null) {
// آن را نمایش دهید
editTextNom.setText(nom);
}
}
}
}
این تغییرات را اعمال کرده و برنامه خود را تست کنید. اکنون وقتی از نما شمارهٔ ۲ به نما شمارهٔ ۱ بازمیگردید، نام واردشده در ابتدا باید نمایش داده شود که قبلاً این اتفاق نمیافتاد.
![]() | ![]() |
1.6.7. چرخه عمر فعالیت
در بخش 1.3.5، چرخهٔ عمر یک فعالیت را تشریح کردیم. در اینجا دو فعالیت داریم و در حین اجرا بین آنها جابهجا میشویم. این فعالیتها شامل دو متد هستند و کاملاً مشخص نیست که کدام یک در رابطه با دیگری فراخوانی میشود: [onCreate] و [afterViews]. دانستن این موضوع مهم است. برای این کار، ما به هر دو فعالیت لاگ اضافه میکنیم:
بنابراین، در کلاس [MainActivity]، مینویسیم:
// تولیدکننده
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
...
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
}
}
- خطوط ۲–۴: میخواهیم بدانیم آیا کلاس [MainActivity] یک بار یا بیش از یک بار نمونه سازی میشود؛
- خط ۸: میخواهیم بدانیم آیا متد [onCreate] یک بار یا بیش از یک بار فراخوانی میشود؛
- خط ۱۴: میخواهیم بدانیم آیا متد [afterViews] یک بار یا بیش از یک بار فراخوانی میشود؛
ما دقیقاً همین کار را در کلاس [SecondActivity] انجام میدهیم.
وقتی برنامه اجرا میشود، لاگهای زیر را داریم:
متدهای [onCreate, afterViews] در فعالیت اول به این ترتیب اجرا شدند. وقتی دکمه [Vue n° 2] کلیک میشود، لاگهای جدید به شرح زیر هستند:
روشهای [onCreate, afterViews] از فعالیت دوم به این ترتیب اجرا شدند. وقتی دکمه [Vue n° 1] کلیک میشود، لاگهای جدید به شرح زیر هستند:
بنابراین کلاس [MainActivity] دوباره نمونه سازی میشود. وقتی دکمه [Vue n° 2] کلیک میشود، لاگهای جدید به شرح زیر است:
کلاس [SecondActivity] بنابراین دوباره نمونه سازی میشود.
بنابراین هر دو فعالیت بهطور سیستماتیک هر بار که فعالیت تغییر میکند، دوباره ایجاد میشوند.
اکنون معماریای را بررسی خواهیم کرد که شامل یک فعالیت واحد است و قادر به مدیریت چندین نمای معروف به فرگمنت (fragment) میباشد. این فعالیت و نماها تنها یکبار نمونهسازی میشوند، برخلاف روش قبلی که در آن یک فعالیت میتوانست چندین بار نمونهسازی شود.
1.7. مثال-۰۶: ناوبری برگهای
در اینجا رابطهای تبدار را بررسی خواهیم کرد. این مثال پیچیده است اما تمام عناصری را که بعداً استفاده خواهیم کرد معرفی میکند: یک فعالیت واحد، یک مدیر قطعه (ویوها)، یک کانتینر قطعه و ناوبری بین قطعات. مفهوم تبها با مفهوم قطعات متفاوت است و از نظر آنچه میخواهیم در این مثال نشان دهیم، اهمیت ثانویهای دارد.
1.7.1. ایجاد پروژه
ما یک پروژه جدید ایجاد میکنیم:
![]() | ![]() |
![]() |
![]() |
- در [7]، یک فعالیت برگهای را انتخاب میکنیم؛
![]() |
- در [10-14]، مقادیر پیشفرض را حفظ کنید؛
- در [15]، برگههایی با نوار عنوان را انتخاب کنید؛
پروژه ایجادشده سپس به شرح زیر است:
![]() | ![]() |
- در [1]، فعالیت؛
- در [2]، نماها؛
یک پیکربندی اجرا، [app]، که به نام ماژول نامگذاری شده است، به طور خودکار به عنوان [2b] ایجاد شد:
![]() |
اکنون میتوان آن را اجرا کرد. سپس پنجرهای با سه زبانه ظاهر میشود: [3-6]:

1.7.2. پیکربندی Gradle
پروژه [Exemple-06] با فایل زیر تولید شد: [build.gradle]:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
}
یک عنصر جدید در مقایسه با آنچه قبلاً دیده شده وجود دارد: خط ۲۵. این کتابخانه برای کامپوننتهای جدیدی که توسط اپلیکیشن تولیدشده استفاده میشوند، ضروری است.
1.7.3. مشاهده [activity_main]
![]() |
نما [activity_main] نمایی است که با فعالیت پروژه [MainActivity] مرتبط است. در حالت [design]، نما به شرح زیر است:

این نما شامل اجزای زیر است:
![]() |
- [main_content] نمای کامل است؛
- [appbar] (جعبه قرمز، ۱) نوار برنامه است. این شامل دو مؤلفه است:
- [toolbar] (جعبه زرد 4) نوار ابزار است؛
- [tabs] (جعبه نارنجی 5) نوار عنوان زبانه است؛
- [container] (جعبه سبز، ۲) میتواند قطعات مختلفی را در خود جای دهد. یک قطعه، یک نما است. بنابراین، یک فعالیت واحد قادر خواهد بود چندین نما (قطعه) را در این کانتینر نمایش دهد؛
- [fab] (کامپوننت ۳) به عنوان یک کامپوننت شناور شناخته میشود؛
در حالت [text]، کد به شرح زیر است:
<?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="exemples.android.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.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
عناصر توصیفشده در بالا را مییابیم:
- خطوط ۲–۴۹: تعریف کامپوننت [main_content] (خط ۵)، که کل نما را تشکیل میدهد. میتوانیم ببینیم که این یک layout (مدیر چیدمان مؤلفه) از نوع [CoordinatorLayout] (خط ۲) است؛
- خطوط ۱۱–۳۳: کانتینر [appbar] (خط ۱۲). این یک layout از نوع [AppBarLayout] (خط ۱۱) است؛
- خطوط ۱۸–۲۴: مؤلفه [toolbar] (خط ۱۹) از نوع [Toolbar] (خط ۱۸);
- خطوط ۲۸–۳۱: کانتینر [tabs] (خط ۲۹). این یک layout از نوع [TabLayout] (خط ۲۸) است. این عنوانهای تب را نمایش خواهد داد؛
- خطوط ۳۵–۳۹: کامپوننت [container] (خط ۳۶). این کانتینر نماهای مختلف فعالیت را نمایش میدهد؛
- خطوط ۴۱–۴۷: کامپوننت [fab] (خط ۴۲) از نوع [FloatingActionButton] (خط ۴۱). این یک دکمه است که میتوان روی آن کلیک کرد. بهطور پیشفرض، در پایین سمت راست نمای کامل قرار دارد؛
ما تلاش نمیکنیم تا معنای تمام ویژگیهای این کامپوننتها را درک کنیم. ما از آنها همانطور که هستند استفاده خواهیم کرد. این از طریق تجربه، و اغلب در حالت [design] است که فرد نقش آنها را کشف میکند. در این حالت، فرد متوجه میشود که کامپوننتها دهها ویژگی دارند. به طور کلی، تنها برخی از آنها مقداردهی اولیه میشوند، در حالی که بقیه مقادیر پیشفرض خود را حفظ میکنند.
با این حال، بیایید چند نکته را روشن کنیم. بیشتر مقاطعی که نماهای مختلف را پیکربندی میکنند، در پوشه [res / values] قرار دارند:
![]() |
این مقادیر در خطوط ۱۵–۱۶، ۲۳، ۳۹ و ۴۶ از فایل [activity_main.xml] ارجاع شدهاند. بیایید یک مثال بزنیم:
- خط ۱۵:
android:paddingTop="@dimen/appbar_padding_top"
توضیح [@dimen] به فایل [res / values / dimens.xml] اشاره دارد:
<resources>
<!-- حاشیههای پیشفرض صفحه، مطابق با دستورالعملهای طراحی اندروید. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen>
</resources>
خط ۱۵ فایل [activity_main.xml] به خط (f) بالا اشاره دارد؛
به همین ترتیب، حاشیهنویسی:
- [@string] به فایل منبعی [res / values / strings.xml] اشاره دارد؛
- [@color] به فایل منبعی [res / values / colors.xml] اشاره دارد؛
- [@style] به فایل منبعی [res / values / styles.xml] اشاره دارد؛
1.7.4. فعالیت
![]() |
کدی که برای این فعالیت تولید میشود به اندازه نمای توصیفشده در بالا پیچیده است: بسیار درهمتنیده است. ما آن را در چند مرحله تحلیل خواهیم کرد.
1.7.4.1. مدیریت قطعات و برگهها
کد مربوط به قطعات و برگهها در [MainActivity] به شرح زیر است:
package exemples.android;
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// مدیر فرگمنت
private SectionsPagerAdapter mSectionsPagerAdapter;
// کانتینر فرگمنت
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// والد
super.onCreate(savedInstanceState);
// نما
setContentView(R.layout.activity_main);
//نوار ابزار
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// مدیر قطعه
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// ظرفیت قطعهها با مدیر قطعهها مرتبط است
//یعنی قطعه شماره i در محفظهٔ قطعات، همان قطعه شماره i است که توسط مدیر قطعات ارائه شده است
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
//نوار زبانه نیز با محفظهٔ قطعه مرتبط است
//یعنی زبانه شماره i، قطعه شماره i را از کانتینر نمایش میدهد
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
//یک قطعه
public static class PlaceholderFragment extends Fragment {
...
}
// مدیر قطعات
//مسئول بازیابی قطعاتی است که باید در نمای اصلی نمایش داده شوند
//باید متدهای [getItem] و [getCount] را تعریف کند – بقیه اختیاری هستند
public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
}
- خط ۲۸: اندروید یک کانتینر ویو از نوع [android.support.v4.view.ViewPager] (خط ۱۲) فراهم میکند. این کانتینر باید به یک مدیر ویو یا فرگمنت مجهز شود. این مورد توسط توسعهدهنده تأمین میشود؛
- خط ۲۵: دستکارگر قطعه (fragment handler) مورد استفاده در این مثال. پیادهسازی آن در خطوط ۶۱–۶۳ قرار دارد؛
- خط ۳۱: متدی که هنگام ایجاد فعالیت اجرا میشود؛
- خط ۳۵: ویو [activity_main.xml] با اکتیویتی مرتبط است؛
- خط ۳۷: مرجع به کامپوننت [toolbar] از ویو از طریق شناسه آن بازیابی میشود؛
- خط ۳۸: این نوار ابزار به نوار اکشن فعالیت تبدیل میشود (مفهومی در اندروید)؛
- خط ۴۰: مدیر فرگمنت نمونهسازی میشود. پارامتر سازنده کلاس اندروید [android.support.v4.app.FragmentManager] (خط ۱۰) است؛
- خط ۴۴: مرجع کانتینر فرگمنت از طریق شناسه (ID) آن از نمای [activity_main.xml] بازیابی میشود؛
- خط ۴۵: مدیر فرگمنت به کانتینر فرگمنت متصل میشود. این بدان معناست که وقتی از کانتینر فرگمنت خواسته میشود فرگمنت شماره i را نمایش دهد، از مدیر فرگمنت خواسته میشود آن را فراهم کند؛
- خط ۴۸: یک مرجع به نوار تب از طریق شناسهٔ آن بازیابی میشود؛
- خط ۴۹: مدیر تب با کانتینر فرگمنت مرتبط میشود. این بدان معناست که وقتی روی تب شماره i کلیک میشود، کانتینر فرگمنت شماره i را نمایش خواهد داد. ارتباط بین مدیر تب و کانتینر فرگمنت به این معناست که ما نیازی به مدیریت دستی تبها نداریم. در نتیجه، نیازی به تعریف یک رویدادپرداز برای کلیک روی یک تب نداریم. ارتباط با کانتینر فرگمنت این قابلیت را بهطور پیشفرض فراهم میکند. ما به مثالی نگاه خواهیم کرد که در آن فرگمنتها بیشتر از تبها هستند. در این حالت، ما این ارتباط را برقرار نمیکنیم.
پردازشگر قطعه [SectionsPagerAdapter] به شرح زیر است:
//مدیر قطعات
// این کامپوننتی است که از آن خواسته میشود قطعاتی را که باید در نمای اصلی نمایش داده شوند، فراهم کند
//باید متدهای [getItem] و [getCount] را تعریف کند – بقیه اختیاری هستند
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// شماره و موقعیت قطعه
@Override
public Fragment getItem(int position) {
//یک قطعه [PlaceHolder] را نمونه سازی میکند و آن را رندر میکند
return PlaceholderFragment.newInstance(position + 1);
}
// تعداد قطعات مدیریتشده را برمیگرداند
@Override
public int getCount() {
return 3;
}
// اختیاری – یک عنوان به قطعات مدیریتشده اختصاص میدهد
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "SECTION 1";
case 1:
return "SECTION 2";
case 2:
return "SECTION 3";
}
return null;
}
}
}
- قطعات نمایش داده شده توسط یک برنامه به خود آن برنامه بستگی دارند. دستکارگر قطعه توسط توسعهدهنده تعریف میشود؛
- خط ۵: مدیر قطعه کلاس Android [android.support.v4.app.FragmentPagerAdapter] را گسترش میدهد. سازنده بر ما تحمیل میشود. ما باید حداقل دو متد زیر را تعریف کنیم:
- int getCount(): تعداد قطعاتی را که باید مدیریت شوند بازمیگرداند؛
- Fragment getItem(i): شماره قطعه i را برمیگرداند؛
متد CharSequence getPageTitle(i)، که عنوان فرگمنت شمارهٔ i را برمیگرداند، اختیاری است. از آنجا که مدیر تب به مدیر قطعه متصل شده است، عنوان تب شمارهٔ i همان عنوان قطعه شمارهٔ i خواهد بود. بنابراین، عناوین در خطوط 27–33 عناوین تب خواهند بود؛
- خطوط ۱۸–۲۱: getCount تعداد فرگمنتهای مدیریتشده را بازمیگرداند، که در این مورد سه عدد است؛
- خطوط ۱۱–۱۵: getItem(i) شماره قطعه i را بازمیگرداند. در اینجا، همه قطعات یکسان خواهند بود و از نوع [PlaceholderFragment] هستند؛
- خطوط ۲۴–۳۵: getPageTitle(int i) عنوان قطعه شماره i را بازمیگرداند؛
1.7.4.2. قطعات نمایش داده شده
![]() |
قطعات در این فعالیت همگی از یک نوع هستند و همگی با نمای زیر مرتبط هستند: XML [fragment_main]:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- خطوط ۱–۱۶: یک layout از نوع [RelativeLayout];
- خطوط ۱۱–۱۴: تنها مؤلفهٔ نما (قطعه): یک [TextView] که توسط [section_label] شناسایی شده است؛
در [MainActivity]، قطعات مدیریتشده از نوع زیر [PlaceholderFragment] هستند:
//یک قطعه
public static class PlaceholderFragment extends Fragment {
// متن نمایشدادهشده در قطعه
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
//یک قطعه حاوی اطلاعات را بازمیگرداند: شماره قطعهای که بهعنوان پارامتر ارسال شده است
public static PlaceholderFragment newInstance(int sectionNumber) {
// قطعه
PlaceholderFragment fragment = new PlaceholderFragment();
// اطلاعات جاسازیشده
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// نتیجه
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//نمای [fragment_main] نمونه برداری شده است
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// [TextView] یافت شده است
TextView textView = (TextView) rootView.findViewById(R.id.section_label);
// محتوای آن تغییر میکند
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
//نما بازگردانده میشود
return rootView;
}
}
- خط ۲: کلاس [PlaceholderFragment] از کلاس Android یعنی [Fragment] ارث میبرد. این حالت عموماً همیشه صادق است؛
- خط ۲: کلاس [PlaceholderFragment] ایستا (static) است. متد آن [newInstance] (خط ۱۰) برای به دست آوردن نمونههایی از نوع [PlaceholderFragment] استفاده میشود؛
- خطوط ۱۰–۱۹: متد [newInstance] یک شیء از نوع [PlaceholderFragment] ایجاد و بازمیگرداند؛
- خطوط 14–16: قطعه با یک آرگومان ایجاد میشود؛
یک قطعه باید متد [onCreateView] را در خط 22 تعریف کند. این متد باید نمای مرتبط با قطعه را بازگرداند.
- خط ۲۵: نمای [fragment_main.xml] با قطعه مرتبط است؛
- خط ۲۷: این نما شامل یک مؤلفه [TextView] است که ارجاع آن از طریق شناسهاش بازیابی میشود؛
- خط ۲۹: متن در [TextView] نمایش داده میشود؛
- [getString] یک متد از کلاس والد [AppCompatActivity] است؛
- آرگومان اول شمارهٔ کامپوننت است. [R.string.section_format] به شمارهٔ کامپوننتی اشاره دارد که توسط [section_format] در فایل [res / values / strings.xml] (خط ۴ زیر) شناسایی شده است:
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
- (ادامه)
- خط (d) بالا %1$d نشان میدهد که آرگومان شماره ۱ (%1) باید به صورت یک عدد صحیح قالببندی شود ($d);
- آرگومان دوم [getString] مقداری است که باید به آرگومان $1 در خط (d) بالا تخصیص داده شود؛
- [getArguments] مرجع بسته آرگومان قطعه را فراهم میکند. در اینجا باید توجه داشت که هر آرگومان با استفاده از بسته زیر (خطوط f–h) ایجاد شده است:
//یک قطعه حاوی یک بخش از اطلاعات را بازمیگرداند: شماره قطعهای که بهعنوان پارامتر ارسال شده است
public static PlaceholderFragment newInstance(int sectionNumber) {
//قطعه
PlaceholderFragment fragment = new PlaceholderFragment();
// اطلاعات جاسازیشده
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// نتیجه
return fragment;
}
- (ادامه)
- بنابراین، getArguments().getInt(ARG_SECTION_NUMBER) مقدار [sectionNumber] را از خطوط (g) و (b) بالا بازخواهد گرداند؛
- خط ۳۱: نمایی که به این ترتیب ایجاد شده است، رندر میشود؛
1.7.4.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"/>
</menu>
- خطوط ۱–۹: منو؛
- خطوط ۵–۸: یک مورد منو که با [action_settings] (خط ۵) مشخص شده است؛
- خط ۶: برچسب گزینهٔ منو. این برچسب در فایل [res / values / strings.xml] (خط (c) زیر) یافت میشود:
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
کد بالا با نمای بصری زیر مطابقت دارد (منو در بالای سمت راست پنجرهٔ زمان اجرای اندروید قرار دارد):
![]() | ![]() |
این منو در فعالیت [MainActivity] به شرح زیر مدیریت میشود:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// منو را گسترش دهید؛ این کار در صورت وجود نوار اقدام، موارد را به آن اضافه میکند.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// در اینجا کلیکهای آیتم نوار اقدام را مدیریت کنید. نوار اقدام خواهد
// به طور خودکار کلیکهای دکمهٔ Home/Up را مدیریت میکند، مشروط بر اینکه
// شما یک فعالیت والد را در AndroidManifest.xml مشخص میکنید.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
- خطوط ۱–۶: این متد زمانی فراخوانی میشود که سیستم آماده ایجاد منوی برنامه باشد. پارامتر ورودی [Menu menu] یک منوی خالی است که هنوز هیچ گزینهای ندارد؛
- خط ۴: فایل [res / menu / menu_main.xml] پردازش میشود. شیء [Menu menu] که بهعنوان پارامتر ارسال شده است، گزینههای منو را که در این فایل تعریف شدهاند، دریافت میکند؛
- خط ۵: مشخص میشود که منو ایجاد شده است؛
- خطوط ۸–۲۱: متد [onOptionsItemSelected] به محض کلیک روی یک گزینه منو اجرا میشود؛
- خط ۱۳: مرجع گزینه منوی کلیکشده؛
- خطوط 16–18: اگر گزینه کلیکشده همان گزینهای باشد که شناسه [action_settings] را دارد، هیچ اقدامی انجام نمیشود و نشان داده میشود که رویداد مدیریت شده است (خط 17);
- خط ۲۰: رویداد به کلاس والد ارسال میشود؛
برای درک بهتر آنچه در این منو رخ میدهد، چند لاگ به کد قبلی اضافه میکنیم:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("menu", "création menu en cours");
// منو را گسترش دهید؛ این کار در صورت وجود، آیتمهایی را به نوار اقدام اضافه میکند.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// در اینجا کلیکهای روی آیتمهای نوار اقدام را مدیریت کنید. نوار اقدام خواهد
// به طور خودکار کلیکهای دکمهٔ Home/Up را مدیریت میکند، مشروط بر اینکه
// شما یک فعالیت والد را در AndroidManifest.xml مشخص کنید.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
// پدر
return super.onOptionsItemSelected(item);
}
1.7.4.4. دکمه شناور
ویوی تولید شده دارای یک دکمه شناور است:
![]() |
این کامپوننت در نمای اصلی [activity-main.xml] تعریف شده است:
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
خط ۷ به تصویری که توسط پلتفرم اندروید ارائه شده است، یعنی یک پاکت نامه، ارجاع میدهد.
این کامپوننت در کلاس [MainActivity] به شرح زیر مدیریت میشود:
//دکمه شناور
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
- خط ۲: ارجاع به دکمه شناور از نمای مرتبط با فعالیت (activity_main) بازیابی میشود؛
- خطوط ۳–۹: یک دستپرداز (handler) برای آن تعیین میشود تا کلیکهای روی دکمه را مدیریت کند؛
- خط ۶: کلاس [Snackbar] با استفاده از متد [Snackbar.make] خود امکان نمایش پیامهای گذرا را در نما فراهم میکند. آرگومان اول نمایی است که [Snackbar] در آن به دنبال نمای والد برای نمایش پیام میگردد. در اینجا [view] نمای پاکت است که کلیک شده است (خط ۵). ویوی والد یافتشده، ویوی [activity_main] است. آرگومان دوم، پیامی است که باید نمایش داده شود. آرگومان سوم، مدت زمان نمایش است (SHORT یا LONG)؛
- خط ۷: میتوانید روی پیام نمایشدادهشده کلیک کنید تا عملی را اجرا کنید. در اینجا، هیچ عملی به کلیک روی پیام مرتبط نیست. در نهایت، متد [show] پیام را نمایش میدهد؛
کلیک روی دکمه شناور نتیجه بصری زیر را ایجاد میکند:
![]() |
1.7.5. اجرای پروژه
اکنون که جزئیات کد تولید شده را توضیح دادیم، میتوانیم بهتر درک کنیم که چگونه اجرا میشود:

وقتی روی برگه شماره i کلیک میکنید، قطعه شماره i در محفظه نما نمایش داده میشود. این موضوع از متن نمایش داده شده در [4] قابل مشاهده است. همچنین متوجه خواهید شد که میتوانید با کشیدن نما به سمت راست یا چپ با ماوس (swipe) بین برگهها جابجا شوید. خواهیم دید که این رفتار قابل کنترل است.
وقتی روی گزینه منوی نشان داده شده به صورت [6] کلیک میکنید، لاگهای زیر نمایش داده میشوند:
![]() |
1.7.6. چرخه عمر قطعه
![]() | ![]() |
- در [1]، میتوانیم ببینیم که متد [onCreateView] و متدهای بعدی، زمانی که قطعه برای اولین بار نمایش داده میشود و هر بار که فعالیت نیاز به نمایش مجدد آن دارد، اجرا میشوند؛
برای ردیابی چرخه عمر فعالیت و قطعات، لاگهای زیر را به کد در [MainActivity] اضافه میکنیم:
// سازنده
public MainActivity(){
Log.d("MainActivity","constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
// پدر
super.onCreate(savedInstanceState);
...
}
// یک قطعه
public static class PlaceholderFragment extends Fragment {
// متن نمایش داده شده در قطعه
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
// یک قطعه حاوی اطلاعات را بازمیگرداند: شماره قطعهای که بهعنوان پارامتر ارسال شده است
public static PlaceholderFragment newInstance(int sectionNumber) {
Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
// قطعه
PlaceholderFragment fragment = new PlaceholderFragment();
...
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
...
}
}
}
ما پروژه را دوباره اجرا میکنیم. اولین لاگها به شرح زیر است:
- خط ۱: ایجاد اکتیویتی؛
- خط ۲: اجرای متد آن [onCreate];
- خطوط ۳–۴: نمونهسازی قطعه شماره ۱؛
- خطوط ۵–۶: نمونهسازی قطعه شماره ۲؛
- خط ۷: инициалиزهسازی قطعه شماره ۲؛
- خط ۸: инициалиزهسازی قطعه شماره ۱؛
- خط ۹: ایجاد منوی فعالیت؛
شایان ذکر است که کد مسئول ایجاد قطعات در اینجا آورده شده است:
// مدیر قطعه
// این چیزی است که برای ارائه قطعات نمایش داده شده در نمای اصلی فراخوانی میشود
//باید متدهای [getItem] و [getCount] را تعریف کند – بقیه اختیاری هستند
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// شماره و موقعیت قطعه
@Override
public Fragment getItem(int position) {
//یک فرگمنت [PlaceHolder] را ایجاد کرده و آن را رندر میکند
return PlaceholderFragment.newInstance(position + 1);
}
...
- خطوط ۱۱–۱۵: یک فرگمنت توسط [newInstance] هر زمان که کانتینر فرگمنت درخواست کند، نمونه سازی میشود؛
لاگهای بالا نشان میدهند که دو فرگمنت اول ایجاد و اولیه شدهاند.
اکنون، بیایید روی زبانه شماره ۲ کلیک کنیم. لاگهای جدید به شرح زیر است:
- خطوط ۱–۳: قطعه شماره ۳ نمونه سازی و اولیه میشود. به یاد داشته باشید که این قطعه شماره ۲ است که نمایش داده میشود؛
حالا، بیایید روی برگه شماره ۳ کلیک کنیم. در اینجا هیچ لاگی وجود ندارد. این احتمالاً به این دلیل است که قطعه شماره ۳ که باید نمایش داده میشد، قبلاً نمونه سازی شده بود. اکنون، بیایید به برگه شماره ۱ بازگردیم. لاگها به شرح زیر است:
قطعه شماره ۱ دوباره نمونه سازی نمیشود، اما متد آن [onCreateView] یک بار دیگر اجرا میشود. این رفتار برای دو قطعه دیگر نیز تکرار میشود.
از روی این لاگها میتوان نتیجه گرفت که:
- فعالیت یکبار نمونهسازی و سپس یکبار مقداردهی اولیه شده است؛
- هر قطعه یک بار نمونه سازی شد؛
- متد [onCreateView] برای هر فرگمنت چندین بار اجرا شده است؛
نکته مهمی که باید به آن توجه داشت – و چیزی که لاگها آن را تأیید میکنند – این است که به طور پیشفرض، هنگامی که قطعه شماره i نمایش داده میشود، قطعات i-1 و i+1 نیز ایجاد و اولیه میشوند، در صورتی که قبلاً ایجاد نشده باشند. این موضوع، برای مثال، توضیح میدهد که چرا در هنگام راهاندازی، زمانی که باید قطعه شماره ۱ نمایش داده شود، قطعات شماره ۱ و ۲ ایجاد و اولیه میشوند. لاگها همچنین نشان میدهند که متد [getItem(i)] تنها یک بار فراخوانی میشود، حتی اگر خود قطعه شماره i چندین بار نمایش داده شود. بنابراین به نظر میرسد که کانتینر قطعه [ViewPager]، که مسئول نمایش قطعه شماره i است، این قطعه را یک بار از مدیر قطعه [SectionsPagerAdapter] درخواست میکند. این کانتینر پس از آن دیگر آن را درخواست نمیکند و به استفاده از قطعهای که به دست آورده است ادامه میدهد.
در نهایت، لاگها اطلاعاتی در مورد متد [onCreateView] قطعات ارائه میدهند:
- در هنگام راهاندازی، قطعات ۱ و ۲ نمونه سازی شدند و متد [onCreateView] آنها اجرا شد؛
- هنگام تغییر از قطعه ۱ به قطعه ۲، متد [onCreateView] قطعه ۲ مجدداً اجرا نمیشود. بنابراین نمیتوان از آن برای بهروزرسانی قطعه ۲ استفاده کرد. با این حال، ممکن است کاربر عملی را با استفاده از قطعه ۱ انجام داده باشد که نتیجه آن باید توسط قطعه ۲ نمایش داده شود. واضح است که نمیتوان از متد [onCreateView] برای بهروزرسانی قطعه ۲ استفاده کرد. باید راهحل دیگری یافت؛
1.8. Example-07: Example-06 با استفاده از کتابخانه [AA] بازنویسی شده است
1.8.1. ایجاد پروژه
ما پروژه [Exemple-06] را به عنوان [Exemple-07] کپی میکنیم تا anotationهای اندروید را به پروژه دوم اضافه کنیم. برای این کار، مراحل بخش 1.4 را دنبال کنید. نتیجه زیر به دست میآید:
![]() | ![]() |
1.8.2. پیکربندی Gradle
![]() |
ما فایل [build.gradle] را به شرح زیر اصلاح میکنیم:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// از پلاگین Gradle اندروید نسخه 0.11 به بعد، باید از 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 {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
ما پیکربندی لازم برای استفاده از کتابخانه [Android Annotations] را اضافه کردهایم (به بخش 1.4 مراجعه کنید).
1.8.3. افزودن اولین حاشیهنویسیها AA
ما حاشیهنویسیهای AA را در [MainActivity] ایجاد خواهیم کرد:
![]() |
کلاس [MainActivity] به شرح زیر تغییر میکند:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
//مدیر قطعه
private SectionsPagerAdapter mSectionsPagerAdapter;
// کانتینر فرگمنت
@ViewById(R.id.container)
protected MyPager mViewPager;
//مدیر تب
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// دکمه شناور
@ViewById(R.id.fab)
protected FloatingActionButton fab;
// سازنده
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
//نوار ابزار
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// مدیر قطعه
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// کنtenر قطعات با مدیر قطعات مرتبط است
//یعنی قطعه شماره i در مخزن قطعات، همان قطعه شماره i است که توسط مدیر قطعات ارائه شده است
mViewPager.setAdapter(mSectionsPagerAdapter);
//نوار زبانه نیز با کانتینر قطعات مرتبط است
//یعنی زبانه شماره i، قطعه شماره i را از کانتینر نمایش میدهد
tabLayout.setupWithViewPager(mViewPager);
//دکمه شناور
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- خط ۱: حاشیهنویسی [@EActivity]، کلاس [MainActivity] را به کلاسی تبدیل میکند که توسط AA مدیریت میشود. پارامتر آن [R.layout.activity_main] شناسه نما [activity_main.xml] مرتبط با فعالیت است؛
- خطوط ۱۱–۱۲: کامپوننت شناساییشده توسط [R.id.tabs] در فیلد [tabLayout] تزریق میشود. این مدیر تب است؛
- خطوط ۱۴–۱۵: کامپوننتی که با شناسه [R.id.fab] شناسایی شده است، در فیلد [fab] تزریق میشود. این دکمه شناور است؛
- خطوط ۲۳–۵۰: کدی که قبلاً در متد [onCreate] بود به متدی با نام دلخواه اما با آنوتیشن [@AfterViews] منتقل میشود (خط ۲۳). در متدی که به این صورت علامتگذاری شده است، میتوانیم مطمئن باشیم که تمام اجزای رابط کاربری که با [@ViewById] علامتگذاری شدهاند، مقداردهی اولیه شدهاند؛
- ما همچنین لاگهایی را برای نظارت بر چرخه عمر فعالیت اضافه کردهایم؛
به یاد داشته باشید که حاشیهنویسی [@EActivity] یک کلاس [MainActivity_] ایجاد خواهد کرد که فعالیت واقعی پروژه خواهد بود. بنابراین فایل [AndroidManifest.xml] باید به شرح زیر اصلاح شود:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- خط ۱۲: فعالیت جدید.
در این مرحله، پروژه را دوباره اجرا کنید و بررسی کنید که همچنان رابط کاربری با برگهها را دریافت میکنید.
1.8.4. بازنویسی قطعات
اکنون بررسی خواهیم کرد که چگونه قطعات در پروژه مدیریت میشوند. در حال حاضر، کلاس [PlaceholderFragment] یک کلاس داخلی ایستا از فعالیت [MainActivity] است. ما به یک مورد استفاده رایجتر بازمیگردیم که در آن قطعات در کلاسهای خارجی تعریف میشوند. علاوه بر این، ما anotationهای AA را برای قطعات معرفی میکنیم.
پروژه [Exemple-07] به شرح زیر در حال تکامل است:
![]() |
در بالا، میتوانیم کلاس [PlaceholderFragment] را ببینیم که به خارج از کلاس [MainActivity] منتقل شده است. این کلاس به صورت زیر بازنویسی شده است:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
//یک فرگمنت نمایی است که توسط یک کانتینر فرگمنت نمایش داده میشود
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// جزء رابط کاربری بصری
@ViewById(R.id.section_label)
protected TextView textViewInfo;
//شماره قطعه
private static final String ARG_SECTION_NUMBER = "section_number";
// سازنده
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// والد
super.onResume();
//نمایش
if (textViewInfo != null) {
Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
}
}
- خط ۱۵: این قطعه با حاشیهنویسی [@EFragment] نشانهگذاری شده است که پارامتر آن شناسه نمای XML مرتبط با قطعه است، که در این مورد نمای [fragment_main.xml] میباشد؛
- خطوط ۱۹–۲۰: مورد ارجاعی از مؤلفه [fragment_main.xml] را که توسط [R.id.section_label] شناسایی شده است، در فیلد [textViewInfo] درج کنید، که از نوع [TextView] است (خط (l) در زیر):
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- خطوط ۴۲–۵۲: متد [onResume] قبل از نمایش ویوی مرتبط با قطعه اجرا میشود. از آن میتوان برای بهروزرسانی رابط کاربری بصری که قرار است نمایش داده شود، استفاده کرد؛
- خط ۴۷: متد با همین نام در کلاس والد باید فراخوانی شود؛
- خط ۴۹: مشخص نیست که آیا متد [onResume] میتواند قبل از مقداردهی اولیه فیلد در خط ۲۰ اجرا شود یا خیر. لاگهایی که برای ردیابی چرخه عمر قطعه تنظیم شدهاند، به ما خواهند گفت. فعلاً و به عنوان یک اقدام احتیاطی، یک بررسی null انجام میشود؛
- خط ۵۱: ما اطلاعات موجود در فیلد [textViewInfo] را با آرگومان عددی که هنگام ایجاد قطعه به آن پاس داده شده بود، بهروزرسانی میکنیم؛
کلاس [MainActivity] کلاس داخلی خود [PlaceholderFragment] را از دست میدهد و مشاهده میکند که دستگردانگر قطعه (fragment handler) آن به شرح زیر تغییر میکند:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
//قطعات
private Fragment[] fragments;
// تعداد قطعات
private static final int FRAGMENTS_COUNT = 3;
// شماره قطعه
private static final String ARG_SECTION_NUMBER = "section_number";
// تولیدکننده
public SectionsPagerAdapter(FragmentManager fm) {
// پدر
super(fm);
// ابتداییسازی آرایهٔ قطعات
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length; i++) {
//یک قطعه ایجاد میشود
fragments[i] = new PlaceholderFragment_();
// میتوان آرگومانها را به قطعه ارسال کرد
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
}
// شماره موقعیت قطعه
@Override
public Fragment getItem(int position) {
Log.d("MainActivity", String.format("getItem[%s]", position));
return fragments[position];
}
// تعداد قطعات مدیریتشده را بازمیگرداند
@Override
public int getCount() {
return fragments.length;
}
// اختیاری – عنوانی را به قطعات مدیریتشده اختصاص میدهد
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
}
- خط ۴: قطعات در یک آرایه قرار داده میشوند؛
- خطوط 16–23: آرایهٔ قطعات در سازندهٔ کلاس مقداردهی اولیه میشود. این آرایه از نوع [PlaceholderFragment_] (خط 18) است و نه [PlaceholderFragment]. کلاس [PlaceholderFragment] در واقع با یک انوتیشن AA علامتگذاری شده و منجر به ایجاد کلاسی به نام [PlaceholderFragment_] میشود که از [PlaceholderFragment] مشتق شده است، و این کلاسی است که فعالیت باید از آن استفاده کند. به هر قطعه ایجادشده یک آرگومان عددی داده میشود که توسط آن قطعه نمایش داده خواهد شد؛
- خطوط ۴۲–۴۵: ما عناوین قطعات را تغییر دادهایم. از آنجایی که اینها عناوین تبها نیز هستند، باید تغییری در نوار تبها مشاهده کنیم؛
بیایید [Make] و [1] را برای این پروژه کامپایل کنیم:
![]() | ![]() |
- به [2]، میتوانیم ببینیم که کلاسهای تولید شده توسط کتابخانه AA در پوشه [app / build / generated / source / apt / debug] قرار دارند (برای مشاهده [2] باید در نمای [Project] باشید)؛
پروژه [Exemple-07] را اجرا کنید و بررسی کنید که هنوز کار میکند.
1.8.5. بررسی لاگها
هنگامی که برنامه اجرا میشود، لاگها به شرح زیر هستند:
- خط ۱: ساخت فعالیت واحد؛
- خط ۲: متد [afterViews] فعالیت: فیلدهای آن که توسط [@ViewById] حاشیهنویسی شدهاند، مقداردهی اولیه شدهاند؛
- خطوط ۳–۵: ساخت سه قطعه؛
- خطوط ۶–۷: کانتینر قطعه [ViewPager] دو قطعه اول را درخواست میکند؛
- خطوط ۸–۹: متدهای قطعه ۲؛
- خطوط ۱۰–۱۱: متدهای قطعه ۱؛
- خطوط ۱۲–۱۳: متد [onResume] از قطعه ۱؛
- خطوط ۱۴–۱۵: متد [onResume] از قطعه ۲؛
- خط ۱۶: ایجاد منوی فعالیت؛
شایان ذکر است که این موضوع پاسخ سؤال مطرحشده در بالا را فراهم میکند: برای مثال، متد [onResume] از قطعه 1 (خط 12)، پس از متد [afterViews] از همان قطعه (خط 11) اجرا میشود. بنابراین، هنگامی که متد [onResume] اجرا میشود، میتواند از فیلدهایی که توسط [@ViewById] علامتگذاری شدهاند استفاده کند. بنابراین اکنون میتوانیم متد [onResume] را به صورت زیر بنویسیم:
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// پدر
super.onResume();
// نمایش
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
حالا بیایید از برگهٔ ۱ به برگهٔ ۲ برویم. لاگهای جدید به شرح زیر است:
- خط ۱: کانتینر قطعه [ViewPager] قطعه شماره ۳ را درخواست میکند؛
- خطوط ۲–۳: متدهای قطعه شماره ۳. توجه داشته باشید که این قطعه هنگام شروع برنامه ایجاد شده بود؛
- خطوط ۴–۵: متد [onResume] از قطعه شماره ۳ اجرا میشود. توجه داشته باشید که این قطعه شماره ۲ است که نمایش داده میشود؛
اکنون از برگه ۲ به برگه ۳ برویم. هیچ لاگی ثبت نمیشود. بنابراین، هیچیک از متدهای [onCreateView, afterViews, onResume] در قطعه شماره ۳ اجرا نمیشوند. این متن [Hello World from section:3] را تنها به این دلیل به درستی نمایش میدهد که این متن قبلاً در مرحله قبلی هنگام نمایش قطعه شماره ۲ ایجاد شده بود. یادآوری میکنیم که در آن مرحله، متد [onResume] از قطعه شماره ۳ اجرا شده بود. در اینجا میبینیم که، درست مانند متد [onCreateView]، متد [onResume] نمیتواند برای بهروزرسانی قطعه ۳ استفاده شود. اگر لازم بود متن نمایشدادهشده توسط قطعه را تغییر دهیم، هیچکدام از این دو متد قادر به انجام این کار نبودند.
اکنون، بیایید از برگه شماره ۳ به برگه شماره ۱ بازگردیم. لاگها در این صورت به شرح زیر است:
میتوانیم ببینیم که تمام متدهای موجود در قطعه ۱ اجرا شدهاند. میتوانیم ببینیم که متد getItem فراخوانی نشده است. همانطور که ذکر شد، این متد فقط یک بار برای هر قطعه فراخوانی میشود؛
اکنون، بیایید از برگه ۱ به برگه ۲ مجاور سوئیچ کنیم. لاگهای زیر را داریم:
شگفتآور است، اینطور نیست؟ تمام متدهای قطعه شماره ۳ دوباره اجرا میشوند.
برای درک این پدیدهها، باید به خاطر داشته باشیم که به طور پیشفرض، هنگامی که کانتینر شبهه در آستانه نمایش شبهه i است، شبههای i-1، i و i+1 را نیز اولیه میکند. بیایید با در نظر گرفتن این اطلاعات، دوباره به لاگها نگاه کنیم.
ابتدا، لاگها هنگام راهاندازی برنامه:
از آنجا که کانتینر فرگمنت فرگمنت ۱ را نمایش میدهد، فرگمنتهای ۱ و ۲ مقداردهی اولیه میشوند (خطوط ۸–۱۵).
اکنون از برگهٔ ۱ به برگهٔ ۲ سوئیچ میکنیم:
از آنجایی که کانتینر فرگمنت قرار است فرگمنت ۲ را نمایش دهد، فرگمنتهای ۱، ۲ و ۳ باید مقداردهی اولیه شوند. فرگمنتهای ۱ و ۲ در مرحله قبلی قبلاً مقداردهی اولیه شدهاند. فرگمنت ۳ در خطوط ۲ تا ۵ مقداردهی اولیه میشود.
از برگه ۲ به برگه ۳ میرویم. هیچ لاگی وجود ندارد. از آنجا که کانتینر شبهه قرار است شبهه ۳ را نمایش دهد، باید شبهههای ۲ و ۳ مقداردهی اولیه شوند. با این حال، از مرحله قبلی، آنها قبلاً مقداردهی شدهاند. چیزی که در اینجا نمیتوانیم ببینیم این است که قطعه ۱، که مجاور قطعه ۳ نیست، وضعیت خود را از دست میدهد و این وضعیت در حافظه حفظ نمیشود.
ما از برگه ۳ به برگه ۱ میرویم. لاگها به شرح زیر است:
از آنجا که کانتینر فرگمنت قرار است فرگمنت ۱ را نمایش دهد، فرگمنت ۲ نیز باید مقداردهی اولیه شود. این مقداردهی از مرحله قبل انجام شده است. در همین مرحله، وضعیت قطعه ۱ از دست رفته بود. بنابراین در خطوط ۱ تا ۴ مجدداً تنظیم میشود. آنچه در اینجا نشان داده نشده این است که قطعه ۳، که مجاور قطعه ۱ نیست، وضعیت خود را از دست میدهد و سپس این وضعیت در حافظه حفظ نمیشود.
هنگام سوئیچ کردن از تب ۱ به تب ۲ مجاور، لاگهای زیر تولید میشوند:
از آنجا که کانتینر قطعه قرار است قطعه ۲ را نمایش دهد، قطعات ۱، ۲ و ۳ باید مقداردهی اولیه شوند. قطعات ۱ و ۲ در مرحله قبلی قبلاً مقداردهی اولیه شدهاند. قطعه ۳ به خطوط ۱–۴ مقداردهی اولیه میشود.
چه چیزهایی یاد گرفتیم؟
- که مدیریت پیشفرض قطعات بسیار خاص است و اگر نمیخواهید خودتان را دیوانه کنید، باید آن را درک کنید. ما میتوانیم این حالت مدیریت را تغییر دهیم، و کمی بعد این کار را انجام خواهیم داد؛
- که با این روش پیشفرض، هیچیک از متدهای [onCreateView, onResume] را نمیتوان برای بهروزرسانی قطعهای که قرار است نمایش داده شود، استفاده کرد، زیرا هیچ تضمینی وجود ندارد که آنها اجرا شوند؛
1.8.6. onDestroyView
متد [onDestroyView] بخشی از چرخه عمر قطعه است (به بخش 1.7.6 مراجعه کنید):
![]() | ![]() |
مشاهده میشود که در چرخهٔ عمر یک قطعه:
- روش [onCreateView] ممکن است چندین بار اجرا شود؛
- قبل از بازگشت به متد [onCreateView] در مرحله بعدی، متد [onDestroyView] [2] باید حتماً فراخوانی شود؛
ما این متدها را در قطعات درج میکنیم تا چرخهٔ زندگی آنها را بهتر ردیابی کنیم. کد قطعه به صورت زیر درمیآید:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
//یک قطعه نمایی است که توسط یک محفظهٔ قطعه نمایش داده میشود
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
...
@Override
public void onDestroyView() {
// لاگ
Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// پدر
super.onDestroyView();
}
}
بیایید برنامه را اجرا کنیم. اولین لاگها به شرح زیر است:
- خط ۱: ساخت فعالیت واحد؛
- خط ۲: متد [afterViews] فعالیت: فیلدهای آن که توسط [@ViewById] حاشیهنویسی شدهاند، مقداردهی اولیه میشوند؛
- خطوط ۳–۵: ساخت سه قطعه؛
- خطوط ۶–۷: کانتینر قطعه [ViewPager] دو قطعه اول را درخواست میکند؛
- خطوط ۸–۹: نمای قطعه ۲ ایجاد میشود (ضروری نیست که نمایش داده شود)؛
- خطوط ۱۰–۱۱: نمای قطعه ۱ ایجاد میشود (نecessarily لزوماً نمایش داده نمیشود)؛
- خطوط ۱۲–۱۳: متد [onResume] از قطعه ۱؛
- خطوط ۱۴–۱۵: متد [onResume] برای قطعه ۲؛
- خط ۱۶: ایجاد منوی فعالیت؛
بیایید از برگهٔ ۱ به برگهٔ ۳ سوئیچ کنیم:
06-03 02:50:02.685 2346-2346/exemples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 1
- خط ۱: کانتینر قطعه، قطعه سوم را درخواست میکند؛
- خطوط ۲–۳: نمای قطعه ۳ ایجاد میشود (ضروری نیست که نمایش داده شود)؛
- خطوط ۴–۵: متد [onResume] از قطعه ۳ اجرا میشود؛
- خط ۶: متد [onDestroyView] از فرگمنت ۱ اجرا میشود. این بدان معناست که وقتی کاربر به فرگمنت ۱ یا یک فرگمنت مجاور بازمیگردد، چرخهٔ زندگی آن فرگمنت مجدداً اجرا خواهد شد؛
بازگشت از برگه ۳ به برگه ۱:
06-03 02:53:46.255 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 3
- خطوط ۱–۴: چرخه عمر قطعه ۱ دوباره اجرا میشود زیرا یک [onDestroyView] را پشت سر گذاشته بود؛
- خط ۵: متد [onDestroyView] قطعه ۳ اکنون اجرا میشود. مجدداً، هنگامی که کاربر به قطعه ۳ یا به یک قطعه مجاور بازمیگردد، چرخه زندگی آن قطعه مجدداً اجرا خواهد شد؛
1.8.7. setUserVisibleHint
متد [onCreateView] چرخهٔ زندگی، نمای مرتبط با قطعه را ایجاد میکند اما لزوماً آن را قابل مشاهده نمیسازد. این چیزی است که اکنون به آن خواهیم پرداخت. متد [Fragment.setUserVisibleHint] هرگاه دیدهشدن قطعه تغییر کند اجرا میشود. ما این متد را به کد قطعه اضافه میکنیم:
package exemples.android;
....
//یک فرگمنت نمایی است که توسط یک کانتینر فرگمنت نمایش داده میشود
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// جزء رابط کاربری بصری
@ViewById(R.id.section_label)
protected TextView textViewInfo;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// لاگ
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
}
}
در زمان راهاندازی، لاگها به شرح زیر است:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- ورودیهای لاگ در خطوط ۷، ۹–۱۰ نشان میدهند که تنها قطعه ۱ قابل مشاهده میشود. همچنین میتوانیم ببینیم که این قطعه قبل از اجرای متد آن [onCreateView] قابل مشاهده میشود؛
بیایید از برگهٔ ۱ به برگهٔ ۲ سوئیچ کنیم:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
- قطعه ۱ پنهان است (خط ۳)، قطعه ۲ نمایش داده میشود (خط ۴)؛
بیایید از برگه ۲ به برگه ۳ برویم:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
- قطعه ۲ پنهان است (خط ۱)، قطعه ۳ نمایش داده شده است (خط ۲)؛
بیایید به برگهٔ ۱ بازگردیم:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
- قطعه ۳ پنهان است (خط ۲)، قطعه ۱ نمایش داده شده است (خط ۳)؛
چه چیزهایی یاد گرفتیم؟
- متد [setUserVisibleHint] یکبار با مقداردهی ویژگی [isVisibleToUser] به true برای قطعهای که قرار است نمایش داده شود، اجرا میشود؛
- معلوم نیست که این متد در چه زمانی از چرخه عمر قطعه اجرا خواهد شد. بنابراین، برای قطعه ۱، متد [setUserVisibleHint, true] در ابتدای چرخه عمر آن قطعه، قبل از متد [onCreateView] اجرا شد، در حالی که برای قطعات ۲ و ۳، عکس این موضوع رخ داد؛
1.8.8. setOffscreenPageLimit
لاگهای قبلی نشان میدهند که وقتی کانتینر قطعه [ViewPager] در آستانه نمایش قطعه شماره i است، در صورتی که قبلاً این کار را انجام نداده باشد، چرخه عمر قطعات مجاور i-1 و i+1 را اجرا میکند. این رفتار را میتوان با استفاده از متد [ViewPager].setOffscreenPageLimit کنترل کرد:
با دستور فوق،
- هنگامی که کانتینر قطعه [ViewPager] در آستانه نمایش قطعه شماره i است، در صورتی که قبلاً این کار را انجام نداده باشد، چرخه عمر قطعات مجاور در بازه [i-n, i+n] را اجرا میکند؛
- اگر در آن زمان قطعه j نمایش داده شود:
- همان فرآیند برای قطعات مجاور در بازه [j-n, j+n] نیز رخ میدهد؛
- قطعاتی که در مرحله ۱ مقداردهی اولیه شدهاند و دیگر در مجاورت [j-n, j+n] قطعه جدید قرار ندارند، ممکن است سپس تحت یک عملیات [onDestroyView] قرار گیرند. با این حال، من در سایر برنامهها، بهویژه در مورد فصل ۳، مشاهده کردهام که این همیشه صادق نبوده است؛
ما روش [MainActivity.afterViews] را به شرح زیر اصلاح میکنیم:
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
//نوار ابزار
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// مدیر قطعه
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// محتویات قطعه با مدیر قطعات مرتبط است
//یعنی قطعه شماره i در محفظهٔ قطعات همان قطعه شماره i است که توسط مدیر قطعات ارائه شده است
mViewPager.setAdapter(mSectionsPagerAdapter);
// کشیدن بین قطعات غیرفعال است
mViewPager.setSwipeEnabled(false);
// جابهجایی قطعه
mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
//نوار برگهها نیز با مخزن قطعات مرتبط است
//یعنی زبانه شماره i، قطعه شماره i را از کانتینر نمایش میدهد
tabLayout.setupWithViewPager(mViewPager);
// دکمه شناور
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- خط ۲۰: ما تعداد قطعات مجاوری را که باید اولیه شوند را روی مجموع تعداد قطعات منهای یک تنظیم میکنیم. بنابراین، در هنگام راهاندازی، هنگامی که محفظه قطعه، قطعه شماره ۱ را نمایش میدهد، همزمان قطعات ۲، ۳، …، n را نیز اولیه خواهد کرد، که در آن n = 1 + mSectionsPagerAdapter.getCount() – 1 = mSectionsPagerAdapter.getCount(). بنابراین اینها تمام قطعاتی هستند که اولیه میشوند. وقتی پنجره نمایش به قطعه دیگری منتقل میشود، مخزن قطعه:
- تشخیص خواهد داد که تمام قطعات مجاور با قطعه جدید از قبل инициалиزه شدهاند و بنابراین آنها را инициалиزه نخواهد کرد؛
- از آنجایی که مجاورت قطعه جدید تمام قطعات را نیز در بر میگیرد، هیچکدام توسط محفظه قطعات «پاکسازی» نخواهند شد؛
در مجموع، باید شاهد instantiate و initialize شدن تمام قطعات هنگام راهاندازی برنامه باشیم و پس از آن دیگر هرگز. این چیزی است که اکنون با بررسی لاگها در حال تأیید آن هستیم.
در زمان راهاندازی، لاگهای زیر را داریم:
- خطوط ۴–۶: ساخت سه قطعه؛
- خطوط ۷، ۹، ۱۱: کانتینر قطعه سه قطعه را ادعا میکند. در نسخه قبلی، آن دو را ادعا میکرد؛
- خطوط 14–25: چرخه عمر سه قطعه اجرا میشود؛
حال از برگه ۱ به برگه ۲ سوئیچ کنیم:
بیایید از برگه ۲ به برگه ۳ سوئیچ کنیم:
سپس از برگهٔ ۳ به برگهٔ ۱:
لاگها این نظریه را تأیید میکنند. تمام قطعات در زمان راهاندازی (start-up) نمونهسازی و اولیه شدهاند. پس از آن، متدهای چرخه عمر آنها دیگر اجرا نمیشوند. این امر رفتار قطعات را بسیار قابل پیشبینی میکند که استفاده از آنها را به شدت ساده میسازد.
آنچه میخواهیم بیابیم، روشی برای بهروزرسانی یک قطعه است که قرار است نمایش داده شود، صرفنظر از همجواری قطعهای که توسعهدهنده انتخاب کرده است. لاگها دو چیز را به ما نشان دادهاند:
- متد [setUserVisibleHint, true] همیشه برای قطعهای که قرار است نمایش داده شود اجرا میشود، اما برای بقیه اجرا نمیشود؛
- این رویداد ممکن است هم قبل و هم بعد از چرخه عمر فرگمنت رخ دهد. این بستگی به همجواری فرگمنتی دارد که توسعهدهنده انتخاب کرده است. این یک مشکل است، زیرا اگر چرخه عمر هنوز رخ نداده باشد، به این معنی است که فرگمنت را نمیتوان با متد [setUserVisibleHint, true] بهروزرسانی کرد؛
لاگها در زمان راهاندازی برنامه، زمانی که همجواری قطعه روی 1 تنظیم شده بود، به شرح زیر بودند:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- میتوانیم ببینیم که وقتی فرگمنت 1 قابل مشاهده میشود، نمای آن هنوز ایجاد نشده است. بنابراین نمیتوان آن را تغییر داد. این کار سپس میتواند در طول چرخه عمر فرگمنت انجام شود، برای مثال در متدهای [onCreateView] (خط 11) یا [onResume] (خطوط 13–14). از آنجایی که از آنوتیشنهای AA استفاده میکنیم، معمولاً نیازی به نوشتن متد [onCreateView] نداریم. بنابراین، متد [onResume] به نظر میرسد مناسبترین گزینه برای بهروزرسانی قطعه ۱ در اینجا باشد؛
وقتی از برگهٔ ۱ به برگهٔ ۲ سوئیچ کردیم، لاگها به شرح زیر بودند:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
این بار، فقط متد [setUserVisibleHint, true] در خط ۴ برای بهروزرسانی قطعه ۲ استفاده شد؛
وقتی از برگه ۲ به برگه ۳ سوئیچ کردیم، لاگها به شرح زیر بودند:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
در اینجا، ما فقط متد [setUserVisibleHint, true] را در خط ۲ برای بهروزرسانی قطعه ۳ داریم؛
وقتی از برگه ۳ به برگه ۱ سوئیچ کردیم، لاگها به شرح زیر بودند:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
در اینجا، برای بهروزرسانی قطعه ۱، باید از متد [onResume] در قطعه ۱ (خطوط ۶–۷) استفاده کنیم.
بنابراین، در این مثال، میبینیم که برای بهروزرسانی یک قطعه که قرار است نمایش داده شود، دو متد در دسترس داریم: [setUserVisibleHint] و [onResume].
ما قصد داریم این راهحل را در یک پروژه جدید پیادهسازی کنیم که در آن هر فرگمنت باید تعداد دفعات نمایش خود را که آن را «بازدید» مینامیم، نمایش دهد. بنابراین، هر بار که فرگمنت نمایش داده میشود، باید نمایش آن را بهروزرسانی کنیم. این دقیقاً مشکلی است که در پی حل آن هستیم.
قبل از آن، بیایید مرحله نهایی در چرخه عمر یک فعالیت یا فرگمنت را بررسی کنیم: مرحلهای که در آن نابود میشود. سیستم ممکن است در صورتی که فعالیتهای دیگر با اولویت بالاتر به منابعی که در دسترس نیستند نیاز داشته باشند، ابتکار عمل برای حذف یک فعالیت را به دست بگیرد. برای آزادسازی این منابع، سیستم ابتکار عمل برای حذف فعالیتهای خاصی را به دست میگیرد. سپس متد [onDestroy] برای فعالیت و قطعات فراخوانی خواهد شد.
1.8.9. OnDestroy
![]() | ![]() | ![]() |
ما به کاربر اجازه میدهیم تا با استفاده از گزینه منوی [5] فعالیت را حذف کند. برای این کار، یک گزینه منوی جدید را در فایل [menu_main.xml] [1] اضافه میکنیم:
<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/action_terminate"
android:title="@string/action_terminate"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
ما به سادگی اولین گزینه منو را کپی و پیست کرده و نتیجه را (خطوط ۹ و ۱۰) تطبیق میدهیم. برچسب این گزینه جدید به فایل [strings.xml] [2] اضافه میشود:
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="action_terminate">Terminate</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
در نهایت، در کلاس [MainActivity]، ما کلیک روی گزینه [Terminate] را مدیریت میکنیم:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// در اینجا کلیکهای آیتم نوار اقدام را مدیریت میکند. نوار اقدام خواهد
// به طور خودکار کلیکهای دکمهٔ Home/Up را مدیریت میکند، مشروط بر اینکه
//: شما یک فعالیت والد را در AndroidManifest.xml مشخص میکنید.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
if (id == R.id.action_terminate) {
Log.d("menu", "action_terminate selected");
//وقتی که
finish();
return true;
}
//والد
return super.onOptionsItemSelected(item);
}
- خطوط ۱۴–۱۹: خطوط ۱۰–۱۳ را کپی و پیست کرده و کد را برای گزینه جدید تطبیق دهید؛
- خط ۱۷: فعالیت توسط یک اقدام نرمافزاری خاتمه مییابد؛
حالا این نسخهٔ جدید را اجرا کنیم و به محض نمایش اولین نما، روی گزینهٔ منوی [Terminate] کلیک کنیم. لاگها به شرح زیر است:
- خطوط ۱–۲: روی گزینه [Terminate] کلیک کنید؛
- خط ۴: متد [onDestroy] از فعالیت فراخوانی میشود؛
- خطوط ۴–۵: متد [onDestroyView] از قطعه ۱ فراخوانی میشود، و به دنبال آن متد [onDestroy] آن فراخوانی میگردد؛
- خطوط ۶–۹: این عملیات برای دو قطعه دیگر تکرار میشود؛
بنابراین باید توجه داشت که متد [onDestroy] فعالیت و قطعات آن زمانی فراخوانی میشود که فعالیت قرار است توسط سیستم، توسعهدهنده یا کاربر حذف شود. این متد میتواند برای ذخیره اطلاعات، برای مثال بهصورت محلی روی تبلت، استفاده شود تا هنگام راهاندازی مجدد برنامه توسط کاربر قابل بازیابی باشد.
1.9. مثال-۰۸: بهروزرسانی یک قطعه با مجاورت متغیر قطعات
1.9.1. ایجاد پروژه
پروژه [Exemple-07] را به عنوان [Exemple-08] کپی کنید. برای این کار، مراحل توصیفشده برای کپی کردن [Exemple-02] به عنوان [Exemple-03] را در بخش 1.4 دنبال کنید.
![]() | ![]() |
1.9.2. بازنویسی قطعه [PlaceholderFragment]
کد جدید برای قطعه [PlaceholderFragment] به شرح زیر است. این کد صرفنظر از همجواری اختصاصیافته به قطعات (۱، جزئی، کامل) کار میکند:
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
//یک قطعه نمایی است که توسط یک مخزن قطعه نمایش داده میشود
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// جزء رابط کاربری بصری
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// دادهها
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
// شماره قطعه
private static final String ARG_SECTION_NUMBER = "section_number";
// سازنده
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
// حافظه
afterViewsDone = true;
// لاگ
Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
if (!initDone) {
// متن اولیه
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// ابتداییسازی کامل
initDone = true;
}
// نمایش متن فعلی
textViewInfo.setText(text);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
@Override
public void onDestroyView() {
...
}
@Override
public void onResume() {
...
}
// بهروزرسانی قطعه
public void update() {
// کار انجامدادنی به شماره بازدید بستگی دارد
if (numVisit > 1) {
// لاگ
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// متن اصلاح شد
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// اطلاعات محلی برای لاگها
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
}
}
- خطوط ۳۴–۴۸: متد [@AfterViews] ممکن است چندین بار اجرا شود. ما قبلاً از آن برای مقداردهی اولیه متن قطعه (خط ۴۲) استفاده میکردیم. هنوز این کار را انجام میدهیم، اما برای اطمینان از اینکه فقط یک بار انجام شود، یک متغیر بولین [initDone] (خط ۴۴) را مدیریت میکنیم تا نشان دهیم که مقداردهی اولیه انجام شده و نیازی به تکرار ندارد؛
- خطوط ۵۶–۵۹: ما متد [onDestroyView] را معرفی میکنیم تا نشان دهیم که در دفعه بعد که قطعه مجدداً نمایش داده میشود، چرخه عمر آن مجدداً اجرا خواهد شد؛
- لاگها نشان دادهاند که ممکن است دو متد پس از متد [@AfterViews] اجرا شوند: متدهای [setUserVisibleHint] و [onResume]. متد [onResume] تنها زمانی اجرا میشود که چرخهٔ زندگی قطعه اجرا شود. اما متد [setUserVisibleHint] همیشه پس از متد [@AfterViews] اجرا نمیشود. لاگها نشان دادهاند که حداقل یکی از این دو پس از متد [@AfterViews] اجرا میشود. لاگها هرگز نشان ندادهاند که هر دو بتوانند پس از متد [@AfterViews] با هم اجرا شوند. یا یکی اجرا میشود یا دیگری. به عنوان یک اقدام احتیاطی، یک متغیر بولین [updateDone] هنگام انجام یک بهروزرسانی تنظیم خواهد شد؛
روشهای [setUserVisibleHint] و [onResume] به شرح زیر هستند:
// دادهها
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// والد
super.setUserVisibleHint(isVisibleToUser);
// حافظه
this.isVisibleToUser = isVisibleToUser;
// لاگ
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// تعداد بازدیدها
if (isVisibleToUser) {
// افزایش
numVisit++;
// بهروزرسانی قطعه
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// قطعه باید پنهان شود
updateDone = false;
}
}
@Override
public void onResume() {
// والد
super.onResume();
// لاگ
Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// بهروزرسانی
if (isVisibleToUser && !updateDone) {
update();
updateDone = true;
}
}
- خط ۱۴: وضعیت قابل مشاهده یا مخفی بودن قطعه ذخیره میشود؛
- خطوط 22–25: اگر قطعه قابل مشاهده باشد و متد [@AfterViews] اجرا شده باشد، متد [update] اجرا میشود و بولین [updateDone] به true ارسال میگردد؛
- خطوط ۲۶–۲۸: اگر قطعه قرار است پنهان شود، متغیر بولی [updateDone] به false بازنشانی میشود. در واقع ما به یک رویداد نیاز داریم تا بولی [updateDone] – که روی true تنظیم شده بود – را به محض فراخوانی متد [update]، دوباره روی false تنظیم کنیم تا بهروزرسانیهای جدید بتوانند انجام شوند. ما برای این کار از این واقعیت استفاده میکنیم که قطعه دیگر قابل مشاهده نیست. هنگامی که دوباره قابل مشاهده میشود، قطعه باید یک بار دیگر بهروزرسانی شود؛
- خطوط ۳۲–۴۲: لاگها نشان میدهند که بسته به همجواری انتخابشده برای قطعات، متد [onResume] ممکن است در حالی که قطعه قابل مشاهده نیست، اجرا شود. اگر قابل مشاهده نباشد، بهروزرسانی انجام نمیشود (خط ۳۹) و، همانطور که برای [setMenuVisibility] انجام دادیم، متغیر بولی [updateDone] را مدیریت میکنیم.
در نهایت، متد [onDestroyView] به شرح زیر است:
@Override
public void onDestroyView() {
// والد
super.onDestroyView();
// پرچم بهروزرسانی
afterViewsDone = false;
// لاگ
Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}
متد [onDestroyView] زمانی که چرخه عمر یک قطعه پایان مییابد، اجرا میشود. چرخه عمر دیگری ممکن است در مرحله بعدی از سر گرفته شود.
- خط ۶: متد [onDestroyView] هرگونه لینک به ویوی متصل به قطعه را حذف میکند. این لینک در چرخهٔ بعدی زندگی قطعه مجدداً ایجاد خواهد شد. فعلاً، باید مقدار بولین [afterViews] را روی false تنظیم کنیم تا نشان دهیم که لینک به ویو دیگر وجود ندارد؛
ما برنامه را با ۵ قطعه که مجاورت ۲ دارند اجرا خواهیم کرد. تغییرات در [MainActivity] انجام شده است:
// تعداد قطعات
private final int FRAGMENTS_COUNT = 5;
// همجواری قطعات
private final int OFF_SCREEN_PAGE_LIMIT=2;
// مدیر قطعه
private SectionsPagerAdapter mSectionsPagerAdapter;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
....
// جابجایی قطعه
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
...
}
لاگهای راهاندازی به شرح زیر است:
05-31 06:23:07.015 32551-32551/exemples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/exemples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/exemples.android D/menu: création menu en cours
- خطوط ۸، ۱۰، ۱۲: کانتینر فرگمنت تمام فرگمنتهای مجاور فرگمنت ۱ را درخواست میکند؛
- خطوط ۹، ۱۱، ۱۳: متد [setUserVisibleHint] برای این قطعات با [visibleToUser] تا false اجرا میشود؛
- خط ۱۴: متد [setUserVisibleHint] از قطعه ۱ با [visibleToUser] تا true اجرا میشود؛
- خطوط ۱۵–۱۷: متد [afterViews] برای سه قطعه مجاور فراخوانی میشود. بنابراین در اینجا میتوانیم موردی را ببینیم که این متد پس از قابل مشاهده شدن یک قطعه (قطعه ۱، خط ۱۴) فراخوانی میشود؛
- خطوط ۱۸–۲۰: متد [onResume] برای سه بخش مجاور فراخوانی میشود؛
ما از برگهٔ ۱ به برگهٔ ۲ سوئیچ میکنیم:
05-31 06:52:36.132 32551-32551/exemples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- از آنجایی که مجاورت قطعات یک پوزیشن به سمت راست جابجا میشود، قطعه ۴ توسط ظرف قطعه ادعا میشود؛
- خط ۲: متد [setUserVisibleHint] از قطعه ۴ با [visibleToUser] به false فراخوانی میشود؛
- خط ۳: متد [setUserVisibleHint] از قطعه ۱ با [visibleToUser] به false فراخوانی میشود. این به این دلیل است که قطعه ۱ اکنون پنهان است؛
- خط ۴: متد [setUserVisibleHint] از قطعه ۲ با [visibleToUser] تا true فراخوانی میشود. قطعه ۲ اکنون قابل مشاهده است؛
- خطوط ۵–۶: چرخهٔ عمر قطعهٔ ۴ ادامه مییابد؛
ما از برگهٔ ۲ به برگهٔ ۳ میرویم:
05-31 06:58:16.228 32551-32551/exemples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- از آنجا که مجاورت قطعه یک موقعیت به سمت راست جابجا شده است، قطعه ۵ توسط ظرف قطعه درخواست میشود؛
- خط ۲: متد [setUserVisibleHint] از قطعه ۵ با [visibleToUser] تا false فراخوانی میشود؛
- خط ۳: متد [setUserVisibleHint] از قطعه ۲ با [visibleToUser] تا false فراخوانی میشود. این به این دلیل است که قطعه ۲ اکنون مخفی است؛
- خط ۴: متد [setUserVisibleHint] در قطعه ۳ با [visibleToUser] تا true فراخوانی میشود. قطعه ۳ اکنون قابل مشاهده است؛
- خطوط ۵–۶: چرخهٔ عمر قطعهٔ ۵ ادامه دارد؛
ما از برگه ۳ به برگه ۴ میرویم:
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- خط ۱: قطعه ۳ اکنون مخفی است؛
- خط ۲: قطعه ۴ اکنون قابل مشاهده است. توجه داشته باشید که چرخهٔ زندگی قطعهٔ ۴ اجرا نمیشود. این کار قبلاً دو مرحله پیش انجام شده است؛
- خط ۳: قطعه ۱ از مجاورت قطعه ۴ نمایشدادهشده خارج میشود. متد آن [onDestroyView] اجرا میشود. دفعه بعد که نمایش داده شود، چرخه نمایش آن [onCreateView, afterViews, onResume] مجدداً اجرا خواهد شد؛
ما از برگه ۴ به برگه ۵ میرویم:
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- خط ۱: قطعه ۴ اکنون پنهان است؛
- خط ۲: قطعه ۵ اکنون قابل مشاهده است. توجه داشته باشید که چرخه عمر قطعه ۵ اجرا نمیشود. این کار قبلاً دو مرحله پیش انجام شده است؛
- خط ۳: قطعه ۲ از مجاورت قطعه نمایشدادهشده ۵ خارج میشود. متد آن [onDestroyView] اجرا میشود؛
ما از برگه ۵ به برگه ۱ سوئیچ میکنیم:
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- خطوط ۱، ۴، ۵، ۶: چرخه عمر قطعه ۱ مجدداً اجرا میشود. این به این دلیل است که ارتباط آن با نمای خود را از دست داده بود؛
- خطوط ۲، ۵، ۸، ۹: به همین دلیل، چرخهٔ عمر قطعهٔ ۲ مجدداً اجرا میشود؛
- خطوط ۱۰–۱۱: قطعات ۴ و ۵ از مجاورت قطعه نمایشدادهشده حذف میشوند؛
- خط ۷: قطعه ۱ بهروزرسانی میشود؛
![]() |
لاگها هرگز نشان ندادهاند که متدهای [setUserVisibleHint] و [onResume] هر دو برای بهروزرسانی قطعه تلاش کرده باشند. این یا یکی است یا دیگری. از خوانندگان دعوت میشود تا آزمایشهای بیشتری انجام دهند و لاگها را برای درک کامل مفاهیم مجاورت و چرخه عمر قطعات نظارت کنند.
اکنون، بیایید فرض کنیم که مجاورت کامل برقرار است و همان آزمایشها را انجام دهیم.
در [MainActivity]:
// تعداد قطعات
private final int FRAGMENTS_COUNT = 5;
// همجواری قطعات
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;
لاگهای مربوط به شروع به کار به شرح زیر است:
05-31 07:34:44.717 28908-28908/exemples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/exemples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/exemples.android D/menu: création menu en cours
- لاگها نشان میدهند که چرخهٔ عمر پنج قطعه اجرا شده است؛
- قطعهٔ ۱ در خط ۱۸ نمایش داده میشود؛
ما از برگه ۱ به برگه ۲ میرویم:
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- خط ۱: قطعه ۱ مخفی است؛
- خط ۲: قطعه ۲ نمایش داده میشود؛
ما از برگه ۲ به برگه ۳ میرویم:
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- خط ۱: قطعه ۲ پنهان است؛
- خط ۲: قطعه ۳ نمایش داده میشود؛
تغییر از برگه ۳ به برگه ۴:
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- خط ۱: قطعه ۳ پنهان است؛
- خط ۲: قطعه ۴ نمایش داده میشود؛
تغییر از برگهٔ ۴ به برگهٔ ۵:
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- خط ۱: قطعه ۴ پنهان است؛
- خط ۲: قطعهٔ ۵ نمایش داده میشود؛
تغییر از برگهٔ ۵ به برگهٔ ۱:
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- خط ۱: قطعه ۵ پنهان است؛
- خط ۲: قطعه ۱ نمایش داده میشود؛
- خط ۳: قطعه ۱ بهروزرسانی میشود؛
ما از برگهٔ ۱ به برگهٔ ۴ میرویم:
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: update 4 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- خط ۱: قطعه ۱ پنهان است؛
- خط ۲: قطعه ۴ نمایش داده میشود؛
- خط ۳: قطعه ۴ بهروزرسانی میشود؛
میتوانیم ببینیم که با مجاورت کامل، رفتار قطعات بسیار قابل پیشبینیتر است.
اکنون بیایید همجواری را روی صفر تنظیم کنیم و ببینیم چه اتفاقی میافتد. کلاس [MainActivity] به شرح زیر تغییر میکند:
// تعداد قطعات
private final int FRAGMENTS_COUNT = 5;
// همجواری قطعات
private final int OFF_SCREEN_PAGE_LIMIT = 0;
لاگهای راهاندازی به شرح زیر است:
06-01 03:11:52.068 5679-5679/exemples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/exemples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/exemples.android D/menu: création menu en cours
- در خطوط ۸ و ۱۰ میبینیم که محفظهٔ قطعه دو قطعه با شمارههای ۱ و ۲ را درخواست کرده است. بنابراین همه چیز طوری رفتار میکند که گویی همبستگی ۱ وجود دارد. در نتیجه، همبستگی ۰ نادیده گرفته شده است.
1.9.3. ارتباط بین قطعات
در معماری قبلی، ما یک فعالیت و n تکهمان داریم. کاربر با تکههای مختلف تعامل میکند. این تعاملات وضعیت برنامه را تغییر میدهند. در اینجا، وضعیت برنامه به مجموعهای از اطلاعاتی گفته میشود که در طول عمر خود ذخیره میکند. سپس مشکل زیر پیش میآید:
- هنگامی که کاربر با قطعه i تعامل میکند، برنامه از حالت E1 به حالت E2 تغییر حالت میدهد؛
- یک اقدام کاربر روی قطعه i باعث نمایش قطعه j میشود؛
- چگونه میتوان قطعه j را با وضعیت فعلی برنامه E2 بهروزرسانی کرد؟
از مثالهای قبلی میدانیم چگونه قطعه j را بهروزرسانی کنیم. اما برای بهروزرسانی آن، وضعیت برنامه E2 را از کجا میتوانیم پیدا کنیم؟
راهحلهای مختلفی برای این مشکل وجود دارد. ما یکی از آنها را دیدهایم: قطعه i میتواند وضعیت برنامه E2 را از طریق آرگومانها به قطعه j منتقل کند. ما این روش را در کلاس [MainActivity] هنگام ایجاد قطعات دیدیم:
for (int i = 0; i < fragments.length; i++) {
// یک قطعه ایجاد میشود
fragments[i] = new PlaceholderFragment_();
// میتوان آرگومانها را به قطعه ارسال کرد
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
این راهحل در اینجا به طور مستقیم قابل اجرا نیست. دلیل آن این است که وقتی کاربر روی برگه j کلیک میکند—که باعث ظاهر شدن قطعه j میشود—کد ما فراخوانی نمیشود. فقط کد سیستم اجرا میشود. در یک پروژه آینده خواهیم دید که چگونه یک کلیک روی برگه را رهگیری کنیم، اما در حال حاضر رویکرد متفاوتی را در پیش میگیریم.
ما در مورد وضعیت برنامه بحث کردیم: تمام دادههایی که برنامه در طول زمان مدیریت میکند. در اینجا، برنامه شامل یک اکتیویتی و n قطعه است که همگی هنگام شروع برنامه یکبار ایجاد میشوند و عمرشان با عمر برنامه مطابقت دارد. بنابراین، هر یک از این عناصر، یا چندین مورد از آنها با هم، میتوانند کاندیدهایی برای ذخیره وضعیت برنامه باشند. هر فرگمنت از طریق متد [Fragment.getActivity()] به اکتیویتی که آن را ایجاد کرده دسترسی دارد. از آنجایی که همه فرگمنتها به اکتیویتی دسترسی دارند، طبیعی به نظر میرسد که وضعیت برنامه را در آن ذخیره کنیم.
با این حال، نتیجه متد [Fragment.getActivity()] به زمانی که در طول چرخه عمر فراخوانی میشود بستگی دارد. ما این نکته را با افزودن چند ورودی لاگ به کلاس [PlaceholderFragment] نشان میدهیم:
// بهروزرسانی قطعه
public void update() {
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// کار انجامدادنی به شماره بازدید بستگی دارد
if (numVisit > 1) {
// لاگ
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// متن اصلاحشده
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// اطلاعات محلی برای لاگها
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
- خطوط 14–16: متد [getInfos] بخشی از وضعیت برنامه را نمایش میدهد؛
ما برنامه را با فاصلهٔ قطعه ۲ اجرا میکنیم. لاگها هنگام شروع برنامه:
06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/exemples.android D/menu: création menu en cours
- خطوط 9، 10، 13، 14: میبینیم که در داخل متدهای [setUserVisibleHint]، [getActivity()==null] در صورتی که قطعه هنوز قابل مشاهده نباشد (isVisibleToUser==false) فراخوانی میشود؛
- خط ۱۹: میبینیم که وقتی جریان اجرایی به متد [update] قطعه ۱ میرسد، متد [getActivity] در واقع فعالیت را بازمیگرداند؛
وقتی همجواری قطعه روی ۴ (همجواری کامل) تنظیم میشود، لاگها به شرح زیر است:
06-01 03:35:23.553 2814-2814/exemples.android D/MainActivity: constructor
06-01 03:35:23.751 2814-2819/exemples.android I/art: Ignoring second debugger -- پذیرش و رهاسازی
06-01 03:35:23.900 2814-2814/exemples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.210 2814-2814/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.211 2814-2814/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.214 2814-2814/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:26.602 2814-2814/exemples.android D/menu: création menu en cours
به نتایج مشابهی میرسیم. بنابراین میتوانیم نتیجه بگیریم که به محض اینکه قطعه قابل مشاهده شود، متد [getActivity] فعالیت قطعه را بازمیگرداند. همچنین متوجه میشویم که وقتی اجرای برنامه به متد [update] از قطعهای که قرار است نمایش داده شود میرسد، متد [getActivity] واقعاً مقداری را بازمیگرداند.
برای نشان دادن ارتباط بین قطعات، ما یک پروژه جدید ایجاد میکنیم.
1.10. مثال-۰۹: ارتباط بین قطعات، کشیدن و پیمایش
1.10.1. ایجاد پروژه
ما پروژه [Exemple-07] را در [Exemple-08] کپی میکنیم. برای این کار، رویهای را که برای کپی کردن [Exemple-02] در [Exemple-03] در بخش 1.4 توضیح داده شده است، دنبال میکنیم.
![]() | ![]() |
1.10.2. جلسه
در این پروژه جدید، میخواهیم قطعات تعداد کل قطعات مشاهدهشده توسط کاربر را نمایش دهند. برای این کار، باید یک شمارنده را حفظ کنیم که برای همه قطعات قابل دسترسی باشد. ما به شیءای که دادههای مشترک بین قطعات را در خود جای میدهد، «سشن» میگوییم. این اصطلاح از توسعه وب آمده است، جایی که دادههایی که باید در بین نماهای مختلف درخواستشده توسط یک کاربر مشترک باشند، در یک سشن ذخیره میشوند. قرار دادن اطلاعات مشترک بین قطعات مختلف در یک شیء واحد، کد را خواناتر میکند.
کلاس [Session] به شرح زیر خواهد بود:
![]() |
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// تعداد قطعات بازدید شده
private int numVisit;
// گیرنده و تنظیمکننده
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
}
- خط ۸: جلسه (session) تعداد قطعات بازدید شده را در بر خواهد گرفت؛
- خط ۵: انوتیشن [EBean] یک انوتیشن از AA است. ویژگی [scope] دامنه (یا عمر) کلاس با این انوتیشن مشخصشده را تعیین میکند. در اینجا، ویژگی [scope = EBean.Scope.Singleton] کلاس [Session] را به یک کلاس تکنمونه (singleton) تبدیل میکند: این کلاس تنها یک بار و فقط یک بار هنگام راهاندازی برنامه نمونهسازی خواهد شد. سپس میتوان مرجعی به کلاسی که با [EBean] نشانهگذاری شده است را به کلاس دیگری تزریق کرد. این مفهوم تزریق وابستگی است؛
1.10.3. فعالیت [MainActivity]
فعالیت [MainActivity] به شرح زیر تکامل مییابد:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
...
//تزریق جلسه
@Bean(Session.class)
protected Session session;
// تعداد قطعات
private final int FRAGMENTS_COUNT = 5;
// همجواری قطعات
private final int OFF_SCREEN_PAGE_LIMIT = 2;
@AfterInject
protected void afterInject(){
Log.d("MainActivity", "afterInject");
// ابتدای جلسه
session.setNumVisit(0);
}
...
- خطوط ۷–۸: تزریق مرجع به singleton جلسه (session singleton) با استفاده از anotation [@Bean]. پارامتر این anotation، کلاس bean مورد نظر برای تزریق است. فیلدی که به این شکل annotate شده است نمیتواند scope [private] را داشته باشد؛
- خط 15: آناوتیشن [@AfterInject] برای مشخص کردن متدی استفاده میشود که پس از تکمیل تمام تزریقها برای کلاس، یک بار فراخوانی میشود. بنابراین، هنگامی که متد [afterInject] در خط 16 وارد میشود، مرجع در خط 8 مقداردهی اولیه شده است؛
- خط ۲۰: شمارشگر بازدید روی صفر تنظیم میشود؛
1.10.4. قطعه [PlaceholderFragment]
قطعه [PlaceholderFragment] به صورت زیر تکامل مییابد:
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
....
// جلسه
protected Session session;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// والد
super.setUserVisibleHint(isVisibleToUser);
// حافظه
this.isVisibleToUser = isVisibleToUser;
// لاگ
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// تعداد بازدیدها
if (isVisibleToUser) {
// بهروزرسانی قطعه
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// قطعه پنهان خواهد شد
updateDone = false;
}
}
// بخش بهروزرسانی
public void update() {
// لاگ
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// جلسه
if (session == null) {
session = ((MainActivity) getActivity()).getSession();
}
// افزایش شمارش بازدید
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// متن اصلاحشده
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- خط ۷: جلسه؛
- خطوط ۳۵–۳۷: میدانیم که وقتی وارد متد [update] میشویم، متد [getActivity] در واقع فعالیت را برمیگرداند. از این فرصت استفاده میکنیم تا جلسه را بازیابی کرده و آن را بهصورت محلی ذخیره کنیم (خط ۳۶)؛
- خطوط ۳۹–۴۱: برای افزایش شمارش بازدید، آن را از جلسه بازیابی میکنیم. میتوانستیم این کد را از خط ۱۹ به بعد در متد [setUserVisibleHint] قرار دهیم، زیرا میدانیم که متد [getActivity] در آن زمان فعالیت را بازمیگرداند. در اینجا، تصمیم گرفتهایم که هیچ نقش مشخصی به این متد اختصاص ندهیم و کد مربوط به قطعه (fragment) را به متد [update] که برای این منظور طراحی شده است، منتقل کنیم؛
- خط ۴۳: شماره بازدید را نمایش میدهد؛
وقتی این برنامه با ۵ قطعه و با ۲ قطعه مجاور اجرا میشود، لاگهای اولیه به شرح زیر است:
05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/exemples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/exemples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
...
- خطوط ۲–۳: میبینیم که متد [afterInject] فعالیت قبل از متد [afterViews] آن اجرا میشود؛
از خوانندگان دعوت میشود این اپلیکیشن جدید را آزمایش کنند.
1.10.5. غیرفعال کردن سوایپ
در اپلیکیشن قبلی، وقتی با ماوس ایمولیتور اندروید را به چپ یا راست میکشید، نمای فعلی با نمای سمت راست یا چپ، بسته به موقعیت، جایگزین میشود. این رفتار پیشفرض همیشه مطلوب نیست. اکنون یاد میگیریم چگونه کشیدن نما را غیرفعال کنیم.
بیایید به نمای اصلی XML بازگردیم:
![]() |
در کد نما (XML)، کد مربوط به کانتینر قطعه را مییابیم:
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
خط ۱ به کلاسی اشاره دارد که صفحات فعالیت را مدیریت میکند. این کلاس را میتوان در فعالیت [MainActivity] یافت:
import android.support.v4.view.ViewPager;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// مدیر قطعه
private SectionsPagerAdapter mSectionsPagerAdapter;
// ظرفیت قطعه
@ViewById(R.id.container)
protected ViewPager mViewPager;
...
در خط ۱۲، کنtejنر قطعه از نوع [android.support.v4.view.ViewPager] (خط ۱) است. برای غیرفعال کردن اسکن، این کلاس باید به صورت زیر مشتق شود:
![]() |
package exemples.android;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// کنترل کشیدن
private boolean isSwipeEnabled;
// سازندهها
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// متدهایی که باید برای مدیریت کشیدن بازنویسی شوند
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// آیا کشیدن مجاز است؟
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// آیا کشیدن مجاز است؟
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- خط ۸: کلاس [MyPager] از کلاس اندروید [ViewPager] (خط ۴) ارث میبرد؛
- با کشیدن دست، ممکن است رویدادپردازها در خطوط ۲۴ و ۳۴ فراخوانی شوند. هر دو یک مقدار بولی بازمیگردانند. آنها صرفاً باید مقدار بولی [false] را برای غیرفعال کردن کشیدن بازگردانند؛
- خط ۱۱: متغیر بولی که برای نشان دادن پذیرفته شدن یا نشدن کشش دست استفاده میشود.
پس از انجام این کار، اکنون باید از دستگیرکننده صفحه جدید خود استفاده کنیم. این کار در نمای XML [activity_main.xml] و در فعالیت اصلی [MainActivity] انجام میشود. در [activity_main.xml]، مینویسیم:
![]() |
<exemples.android.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
در خط ۱، کلاس جدید استفاده میشود. در [MainActivity]، کد به شرح زیر تغییر میکند:
package exemples.android;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
//مدیر قطعه
private SectionsPagerAdapter mSectionsPagerAdapter;
// ظرفیت قطعه
@ViewById(R.id.container)
protected MyPager mViewPager;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
// ظرفیت قطعه با مدیر قطعات مرتبط است
//یعنی قطعه شماره i در محفظهٔ قطعات همان قطعه شماره i است که توسط مدیر قطعات ارائه شده است
mViewPager.setAdapter(mSectionsPagerAdapter);
// کشیدن بین قطعات غیرفعال است
mViewPager.setSwipeEnabled(false);
//نوار تب نیز با مخزن قطعه مرتبط است
...
- خط ۱۲: مدیر صفحه اکنون دارای نوع [MyPager] است؛
- خط ۲۳: اینکه آیا پیمایش دستی غیرفعال باشد یا خیر.
این نسخهٔ جدید را آزمایش کنید. انتخاب کنید که آیا پیمایش را غیرفعال کنید یا نه و تفاوت در نحوهٔ رفتار نماها را وقتی آنها را با ماوس به سمت راست یا چپ میکشید، مشاهده کنید. در تمام برنامههای آینده، پیمایش بهطور پیشفرض غیرفعال خواهد بود. ما این موضوع را به شما یادآوری نخواهیم کرد.
1.10.6. غیرفعال کردن پیمایش بین قطعات
بیایید با بهبودی در مدیر تبها ادامه دهیم. هنگام سوئیچ کردن از تب ۱ به تب ۴، دو تب میانی، ۲ و ۳، به سرعت رد میشوند. در اصطلاح اندروید، این به عنوان smoothScrolling شناخته میشود. این رفتار در صورتی که تعداد تبها زیاد باشد میتواند آزاردهنده شود. با افزودن کد زیر به مدیر فرگمنت [MyPager] میتوان آن را غیرفعال کرد:
//کنترل کشیدن
private boolean isSwipeEnabled;
//کنترل پیمایش را بر عهده دارد
private boolean isScrollingEnabled;
...
// پیمایش
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// مقداردهیها
...
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
از آنجایی که مدیر تب با مدیر قطعه [MyPager] مرتبط شده است، هنگامی که تب شماره i کلیک میشود، قطعه شماره i توسط کانتینر قطعه با استفاده از متد [setCurrentItem] فوق (خط 9) نمایش داده میشود. [position] شمارهٔ قطعهٔ نمایشدادهشده است؛
- خط ۱۰: متد [setCurrentItem] از کلاس والد فراخوانی میشود. آرگومان دوم متد [false] یک انتقال فوری بین قطعات قدیمی و جدید را مشخص میکند (بدون پیمایش)، در حالی که آرگومان دوم متد [true] یک انتقال از طریق scrolling را مشخص میکند. در اینجا، آرگومان دوم مقدار فیلد در خط ۴ است، فیلدی که توسعهدهنده میتواند با استفاده از متد در خطوط ۱۶–۱۸ آن را تنظیم کند؛
اگر میخواهید پیمایش را غیرفعال کنید، کلاس [MainActivity] به صورت زیر خواهد بود:
...
// جابهجایی قطعه
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// کشیدن بین قطعات را غیرفعال میکند
mViewPager.setSwipeEnabled(false);
// بدون پیمایش
mViewPager.setScrollingEnabled(false);
...
پروژه را دوباره اجرا کنید و بررسی کنید که برای مثال، دیگر هیچ scrolling بین برگههای ۱ و ۴ وجود ندارد. از این پس، ما همیشه پیمایش را غیرفعال خواهیم کرد. دیگر به این موضوع اشاره نخواهیم کرد.
1.10.7. یک قطعه جدید
در مثال ما، همه قطعات از یک نوع هستند: [PlaceHolderFragment]. اکنون یاد میگیریم چگونه یک قطعه جدید ایجاد کرده و آن را نمایش دهیم.
ابتدا، بیایید نمای [vue1.xml] را از پروژه [Exemple-04] در پروژه [Exemple-09] [1] کپی کنیم:
![]() | ![]() |
- در [1]، نما [vue1.xml]؛
- در [3]، نما شامل خطاهایی است که به دلیل وجود متنهای ناقص در فایل [res/values/strings.xml] ایجاد شدهاند؛
در [2]، متن مفقود با برداشتن آن از فایل [res/values/strings.xml] در پروژه [Exemple-04] اضافه میشود
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- نما ۱ -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
- در بالا، خطوط ۶ تا ۹ اضافه شدهاند؛
اکنون کلاس [Vue1Fragment] را ایجاد میکنیم که مسئول نمایش نمای [vue1.xml] خواهد بود:
![]() |
کلاس [Vue1Fragment] به شرح زیر خواهد بود:
package exemples.android;
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
// عناصر رابط کاربری بصری
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// پردازشگر رویداد
@Click(R.id.buttonValider)
protected void doValider() {
// نام وارد شده نمایش داده میشود
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- خط ۱۰: حاشیهنویسی [@EFragment] به این معنی است که قطعه مورد استفاده توسط فعالیت در واقع کلاس [Vue1Fragment_] خواهد بود. توجه به این نکته مهم است. این قطعه با نما [vue1.xml] مرتبط است؛
- خطوط ۱۴–۱۵: کامپوننت با شناسه [R.id.editTextNom] در فیلد [editTextNom] در خط ۱۵ تزریق میشود؛
- خطوط ۱۸–۲۰: متد [doValider] رویداد «کلیک» روی دکمهای را که با [R.id.buttonValider] شناسایی شده است، مدیریت میکند؛
- خط ۲۱: اولین پارامتر [Toast.makeText] از نوع [Activity] است. متد [Fragment.getActivity()] فعالیتی را که قطعه در آن قرار دارد بازیابی میکند. این فعالیت [MainActivity] است، زیرا در این معماری ما تنها یک فعالیت داریم که نماها یا قطعات مختلفی را نمایش میدهد؛
در کلاس [MainActivity]، مدیر قطعه به شرح زیر تکامل مییابد:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// قطعات
private Fragment[] fragments;
// شماره قطعه
private static final String ARG_SECTION_NUMBER = "section_number";
// سازنده
public SectionsPagerAdapter(FragmentManager fm) {
// پدر
super(fm);
// ابتداییسازی آرایهٔ قطعات
fragments = new Fragment[FRAGMENTS_COUNT];
for (int 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[fragments.length - 1] = new Vue1Fragment_();
}
...
}
- خط ۱۳: فراکتهای [FRAGMENTS_COUNT] وجود دارند: فراکتهای [FRAGMENTS_COUNT-1] از نوع [PlaceholderFragment] (خطوط ۱۴–۲۱) و یک فراکت از نوع [Vue1Fragment_]، خط ۲۳ (به زیرخط توجه کنید)؛
پروژه [Exemple-09] را کامپایل کرده و سپس اجرا کنید. زبانهی ۵ اکنون باید متفاوت به نظر برسد:
![]() |
1.10.8. تمام قطعات را از یک کلاس انتزاعی واحد مشتق کنید
قطعه جدید [Vue1Fragment] نیز باید هنگام نمایش بهروزرسانی شود. برای این کار، باید کدی مشابه آنچه برای قطعه [PlaceholderFragment] ایجاد شد، بسازیم. برای جلوگیری از تکرار، آنچه قابل تفکیک است را در یک کلاس انتزاعی قرار میدهیم که همهٔ قطعات در برنامه از آن ارثبری خواهند کرد.
برای این کار، یک پروژه جدید ایجاد خواهیم کرد.
1.11. مثال ۱۰: ارثبری همه قطعات از یک کلاس انتزاعی
1.11.1. ایجاد پروژه
ما پروژه [Exemple-09] را در [Exemple-10] کپی میکنیم:
![]() | ![]() |
1.11.2. مدیریت حالت اشکالزدایی
ما در حال افزودن گزینهای به پروژه هستیم تا گزارشهای حالت اشکالزدایی را نمایش یا پنهان کند. برای این کار، یک ثابت ایستا به کلاس [MainActivity] اضافه میکنیم:
// حالت اشکالزدایی
public static final boolean IS_DEBUG_ENABLED = false;
1.11.3. کلاس پایه انتزاعی همه قطعات
![]() |
کلاس [AbstractFragment] به شرح زیر است:
package exemples.android;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
public abstract class AbstractFragment extends Fragment {
// دادههای خصوصی
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private String className;
// دادهها قابل دسترسی برای کلاسهای فرزند
protected boolean afterViewsDone = false;
protected boolean isDebugEnabled = true;
// فعالیت
protected MainActivity activity;
// جلسه
protected Session session;
// سازنده
public AbstractFragment() {
// ابتدایی
isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
className = getClass().getSimpleName();
// لاگ
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// پدر
super.setUserVisibleHint(isVisibleToUser);
...
}
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
...
}
@Override
public void onResume() {
// پدر
super.onResume();
...
}
// اطلاعات محلی
protected String getParentInfos() {
return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
}
// بهروزرسانی قطعه
protected void update() {
...
// از کلاس فرزند خواسته میشود که خود را بهروزرسانی کند
updateFragment();
}
protected abstract void updateFragment();
}
- خط ۷: کلاس [AbstractFragment] از کلاس اندروید [Fragment] ارث میبرد؛
- هر فرگمنت باید بتواند خود را بهروزرسانی کند. به همین دلیل است که کلاس والد [AbstractFragment] نیاز دارد که کلاسهای فرزندش متد [updateFragment] (خط 68) را داشته باشند، متدی که خود آن را فراخوانی میکند (خط 65)؛
- خط ۱۹: کلاس یک مرجع به اکتیویتی برنامه را ذخیره خواهد کرد؛
- خط ۲۲: کلاس یک مرجع به سشن که در آن دادههای مشترک بین قطعات و فعالیت جمعآوری میشود را ذخیره خواهد کرد؛
- خطوط ۲۵–۳۳: سازنده برای کلاس انتزاعی؛
- خط ۲۷: ایجاد یک کپی از مقدار ثابت [MainActivity.IS_DEBUG_ENABLED] در فیلد خط ۱۶؛
- خط ۲۸: نام کلاس نمونهسازیشده ذخیره میشود، یعنی نام یک کلاس فرزند؛
- خطوط ۱۵–۲۲: این فیلدها دارای ویژگی [protected] هستند تا کلاسهای فرزند بتوانند به آنها دسترسی داشته باشند. توجه کنید که کلاسهای فرزند از وجود بولیهای [isVisibleToUser] و [updateDone] (خطوط ۱۰–۱۱) بیخبر هستند؛
- خط 57: متد [getParentInfos] دارای ویژگی [protected] است تا کلاسهای فرزند بتوانند آن را فراخوانی کنند؛
متدهای [setUserVisibleHint, onDestroyView, onResume] همانند آنچه در کلاس [PlaceholderFragment] پروژه قبلی بودند، باقی میمانند:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// والد
super.setUserVisibleHint(isVisibleToUser);
// حافظه
this.isVisibleToUser = isVisibleToUser;
// لاگ
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
}
// مواردهایی که در آن قطعه قابل مشاهده خواهد شد
if (isVisibleToUser) {
//بهروزرسانی قطعه
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
//خروج از قطعه
updateDone = false;
}
}
@Override
public void onDestroyView() {
// والد
super.onDestroyView();
// علامتگذاری بهروزرسانی
afterViewsDone = false;
// لاگ
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
}
}
@Override
public void onResume() {
// والد
super.onResume();
// لاگ
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
}
if (isVisibleToUser) {
// بهروزرسانی
if (!updateDone) {
update();
updateDone = true;
}
}
}
متد [update] به شرح زیر است:
// بهروزرسانی قطعه
protected void update() {
//بازیابی فعالیت و جلسه
if (activity == null) {
Activity activity = getActivity();
if (activity != null) {
this.activity = (MainActivity) activity;
this.session = this.activity.getSession();
}
}
// به کلاس فرزند دستور داده میشود که خود را بهروزرسانی کند
updateFragment();
}
طبق کد بالا، هنگامی که متد [update] یک فراگمنت اجرا میشود، آن فراگمنت قابل مشاهده میشود. این موضوع مهم است زیرا به این معنی است که متد [Fragment.getActivity] سپس مرجعی به اکتیویتی برنامه بازمیگرداند (به بخش 1.10.8 مراجعه کنید)، که به نوبه خود دسترسی به سشن را فراهم میکند.
- خطوط ۴–۱۰: اگر این کار قبلاً انجام نشده باشد، فعالیت و جلسه راهاندازی میشوند؛
- خط ۱۲: متد [updateFragment] از کلاس فرزند فراخوانی میشود. وقتی این متد اجرا شود، فیلدهای [activity] و [session] که به آنها دسترسی دارد قبلاً مقداردهی شدهاند؛
1.11.4. کلاس [PlaceholderFragment]
![]() |
کلاس [PlaceholderFragment] به شرح زیر تکامل مییابد:
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
//یک فرگمنت نمایی است که توسط یک کانتینر فرگمنت نمایش داده میشود
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
// جزء رابط کاربری
@ViewById(R.id.section_label)
protected TextView textViewInfo;
//دادهها
private boolean initDone;
// دادهها
private String text;
private int numVisit;
// شماره قطعه
private static final String ARG_SECTION_NUMBER = "section_number";
// سازنده
public PlaceholderFragment() {
super();
// لاگ
if (isDebugEnabled) {
Log.d("PlaceholderFragment", "constructor");
}
}
@AfterViews
protected void afterViews() {
// حافظه
afterViewsDone = true;
...
}
// بخش بهروزرسانی
public void updateFragment() {
...
}
}
- خط ۱۰: کلاس [PlaceholderFragment] از کلاس [AbstractFragment] ارث میبرد. با این معماری، نوشتن یک قطعه شامل موارد زیر است:
- نوشتن متد [@AfterViews]، که برای راهاندازی قطعه در طول اولین چرخه عمر آن یا ریست کردن آن در صورتی که قبلاً [onDestroyView] فراخوانی شده باشد، استفاده میشود. خط 39 برای مدیریت صحیح چرخه عمر قطعه الزامی است؛
- نوشتن متد [updateFragment] که درست قبل از نمایش قطعه، آن را بهروزرسانی میکند. این متد ممکن است از جلسه کلاس والد خود استفاده کند؛
- برای نوشتن رویدادپردازهای قطعه. این کاری است که در پروژههای آینده انجام خواهیم داد؛
متدهای [@AfterViews] و [updateFragment] همانند پروژه قبلی بدون تغییر باقی میمانند:
@AfterViews
protected void afterViews() {
//حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
if (!initDone) {
// متن اولیه
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// ابتکاری کامل شد
initDone = true;
}
// نمایش متن فعلی
textViewInfo.setText(text);
}
// بهروزرسانی قطعه
public void updateFragment() {
// لاگ
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
//افزایش شماره بازدید
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// متن اصلاح شد
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
// اطلاعات محلی برای لاگها
protected String getLocalInfos() {
return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
numVisit, initDone, getActivity() == null);
}
- خطوط ۷ و ۲۳: در لاگها، با استفاده از متد ارثبریشده [getParentInfos]، اطلاعاتی درباره کلاس والد نمایش میدهیم؛
1.11.5. کلاس [Vue1Fragment]
![]() |
کلاس [Vue1Fragment] ساختار مشابهی با کلاس [PlaceholderFragment] دارد:
package exemples.android;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر رابط کاربری بصری
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// دادهها
private int numVisit;
@AfterViews
protected void afterViews() {
// حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// مدیر رویداد
@Click(R.id.buttonValider)
protected void doValider() {
// نام وارد شده نمایش داده میشود
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// اطلاعات محلی برای لاگها
protected String getLocalInfos() {
return String.format("numVisit=%s", numVisit);
}
// بهروزرسانی قطعه
@Override
protected void updateFragment() {
// افزایش شماره بازدید
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// شماره بازدید را نمایش میدهد
Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
- خط ۹: کلاس [Vue1Fragment] از کلاس [AbstractFragment] ارث میبرد؛
- خطوط ۱۸–۲۶: متد [@AfterViews] کار خاصی برای انجام دادن ندارد. با این حال، باید برای تنظیم بولین [afterViewsDone] به true نوشته شود، زیرا این اطلاعات توسط کلاس والد استفاده میشود؛
- خطوط ۴۲–۴۹: متد [updateFragment] شامل نمایش یک پیام کوتاه حاوی شماره بازدید (خط ۴۸) و افزایش این شماره در جلسه (سشن) (خطوط ۴۴–۴۶) است؛
از خوانندگان دعوت میشود این پروژهٔ جدید را آزمایش کنند.
ما این معماری را در تمام پروژههای آینده به کار خواهیم برد:
- یک فعالیت و n قطعه؛
- تمام قطعات از کلاس [AbstractFragment] ارث میبرند؛
- دادههایی که باید بین قطعات و همچنین بین قطعات و فعالیت به اشتراک گذاشته شوند، در کلاس [Session] قرار میگیرند؛
1.11.6. ارتباط بین تبها و قطعات
در کلاس [MainActivity] که مسئول مدیریت تبها است، موارد زیر نوشته شده است:
//نوار زبانه همچنین با کانتینر قطعه مرتبط است
//یعنی زبانه شماره i، قطعه شماره i را از کانتینر نمایش میدهد
tabLayout.setupWithViewPager(mViewPager);
خط ۳، مدیر تب را با کانتینر فرگمنت مرتبط میسازد. ما یک پیامد این ارتباط را دیدهایم: وقتی کاربر روی تب شماره i کلیک میکند، کانتینر فرگمنت، فرگمنت شماره i را نمایش میدهد. ما عکس این حالت را ندیدهایم: وقتی به کانتینر فرگمنت دستور داده میشود که فرگمنت شماره i را نمایش دهد، تب شماره i به طور خودکار انتخاب میشود.
برای نشان دادن این رفتار، ما گزینههای [Fragment 1, Fragment 2, ...] را به منوی فعلی اضافه خواهیم کرد. هنگامی که کاربر روی گزینه [Fragment i] کلیک میکند، ما به کانتینر شیبهقطعه دستور میدهیم که شیبهقطعه شماره i را نمایش دهد. سپس بررسی خواهیم کرد که آیا برگه شماره i انتخاب شده است یا خیر.
این مرحله با ویرایش منوی برنامه آغاز میشود:
![]() | ![]() |
محتویات فایل [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="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"/>
<item android:id="@+id/fragment5"
android:title="@string/fragment5"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- خطوط ۹–۲۸: پنج گزینهٔ جدید منو؛
- برچسبهای گزینهها (خطوط ۱۰، ۱۴، ۱۸، ۲۲، ۲۶) در فایل [res / values / strings.xml] [2] تعریف شدهاند:
<resources>
<string name="app_name">Exemple-10</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!--نمای ۱ -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
<!--منو -->
<string name="fragment1">Fragment 1</string>
<string name="fragment2">Fragment 2</string>
<string name="fragment3">Fragment 3</string>
<string name="fragment4">Fragment 4</string>
<string name="fragment5">Fragment 5</string>
</resources>
نتیجهٔ بصری به این صورت است:
![]() |
پردازش کلیکها روی این گزینههای منو در کلاس [MainActivity] مدیریت میشود:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
//پردازش گزینههای منو
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "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;
}
case R.id.fragment5: {
showFragment(4);
break;
}
}
// آیتم پردازش شد
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// قطعه نمایشدادهشده تغییر میکند
mViewPager.setCurrentItem(i);
}
}
- خط ۲: متد [onOptionsItemSelected] زمانی که روی یکی از گزینههای منو کلیک میشود، فراخوانی میشود؛
- خط ۸: شناسه (ID) گزینه کلیکشده بازیابی میشود؛
- خطوط ۹–۳۶: موارد مختلف توسط switch مدیریت میشوند؛
- خطوط 16–36: کلیک بر روی گزینه [Fragment i]، متد [showFragment(i-1)] را در خطوط 41–45 فراخوانی میکند؛
- خط ۴۳: به کانتینر قطعه دستور داده میشود تا قطعه درخواستی را نمایش دهد؛
- خط ۴۲: ابتدا بررسی میکنیم که این کار ممکن است (شرط ۱) و ضروری است (شرط ۲)؛
از خوانندگان دعوت میشود این نسخه جدید را آزمایش کنند. مشاهده میشود که وقتی نمایش قطعه شماره i درخواست میشود، آن قطعه واقعاً نمایش داده میشود و خود زبانه شماره i نیز انتخاب میشود.
اکنون که دیدیم وابستگی تب/قطعه چگونه کار میکند، به سناریوی دیگری میپردازیم: سناریویی که در آن مدیریت تب از مدیریت قطعه مستقل شده است. این حالت، برای مثال، زمانی رخ میدهد که تعداد تبها کمتر از قطعات باشد. برای تشریح این مورد استفاده جدید، یک پروژه جدید ایجاد خواهیم کرد.
1.12. مثال ۱۱: برگهها جدا از قطعات
1.12.1. ایجاد پروژه
ما پروژه [Exemple-10] را در [Exemple-11] کپی میکنیم:
![]() | ![]() |
1.12.2. اهداف
برنامه جدید دو تب خواهد داشت:
- زبانهی اول همیشه قطعه [Vue1] را نمایش میدهد؛
- زبانهی دوم قطعهای را که از منو انتخاب شده نمایش میدهد؛

- در [1]، قطعه [Vue1]؛
- در [2]، قطعه از نوع [PlaceholderFragment] که توسط کاربر انتخاب شده است؛
- در [3]، بازدیدها همچنان شمارش میشوند؛
1.12.3. جلسه
![]() |
جلسه جدید به شرح زیر خواهد بود:
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// تعداد قطعات بازدید شده
private int numVisit;
//شناسه قطعه از نوع [PlaceholderFragment] نمایش داده شده در زبانه دوم
private int numFragment;
// گیرنده و تنظیمکننده
...
}
- خط ۱۰: ما کلیکهای تب را خودمان مدیریت خواهیم کرد. وقتی یک تب کلیک میشود، باید قطعهای را که آخرین بار هنگام انتخاب آن نمایش داده شده بود، بازیابی کنیم. میدان [numFragment] شمارهٔ قطعه را برای زبانهٔ شمارهٔ ۲ ذخیره میکند، عددی در [0, Fragments_COUNT-2]. وقتی زبانهٔ شمارهٔ ۲ کلیک میشود، ما شمارهٔ قطعهای را که باید نمایش داده شود از جلسه بازیابی خواهیم کرد؛
1.12.4. منو
![]() |
منوی [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="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>
زبانهی شمارهٔ ۲ یکی از چهار قطعه را از خطوط ۹ تا ۲۴ نمایش میدهد. قطعهی پنجم [Vue1Fragment] است که همیشه در زبانهی شمارهٔ ۱ نمایش داده میشود.
1.12.5. کلاس [MainActivity]
کلاس [MainActivity] اکنون باید زبانهها و ناوبری بین آنها را مدیریت کند، کاری که قبلاً انجام نمیداد. کد آن به شرح زیر تغییر میکند:
//مدیر زبانهها
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
...
@AfterViews
protected void afterViews() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
...
// بدون پیمایش
mViewPager.setScrollingEnabled(false);
// نمایش View1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
//در ابتدا تنها یک زبانه وجود دارد
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// پردازشگر رویداد
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
//یک برگه انتخاب شده است – قطعه نمایش داده شده توسط محفظهٔ قطعه تغییر میکند
...
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
...
}
- خط 17: اولین قطعهای که توسط محفظهٔ قطعه نمایش داده میشود، قطعهٔ [Vue1Fragment] خواهد بود. طبق طراحی، این آخرین قطعه در محفظه خواهد بود؛
- خطوط ۲۰–۲۲: از آنجا که ما هیچ ارتباطی بین تبها و کانتینر فرگمنت برقرار نکردهایم، باید خودمان تبها را مدیریت کنیم. در ابتدا، نوار تب [tabLayout] در خط ۳ هیچ تبی ندارد؛
- خط ۲۰: اولین تب را ایجاد میکنیم؛
- خط ۲۱: یک عنوان برای آن در نظر میگیریم. در مثالهای قبلی، عناوین تبها همان عناوین قطعات بودند. این دیگر صادق نیست. در نتیجه، متد [getPageTitle] را از مدیر قطعه حذف میکنیم. دیگر به آن نیازی نداریم:
//اختیاری – عنوانی را به قطعات مدیریتشده اختصاص میدهد
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
- خط ۲۲: زبانه ایجاد شده به نوار زبانه اضافه میشود. نوار زبانه ما اکنون یک زبانه دارد. این زبانه چه چیزی را نمایش میدهد؟ مهم است که درک کنیم زبانه و قطعه دو مفهوم جداگانه هستند. قطعه نمایش داده شده همیشه همان قطعهای است که توسط محفظه قطعه انتخاب شده است. اگر بین تبها جابجا شویم و از کانتینر نخواهیم که قطعه نمایش داده شده را تغییر دهد، هیچ اتفاقی نمیافتد: همان قطعه همچنان نمایش داده میشود، اما تب انتخابشده تغییر کرده است. بنابراین در اینجا، قطعه نمایش داده شده همان قطعهای است که در خط 17 انتخاب شده است: قطعه [Vue1Fragment]؛
- خطوط ۲۶–۳۰: متدی که باید برای رسیدگی به تغییر تب کاربر نوشته شود؛
متد [onTabSelected] در خطوط 26–30 به محض تغییر زبانه فراخوانی میشود (اگر کاربر روی زبانه انتخابشدهای کلیک کند، هیچ اتفاقی نمیافتد). کد آن به شرح زیر است:
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (IS_DEBUG_ENABLED) {
Log.d("onglets", "onTabSelected");
}
//یک برگه انتخاب شده است – قطعه نمایشدادهشده توسط محفظهٔ قطعه تغییر میکند
//موقعیت زبانه
int position = tab.getPosition();
// شمارهٔ قطعهٔ نمایشدادهشده
int numFragment;
switch (position) {
case 0:
// شماره قطعه [Vue1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// شماره قطعه [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// نمایش قطعه
mViewPager.setCurrentItem(numFragment);
}
- خط ۸: موقعیت زبانه کلیکشده بازیابی میشود. مقدار بازیابیشده در اینجا یا ۰ یا ۱ خواهد بود؛
- خطوط ۱۲–۱۵: اگر اولین تب کلیک شده باشد، ما برای نمایش قطعه [Vue1Fragment] آماده میشویم؛
- خطوط ۱۶–۱۸: در سایر موارد (کلیک روی زبانه شماره ۲)، ما برای نمایش مجدد قطعهای که آخرین بار هنگام انتخاب زبانه شماره ۲ نمایش داده شده بود، آماده میشویم. شناسه (ID) این قطعه قبلاً در جلسه (session) برنامه ذخیره شده بود؛
- خط ۲۱: به کانتینر قطعه دستور داده میشود تا قطعه مورد نظر را نمایش دهد؛
اکنون بیایید به نحوهٔ رسیدگی به گزینههای منو (هنوز در [MainActivity]) نگاهی بیندازیم:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// پردازش گزینههای منو
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "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;
}
- خطوط ۱۶–۳۱: رسیدگی به چهار گزینه منو. هر هندلر متد [showFragment] را با شماره قطعه مورد نظر برای نمایش فراخوانی میکند؛
متد [showFragment] به شرح زیر است:
// برگه شمارهٔ ۲
private TabLayout.Tab tab2 = null;
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// اگر زبانه دوم هنوز وجود ندارد، آن را ایجاد کن
if (tab2 == null) {
tab2 = tabLayout.newTab();
tabLayout.addTab(tab2);
}
// عنوان زبانه دوم را تنظیم کنید
tab2.setText(String.format("Fragment n° %s", (i + 1)));
// تغییر قطعه نمایش داده شده
mViewPager.setCurrentItem(i);
// شماره قطعه نمایشدادهشده در جلسه ذخیره میشود
session.setNumFragment(i);
// زبانهی دوم را انتخاب کنید – اگر از قبل انتخاب شده باشد، هیچ کاری انجام ندهید
tab2.select();
}
}
- به خاطر داشته باشید که وقتی برنامه اجرا میشود، تنها یک تب وجود دارد؛
- خط ۲: اشارهای به برگه شماره ۲، null در ابتدا؛
- خط ۵: شرایط نمایش نسبت به نسخه قبلی تغییر نکرده است؛
- خطوط ۷–۱۰: اگر برگه شماره ۲ هنوز وجود نداشته باشد، ایجاد میشود (خط ۸) و به نوار برگهها اضافه میشود (خط ۹);
- خط ۱۲: شماره قطعه نمایش داده شده بهعنوان عنوان زبانه دوم تنظیم میشود، با شمارهگذاری از ۱ شروع میشود؛
- خط ۱۴: قطعهٔ مورد نظر نمایش داده میشود؛
- خط ۱۶: شمارهی آن در جلسه ذخیره میشود؛
- خط ۱۸: برگه شماره ۲ انتخاب میشود. اگر قبلاً انتخاب شده باشد، هیچ اتفاقی نمیافتد: متد [onTabSelected] اجرا نخواهد شد. اگر قبلاً انتخاب نشده باشد، متد [onTabSelected] فراخوانی میشود. سپس این متد به کانتینر قطعه دستور میدهد تا قطعهای را که در خط 14 قبلاً نمایش داده شده است، نمایش دهد. یک بررسی ساده در داخل متد [onTabSelected] از این سناریو جلوگیری میکند:
// قطعه را فقط در صورت لزوم نمایش دهید
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
از خوانندگان دعوت میشود تا این نسخه جدید را آزمایش کنند.
1.12.6. بهبودها
اکنون درک خوبی از قطعات، چرخهٔ عمر آنها، مفهوم مجاورت قطعات و رابطهشان با نوار تب داریم. همچنین معماری مستحکمی داریم که بهتازگی در مثال ۱۱ آزمون شده است:
- یک فعالیت و n قطعه؛
- تمام قطعات از کلاس [AbstractFragment] ارث میبرند؛
- دادههایی که باید بین قطعات و همچنین بین قطعات و فعالیت به اشتراک گذاشته شوند، در کلاس [Session] قرار میگیرند؛
در یک پروژه جدید، ما روابط بین فعالیت و قطعات را با افزودن یک رابط مشخص خواهیم کرد.
1.13. مثال ۱۲: تعریف روابط بین فعالیت و قطعات
در این مثال، میخواهیم حداقل روابط بین فعالیت و قطعات را تعریف کنیم. برای این کار، از موارد زیر استفاده خواهیم کرد:
- یک رابط [IMainActivity] که مشخص میکند فرگمنتها چه چیزهایی را میتوانند از فعالیت درخواست کنند؛
- یک کلاس انتزاعی [AbstractFragment] که وضعیت و متدهایی را که هر قطعه باید داشته باشد، تعریف میکند؛
1.13.1. ایجاد پروژه
ما پروژه [Exemple-11] را طبق رویه بخش 1.4 به [Exemple-12] کپی میکنیم. نتیجه زیر به دست میآید:
![]() | ![]() |
1.13.2. رابط [IMainActivity]
از مثالهای قبلی، واضح است که قطعات نیاز به دسترسی به جلسهای دارند که توسط فعالیت ایجاد شده است. علاوه بر این، اگرچه در این مثالها قابل مشاهده نیست، انتظار میرود که گاهی اوقات دستگیرههای رویداد قطعه منجر به تغییر نما شوند. از فعالیت خواسته خواهد شد تا این تغییر را انجام دهد. رابط [IMainActivity] میتواند به شکل زیر باشد:
![]() |
package exemples.android;
public interface IMainActivity {
// دسترسی به جلسه
Session getSession();
// تغییر نما
void navigateToView(int position);
// حالت اشکالزدایی
boolean IS_DEBUG_ENABLED = true;
}
در خط ۱۲، به وجود یک مقدار ثابت توجه کنید که قبلاً در کلاس [MainActivity] قرار داشت. ما میخواهیم وابستگی بین قطعات و فعالیت را کاهش دهیم و آن را به یک وابستگی بین [AbstractFragment] و [IMainActivity] محدود کنیم. سپس میتوان به این فعالیت نامی غیر از [MainActivity] داد. از آنجا که ثابت [IS_DEBUG_ENABLED] در قطعات استفاده میشود، به رابط [IMainActivity] منتقل میشود.
1.13.3. کلاس انتزاعی [AbstractFragment]
کلاس انتزاعی [AbstractFragment] تغییر بسیار کمی دارد:
// دادهها قابل دسترسی برای کلاسهای فرزند
protected boolean afterViewsDone = false;
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// فعالیت
protected IMainActivity mainActivity;
protected Activity activity;
...
// بهروزرسانی قطعه
protected void update() {
//بازیابی فعالیت و جلسه
if (mainActivity == null) {
this.activity = getActivity();
if (this.activity != null) {
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
}
}
// به کلاس فرزند دستور داده میشود که خود را بهروزرسانی کند
updateFragment();
}
- خطوط ۶ و ۷: دو نوع ارجاع به فعالیت نگهداری میشوند:
- خط ۶: مرجعی به اکتیویتی که رابط [IMainActivity] را پیادهسازی میکند؛
- خط ۷: یک مرجع به اکتیویتی که از کلاس Android با نام [Activity] ارث میبرد. این مورد برای هر اکتیویتی صادق است؛
این دو مرجع به طور طبیعی به یک شی واحد اشاره میکنند. با این حال، این شی از دو نوع متفاوت تلقی میشود. این امر از تبدیل انواع در زمان اجرا جلوگیری میکند؛
- خط ۱۴: ما با استفاده از متد [getActivity] یک مرجع به اکتیویتی را بازیابی میکنیم؛
- خط ۱۵: اگر این مرجع null نباشد، آنگاه میتوانیم به session دسترسی پیدا کنیم؛
- خطوط 16–17: ما فعالیت را به همراه جلسه به عنوان پیادهسازی رابط [IMainActivity] ذخیره میکنیم؛
1.13.4. اصلاح دستکارگرهٔ قطعه
پردازشگر قطعه [SectionsPagerAdapter] در کلاس [MainActivity] از یک جنبه تغییر یافته است: به جای پردازش قطعات از نوع [Fragment]، اکنون قطعات از نوع [AbstractFragment] را پردازش میکند:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// قطعات
private AbstractFragment[] fragments;
// شماره قطعه
private static final String ARG_SECTION_NUMBER = "section_number";
// سازنده
public SectionsPagerAdapter(FragmentManager fm) {
// والد
super(fm);
// ابتداییسازی آرایهٔ قطعات
fragments = new AbstractFragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
...
}
// یک قطعه از +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
// شماره قطعه، موقعیت
@Override
public AbstractFragment getItem(int position) {
...
}
// تعداد قطعات مدیریتشده را برمیگرداند
@Override
public int getCount() {
...
}
}
1.13.5. اصلاح کلاس [MainActivity]
کلاس [MainActivity] باید رابط [IMainActivity] را پیادهسازی کند:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
...
//تزریق جلسه
@Bean(Session.class)
protected Session session;
...
// گیرنده جلسه
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
//نما را در موقعیت نمایش میدهد
if(mViewPager.getCurrentItem()!=position){
// نمایش قطعه
mViewPager.setCurrentItem(position);
}
}
- خطوط ۱۰–۱۲: متد [getSession] از قبل وجود داشت؛
- خطوط ۱۵–۲۲: متد [navigateToView] قطعه شماره [position] را نمایش میدهد؛
- خط ۱۷: سیستم بررسی میکند که آیا کاری برای انجام دادن وجود دارد یا خیر؛
- خط ۱۹: قطعه شماره [position] نمایش داده میشود؛
در این مرحله، برنامه را اجرا کنید. باید کار کند.
1.13.6. تغییر نمایش قطعات در [MainActivity]
در حال حاضر، کلاس [MainActivity] یک قطعه را با استفاده از عبارت زیر نمایش میدهد:
//نمایش View1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
از آنجایی که متد [navigateToView] کار مشابهی انجام میدهد، این نوع عبارت را در همه جا (۲ مورد) با عبارت زیر جایگزین کنید:
سپس برنامه را اجرا کنید. باید همچنان کار کند.
1.13.7. نتیجهگیری
از این پس، ما همیشه از معماری قبلی استفاده خواهیم کرد:
- یک فعالیت که رابط [IMainActivity] را پیادهسازی میکند؛
- قطعاتی که کلاس [AbstractFragment] را گسترش میدهند، که از آنها میخواهد متد [updateFragment] را پیادهسازی کنند. اینها نیز باید دارای متد [@AfterViews] باشند که در آن مقدار boolean [afterViewsDone] را به true تنظیم میکنند؛
- یک جلسه که دادههای مورد اشتراکگذاری بین قطعات و فعالیت را در بر میگیرد؛
1.14. مثال ۱۳: مثال ۰۵ با قطعات
در پروژه [Exemple-05]، ما ناوبری بین ویوها را معرفی کردیم. در آن زمان، این شامل ناوبری بین فعالیتها بود: 1 ویو = 1 فعالیت. در اینجا، ما پیشنهاد میکنیم که یک فعالیت واحد با چندین ویو از نوع [AbstractFragment] داشته باشیم.
1.14.1. ایجاد پروژه
ما پروژه قبلی [Exemple-12] را با دنبال کردن رویه بخش 1.4، در [Exemple-13] کپی میکنیم. نتیجه زیر به دست میآید:
![]() | ![]() |
1.14.2. ساختار پروژه
اکنون شروع به استفاده از پکیجها برای سازماندهی کد میکنیم. در حال حاضر، میتوانیم بین دو حوزه مجزا تمایز قائل شویم:
- مدیریت فعالیت؛
- مدیریت قطعه؛
ما برای آنها دو بسته ایجاد میکنیم، [exemples.android.activity] و [exemples.android.fragments]:
![]() |
![]() | ![]() |
ما برای ایجاد بسته [exemples.android.fragments] از همان رویه پیروی میکنیم:
![]() | ![]() |
در [8]، ما یک بسته سوم به نام [architecture] ایجاد میکنیم که در آن، اجزای [IMainActivity, AbstractFragment, Session, MyPager] را قرار خواهیم داد؛ این اجزا، بلوکهای سازنده معماری برنامه ما را تشکیل میدهند. این یادآوری است که ما یک انتخاب معماری خاص انجام دادهایم. سپس، عناصر موجود پروژه را همانطور که در [9] نشان داده شده است، جابجا کنید. هر جابجایی باید با کلیک روی دکمه [Refactor] تأیید شود.
در این مرحله، برنامه را کامپایل کنید. خطاهای زیر در [MainActivity ظاهر میشوند:
![]() |
هنگام انتقال کلاسها به پکیجها، اندروید استودیو تغییرات لازم را در کد برنامه اعمال کرد (برای مثال، خطوط ۱۸–۲۱). کلاسهای تحت تأثیر خطوط ۱۵ و ۱۷ منتقل نشدهاند. آنها توسط کتابخانه Android Annotations تولید شدهاند. برای این کلاسها باید *imports* را بهصورت دستی تغییر دهید. بنابراین این خطوط به شکل زیر درمیآیند:
![]() |
پس از انجام این کار، دیگر خطای کامپایل وجود ندارد. برنامه را اجرا کنید. سپس خطای زیر ظاهر میشود:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}:
این خطا از فایل مانیفست برنامه نشأت میگیرد:
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
خطوط ۳ و ۱۲ مشخص میکنند که فعالیت تعیینشده [exemples.android.MainActivity_] است. با این حال، از آنجا که این فعالیت به بسته [activity] منتقل شده است، خط ۱۲ باید به این صورت تغییر یابد:
android:name=".activity.MainActivity_"
به نقطه قبل از [activity] توجه کنید. بار دیگر، اندروید استودیو نتوانست منیفست را بهروزرسانی کند زیرا به کلاسی از Android Annotations اشاره میکند که منتقل نشده است. بنابراین، استفاده از کتابخانه AA با تعدادی مشکل همراه است.
1.14.3. پاکسازی پروژه
در پروژه جدید:
- دیگر هیچ برگه، دکمه شناور یا منویی وجود ندارد؛
- قطعات [PlaceholderFragment] ناپدید شدهاند. این برنامه دو قطعه را مدیریت خواهد کرد: [Vue1Fragment] که از قبل داریم، و [Vue2Fragment] که باید ایجاد شود؛
- جلسه دیگر یکسان نیست؛
1.14.3.1. پاکسازی قطعات
کلاسهای [PlaceHolderFragment] و [1] را حذف کنید:
![]() | ![]() |
به همین ترتیب، نمای [res / layout / fragment_main.xml] را که با این قطعه [2] مرتبط است، حذف کنید.
1.14.3.2. پاک کردن جلسه
جلسه در حال حاضر به شرح زیر است:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// تعداد قطعات بازدید شده
private int numVisit;
//شناسه قطعه از نوع [PlaceholderFragment] نمایش داده شده در زبانه دوم
private int numFragment;
// گیرندهها و تنظیمکنندهها
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
public int getNumFragment() {
return numFragment;
}
public void setNumFragment(int numFragment) {
this.numFragment = numFragment;
}
}
ما از این جلسه هیچ چیزی را نگه نمیداریم.
پروژه را کامپایل کنید. خطوط حاوی خطا، خطوطی هستند که از محتویات جلسه استفاده کردهاند. آنها را حذف کنید. در کلاس [Vue1Fragment]، متغیر [numVisit] را نیز از کد حذف میکنیم، که در نتیجه کد به شکل زیر درمیآید:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر رابط کاربری بصری
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
//حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// پردازشگر رویداد
@Click(R.id.buttonValider)
protected void doValider() {
// نام وارد شده نمایش داده میشود
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// بهروزرسانی قطعه
@Override
protected void updateFragment() {
}
}
1.14.3.3. حذف زبانه ها، دکمه شناور و منو
زبانهها و دکمه شناور در دو مکان حذف میشوند:
- در نمای [res / layout / activity-main.xml] که این عناصر و موقعیت آنها را در داخل نما تعریف میکند؛
- در کد فعالیت [MainActivity]؛
منو نیز در دو مکان حذف میشود:
- در نمای [res / menu / menu-main.xml] که گزینههای منو را تعریف میکند؛
- در کد فعالیت [MainActivity]؛
کد مربوط به نمای [res / layout / 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.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
- خطوط [28-31, 41-47] حذف شدهاند؛
- همچنین نوار ابزار را از خطوط ۱۸–۲۴ حذف کنید؛
کد منوی [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">
<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>
- خطوط ۹ تا ۲۴ حذف خواهند شد. این کار یک گزینه را که استفاده نخواهد شد باقی میگذارد، صرفاً برای ارائه مثالی از نحوه اعلام یک گزینه منو که میتوان با کپی و پیست آن را تکثیر کرد؛
در کلاس [MainActivity]، همه چیزهایی را که به تبها، دکمه شناور، نوار ابزار و منو اشاره دارند، حذف میکنیم. سادهترین راه برای یافتن این ارجاعات، حذف اعلانهای آنهاست:
// مدیر برگه
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// دکمه شناور
@ViewById(R.id.fab)
protected FloatingActionButton fab;
و برنامه را دوباره کامپایل کنید. خطوطی که باعث خطا میشوند، آنهایی هستند که به عناصری اشاره دارند که حذف شدهاند. تمام این خطوط را حذف کنید. علاوه بر این، دستکارگره (fragment handler) را طوری تغییر دهید که دیگر به قطعه [PlaceholderFragment] که آن را حذف کردهایم، اشاره نکند:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// قطعات
private AbstractFragment[] fragments;
// سازنده
public SectionsPagerAdapter(FragmentManager fm) {
// والد
super(fm);
}
// شماره موقعیت قطعه
@Override
public AbstractFragment getItem(int position) {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// تعداد قطعات مدیریتشده را بازمیگرداند
@Override
public int getCount() {
return fragments.length;
}
}
- خطوط ۷–۱۰: تمام تولید قطعات را حذف کردهایم؛
در این مرحله، دیگر نباید هیچ خطای کامپایل وجود داشته باشد. در کلاس [MainActivity]، به کد میانی زیر رسیدهایم:
package exemples.android.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.IMainActivity;
import exemples.android.architecture.MyPager;
import exemples.android.architecture.Session;
import exemples.android.fragments.Vue1Fragment_;
import org.androidannotations.annotations.*;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
// ظرفیت قطعه
@ViewById(R.id.container)
protected MyPager mViewPager;
//نوار ابزار
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
//تزریق جلسه
@Bean(Session.class)
protected Session session;
// تعداد قطعات
private final int FRAGMENTS_COUNT = 5;
// همجواری قطعات
private final int OFF_SCREEN_PAGE_LIMIT = 2;
// حالت اشکالزدایی
public static final boolean IS_DEBUG_ENABLED = true;
// مدیر قطعات
private SectionsPagerAdapter mSectionsPagerAdapter;
// سازنده
public MainActivity() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "constructor");
}
}
@AfterViews
protected void afterViews() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
//نوار ابزار – جایی که نام برنامه نمایش داده میشود
setSupportActionBar(toolbar);
// مدیر قطعه
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// کانتینر قطعات با مدیر قطعات مرتبط است
//یعنی قطعه شماره i در محفظه قطعات، همان قطعه شماره i است که توسط مدیر قطعات بازگردانده شده است
mViewPager.setAdapter(mSectionsPagerAdapter);
// جابهجایی قطعه
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// حرکت کشویی بین قطعات غیرفعال است
mViewPager.setSwipeEnabled(false);
//بدون پیمایش
mViewPager.setScrollingEnabled(false);
// View1 نمایش
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// دریافتکننده جلسه
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
//نمایش نمای موقعیت
if (mViewPager.getCurrentItem() != position) {
// نمایش قطعه
mViewPager.setCurrentItem(position);
}
}
// مدیر قطعه
// این همان چیزی است که از آن خواسته میشود تا قطعاتی را که باید در نمای اصلی نمایش داده شوند، فراهم کند
//باید متدهای [getItem] و [getCount] را تعریف کند – بقیه اختیاری هستند
public class SectionsPagerAdapter extends FragmentPagerAdapter {
//قطعات
private AbstractFragment[] fragments;
// سازنده
public SectionsPagerAdapter(FragmentManager fm) {
// والد
super(fm);
}
// شماره موقعیت قطعه
@Override
public AbstractFragment getItem(int position) {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// تعداد قطعات مدیریتشده را برمیگرداند
@Override
public int getCount() {
return fragments.length;
}
}
}
هنوز چند تغییر باقی مانده است:
- خط ۳۱ را حذف کنید، که دیگر مورد نیاز نیست؛
- خط ۳۳: همجواری قطعه را روی ۱ تنظیم کنید؛
- خط ۷۶: به نمای ۰ بروید. این اولین نمایی خواهد بود که نمایش داده میشود؛
- خط ۱۰۸: آرایه را با قطعه [Vue1Fragment_] مقداردهی اولیه کنید:
// قطعات
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
بنابراین تنها یک قطعه وجود دارد. برنامه را اجرا کنید. باید نتیجه زیر را دریافت کنید:

دکمه [Valider] باید کار کند.
1.14.4. ایجاد قطعات و نماهای مرتبط
برنامه دو نما خواهد داشت، نماهای مربوط به پروژه [Exemple-05]. ما در پروژهٔ جاری، نمای [vue1.xml] را از قبل داریم. اکنون [vue2.xml] را از [Exemple-05] به [Exemple-12] کپی میکنیم (هر دو پروژه را باز کرده و بین آنها کپی و پیست کنید).
![]() | ![]() |
- به [1]، نمای جدید. هنگامی که سعی میکنید آن را ویرایش کنید، خطاهایی در [2] ظاهر میشوند. ما باید فایلهای [strings.xml] و [3] را اصلاح کنیم تا رشتههایی را که این نمای جدید به آنها ارجاع میدهد، اضافه کنیم:
<resources>
<string name="app_name">Exemple-13</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- نما ۱ -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<!-- نمایش ۲ -->
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
</resources>
ما کلاس [Vue1Fragment] را در [Vue2Fragment] کپی میکنیم:
![]() |
و کد کپیشده را به شرح زیر اصلاح میکنیم:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
//حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// بهروزرسانی قطعه
@Override
protected void updateFragment() {
}
}
- خط ۹: این قطعه با نمای [res / layout / vue2.xml] مرتبط است؛
- خط ۱۰: کلاس از کلاس انتزاعی [AbstractFragment] ارث میبرد؛
- خطوط ۱۲–۲۰: متد الزامی [@AfterViews];
- خطوط ۲۳–۲۵: روش [updateFragment] الزامی است؛
1.14.5. پیادهسازی قطعات و ناوبری بین آنها
اکنون این فعالیت دو فرگمنت را مدیریت خواهد کرد. کلاس آن [SectionsPagerAdapter] به شرح زیر بهروزرسانی شده است:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// قطعات
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
}
رابط [IMainActivity] با استفاده از متد [navigateToView] خود، ناوبری بین ویوها را مدیریت میکند. ما کلیک روی دکمه [Vue n° 2] را در قطعه [Vue1Fragment] مدیریت خواهیم کرد:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر رابط کاربری بصری
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
//حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// پردازندههای رویداد ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// نام واردشده نمایش داده میشود
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
mainActivity.navigateToView(1);
}
// بخش بهروزرسانی
@Override
protected void updateFragment() {
}
}
- خطوط ۳۷–۴۰: متد [showVue2] رویداد «کلیک» روی دکمه [Vue n° 2] را مدیریت میکند؛
- خط ۳۹: ناوبری با استفاده از متد [navigateToView] فعالیت انجام میشود. باید توجه داشت که فعالیت در کلاس والد به شکل زیر ذخیره شده است:
// فعالیت
protected IMainActivity mainActivity;
و اینکه این فعالیت پیش از ورود به هر رویدادپرداز، قبلاً مقداردهی اولیه شده است.
- خط ۳۴: این عبارت از متغیر [activity] از کلاس والد استفاده میکند که مرجعی به فعالیت به عنوان نمونهای از نوع اندروید [Activity] است؛
protected Activity activity;
کد مشابهی را میتوان برای قطعه [Vue2Fragment] یافت:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// پردازشگرهای رویداد ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// بهروزرسانی قطعه
@Override
protected void updateFragment() {
}
}
- خطوط ۲۴–۲۷: متد [showVue1] رویداد «کلیک» روی دکمه [Vue n° 1] را مدیریت میکند؛
پروژه را اجرا کنید و بررسی کنید که ناوبری بین ویوها کار میکند.
1.14.6. تعریف جلسه
برنامه به شرح زیر کار میکند:
- یک نام در نما شمارهٔ ۱ وارد میشود؛
- این نام در نما شمارهٔ ۲ نمایش داده میشود؛
برای اینکه نمای ۱ بتواند نام واردشده را به نمای ۲ منتقل کند، از جلسه زیر استفاده خواهیم کرد:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// نام
private String nom;
// گیرندهها و تنظیمکنندهها
...
}
- خط ۸: نام وارد شده؛
کلاس [MainActivity] جلسه را به شرح زیر راهاندازی میکند:
//تزریق جلسه
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// ابتدای جلسه
session.setNom("");
}
1.14.7. نوشتن نهایی قطعات
در قطعه [Vue1Fragment]، کد مربوط به رویداد کلیک دکمه [Valider] را اصلاح میکنیم:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر رابط کاربری بصری
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
...
// پردازشگرهای رویداد ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// نام وارد شده ذخیره میشود
String nom = editTextNom.getText().toString();
// نمایش آن
Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
// نام وارد شده در جلسه ذخیره میشود
session.setNom(editTextNom.getText().toString());
// به نما شمارهٔ ۲ هدایت میشود
mainActivity.navigateToView(1);
}
// بهروزرسانی قطعه
@Override
protected void updateFragment() {
}
}
- خطوط: 31–37: ما کلیک روی دکمه [Vue n° 2] را مدیریت میکنیم؛
- خط ۳۴: قبل از هدایت به نمای شماره ۲، نام وارد شده را در جلسه ذخیره میکنیم تا نمای جدید بتواند به آن دسترسی داشته باشد؛
نما [Vue2Fragment] به شرح زیر تغییر میکند:
package exemples.android.fragments;
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// اجزای رابط کاربری
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// پردازشگرهای رویداد ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
//بهروزرسانی قطعه
@Override
protected void updateFragment() {
// نام واردشده در طول جلسه را بازیابی میکند
String nom = session.getNom();
// نمایش آن
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
هنگامی که نما شماره ۲ نمایش داده میشود، نام وارد شده در نما شماره ۱ باید نمایش داده شود. میدانیم که بلافاصله پس از نمایش آن، متد آن [updateFragment] اجرا خواهد شد. بنابراین در داخل این متد (خطوط ۳۶–۴۲) است که میتوانیم کد نمایش نام را قرار دهیم.
- خطوط ۱۶–۱۷: اعلان تنها مؤلفهٔ بصری ویو؛
- خط ۳۹: نام وارد شده در نما شماره ۱ از جلسه بازیابی میشود؛
- خط ۴۱: برچسب [textViewBonjour] بهروزرسانی میشود؛
پروژه را اجرا کنید و بررسی کنید که کار میکند.
1.14.8. مدیریت چرخهٔ عمر قطعه
در قطعه [Vue1Fragment]، متد [@AfterViews] به شرح زیر است:
@AfterViews
protected void afterViews() {
// حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
این متد ناقص است. در واقع، شما باید همیشه احتمال بازیافت مجدد قطعه پس از یک عملیات [onDestroyView] را در نظر بگیرید. در این حالت، نمای قطعه ۱ مجدداً تولید میشود و هر نامی که ممکن است قبلاً وارد شده باشد از نما ناپدید خواهد شد. ما نمیخواهیم این اتفاق بیفتد. در حال حاضر، نام وارد شده نمایش داده میشود زیرا مجاورت قطعات در 1 به این معنی است که چرخه عمر قطعه [Vue1Fragment] تنها یک بار اجرا میشود. با این حال، ترجیح داده میشود که احتمال بازیافت قطعه را نیز در نظر گرفت.
چندین راه برای حل این مشکل وجود دارد:
- ما میتوانیم از این واقعیت که متد [update] هر بار که قطعه نمایش داده میشود به طور سیستماتیک اجرا میشود، استفاده کنیم تا نام وارد شده را بهروزرسانی کنیم؛
- میتوانید این بهروزرسانی را تنها زمانی انجام دهید که متد [@AfterViews] مجدداً اجرا شود. این رویکرد اخیر است که ما در پیش میگیریم؛
ما کد [Vue1Fragment] را به شرح زیر اصلاح میکنیم:
// عناصر رابط کاربری بصری
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// دادهها
private String nom;
@AfterViews
protected void afterViews() {
//حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// متن نمایشدادهشده (دوباره) راهاندازی میشود
editTextNom.setText(nom);
}
// پردازشگرهای رویداد ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showVue2() {
// نام وارد شده ثبت میشود تا در صورت استفاده مجدد از قطعه قابل بازیابی باشد
nom = editTextNom.getText().toString();
// نام واردشده در جلسه ذخیره میشود
session.setNom(nom);
// حرکت به نما شمارهٔ ۲
activity.navigateToView(1);
}
- خط ۲۷: درست قبل از تغییر از نمای ۱ به نمای ۲، نام وارد شده را ذخیره میکنیم؛
- خط ۱۷: هر بار که چرخهٔ عمر قطعه مجدداً اجرا میشود، آخرین نام وارد شده دوباره نمایش داده میشود؛
برای قطعه [Vue2Fragment]، کد موجود کافی است:
// اجزای رابط کاربری بصری
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// بخش بهروزرسانی
@Override
protected void updateFragment() {
//بازیابی نام وارد شده در طول جلسه
String nom = session.getNom();
// نمایش آن
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
- تنها مؤلفهٔ بصری نما (خط ۳) هر بار که نما نمایش داده میشود (خط ۲۱) بهروزرسانی میشود. بنابراین، متد [@AfterViews] چیزی برای افزودن ندارد؛
1.14.9. نتیجهگیری
در این مرحله، ما بار دیگر اهمیت معماری خود را نشان دادهایم:
- یک فعالیت که رابط [IMainActivity] را پیادهسازی میکند؛
- قطعاتی که کلاس [AbstractFragment] را گسترش میدهند، که از آنها میخواهد متد [updateFragment] را پیادهسازی کنند. اینها همچنین باید یک متد [@AfterViews] داشته باشند که در آن مقدار بولی [afterViewsDone] را به true تنظیم میکنند؛
- یک جلسه که دادههای مورد اشتراکگذاری بین قطعات و فعالیت را در بر میگیرد؛
1.15. مثال 14: معماری دو لایه
ما قصد داریم یک برنامه تکنظری با معماری زیر بسازیم:
![]() |
1.15.1. ایجاد پروژه
ما پروژه قبلی [Exemple-12] را با دنبال کردن رویه بخش 1.4 به [Exemple-13] کپی میکنیم. نتیجه زیر را به دست میآوریم:
![]() | ![]() |
1.15.2. نما [vue1]
برنامه تنها یک نما خواهد داشت، [vue1.xml]. بنابراین نمای دیگر، [vue2.xml]، به همراه قطعهٔ مرتبط با آن حذف خواهد شد:
![]() | ![]() |
برنامه را کامپایل کنید. خطاهایی در [MainActivity] ظاهر میشوند:
![]() |
خط ۴ زیر را در مدیر قطعه [SectionsPagerAdapter] اصلاح کنید
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// قطعات
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
خط ۴ بالا به این صورت درمیآید:
// قطعات
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
وارداتهایی را که دیگر مورد نیاز نیستند حذف کنید: [Ctrl-Shift-O]. دیگر نباید هیچ خطای کامپایل وجود داشته باشد. پروژه را اجرا کنید: نما شماره ۱ باید ظاهر شود. اکنون این نما را اصلاح خواهیم کرد.
ما نمای [vue1.xml] را ایجاد خواهیم کرد که اعداد تصادفی تولید میکند:
![]() |
اجزای آن به شرح زیر است:
کد آن XML به شرح زیر است:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:orientation="vertical" >
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/txt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_Titre2"
android:layout_marginTop="20dp"
android:text="@string/txt_nbaleas" />
<EditText
android:id="@+id/edt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_nbaleas"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorNbAleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_nbaleas"
android:text="@string/txt_errorNbAleas"
android:textColor="@color/red" />
<TextView
android:id="@+id/txt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_nbaleas"
android:layout_marginTop="20dp"
android:text="@string/txt_a" />
<EditText
android:id="@+id/edt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_a"
android:inputType="number" />
<TextView
android:id="@+id/txt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_a"
android:text="@string/txt_b" />
<EditText
android:id="@+id/edt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_b"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorIntervalle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_b"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_b"
android:text="@string/txt_errorIntervalle"
android:textColor="@color/red" />
<Button
android:id="@+id/btn_Executer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_a"
android:layout_marginTop="20dp"
android:text="@string/btn_executer" />
<TextView
android:id="@+id/txt_Reponses"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btn_Executer"
android:layout_marginTop="30dp"
android:text="@string/list_reponses"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/blue" />
<ListView
android:id="@+id/lst_reponses"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_Reponses"
android:layout_marginTop="40dp"
android:background="@color/wheat"
android:clickable="true"
tools:listitem="@android:layout/simple_list_item_1" >
</ListView>
</RelativeLayout>
نمای قبلی از برچسبهای تعریفشده در فایل [res / values / strings.xml] استفاده میکند:
<resources>
<string name="app_name">Exemple-14</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!--نمای ۱ -->
<string name="titre_vue1">Vue n° 1</string>
<string name="list_reponses">Liste des réponses</string>
<string name="btn_executer">Exécuter</string>
<string name="aleas">Génération de N nombres aléatoires</string>
<string name="txt_nbaleas">Valeur de N :</string>
<string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
<string name="txt_b">"b : "</string>
<string name="txt_dummy">Dummy</string>
<string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
<string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>
رنگهای مورد استفاده در [vue1.xml] در فایل [res / values / colors.xml] تعریف شدهاند:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</colour>
<color name="colorAccent">#FF4081</colour>
<!--رنگها اعمال شدند -->
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
</resources>
1.15.3. جلسه
![]() |
از آنجا که در اینجا تنها یک قطعه وجود دارد، نیازی به فراهم کردن ارتباط بین قطعات نیست. بنابراین جلسه خالی خواهد بود:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}
در این مرحله، برنامه را کامپایل کنید. خطاهایی روی خطهایی که از عناصر جلسه اکنون خالی استفاده کردهاند، ظاهر خواهند شد. این خطها را حذف کرده و بررسی کنید که کامپایل دیگر هیچ خطایی تولید نمیکند.
1.15.4. قطعه [Vue1Fragment]
![]() |
ما قطعه موجود [Vue1Fragment] را به شرح زیر اصلاح میکنیم:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر رابط کاربری بصری
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// فهرست پاسخها به یک فرمان
private List<String> reponses = new ArrayList<>();
// آداپتور ListView
private ArrayAdapter<String> adapterReponses;
// میدانهای ورودی
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// حافظه
afterViewsDone = true;
// لاگ
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// پنهان کردن هرگونه پیام خطا
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Executer)
void doExecuter() {
// هرگونه پیام خطای قبلی پنهان شدهاند
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
//اعتبار ورودیها را بررسی میکند
if (!isPageValid()) {
return;
}
}
//اعتبار دادههای واردشده بررسی میشود
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// لاگ
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- در اینجا تنها یک قطعه وجود دارد که چرخهٔ عمر آن فقط یک بار، هنگام راهاندازی برنامه، اجرا خواهد شد. به همین دلیل، متدهای [@AfterViews] (خطوط ۴۶–۵۷) و [udateFragment] (خطوط ۷۵–۸۱) تنها یک بار هنگام شروع برنامه اجرا خواهند شد؛
- خطوط ۵۵–۵۶: دو پیام خطا در نما (که در زیر نشان داده شده) توسط [1-2] پنهان میشوند؛
![]() |
- خطوط 59–60: متدی که هنگام کلیک روی دکمه [Exécuter] اجرا میشود؛
- خطوط ۷۱–۷۳: اعتبار ورودی بررسی میشود؛
متد [isPageValid] به شرح زیر است:
// ورودیها
private int nbAleas;
private int a;
private int b;
...
// اعتبار دادههای واردشده در حال بررسی است
private boolean isPageValid() {
// تعداد اعداد تصادفی را وارد کنید
nbAleas = 0;
Boolean erreur;
int nbErreurs = 0;
try {
nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
erreur = (nbAleas < 1);
} catch (Exception ex) {
erreur = true;
}
// خطا؟
if (erreur) {
nbErreurs++;
txtErrorAleas.setVisibility(View.VISIBLE);
}
// یک را وارد کنید
a = 0;
erreur = false;
try {
a = Integer.parseInt(edtA.getText().toString());
} catch (Exception ex) {
erreur = true;
}
//خطا؟
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// 'b' را وارد کنید
b = 0;
erreur = false;
try {
b = Integer.parseInt(edtB.getText().toString());
erreur = b < a;
} catch (Exception ex) {
erreur = true;
}
// خطا؟
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
//بازگشت
return (nbErreurs == 0);
}
- خطوط ۲–۴: این سه فیلد توسط متد [isPageValid] مقداردهی اولیه میشوند. علاوه بر این، این متد در صورتی که همه ورودیها معتبر باشند، مقدار true را برمیگرداند و در غیر این صورت مقدار false را. اگر هر یک از ورودیها نامعتبر باشند، پیامهای خطای مربوطه نمایش داده میشوند؛
در این مرحله، برنامه آماده اجرا است. با وارد کردن دادههای نادرست، بررسی کنید که متد [isPageValid] بهدرستی کار میکند.
1.15.5. لایه [métier]
![]() |
![]() |
لایه [métier] دارای رابط [IMetier] زیر است:
package exemples.android.metier;
import java.util.List;
public interface IMetier {
List<Object> getAleas(int a, int b, int n);
}
متد [getAleas(a,b,n)] معمولاً n عدد صحیح تصادفی را در بازه [a,b] برمیگرداند. همچنین طوری طراحی شده است که در هر سه فراخوانی یکبار یک استثنا پرتاب کند؛ این استثنا نیز در پاسخهای بازگرداندهشده توسط متد گنجانده میشود. در نهایت، متد فهرستی از اشیاء از نوع [Exception] یا [Integer] را بازمیگرداند.
پیادهسازی [Metier] این رابط به شرح زیر است:
package exemples.android.metier;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@EBean(scope = EBean.Scope.Singleton)
public class Metier implements IMetier {
public List<Object> getAleas(int a, int b, int n) {
// فهرست اشیاء
List<Object> réponses = new ArrayList<Object>();
// برخی بررسیها
if (n < 1) {
réponses.add(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));
}
if (a < 0) {
réponses.add(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));
}
if (b < 0) {
réponses.add(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));
}
if (a >= b) {
réponses.add(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));
}
// خطا؟
if (réponses.size() != 0) {
return réponses;
}
//تولید اعداد تصادفی
Random random = new Random();
for (int i = 0; i < n; i++) {
//یک خطای تصادفی در هر ۳ بار پرتاب میشود
int nombre = random.nextInt(3);
if (nombre == 0) {
réponses.add(new AleaException("Exception aléatoire"));
} else {
//در غیر این صورت، یک عدد تصادفی بین دو حد بازگردانده میشود [a,b]
réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
}
}
// نتیجه
return réponses;
}
}
- خط ۹: حاشیهنویسی AA [@EBean] بر کلاس [Metier] اعمال میشود تا ارجاعها به آن بتوانند در لایه [Présentation] تزریق شوند. ویژگی (scope = EBean.Scope.Singleton) تضمین میکند که تنها یک نمونه از کلاس [Metier] ایجاد شود. بنابراین، همان مرجع همیشه تزریق میشود، حتی اگر چندین بار به لایه [Présentation] تزریق شود؛
- بقیه کد استاندارد است؛
نوع [AleaException] که توسط کلاس [Metier] استفاده میشود به شرح زیر است:
package exemples.android.metier;
public class AleaException extends RuntimeException {
private static final long serialVersionUID = 1L;
public AleaException() {
}
public AleaException(String detailMessage) {
super(detailMessage);
}
public AleaException(Throwable throwable) {
super(throwable);
}
public AleaException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
}
- خط ۳: کلاس [AleaException] از کلاس سیستمی [RuntimeException] ارث میبرد، که آن را به یک استثنای بررسینشده (unchecked exception) تبدیل میکند: نیازی به رسیدگی به آن در بلوک try/catch نیست و همچنین لازم نیست در امضاهای متد ذکر شود؛
1.15.6. بازنگری در فعالیت [MainActivity]
![]() |
لایه
[metier]
فعالیت
مشاهده
کاربر
فعالیت رابط [IMetier] لایه [métier] را پیادهسازی خواهد کرد. در نتیجه، قطعه/ویو تنها با فعالیت تعامل خواهد داشت.
فعالیت [MainActivity] در حال حاضر رابط [IMainActivity] را پیادهسازی میکند. برای اطمینان از اینکه این فعالیت رابط [IMetier] را نیز پیادهسازی میکند، میتوانید:
- رابط [IMetier] را به رابطهای پیادهسازیشده توسط فعالیت اضافه کنید؛
- اطمینان حاصل کنید که خود رابط [IMainActivity] از رابط [IMetier] ارث میبرد. این رویکردی است که ما در پیش گرفتهایم؛
رابط [IMainActivity] به صورت زیر درمیآید:
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// دسترسی به جلسه
Session getSession();
//تغییر نما
void navigateToView(int position);
//حالت اشکالزدایی
public static final boolean IS_DEBUG_ENABLED = true;
}
- خط ۵: رابط [IMainActivity] رابط [IMetier] را گسترش میدهد
کلاس [MainActivity] به شرح زیر تغییر میکند:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
...
//تزریق جلسه
@Bean(Session.class)
protected Session session;
//تزریق منطق کسبوکار
@Bean(Metier.class)
protected IMetier metier;
...
// پیادهسازی IMetier --------------------------------------------------------------------
@Override
public List<Object> getAleas(int a, int b, int n) {
return metier.getAleas(a, b, n);
}
- خطوط ۱۱–۱۲: لایه [métier] به فعالیت تزریق میشود. برای این کار، از anotation AA [@Bean] استفاده میشود که پارامتر آن کلاسی است که anotation AA [@EBean] را حمل میکند؛
- خط ۲: این فعالیت رابط [IMainActivity] و در نتیجه رابط [IMetier] از لایه [métier] را پیادهسازی میکند؛
- خطوط ۱۶–۱۹: پیادهسازی تنها متد رابط [IMetier]. این فراخوانی به سادگی به لایه [métier] واگذار میشود؛
1.15.7. نگاهی دقیقتر به قطعه [Vue1Fragment]
![]() |
کد کلاس [Vue1Fragment] به شرح زیر تغییر میکند:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر رابط کاربری بصری
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// فهرست پاسخها به یک فرمان
private List<String> reponses = new ArrayList<>();
// آداپتور ListView
private ArrayAdapter<String> adapterReponses;
// ورودیهای داده
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
...
}
@Click(R.id.btn_Executer)
void doExecuter() {
...
}
// اعتبارسنجی دادههای واردشده
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// لاگ
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// فقط یکبار هنگام راهاندازی برنامه اجرا میشود
// آداپتور ListView ایجاد میشود – برای این کار، متغیر [activity] باید مقداردهی اولیه شده باشد
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
}
- خطوط ۶۹–۷۰: آداپتور برای کامپوننت [ListView] تنظیم شده است؛
کامپوننت [ListView] برای نمایش فهرستی از آیتمها استفاده میشود. این کار را با استفاده از یک اداپتور از نوع [ListAdapter] انجام میدهد که خود به منبع دادهای متصل است که باید [ListView] را تغذیه کند. برای تعریف آداپتور برای [ListView]، روش زیر از [ListView.setAdapter] در دسترس است:
public void setAdapter (ListAdapter adapter)
[ListAdapter] یک رابط است. کلاس [ArrayAdapter] کلاسی است که این رابط را پیادهسازی میکند. سازندهٔ مورد استفاده در خط ۶۹ بالا به شرح زیر است:
- [context] فعالیتی است که [ListView] را نمایش میدهد؛
- [resource] عدد صحیحی است که نمای مورد استفاده برای نمایش یک عنصر از [ListView] را شناسایی میکند. این نما میتواند هر میزان پیچیدگی داشته باشد. این توسعهدهنده است که آن را بر اساس نیازهای خود میسازد؛
- [textViewResourceId] عددی صحیح است که یک مؤلفه [TextView] را در نمای [resource] شناسایی میکند. این رشته توسط این مؤلفه نمایش داده خواهد شد؛
- [objects]: فهرست اشیایی که توسط [ListView] نمایش داده میشوند. از متد [toString] اشیاء برای نمایش شیء در [TextView] که توسط [textViewResourceId] در نمای مشخصشده توسط [resource] شناسایی شده است، استفاده میشود.
وظیفه توسعهدهنده ایجاد نمای [resource] است که هر عنصر از [ListView] را نمایش میدهد. برای مورد ساده که فقط میخواهیم یک رشته از کاراکترها را نمایش دهیم، همانند اینجا، اندروید ویوی با شناسه [android.R.layout.simple_list_item_1] را فراهم میکند. این ویو شامل یک کامپوننت [TextView] است که با شناسه [android.R.id.text1] مشخص شده است. این متدی است که در خط ۶۹ برای ایجاد اداپتور [ListView] استفاده میشود. این اداپتور تنها یک بار نیاز به تعریف دارد. برای امکان استفاده مجدد از آن، به عنوان یک متغیر نمونه کلاس (خط ۳۹) تعریف شده است. بیایید دوباره به خط ۶۹ نگاه کنیم:
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
پارامتر اول سازنده [ArrayAdapter]، فعالیتی است که در یک قطعه توسط [getActivity] به دست آمده و در اینجا در متغیر [activity] از کلاس والد ذخیره شده است. این فیلد همیشه مقدار ندارد. بنابراین، لاگها نشان میدهند که وقتی متد [@AfterViews] فراخوانی میشود، هنوز مقداردهی اولیه نشده است و در نتیجه خطوط ۶۹–۷۰ نمیتوانند در این متد گنجانده شوند. در متد [updateFragment]، این کار ممکن است زیرا میدانیم که وقتی این متد اجرا میشود، [activity!=null] لزوماً موجود است. اداپتور در اینجا با منبع داده [reponses] که در خط ۳۷ تعریف شده است، مرتبط است؛
متد [doExecuter] کلیک روی دکمه [Exécuter] را مدیریت میکند. کد آن به شرح زیر است:
@Click(R.id.btn_Executer)
void doExecuter() {
// هرگونه پیام خطای قبلی پاک میشوند
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// هر پاسخ قبلی را پاک کنید
reponses.clear();
adapterReponses.notifyDataSetChanged();
//اعتبار ورودیها بررسی میشود
if (!isPageValid()) {
return;
}
// اعداد تصادفی از فعالیت درخواست میشوند
List<Object> data = mainActivity.getAleas(a, b, nbAleas);
// از این دادهها فهرستی از رشتهها ایجاد میکند
for (Object o : data) {
if (o instanceof Exception) {
reponses.add(((Exception) o).getMessage());
} else {
reponses.add(o.toString());
}
}
// لیستویو را تازهسازی کنید
adapterReponses.notifyDataSetChanged();
}
- خطوط ۷–۸: ما میخواهیم ListView را پاک کنیم. برای این کار، منبع داده [reponses] را پاک کرده و به آداپتور مرتبط با ListView دستور میدهیم که تازه شود؛
- خطوط ۱۰–۱۲: قبل از اجرای اقدام درخواستی، بررسی میکنیم که مقادیر وارد شده صحیح هستند؛
- خط ۱۴: لیست اعداد تصادفی از فعالیت درخواست میشود. یک لیست از اشیاء بازگردانده میشود که در آن هر شیء از نوع [Integer] یا [AleaException] است؛
- خطوط 16–22: با استفاده از فهرست اشیاء بهدستآمده، منبع داده [reponses] که توسط ListView نمایش داده میشود، بهروزرسانی میگردد؛
- خط ۲۴: به آداپتور ListView دستور تازهسازی داده میشود؛
1.15.8. اجرا
پروژه را اجرا کنید و بررسی کنید که بهدرستی کار میکند.
1.16. مثال ۱۵: معماری کلاینت/سرور
ما در حال بررسی یک معماری رایج برای یک برنامه اندروید هستیم، معماریای که در آن برنامه اندروید با سرویسهای وب راه دور ارتباط برقرار میکند. اکنون معماری زیر را داریم:
![]() |
یک لایه [DAO] به اپلیکیشن اندروید اضافه شده است تا با سرور راه دور ارتباط برقرار کند. این لایه با سروری ارتباط خواهد داشت که اعداد تصادفی نمایشدادهشده توسط تبلت اندروید را تولید میکند. این سرور دارای معماری دو لایهای زیر خواهد بود:
![]() |
کلاینتها از لایه [web / jSON]، برخی سوابق URL را استعلام میکنند و یک پاسخ متنی در قالب jSON (نشانهگذاری شیء JavaScript) دریافت میکنند. در اینجا سرویس وب ما یک شیء واحد از نوع [/a/b] را پردازش میکند که یک عدد تصادفی در بازه [a,b] بازمیگرداند. ما برنامه را به ترتیب زیر توضیح خواهیم داد:
سرور
- لایه [métier] آن؛
- سرویس [web / jSON] آن، که با استفاده از Spring MVC پیادهسازی شده است؛
کلاینت
- لایه [DAO] آن. هیچ لایه [métier] وجود نخواهد داشت؛
1.16.1. سرور [web / jSON]
ما میخواهیم معماری زیر را بسازیم:
![]() |
1.16.1.1. ایجاد پروژه
ما سرویس وب را با استفاده از اکوسیستم Spring [http://spring.io/] خواهیم ساخت. به وبسایت [http://start.spring.io/] (ژوئن ۲۰۱۶) میرویم که به ما امکان میدهد یک پروژه Gradle را با وابستگیهای مورد نیاز برای پروژه خودمان تولید کنیم – که یک پروژه اندروید نیست و در حال حاضر اندروید استودیو هیچ حمایتی از آن ارائه نمیدهد:
![]() |
- در [1]: یک پروژه Gradle را انتخاب کنید؛
- در [2-3]: جزئیات وابستگی jar تولید شده توسط پروژه (به زیر مراجعه کنید);
- در [4]: وابستگی وب [5] را انتخاب کنید تا باینریهای مورد نیاز برای سرویس وب ما در دسترس باشند؛
- در [6]: پروژه را تولید کنید. سپس یک فایل ZIP حاوی یک پروژه اسکلت Gradle تولید و برای دانلود در دسترس قرار میگیرد؛
در [2-3] چه چیزی باید وارد شود؟ ما قبلاً از وابستگیهای Gradle استفاده کردهایم. برای مثال، وابستگی مربوط به پروژه قبلی به شرح زیر بود:
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// از نسخه ۰.۱۱ پلاگین Gradle اندروید، باید از android-apt نسخه ۱.۳ یا بالاتر استفاده کنید
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
...
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
- خط ۲۲: یک وابستگی به شکل [groupId:artifactId:version] است. در فرم وبسایت [http://start.spring.io/] چه چیزی درخواست میشود:
- در [2] برابر است با [groupId];
- در [3]، [artifactId] است؛
فایل ZIP زیر را در پوشه «Other Projects» از حالت فشرده خارج کنید:
![]() | ![]() ![]() | ![]() |
با استفاده از اندروید استودیو، پروژه Gradle [server-01] [1-2] را باز کنید. پروژهای که باز میشود [3] (منظره پروژه) است.
1.16.1.2. پیکربندی Gradle
![]() |
فایل Gradle تولید شده (ژوئن ۲۰۱۶) به شرح زیر است:
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
eclipse {
classpath {
containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
}
}
- خطوط ۱۴ و ۳۴–۳۸ مربوط به پروژهٔ IDE در Eclipse هستند. ما آنها را حذف میکنیم؛
- خطوط ۱–۱۱ و ۱۵ برای افزودن پلاگینی به نام [spring-boot] به پروژه Gradle ما استفاده میشوند. Spring Boot یک پروژه در اکوسیستم Spring [http://projects.spring.io/spring-boot/] است. این پلاگین نسخههای وابستگیهایی را که معمولاً با Spring استفاده میشوند، تعریف میکند. این بدان معناست که ما نیازی به مشخص کردن نسخههای آنها (خطوط ۳۰ و ۳۱) نداریم. در این صورت، نسخه، همان نسخهای است که توسط Spring Boot مورد استفاده تعریف شده است (خط ۳);
- خطوط ۲۲–۲۳: نسخهٔ جاوای مورد استفاده، در این مورد نسخهٔ ۱.۸؛
- خطوط ۲۵–۲۷: مخازن باینری که برای دانلود وابستگیها استفاده میشوند؛
- خط ۲۶: مخزن مرکزی Maven را مشخص میکند. این در حال حاضر بزرگترین مخزن باینری متنباز موجود است؛
- خطوط ۲۹–۳۲: وابستگیهای مورد نیاز برای پروژه:
- خط ۳۰: این وابستگی شامل تمام باینریهای مورد نیاز برای ساخت یک سرویس وب Spring است؛
- خط ۳۱: این وابستگی شامل تمام باینریهای مورد نیاز برای تست، بهویژه برای تستهای JUnit است؛
- وابستگی [compile] نشان میدهد که این وابستگی برای کامپایل پروژه ضروری است. وابستگی [testCompile] نشان میدهد که این وابستگی صرفاً برای اجرای تستها ضروری است. بنابراین در باینری پروژه گنجانده نمیشود؛
ما با پاکسازی فایل Gradle شروع میکنیم:
//Spring Boot
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
//افزونهها
apply plugin: 'java'
apply plugin: 'spring-boot'
// باینری پروژه
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
//نسخههای جاوا
sourceCompatibility = 1.8
targetCompatibility = 1.8
//مخازن Maven
repositories {
mavenLocal()
mavenCentral()
}
// وابستگیها
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- خط ۳۰: مخزن محلی Maven برای ماشین توسعه را اضافه کردهایم. این مخزن هنگام نصب Maven ایجاد میشود (به بخش ۶.۱۰ مراجعه کنید). اگر وابستگی مورد نیاز از قبل در مخزن محلی Maven موجود باشد، از مخزن مرکزی Maven دانلود نخواهد شد؛
- خطوط ۱۹–۲۲: یک وظیفه Gradle برای تولید باینری پروژه. ما از این وظیفه برای مشاهده آنچه در حال انجام است استفاده خواهیم کرد؛
![]() | ![]() | ![]() |
- در [1-4]، وظیفه [jar] را که در فایل [build.gradle] تعریف شده است، اجرا کنید ([1] در بالا سمت راست و در کنار IDE قرار دارد)؛
مرحلهٔ قبلی آرشیو JAR پروژه را ایجاد کرده و آن را در پوشهٔ [build / libs] به نام [5] قرار میدهد:
![]() |
نام آرشیو مستقیماً از اطلاعات ارائهشده برای وظیفه [jar] در فایل [build.gradle] (خطوط ۱۹–۲۲) گرفته میشود.
تمام وابستگیهای پروژه را میتوان به صورت زیر مشاهده کرد:
![]() |
در [1] دیده میشود که تنها وابستگی پروژه [compile('org.springframework.boot:spring-boot-starter-web')] دهها باینری را به همراه خود آورده است. Spring Boot برای وب وابستگیهایی را که یک برنامه وب Spring، MVC، احتمالاً به آنها نیاز دارد، شامل شده است. این بدان معناست که برخی از آنها ممکن است غیرضروری باشند. Spring Boot برای یک آموزش ایدهآل است:
- وابستگیهایی را که احتمالاً به آنها نیاز خواهیم داشت، فراهم میکند؛
- این شامل یک سرور Tomcat داخلی [1] است که ما را از نیاز به استقرار برنامه روی یک سرور وب خارجی بینیاز میکند؛
شما میتوانید نمونههای فراوانی از استفاده از Spring Boot را در وبسایت اکوسیستم Spring به آدرس [http://spring.io/guides] بیابید.
اکنون فایل [build.gradle] را به شرح زیر تکمیل میکنیم:
// Spring Boot
...
// وابستگیها
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
// پلاگینی برای ایجاد باینری سازگار با Maven در مخزن محلی Maven
apply plugin: 'maven-publish'
publishing {
publications {
maven(MavenPublication) {
groupId 'istia.st.exemples.android'
artifactId 'server-01'
version '0.0.1-SNAPSHOT'
from components.java
}
}
repositories {
maven {
//تغییر را برای اشاره به مخزن خود، e.g، اعمال کنید. http://my.org/repo
url 'file://D:\\maven'
}
}
}
- خط ۱۰: ما یک پلاگین Gradle به نام [maven-publish] را وارد میکنیم که امکان انتشار باینری پروژه را مطابق با استانداردهای Maven در یک مخزن Maven فراهم میکند؛
- خط ۱۱: یک وظیفه Gradle به نام [publishing]؛
- خطوط ۱۴–۱۵: ویژگیهای باینری Maven که قرار است ایجاد شود؛
- خط ۲۳: مخزن Maven که پروژه در آن منتشر خواهد شد؛ در این مورد، یک مخزن محلی Maven؛
افزودن پلاگین [maven-publish]، وظایف جدیدی را در پروژه Gradle ایجاد کرده است:
![]() | ![]() |
اگر در [2]، وظیفه [publish] اجرا شود، باینری پروژه ایجاد شده و در پوشه مشخصشده در خط ۲۳ فایل [build.gradle] نصب میشود:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() |
وظیفه [jar] باینری پروژه را تولید میکند. این باینری وابستگیهای خود را شامل نمیشود و بنابراین قابل اجرا نیست. میتوان یک باینری تولید کرد که تمام وابستگیهای خود را شامل شود و قابل اجرا باشد. برای این کار، کد زیر را به فایل [build.gradle] اضافه میکنیم:
//ایجاد یک باینری با تمام وابستگیهای آن
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'istia.st.exemples.android.Server01Application'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
- خط ۶: نام کامل کلاس قابل اجرای پروژه را وارد کنید:
![]() |
کد این کلاس به شرح زیر خواهد بود:
package istia.st.exemples.android;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Server01Application {
public static void main(String[] args) {
System.out.println("Server01Application running");
//SpringApplication.run(Server01Application.class, args);
}
}
پروژه Gradle را تازه کنید و سپس وظیفه [fatJar] را اجرا کنید:
![]() | ![]() |
فایل باینری در پوشه [build / libs] تولید میشود و میتوان آن را به عنوان [1-7] اجرا کرد:
![]() | ![]() |
1.16.1.3. پیکربندی پروژه
پیکربندی Gradle کافی نیست. ما همچنین باید پروژه را پیکربندی کنیم. از آنجایی که این یک پروژه اندروید تولید شده توسط IDE نیست، این پیکربندی – که تا به حال انجام نداده ایم – باید در اینجا انجام شود.
![]() | ![]() |
- در [3-4]: از JDK 1.8 استفاده کنید؛
برای کامپایل پروژه، دکمهٔ موجود برای پروژههای اندروید دیگر وجود ندارد. ما از گزینهای در منوی [1-2] استفاده خواهیم کرد:
![]() | ![]() |
سپس، از خواننده دعوت میشود تا پروژه زیر را ایجاد کند. ما در مورد کد نهایی پروژه [3] توضیح خواهیم داد.
1.16.1.4. لایه [métier]
![]() |
![]() |
لایه [métier] رویکرد مشابهی با لایه [métier] در مثال قبلی دارد. این لایه دارای رابط [IMetier] زیر خواهد بود:
package exemples.android.server.metier;
public interface IMetier {
// شمارهٔ تصادفی در [a,b]
int getAlea(int a, int b);
}
- خط ۵: متدی که یک عدد تصادفی در [a,b] تولید میکند
کد کلاس [Metier] که این رابط را پیادهسازی میکند به شرح زیر است:
package exemples.android.server.metier;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class Metier implements IMetier {
@Override
public int getAlea(int a, int b) {
// برخی بررسیها
if (a < 0) {
throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0", 2);
}
if (b < 0) {
throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0", 3);
}
if (a >= b) {
throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b", 4);
}
//تولید نتیجه
Random random=new Random();
random.setSeed(new Date().getTime());
return a + random.nextInt(b - a + 1);
}
}
ما در مورد این کلاس توضیحی نمیدهیم: این کلاس مشابه مثالی است که در مثال قبلی دیدیم، با این تفاوت که به صورت تصادفی استثنا پرتاب نمیکند. فقط به آناوتیشن Spring در خط ۸، [@Service]، توجه کنید که تضمین میکند Spring این کلاس را به عنوان یک نمونه واحد (singleton) ایجاد کرده و مرجع آن را برای سایر کامپوننتهای Spring در دسترس قرار دهد. میتوانست از آناوتیشنهای دیگر Spring نیز برای دستیابی به همین اثر در اینجا استفاده شود. کامپوننتهای Spring دارای نامهای پیشفرض هستند که میتوانند بهعنوان یک ویژگی (attribute) در آنوتیشن (annotation) مورد استفاده مشخص شوند. بدون این ویژگی، مانند اینجا، نام کامپوننت Spring بر اساس نام کلاس، با حرف اول کوچک، انتخاب میشود. بنابراین، در اینجا، نام کامپوننت Spring بهطور پیشفرض [metier] است؛
کلاس [Metier] استثناهایی از نوع [AleaException] را پرتاب میکند:
package exemples.android.server.metier;
public class AleaException extends RuntimeException {
// کد خطا
private int code;
// سازندهها
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// گیرنده و تنظیمکننده
....
}
- خط ۳: [AleaException] کلاس [RuntimeException] را گسترش میدهد. بنابراین این یک استثنای گرفتهنشده است (نیازی به مدیریت آن با بلوک try/catch نیست)؛
- خط ۶: یک کد خطا به کلاس [RuntimeException] اضافه میشود؛
1.16.1.5. سرویس وب / jSON
![]() |
![]() |
سرویس وب / jSON توسط Spring MVC پیادهسازی شده است. اسپرینگ MVC الگوی معماری معروف به MVC (مدل–نما–کنترلکننده) را به شرح زیر پیادهسازی میکند:
![]() |
پردازش یک درخواست مشتری به شرح زیر انجام میشود:
- درخواست – URL درخواستی به شکل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... است. [Dispatcher Servlet] کلاس Spring است که URL ورودی را پردازش میکند. این URL را به اکشنی که باید آن را پردازش کند، مسیریابی میکند. این اکشنها متدهایی از کلاسهای خاصی هستند که [Contrôleurs] نامیده میشوند. حرف «C» در MVC در این مورد، رشته [Dispatcher Servlet, Contrôleur, Action] است. اگر هیچ اکشنی برای رسیدگی به URL ورودی پیکربندی نشده باشد، سرولت [Dispatcher Servlet] پاسخ خواهد داد که URL درخواستی یافت نشد (خطای ۴۰۴ NOT FOUND);
- پردازش
- عملکرد انتخابشده میتواند از پارامترهای parami که توسط servlet [Dispatcher Servlet] به آن ارسال شده است، استفاده کند. این پارامترها ممکن است از چندین منبع منشأ بگیرند:
- مسیر [/param1/param2/...] از URL،
- پارامترهای [p1=v1&p2=v2] از URL,
- پارامترهای ارسالشده توسط مرورگر در درخواست آن؛
- هنگام پردازش درخواست کاربر، ممکن است اقدام به لایه [metier] [2b] نیاز داشته باشد. پس از پردازش درخواست مشتری، ممکن است پاسخهای مختلفی ایجاد شود. یک مثال معمول عبارت است از:
- یک صفحهٔ خطا اگر درخواست نتوانست بهدرستی پردازش شود
- در غیر این صورت، یک صفحه تأیید
- این عمل دستور نمایش یک نمای مشخص را صادر میکند: [3]. این نما دادههایی را که به عنوان مدل نما شناخته میشوند، نمایش میدهد. این همان M در MVC است. اقدام این مدل M [2c] را ایجاد کرده و به نمایش یک نما V [3] دستور میدهد؛
- پاسخ – نمای انتخابشده V از مدل M ساختهشده توسط اقدام برای راهاندازی بخشهای پویا از پاسخی که باید به کلاینت ارسال کند، استفاده میکند و سپس این پاسخ را ارسال میکند.
برای یک سرویس وب / jSON، معماری قبلی کمی تغییر یافته است:
![]() |
- در [4a]، مدل—که یک کلاس جاوا است—توسط یک کتابخانه jSON به رشته jSON تبدیل میشود؛
- در [4b]، این رشته jSON به مرورگر ارسال میشود؛
یک مثال از سریالیسازی یک شیء جاوا به رشته jSON و دِسریالیسازی رشته jSON به یک شیء جاوا در ضمیمههای بخش 6.14 ارائه شده است.
بیایید به لایه [web] برنامه خود بازگردیم:
![]() |
در برنامهٔ ما تنها یک کنترلر وجود دارد:
![]() |
سرویس وب /jSON پاسخ خود را از نوع [Response] به مشتریانش به شرح زیر ارسال میکند:
package exemples.android.server.web;
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;
}
// گیرنده و تنظیمکننده
...
}
- خط ۱۳: فیلد [T body] پاسخی است که توسط کلاینت انتظار میرود. ما تصمیم گرفتهایم در اینجا به جای نوع Integer برای عدد تصادفی مورد انتظار، از یک پاسخ عمومی از نوع T استفاده کنیم. ما میخواهیم بتوانیم این کلاس را در موقعیتهای دیگر مجدداً استفاده کنیم. در حین پردازش درخواست مشتری، سرور ممکن است با مشکلی مواجه شود که سپس در دو فیلد دیگر خلاصه میشود؛
- خط ۸: یک کد وضعیت (۰ اگر خطایی وجود نداشته باشد)؛
- خط ۹: اگر status!=0 باشد، فهرستی از پیامهای خطا، معمولاً پیامهای مربوط به استک استثنا در صورت وقوع استثنا؛ null اگر خطایی وجود نداشته باشد؛
بررسی [WebController] به شرح زیر است:
package exemples.android.server.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import exemples.android.server.metier.IMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;
@Controller
public class WebController {
// لایه کسبوکار
@Autowired
private IMetier metier;
// نقشهساز JSON
@Autowired
private ObjectMapper mapper;
// اعداد تصادفی
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
// پاسخ
Response<Integer> response = new Response<>();
// لایه کسبوکار استفاده میشود
try {
response.setBody(metier.getAlea(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// ما پاسخ را بازمیگردانیم
return mapper.writeValueAsString(response);
}
private List<String> getMessagesFromException(Throwable e) {
// فهرست پیامها
List<String> messages = new ArrayList<String>();
// ما روی پشتهٔ خطاها تکرار میکنیم
Throwable th = e;
while (th != null) {
messages.add(e.getMessage());
th = th.getCause();
}
// بازگرداندن نتیجه
return messages;
}
}
- خط ۱۷: تذکر [@Controller] نشان میدهد که این کلاس یک کنترلر MVC است که متدهای آن درخواستهای مربوط به برخی URL از برنامه وب را مدیریت میکند؛
- خطوط ۲۱–۲۲: anotation [@Autowired] به Spring دستور میدهد تا یک کامپوننت از نوع [IMetier] را در فیلد تزریق کند. این همان کلاس [Metier] قبلی است. به این دلیل که ما انوتیشن [@Service] را روی آن اعمال کردهایم، به عنوان یک کامپوننت Spring در نظر گرفته میشود؛
- خطوط ۲۴–۲۵: ما همین کار را با یک مپر از نوع jSON انجام میدهیم که بعداً آن را تعریف خواهیم کرد. سرویس وب ما پاسخ خود را به صورت یک رشته jSON ارسال خواهد کرد. این مپر است که پاسخ را به jSON سریالیزه میکند؛
- خط ۳۰: متدی که عدد تصادفی تولید میکند. نام آن اهمیتی ندارد. وقتی اجرا میشود، پارامترهای آن توسط Spring MVC مقداردهی شدهاند. خواهیم دید چگونه. علاوه بر این، اگر این متد اجرا شود، به این دلیل است که وبسرور در خط ۲۸ درخواستی برای URL دریافت کرده است؛
- خط ۲۸: حاشیهنویسی [@RequestMapping] ویژگیهای خاصی از متد حاشیهنویسیشده را تعریف میکند:
- [value]: URL پذیرفتهشده توسط متد؛
- [method]: HTTP پذیرفتهشده توسط متد. عمدتاً دو مورد از اینها وجود دارد: GET و POST. روش [POST] زمانی استفاده میشود که مشتری بخواهد سندی را به درخواست HTTP خود پیوست کند؛
- [produces]: یکی از سربرگها را در پاسخ HTTP که به مشتری ارسال میشود، تنظیم میکند. در اینجا، در میان سربرگهای HTTP که همراه با پاسخ برای مشتری ارسال میشوند، یک سربرگ به آنها اطلاع میدهد که پاسخ به صورت یک رشته jSON ارسال میشود. این هدر الزامی نیست. این هدر برای اطلاعرسانی به کلاینت ارائه میشود، در صورتی که کلاینت انتظار پاسخهایی را دارد که ممکن است اشکال مختلفی داشته باشند؛
- [consumes]: در اینجا وجود ندارد. این هدر برای مشخص کردن هدرهای HTTP است که باید درخواست HTTP مشتری را همراهی کنند تا پذیرفته شود؛
- خط ۲۹: حاشیهنویسی [@ResponseBody] نشان میدهد که نتیجهی تولیدشده توسط متد باید به کلاینت ارسال شود. بدون این حاشیهنویسی، پاسخ متد بهعنوان کلیدی برای انتخاب صفحهی HTML که باید به کلاینت ارسال شود، در نظر گرفته میشود. در یک سرویس وب / jSON، هیچ صفحه HTML وجود ندارد؛
- خط ۲۸: URL پردازششده به شکل /{a}/{b} است، که در آن {x} یک متغیر را نشان میدهد. متغیرهای {a} و {b} در خط ۳۰ به پارامترهای متد تخصیص داده میشوند. این کار از طریق تگ @PathVariable("x") انجام میشود. توجه داشته باشید که {a} و {b} اجزای یک URL هستند و بنابراین از نوع String میباشند. تبدیل از String به نوع پارامتر ممکن است ناموفق باشد. سپس Spring MVC یک استثنا پرتاب میکند. خلاصه اینکه: اگر با استفاده از یک مرورگر، من URL /100/200 را درخواست کنم، متد getAlea در خط ۳۰ با پارامترهای عددی a=100, b=200 اجرا خواهد شد؛
- خط ۳۶: از لایه [métier] خواسته میشود یک عدد تصادفی در بازه [a,b] تولید کند. به یاد داشته باشید که متد [metier].getAlea ممکن است یک استثنا (exception) ایجاد کند؛
- خط ۳۷: خطا وجود ندارد؛
- خط ۳۹: کد خطا؛
- خط ۴۰: لیست پیامهای پاسخ همان لیست استک استثناها (خطوط ۴۶–۵۷) است. در اینجا میدانیم که استک تنها شامل یک استثنا است، اما میخواستیم یک روش عمومیتر را نشان دهیم؛
- خط ۴۳: پاسخ از نوع [Response<Integer>] به صورت یک رشته jSON بازگردانده میشود؛
1.16.1.6. پیکربندی پروژه Spring
![]() |
راههای مختلفی برای پیکربندی اسپرینگ وجود دارد:
- با فایلهای XML؛
- با کد جاوا؛
- با ترکیبی از هر دو؛
ما تصمیم گرفتهایم که اپلیکیشن وب خود را با استفاده از کد جاوا پیکربندی کنیم. کلاس [Config] زیر این پیکربندی را مدیریت میکند:
package exemples.android.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(basePackages = { "exemples.android.server.metier", "exemples.android.server.web" })
@EnableWebMvc
public class Config {
//پیکربندی وب ------------------------------------
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// jSON مَپر
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- خط ۱۲: به اسپرینگ میگوییم که دو کامپوننت مورد نیاز برای مدیریت را در کدام پکیجها پیدا کند:
- کامپوننت [Metier]، با تگ [@Service]، در پکیج [exemples.android.server.metier]؛
- کامپوننت [WebController]، که با تگ [@Controller] مشخص شده است، در پکیج [exemples.android.server.web]؛
- خط ۱۳: anotation [@EnableWebMvc] به Spring Boot اجازه میدهد تا تعدادی پیکربندی استاندارد را بهطور خودکار برای یک برنامه Spring با شناسه MVC اعمال کند. این امر بار کاری توسعهدهنده را کاهش میدهد؛
- خطوط 16، 22، 27 و 33: آناوتیشن [@Bean] نیز همانند دو آناوتیشن قبلی (@Service، @Controller) کامپوننتهای اسپرینگ (بیینها) را تعریف میکند. در اینجا، انوتیشن [@Bean] یک متد را به جای یک کلاس نشانهگذاری میکند و این نتیجه متد است که مؤلفه اسپرینگ را تشکیل میدهد. در غیاب یک ویژگی نامگذاری (naming attribute) در داخل انوتیشن [@Bean]، مؤلفه اسپرینگ ایجاد شده نام متد نشانهگذاری شده را به خود میگیرد؛
- خطوط 16–20: تعریف کردن بیان [dispatcherServlet]. این یک نام از پیش تعریفشده در Spring است، MVC، که کنترلکنندهٔ جلویی (front controller) برنامه، MVC، را تعریف میکند، یک شیء که تمام درخواستهای مشتری از طریق آن عبور میکنند و آنها را (از این رو نام آن) به اجزای مختلف [@Controller] از برنامه Spring MVC ارسال میکند؛
- خط ۱۸: بین [dispatcherServlet] نمونهای از کلاس [DispatcherServlet] است که توسط Spring MVC ارائه میشود؛
- خطوط ۲۲–۲۵: بیون [servletRegistrationBean] برای تعریف اینکه کدام URL توسط برنامه پذیرفته میشوند، استفاده میشود. در خط ۲۴، همه URL پذیرفته میشوند؛
- خطوط ۲۷–۳۰: بیان [embeddedServletContainerFactory] برای تعریف سرور تعبیهشده در وابستگیهای پروژه که میزبان برنامه وب خواهد بود، استفاده میشود. خط ۲۹ مشخص میکند که این یک سرور Tomcat است و بر روی پورت ۸۰۸۰ اجرا خواهد شد. به طور پیشفرض، باینریهای این وب سرور توسط وابستگی [org.springframework.boot:spring-boot-starter-web] در فایل Gradle فراهم میشوند؛
1.16.1.7. اجرای سرویس وب / jSON
![]() |
پروژه از طریق کلاس اجرایی زیر، [Boot]، اجرا میشود:
package exemples.android.server.boot;
import exemples.android.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Boot {
public static void main(String[] args) {
//اجرای برنامه
SpringApplication.run(Config.class, args);
}
}
- کلاس [Boot] یک کلاس قابل اجرا است (خطوط ۷–۱۰);
- خط ۹: متد استاتیک [SpringApplication.run] یک متد از کلاس [spring Boot] (خط ۴) است که برنامه را راهاندازی میکند. پارامتر اول آن کلاس جاوا است که پروژه را پیکربندی میکند. در این مورد، کلاس [Config] است که همیناکنون آن را توصیف کردیم. پارامتر دوم، آرایهٔ آرگومانهایی است که به متد [main] (خط ۷) پاس میشود؛
برنامه وب را میتوان به روشهای مختلفی اجرا کرد، از جمله موارد زیر:
![]() |
سپس تعدادی ورودی لاگ در کنسول ظاهر میشوند:
- خطوط ۱۲–۱۴: سرور جاسازیشده Tomcat راهاندازی میشود؛
- خطوط ۱۵–۱۹: سروِلِت Spring با شناسه [DispatcherServlet] بارگذاری و پیکربندی میشود؛
- خط ۲۰: URL [/{a}/{b}] وبسرور تشخیص داده میشود؛
اکنون، بیایید یک مرورگر را باز کرده و URL / jSON سرویس وب را آزمایش کنیم:
![]() |
![]() |
![]() |
![]() |
هر بار، ما نمایش jSON یک شیء از نوع [Response<Integer>] را به دست میآوریم.
به جای استفاده از یک مرورگر استاندارد، اکنون افزونه مرورگر کروم [Advanced Rest Client] (رجوع کنید به ضمیمهها، بند 6.13) را در نظر بگیرید:

- به [1]، URL درخواستی؛
- به [2]، با استفاده از GET؛
- به [3]، درخواست ارسال میشود؛

- در [4]، سربرگهای HTTP از پاسخ سرور دریافت میشود. توجه داشته باشید که این نشان میدهد سند ارسالشده یک رشته jSON است؛
- در [5]، رشته jSON دریافت شد؛
1.16.1.8. تولید فایل اجرایی JAR پروژه
در بخش 1.16.1.2، نشان دادیم چگونه فایل Gradle را پیکربندی کنیم تا یک فایل اجرایی برای برنامه به همراه تمام وابستگیهای آن تولید شود. این پیکربندی با توجه به این برنامه به شکل زیر درمیآید:
//یک باینری با تمام وابستگیهای آن ایجاد میکند
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'exemples.android.server.boot.Boot'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
برای تولید این فایل اجرایی، مراحل زیر را انجام دهید [1-5]:
![]() | ![]() |
برای اجرای آن، اگر سرویس وب در حال اجرا است، آن را متوقف کنید ([1])، سپس آرشیو را اجرا کنید ([2-4]):
![]() | ![]() |
یک مرورگر باز کنید و URL و [localhost:8080/100/200] را درخواست کنید. باید همان نتایج قبلی را دریافت کنید.
1.16.1.9. مدیریت لاگها
هنگام اجرای آرشیو اجرایی، متوجه خواهید شد که لاگها با زمانی که پروژه را از IDE اجرا میکنید، یکسان نیستند. لاگها در حالت [DEBUG] هستند:
...
09:32:03.741 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletConfigInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletContextInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:32:03.810 [main] INFO org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:32:03.813 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.984 seconds (JVM running for 2.206)
میتوانید با افزودن یک فایل [logback.xml] به پوشه [resources] پروژه، سطح لاگ را مدیریت کنید:
![]() |
این فایل میتواند محتوای زیر را داشته باشد:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!--به رمزگذارها بهطور پیشفرض نوع اختصاص داده میشود
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!--کنترل سطح لاگ -->
<root level="info"> <!--info، debug، warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
سطح لاگ در خط ۱۲ کنترل میشود. اگر اکنون آرشیو اجرایی را دوباره تولید کرده و آن را اجرا کنیم، تنها لاگهایی با سطح [info] دریافت میکنیم:
...
09:36:52.433 [main] INFO o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:36:52.923 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:36:52.926 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.865 seconds (JVM running for 2.203)
1.16.2. کلاینت اندروید سرور وب / jSON
کلاینت اندروید معماری زیر را خواهد داشت:
![]() |
کلاینت دارای دو مؤلفه خواهد بود:
- یک لایه [Présentation] (view+activity) مشابه لایهای که در مثال [Exemple-14] بررسی کردیم؛
- لایه [DAO] که با سرویس [web / jSON] که قبلاً بررسی کردیم، ارتباط برقرار میکند.
1.16.2.1. ایجاد پروژه
ما پروژه قبلی [Exemple-14] را با نام [Exemple-15] با دنبال کردن رویه بخش 1.4 کپی میکنیم. نتیجه زیر به دست میآید:
![]() | ![]() |
در ادامه، از خواننده دعوت میشود تا پروژه زیر را ایجاد کند.
1.16.2.2. پیکربندی Gradle
![]() |
فایل [build.gradle] به شرح زیر است:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// از نسخهٔ ۰.۱۱ افزونهٔ Gradle اندروید، باید از android-apt نسخهٔ ۱.۳ یا جدیدتر استفاده کنید
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 {
applicationId "exemples.android"
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 fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
ما فقط در مورد عناصری که قبلاً پوشش داده نشدهاند، توضیح میدهیم:
- خطوط ۴۶–۴۷: درج یک پلاگین AA. پلاگین [rest-spring-api] امکان واگذاری ارتباط کلاینت/سرور به کتابخانه AA را فراهم میکند؛
- خط ۵۰: کتابخانه [spring-android-rest-template] همان کتابخانهای است که توسط AA برای مدیریت ارتباط کلاینت/سرور استفاده میشود. نسخه [2.0.0.M3] یک نسخه موسوم به «milestone» است که در مخازن معمول Maven در دسترس نیست. بنابراین، در خطوط ۵۶–۵۹، باید مخزن مورد استفاده (خط ۵۸) را برای یافتن کتابخانه مشخص کنید؛
- خط ۵۱: یک کتابخانه jSON؛
- خطوط ۳۳–۳۹: بدون این ویژگی، هنگام تولید باینری پروژه APK خطاهایی رخ میدهد؛
1.16.2.3. بیانیهٔ برنامهٔ اندروید
![]() |
فایل [AndroidManifest.xml] باید بهروزرسانی شود. بهطور پیشفرض، دسترسی به اینترنت غیرفعال است. باید با استفاده از یک دستور ویژه فعال شود:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.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>
- خط ۵: دسترسی به اینترنت مجاز است؛
1.16.2.4. لایه [DAO]
![]() |
![]() |
1.16.2.4.1. رابط [IDao] لایه [DAO]
رابط لایه [DAO] به شرح زیر خواهد بود:
package exemples.android.dao;
public interface IDao {
// شمارهٔ تصادفی
int getAlea(int a, int b);
// URL از سرویس وب
void setUrlServiceWebJson(String url);
// حداکثر زمان انتظار پاسخ سرور (ms)
void setTimeout(int timeout);
// زمان انتظار کلاینت به میلیثانیه قبل از ارسال درخواست
void setDelay(int delay);
}
- خط ۶: متد سرویس وب / jSON برای دریافت یک عدد تصادفی در محدوده [a,b] این سرویس وب؛
- خط ۹: URL از سرویس وب / jSON برای تولید اعداد تصادفی؛
- خط ۱۲: یک تایماوت حداکثر در حین انتظار برای پاسخ سرور تنظیم میشود؛
- خط ۱۵: میخواهیم قبل از ارسال درخواست به سرور، یک تایماوت تنظیم کنیم تا به کاربر فرصت لغو درخواستش را بدهیم؛
1.16.2.4.2. رابط [WebClient]
![]() |
رابط [WebClient] ارتباط با سرویس وب را مدیریت میکند. کد آن به شرح زیر است:
package exemples.android.dao;
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.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// ۱ عدد تصادفی در بازه [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- خط ۱۲: [WebClient] یک رابط است که کتابخانه AA با استفاده از آنوتیشنهایی که به آن اضافه خواهیم کرد، خود آن را پیادهسازی خواهد کرد. این رابط باید فراخوانیهای URL را که توسط سرویس وب / jSON ارائه شدهاند، پیادهسازی کند:
// عدد تصادفی
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
- خط ۱۱: حاشیهنویسی [@Rest] یک حاشیهنویسی AA است. مقدار ویژگی [converters] یک آرایه از مبدلها است. در اینجا، مبدل [MappingJackson2HttpMessageConverter.class] تضمین میکند که وقتی سرور یک رشته jSON را ارسال میکند، بهطور خودکار سریالیزه شود. بنابراین، همانطور که در خط (d) مشاهده میشود، URL [/{a}/{b}] یک نوع String را بازمیگرداند که در واقع یک رشته jSON است (خط b). با استفاده از این اطلاعات، همراه با نوع مورد انتظار در خط ۱۶، نمونه [WebClient] کلاینت، رشته دریافتی را به نوع [Response<Integer>] تبدیل خواهد کرد؛
- خط ۱۵: یک حاشیهنویسی AA که نشان میدهد URL باید با استفاده از متد HTTP، GET، فراخوانی شود. پارامتر این حاشیهنویس [@Get]، قالب URL مورد انتظار سرویس وب است. بهسادگی پارامتر [value] را از انوتیشن [@RequestMapping] (خط b) متدی که در کنترلر [WebController] سرور فراخوانی شده است، دریافت کنید. کروشههای منحنی {} پارامترهای URL را در بر میگیرند که باید در پارامترهای متد در خط 16 گنجانده شوند. سینتکس [@Path("a") int a] به این معنی است که پارامتر متد [a] مقدار {a} از URL را دریافت میکند. وقتی پارامتر URL و پارامتر متد نام یکسانی دارند، مانند اینجا، میتوان آن را سادهتر به صورت [@Path int a] نوشت؛
در مورد یک پرسوجو HTTP POST، متد فراخوانی امضای زیر را خواهد داشت:
@Post("/{a}/{b}")
Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);
این حاشیهنویسی [@Body] است که مقدار ارسالشده را مشخص میکند. این مقدار بهطور خودکار به jSON سریال میشود. در سمت سرور، امضا به شکل زیر خواهد بود:
// اعداد تصادفی
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
- خط ۲: مشخص شده است که یک درخواست HTTP POST انتظار میرود و اینکه بدنه این درخواست (شیء ارسالشده) باید به صورت یک رشته jSON ارسال شود (ویژگی consumes);
- خط ۴: مقدار ارسالشده در پارامتر [@RequestBody T body] متد بازیابی خواهد شد؛
بیایید به کد کلاس [WebClient] بازگردیم:
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
- ما باید بتوانیم URL سرویس وب مورد نظر برای تماس را مشخص کنیم. این امر با گسترش رابط [RestClientRootUrl] که توسط AA ارائه میشود، محقق میگردد. این رابط یک متد [setRootUrl(urlServiceWeb] را در دسترس قرار میدهد که به ما امکان میدهد URL سرویس وبی را که باید با آن تماس گرفته شود، تنظیم کنیم؛
- علاوه بر این، ما میخواهیم تماس با سرویس وب را کنترل کنیم زیرا میخواهیم زمان انتظار پاسخ را محدود کنیم. برای این کار، ما رابط [RestClientSupport] را گسترش میدهیم که متد [setRestTemplate] را در دسترس قرار میدهد و به ما این امکان را میدهد که:
- خودمان شیء [RestTemplate] را ایجاد میکنیم که برای مدیریت ارتباط کلاینت/سرور استفاده میشود؛
- این شیء را پیکربندی کنید تا حداکثر زمان انتظار پاسخ را تنظیم کند؛
1.16.2.4.3. کلاس [Response]
متد [getAlea] از رابط [IDao] یک پاسخ از نوع [Response] را به شرح زیر بازمیگرداند:
package exemples.android.dao;
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;
}
// گیرنده و تنظیمکننده
...
}
این کلاس [Response] که پیش از این در سمت سرور استفاده شده است (بخش 1.16.1.5). در واقع، از منظر برنامهنویسی، گویی لایه [DAO] کلاینت مستقیماً با کنترلکننده [WebController] سرویس وب در ارتباط است:
![]() |
ارتباط شبکه بین کلاینت و سرور، و همچنین سریالیسازی و دِسریالیسازی اشیاء جاوا در سمت کلاینت، برای برنامهنویس شفاف است.
1.16.2.4.4. پیادهسازی لایه [DAO]
![]() |
رابط [IDao] با استفاده از کلاس زیر [Dao] پیادهسازی شده است:
package exemples.android.dao;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@EBean
public class Dao implements IDao {
// سرویس مشتری REST
@RestService
protected WebClient webClient;
// نقشهساز jSON
private ObjectMapper mapper = new ObjectMapper();
//زمان انتظار قبل از اجرای درخواست
private int delay;
//رابط IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setDelay(int delay) {
this.delay = delay;
}
}
- خط ۱۵: ما کلاس [Dao] را با آنوتیشن [@EBean] نشانهگذاری میکنیم تا آن را به یک بین (bean) AA تبدیل کنیم که میتوانیم در جای دیگری تزریق کنیم؛
- خطوط ۱۹–۲۰: ما پیادهسازی رابط [WebClient] را که شرح دادهایم، تزریق میکنیم. anotation [@RestService] این تزریق را تضمین میکند؛
- سایر متدها رابط [IDao] را پیادهسازی میکنند (خطوط 27–46);
متد [setTimeout]
روش [setTimeout] به شرح زیر است:
@Override
public void setTimeout(int timeout) {
// زمان انتظار درخواست مشتری روی REST تنظیم شده است
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// ساختار restTemplate را ایجاد میکند
RestTemplate restTemplate = new RestTemplate(factory);
//تبدیلکننده jSON را تنظیم کنید
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// restTemplate را برای کلاینت وب تنظیم کنید
webClient.setRestTemplate(restTemplate);
}
- رابط [WebClient] توسط کلاس AA با استفاده از وابستگی Gradle [org.springframework.android:spring-android-rest-template] پیادهسازی خواهد شد. [spring-android-rest-template] تعامل کلاینت با وب سرور / jSON را از طریق کلاسی از نوع [RestTemplate] پیادهسازی میکند؛
- خط ۴: کلاس [SimpleClientHttpRequestFactory] توسط وابستگی [spring-android-rest-template] فراهم میشود. این به ما امکان میدهد حداکثر زمان انتظار برای پاسخ سرور را تنظیم کنیم (خطوط ۵–۶);
- خط ۸: ما شیء از نوع [RestTemplate] را ایجاد میکنیم که بهعنوان واسطه ارتباط با سرویس وب عمل خواهد کرد. ما شیء [factory] را که همین حالا ایجاد شده است، بهعنوان پارامتر به آن میدهیم؛
- خط ۱۰: دیالوگ کلاینت/سرور میتواند اشکال مختلفی داشته باشد. دادهها از طریق خطوط متن مبادله میشوند و ما باید به شیء از نوع [RestTemplate] بگوییم که با هر خط متن چه کاری انجام دهد. برای این کار، ما مبدلها – کلاسهایی که قادر به پردازش خطوط متن هستند – را در اختیار آن قرار میدهیم. انتخاب مبدل معمولاً از طریق سربرگهای HTTP که همراه خط متن ارسال میشوند، انجام میشود. در اینجا، میدانیم که ما فقط خطوط متنی را در قالب jSON دریافت میکنیم. علاوه بر این، همانطور که در بخش 1.16.1.7 دیدیم، سرور هدر HTTP را ارسال کرده است:
Content-Type: application/json;charset=UTF-8
در خط ۱۰، تنها مبدل برای [RestTemplate]، یک مبدل jSON خواهد بود که با استفاده از کتابخانه [Jackson] پیادهسازی شده است. یک ویژگی خاص در مورد این مبدلها وجود دارد: AA از ما میخواهد که آن را در حاشیهنویسی برای کلاینت وب [WebClient] نیز بگنجانیم:
@Rest(converters = {MappingJacksonHttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
در خط ۱، ملزم به مشخص کردن یک مبدل هستیم، هرچند که قبلاً آن را به صورت برنامهای مشخص کردهایم.
- خط ۱۲: شیء [RestTemplate] که به این صورت ساخته شده است، در پیادهسازی رابط [WebClient] تزریق میشود، و این شیء است که ارتباط کلاینت/سرور را مدیریت خواهد کرد؛
متد [getAlea]
متد [getAlea] به شرح زیر است:
@Override
public int getAlea(int a, int b) {
//اجرای سرویس
Response<Integer> info;
DaoException ex;
try {
//در حال انتظار
waitSomeTime(delay);
//اجرای سرویس
info = webClient.getAlea(a, b);
int status = info.getStatus();
if (status == 0) {
//نتیجه را بازمیگرداند
return info.getBody();
} else {
//ثبت خطا
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// استثنا را ثبت کنید
ex = new DaoException(e, 100);
}
// انداختن استثنا
throw ex;
}
...
// متدهای خصوصی -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- خط ۸: منتظر [delay] میلیثانیه بمانید؛
- خط ۱۰: به سادگی متد با همان امضا را در کلاسی که رابط [WebClient] را پیادهسازی میکند، فراخوانی کنید؛
- خط ۱۱: پاسخ دریافتی از سرور با بررسی [status] آن تحلیل میشود؛
- خطوط ۱۲–۱۴: اگر خطای سمت سرور وجود نداشت (status=0)، در این صورت نتیجه متد را برمیگردانیم؛
- خط 17: اگر خطایی از سمت سرور (status!=0) وجود داشته باشد، یک استثنا آماده میشود اما پرتاب نمیشود. سرور فهرستی از پیامهای خطا ارسال کرده است. ما یک استثنا ایجاد میکنیم که پیام آن صرفاً عبارت jSON از فهرست پیامهای سرور است؛
- خطوط ۱۹–۲۲: موارد استثنای دیگر؛
- خط ۲۴: وقتی به این نقطه میرسیم، یک استثنا باید رخ داده باشد. بنابراین آن را پرتاب میکنیم؛
استثنای [DaoException] که توسط این کد استفاده میشود به شرح زیر است:
package exemples.android.dao;
import java.util.ArrayList;
import java.util.List;
public class DaoException extends RuntimeException {
// کد خطا
private int code;
// سازندهها
public DaoException() {
}
public DaoException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public DaoException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۶: استثنای [DaoException] یک استثنای مدیریتنشده است؛
متد [setUrlServiceWebJson]
متد [setUrlServiceWebJson] به شرح زیر است:
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
//مقدار URL سرویس REST تنظیم میشود
webClient.setRootUrl(urlServiceWebJson);
}
- خط ۴: URL سرویس وب از طریق متد [setRootUrl] از رابط [WebClient] تنظیم میشود. این متد وجود دارد زیرا این رابط از رابط [RestClientRootUrl] ارث میبرد؛
1.16.2.5. پکیج [architecture]
پکیج [architecture] شامل عناصری است که ساختار برنامه را تشکیل میدهند:
![]() |
![]() |
1.16.2.5.1. رابط [IMainActivity]
رابط [IMainActivity] روشهایی را که فعالیت برنامه باید پیادهسازی کند، فهرست میکند:
package exemples.android.architecture;
import exemples.android.dao.IDao;
public interface IMainActivity extends IDao {
//دسترسی به جلسه
Session getSession();
// تغییر نما
void navigateToView(int position);
// انتظار
void beginWaiting();
void cancelWaiting();
// حالت اشکالزدایی
boolean IS_DEBUG_ENABLED = true;
// اتمام زمان پاسخ
int TIMEOUT = 1000;
// همجواری قطعات
int OFF_SCREEN_PAGE_LIMIT = 1;
}
- خط ۵: رابط [IMainActivity] رابط [IDao] را گسترش میدهد؛
- خطوط ۱۳–۱۶: علاوه بر متدهای موجود در مثالهای قبلی (خطوط ۷–۱۱)، ما دو متد برای مدیریت تصویر بارگذاری برنامه (خطوط ۱۴، ۱۶) اضافه کردهایم؛
- خط ۲۱: ما حداکثر زمان انتظار برای پاسخ سرور را روی ۱ ثانیه تنظیم میکنیم؛
1.16.2.5.2. کلاس [Utils]
کلاس [Utils] شامل متدهای کمکی ایستا است که میتوان از بخشهای مختلف معماری برنامه آنها را فراخوانی کرد:
package exemples.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 getMessagesForAlert(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();
}
}
- خطوط ۹–۱۸: لیست پیامهای خطا را که در Throwable موجود است ایجاد میکند؛
- خطوط ۲۱–۳۲: بر پایه روش قبلی، از فهرست پیامهای بهدستآمده، متن نمایشی در یک پیام هشدار اندروید را میسازد؛
- خطوط ۲۷–۲۸: پیامها شمارهگذاری میشوند. کمترین شماره (۱) مربوط به اولین استثنا و بیشترین شماره مربوط به جدیدترین استثنا در پشته استثناها است؛
1.16.2.5.3. کلاس انتزاعی [AbstractFragment]
کلاس [AbstractFragment] دو هدف دارد:
- تا اطمینان حاصل شود که متد [updateFragments] کلاسهای فرزند هنگام نمایش قطعه، همیشه و تنها یک بار فراخوانی شود؛
- استخراج وضعیت و متدهای کلاسهای فرزند که قابل استخراج هستند؛
هدف دوم ما را به قرار دادن عملیات مدیریت تصویر جایگیر در این کلاس وادار میکند: همهٔ قطعات در یک برنامهٔ اندروید ناهمزمان باید با این نوع مسئله سروکار داشته باشند:
// مدیریت حالت انتظار
protected void beginWaiting() {
// نمایش ساعت شنی
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// حذف ساعت شنی
mainActivity.cancelWaiting();
}
1.16.2.6. ویو
![]() |
1.16.2.6.1. ویو [vue1.xml]
![]() |
در مقایسه با مثال قبلی، نما [vue1.xml] به شرح زیر تغییر میکند:
![]() |
![]() |
- در [1]، کاربر باید URL سرویس وب و همچنین زمانانتظار [2] را قبل از هر فراخوانی به سرویس وب مشخص کند؛
- در [3]، پاسخها شمرده میشوند؛
- در [4]، کاربر میتواند درخواست خود را لغو کند؛
- در [5]، هنگامی که اعداد درخواست میشوند، یک نشانگر انتظار نمایش داده میشود. این نشانگر پس از دریافت تمام اعداد یا لغو عملیات، پاک میشود؛

- در [6]، اعتبار ورودیها بررسی میشود؛
از کاربر خواسته میشود تا فایل [vue1.xml] را از میان مثالها بارگذاری کند. برای بقیه این بخش، شناسههای اجزای جدید را ارائه میدهیم:

دکمههای [10-11] بهصورت فیزیکی روی هم قرار دارند. در هر لحظه، تنها یکی از این دو قابل مشاهده خواهد بود.
1.16.2.6.2. قطعه [Vue1Fragment]
![]() |
اسکلت قطعه [Vue1Fragment] به شرح زیر است:
package exemples.android.fragments;
import android.app.AlertDialog;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.*;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.*;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.api.BackgroundExecutor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// عناصر رابط کاربری بصری
@ViewById(R.id.editTextUrlServiceWeb)
EditText edtUrlServiceRest;
@ViewById(R.id.textViewErreurUrl)
TextView txtMsgErreurUrlServiceWeb;
@ViewById(R.id.editTextDelay)
EditText edtDelay;
@ViewById(R.id.textViewErreurDelay)
TextView textViewErreurDelay;
@ViewById(R.id.lst_reponses)
ListView listReponses;
@ViewById(R.id.txt_Reponses)
TextView infoReponses;
@ViewById(R.id.edt_nbaleas)
EditText edtNbAleas;
@ViewById(R.id.edt_a)
EditText edtA;
@ViewById(R.id.edt_b)
EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
TextView txtErrorIntervalle;
@ViewById(R.id.btn_Executer)
Button btnExecuter;
@ViewById(R.id.btn_Annuler)
Button btnAnnuler;
...
// دادههای محلی
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
@AfterViews
void afterViews() {
// حافظه
afterViewsDone=true;
// در ابتدا هیچ پیام خطایی وجود ندارد
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
//[Annuler] دکمه مخفی
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
// فهرست پاسخها
reponses = new ArrayList<>();
}
...
- خطوط ۲۴–۴۹: ارجاع به اجزای نما [vue1.xml] (خط ۲۰);
- خطوط ۵۵–۶۹: متد [@AfterViews] زمانی اجرا میشود که ارجاعات در خطوط ۲۴–۴۹ مقداردهی اولیه شدهاند؛
- خط ۵۸: این را فراموش نکنید – برای چرخه عمر قطعه ضروری است؛
- خطوط ۶۰–۶۳: پیامهای خطا پنهان شدهاند؛
- خطوط ۶۵–۶۶: دکمه [Annuler] (خط ۶۵) مخفی شده و دکمه [Exécuter] (خط ۶۶) نمایش داده میشود. توجه داشته باشید که آنها از نظر فیزیکی روی هم قرار دارند؛
- خط ۶۸: فیلد در خط ۵۲ شامل فهرست رشتههای متنی خواهد بود که توسط متد ListView برای پاسخها نمایش داده میشوند؛
بلافاصله پس از متد [@AfterViews]، متد زیر [updateFragment] اجرا خواهد شد:
@Override
protected void updateFragment() {
// آداپتور لیست پاسخها ایجاد میشود
adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
- خطوط ۴–۵: آداپتور ListView برای پاسخها ایجاد میشود. این آداپتور در یک متغیر نمونه ذخیره میشود تا برای سایر متدهای کلاس در دسترس باشد؛
کلیک روی دکمه [Exécuter] اجرای متد زیر را آغاز میکند:
// ورودیها
private int nbAleas;
private int a;
private int b;
private String urlServiceWebJson;
private int delay;
// دادههای محلی
private int nbInfos;
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
private boolean hasBeenCanceled;
@Click(R.id.btn_Executer)
protected void doExecuter() {
//پاک کردن پاسخهای قبلی
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// شمارنده پاسخ را روی 0 تنظیم مجدد کنید
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// اعتبار ورودیها را بررسی میکند
if (!isPageValid()) {
return;
}
// ابتدای فعالیت
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// درخواست اعداد تصادفی
for (int i = 0; i < nbAleas; i++) {
getAlea(a, b);
}
// شروع انتظار
beginWaiting();
}
@Background(id = "alea")
void getAlea(int a, int b) {
// در اینجا باید تا حد امکان کمترین کار انجام شود
//در هر صورت، هیچ خروجی وجود ندارد – این باید در UiThead رسیدگی شود
try {
// نتیجه در UiThread نمایش داده میشود
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
//استثنا در UiThread نمایش داده میشود
showAlert(e);
}
}
- خطوط 17–18: لیست قبلی پاسخها از سرور پاک میشود. برای این کار، در خط 17، منبع داده [reponses] که با آداپتور ListView مرتبط است، خالی میشود؛
- خط ۱۹: یک متغیر بولی که به ما خواهد گفت آیا کاربر درخواست خود را لغو کرده است یا خیر؛
- خطوط 21–22: یک شمارنده که روی صفر تنظیم شده است برای نمایش تعداد پاسخها نمایش داده میشود؛
- خطوط ۲۴–۲۶: ما ورودیها را از رکوردهای [2-6] بازیابی کرده و اعتبار آنها را بررسی میکنیم. اگر هر یک از آنها نامعتبر باشد، متد متوقف میشود (خط ۲۵) و کاربر به رابط کاربری گرافیکی بازگردانده میشود؛
- خطوط ۲۸–۲۹: اگر تمام دادههای وارد شده معتبر باشند، URL سرویس وب (خط ۲۸) به همراه دوره زمانی انتظار قبل از هر فراخوانی به سرویس (خط ۲۹) به فعالیت ارسال میشود. این اطلاعات توسط لایه [DAO] مورد نیاز است و باید توجه داشت که این فعالیت است که با این لایه ارتباط برقرار میکند؛
- خطوط ۳۱–۳۳: اعداد تصادفی یکی یکی از متد [getAlea] در خط ۳۹ درخواست میشوند؛
- خط ۳۸: متد [getAlea] با حاشیهنویسی AA [@Background] علامتگذاری شده است، که به این معنی است که این متد در یک نخ (thread) متفاوت اجرا خواهد شد (جریان اجرایی، فرآیند) دیگری غیر از آنی که رابط کاربری بصری در آن در حال اجرا است. در واقع، اجرای هر فراخوانی اینترنتی در رشتهای متفاوت از رشته رابط کاربری بصری الزامی است. بنابراین، در هر لحظه ممکن است چندین رشته وجود داشته باشد:
- آن که رابط کاربری UI (رابط کاربری) را نمایش میدهد و رویدادهای آن را مدیریت میکند،
- رسانههای [nbAleas] که هر یک یک عدد تصادفی را از سرویس وب درخواست میکنند. این رشتهها بهصورت ناهمزمان راهاندازی میشوند: رشته UI یک رشته [getAlea] را راهاندازی میکند (خط ۳۲) که یک عدد تصادفی را از سرویس وب درخواست میکند و منتظر اتمام آن نمیماند. اتمام آن از طریق یک رویداد به آن اطلاع داده میشود. بنابراین، نخهای [nbAleas] بهصورت موازی راهاندازی خواهند شد. میتوان برنامه را طوری پیکربندی کرد که تنها یک نخ را در هر زمان راهاندازی کند. در این صورت، صفّی از نخها برای اجرا وجود دارد؛
خط ۳۸: پارامتر [id] نامی را به نخ تولید شده اختصاص میدهد. در اینجا، تمام رشتههای [nbAleas] نام یکسانی دارند: [alea]. این به ما امکان میدهد تا همه آنها را همزمان لغو کنیم. این پارامتر اختیاری است اگر لغو رشته مدیریت نشود؛
- خط ۴۴: متد [getAlea] فعالیت فراخوانی میشود. بنابراین، این متد در یک نخ جداگانه از نخ UI فراخوانی خواهد شد. این متد تماس با سرویس وب را برقرار میکند و منتظر پاسخ نمیماند. بعداً از طریق یک رویداد به آن اطلاع داده میشود که پاسخ در دسترس است. در همین نقطه است که، در خط ۴۴، متد [showInfo] با پاسخ دریافتی بهعنوان پارامتر فراخوانی خواهد شد؛
- خطوط ۴۵–۴۷: اجرای درخواست وب ممکن است یک استثنا (exception) ایجاد کند. سپس درخواست میکنیم که پیامهای خطای مربوط به استثنا در یک پیام هشدار نمایش داده شوند؛
- خط ۳۵: منتظر نتایج میمانیم:
- یک نشانگر بارگذاری نمایش داده خواهد شد؛
- دکمه [Annuler] جایگزین دکمه [Exécuter] میشود. از آنجا که نخهای راهاندازیشده غیرهمزمان هستند، نخ UI منتظر آنها نمیماند و خط ۳۵ قبل از اتمام آنها اجرا میشود. پس از اتمام متد [beginWaiting]، متد UI میتواند بار دیگر به ورودی کاربر، مانند کلیک روی دکمه [Annuler]، پاسخ دهد. اگر نخهای راهاندازیشده همگام بودند، خط ۳۵ تنها زمانی قابل دسترسی بود که همه نخها به پایان رسیده باشند. در این صورت لغو آنها دیگر معنایی نداشت؛
متد [showInfo] به شرح زیر است:
@UiThread
protected void showInfo(int alea) {
if (!hasBeenCanceled) {
// اطلاعات بیشتر
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
//کارمان تمام شد؟
if (nbInfos == nbAleas) {
//پایان انتظار
cancelWaiting();
}
// افزودن اطلاعات به فهرست پاسخها
reponses.add(0, String.valueOf(alea));
// نمایش پاسخها
adapterReponses.notifyDataSetChanged();
}
}
- متد [showInfo] در داخل تِرد [getAlea] که با [@Background] نشانهگذاری شده است، فراخوانی میشود. این متد رابط کاربری بصری UI را بهروزرسانی خواهد کرد. این کار را تنها زمانی میتواند انجام دهد که در داخل نخ UI اجرا شود. این معنای حاشیهنویسی [@UiThread] در خط ۱ است؛
- خط ۲: متد یک عدد تصادفی دریافت میکند؛
- خط ۳: بدنه متد تنها در صورتی اجرا میشود که کاربر درخواست خود را لغو نکرده باشد؛
- خطوط ۵–۶: شمارنده پاسخ افزایش یافته و نمایش داده میشود؛
- خطوط ۸–۱۱: اگر تمام پاسخهای مورد انتظار دریافت شده باشند، انتظار خاتمه مییابد (پایان سیگنال انتظار؛ دکمه [Exécuter] جایگزین دکمه [Annuler] میشود)؛
- خطوط ۱۲–۱۵: عدد تصادفی دریافتی به لیست پاسخهای نمایشدادهشده توسط کامپوننت [ListView listReponses] اضافه میشود و لیست تازهسازی میشود؛
روش [showAlert] به شرح زیر است:
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
//لغو همه چیز
doAnnuler();
//آن را نمایش دهد
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
منطق مشابه روش [showInfo] است:
- خط ۱: تگ [@UiThread] الزامی است؛
- خط ۲: متد استثناء رخداده را دریافت میکند؛
- خط ۳: این متد تنها در صورتی اجرا میشود که کاربر درخواست خود را لغو نکرده باشد؛
- خط ۵: درخواست کاربر لغو میشود، گویی که خودشان دکمه [Annuler] را فشار دادهاند؛
- خط ۷: هشدار با استفاده از کلاس اندروید [AlertDialog] نمایش داده میشود:
- [activity]: فعالیتی از نوع [Activity] است که در کلاس والد [AbstractFragment] ذخیره شده است؛
- [setTitle]: عنوان پنجرهٔ هشدار [1] را تنظیم میکند؛
- [setMessage]: پیامی را که توسط پنجره هشدار [2] نمایش داده میشود، تعیین میکند؛
- [setNeutral]: دکمهای را که پنجره هشدار [3] را میبندد، تعیین میکند؛
- [show]: درخواست نمایش پنجره هشدار را دارد؛
![]() |
یک «کلیک» روی دکمه [Annuler] با استفاده از روش زیر پردازش میشود:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
//حافظه
hasBeenCanceled=true;
// وظیفهٔ ناهمزمان لغو میشود
BackgroundExecutor.cancelAll("alea", true);
// پایان انتظار
cancelWaiting();
}
- خط ۴: ثبت میکند که کاربر درخواست خود را لغو کرده است؛
- خط ۶: تمام وظایفی را که با رشته [alea] مشخص شدهاند لغو میکند. پارامتر دوم، [true]، به این معنی است که آنها باید لغو شوند حتی اگر قبلاً اجرا شده باشند. شناسهی [alea] همان شناسه است که برای مشخص کردن متد [getAlea] در قطعه (خط ۱ زیر) استفاده میشود:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
توجه: مشخص شد که خط ۶ کد برای متد [doAnnuler] به درستی کار نمیکرد. به همین دلیل متغیر بولین [hasBeenCanceled] اضافه شد. در واقع، در صورت رخ دادن خطا (دسترس نبودن سرور)، پنجره هشدار n بار ظاهر میشد اگر اعداد تصادفی n درخواست شده بودند.
1.16.2.7. فعالیت [MainActivity]
![]() |
1.16.2.7.1. نما [activity-main.xml]
![]() |
در مقایسه با مثال قبلی، ما یک تصویر بارگذاری به نمای مرتبط با فعالیت [MainActivity] اضافه کردهایم:
...
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
<!-- تصویر در حال انتظار -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- تصویر نگهدارنده -->
</android.support.design.widget.AppBarLayout>
...
- خطوط 17–21: تصویر بارگذاری؛
1.16.2.7.2. فعالیت [MainActivity]
فعالیت [MainActivity] تغییر چندانی نسبت به آنچه در [Exemple-14] بود، نداشته است. ابتدا لایه [DAO] به آن تزریق میشود:
//تزریق DAO
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// پیکربندی لایه [DAO]
setTimeout(TIMEOUT);
}
- خطوط ۲–۳: تزریق لایه [DAO] از طریق یک حاشیهنویسی AA؛
- خطوط ۵–۱۳: کدی که پس از این تزریق اجرا میشود؛
- خط ۱۲: لایه timeout از [DAO] تنظیم میشود
علاوه بر این، فعالیت [MainActivity] باید رابط [IMainActivity] را پیادهسازی کند، که خود رابط [IDao] را گسترش میدهد:
// پیادهسازی IMainActivity --------------------------------------------------------------------
@Override
public void navigateToView(int position) {
//نمایش نمای موقعیت
if (mViewPager.getCurrentItem() != position) {
// نمایش قطعه
mViewPager.setCurrentItem(position);
}
}
// مدیریت تصویر جایگیر
public void cancelWaiting() {
loadingPanel.setVisibility(View.INVISIBLE);
}
public void beginWaiting() {
loadingPanel.setVisibility(View.VISIBLE);
}
// پیادهسازی IDao --------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
//اجرا
return dao.getAlea(a, b);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
1.16.2.8. اجرای پروژه
سرویس وب را راهاندازی کنید (بخش 1.16.1.7) و سپس کلاینت اندروید را اجرا کنید:

برای اطلاع از اینکه چه چیزی را باید در [1] وارد کنید، مراحل زیر را دنبال کنید. یک پنجرهٔ فرمان (command prompt) را باز کرده و دستور زیر را تایپ کنید:
C:\Program Files\Console2>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 3 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet VirtualBox Host-Only Network :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::e481:1583:cd2a:c47%27
Adresse IPv4. . . . . . . . . . . . . .: 192.168.82.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet VirtualBox Host-Only Network #2 :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::8191:14ad:407d:b840%54
Adresse IPv4. . . . . . . . . . . . . .: 192.168.64.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet Ethernet :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::d972:ad53:3b8a:263f%28
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . : uang ad.univ-angers.fr univ-angers.fr
اگر [GenyMotion] را نصب کرده باشید، ماشین مجازی VirtualBox آدرسهای IP را به رایانه شما (خطوط ۱۰ و ۱۸) اضافه کرده است. این آدرسها به ویژه مفید هستند زیرا توسط فایروال ویندوز مسدود نمیشوند. خط ۳۰ آدرس IP را برای رایانه شما در یک شبکه محلی ارائه میدهد. برای استفاده از این آدرس، معمولاً باید فایروال ویندوز را غیرفعال کنید. اگر به یک شبکه وایفای متصل هستید، از آدرس وایفای استفاده کنید و دوباره، اگر فایروال دارید، آن را غیرفعال کنید.
اپلیکیشن را در سناریوهای زیر آزمایش کنید:
- ۱۰۰ عدد تصادفی در بازه [1000, 2000] بدون محدودیت زمانی؛
- ۲۰۰۰ عدد تصادفی در بازه [10000, 20000] بدون محدودیت زمانی، و لغو انتظار قبل از اتمام تولید؛
- ۵ عدد تصادفی در بازه [100, 200] با زمان انتظار ۵۰۰۰ میلیثانیه، و لغو انتظار قبل از اتمام تولید؛
1.16.2.9. پردازش لغو
برای ردیابی آنچه هنگام درخواست کاربر برای لغو یا زمانی که لغو توسط یک استثنا فعال میشود رخ میدهد، متد زیر را به رابط [IDao] اضافه میکنیم (به بخش 1.16.2.4.1 مراجعه کنید):
package exemples.android.dao;
public interface IDao {
...
// حالت اشکالزدایی
void setDebugMode(boolean isDebugEnabled);
}
در کلاس [Dao]، کد زیر را اضافه میکنیم:
// حالت اشکالزدایی
private boolean isDebugEnabled;
// نام کلاس
private String className;
..
// سازنده
public Dao() {
// نام کلاس
className = getClass().getSimpleName();
}
...
// رابط IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// لاگ
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// اجرای سرویس
Response<Integer> info;
...
@Override
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
- خط ۹: نام کلاس را یادداشت میکنیم؛
- خطوط ۱۶–۱۸: هر بار که متد [getAlea] فراخوانی میشود، یک ورودی لاگ مینویسیم؛
علاوه بر این، در قطعه [Vue1Fragment]، موارد لاگ زیر را اضافه میکنیم:
@UiThread
protected void showInfo(int alea) {
// لاگ
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
....
}
@UiThread
protected void showAlert(Throwable th) {
// لاگ
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
...
}
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
...
}
هر بار که قطعه [Vue1Fragment] اطلاعاتی را از لایه [DAO] دریافت میکند، یک ورودی لاگ ایجاد میشود. علاوه بر این، هنگامی که متد [doAnnuler] فراخوانی میشود، رویداد ثبت میگردد.
آزمون ۱
در حالی که سرور راهاندازی نشده است، پنج عدد درخواست میشود. لاگهای زیر تولید میشوند:
- خطوط ۱–۵: متد [getAlea] از کلاس [Dao] پنج بار فراخوانی میشود. توجه به این نکته ضروری است که اینها فراخوانیهای غیرهمزمان (asynchronous) هستند که توسط قطعه [VueFragment] انجام میشوند و این قطعه منتظر نتیجه فراخوانی خود نمیماند؛
- خط ۷: اولین درخواست، HTTP، انجام شده و قطعه [VueFragment] اولین استثنای خود را دریافت کرده است؛
- خط ۸: سپس درخواست میکند که همه درخواستها لغو شوند؛
- خطوط ۹–۱۲: با این حال، میبینیم که چهار استثنای زیر را دریافت میکند. بنابراین، تمام درخواستهای ناهمزمانی که در انتظار بودند، اجرا شدهاند؛
آزمون ۲
حالا سرور را راهاندازی کرده و با تأخیر ۵ ثانیهای ۵ عدد را درخواست میکنیم، سپس قبل از پایان آن تأخیر روی [Annuler] کلیک میکنیم. لاگها به شرح زیر است:
- خطوط ۱–۵: متد [getAlea] از کلاس [Dao] پنج بار فراخوانی میشود؛
- خط ۷: کاربر درخواست لغو درخواستها را داده است؛
- خط ۸: میبینیم که [Vue1_Fragment] پنج مقدار دریافت میکند. بار دیگر، تمام درخواستهای ناهمزمان در انتظار اجرا شدهاند؛
به همین دلیل مجبور شدیم متغیر بولی [hasBeenCanceled] را مدیریت کنیم تا هنگام درخواست لغو، هیچ چیزی نمایش داده نشود. در کد لغو:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// حافظه
hasBeenCanceled = true;
// وظیفهٔ ناهمزمان لغو شد
BackgroundExecutor.cancelAll("alea",true);
//پایان انتظار
cancelWaiting();
}
کد در خط ۱۰ همانطور که انتظار میرود رفتار نمیکند. این ممکن است به این دلیل باشد که وظایف ناهمزمان، متد یکسانی را با تگ [@Background] به اشتراک میگذارند:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. مثال ۱۶: مدیریت ناهمزمانی با RxAndroid
اکنون پیشنهاد میکنیم که رفتار غیرهمزمان مورد نیاز برنامههای اندروید را با استفاده از کتابخانهای به نام RxJava [http://reactivex.io/] و نسخه مشتق آن برای محیط اندروید، [RxAndroid]، مدیریت کنیم. برای این کار، از دوره [Introduction à RxJava. Application aux environnements Swing et Android] استفاده خواهیم کرد.
1.17.1. ایجاد پروژه
ما پروژه [Exemple-1] را در [Exemple-16] کپی میکنیم:
![]() | ![]() |
1.17.2. پیکربندی Gradle
![]() |
در [build.gradle]، ما وابستگی به کتابخانه [RxAndroid] را اضافه میکنیم:
dependencies {
...
compile 'io.reactivex:rxandroid:1.2.0'
}
1.17.3. لایه [DAO]
![]() |
1.17.4. رابط [IDao]
رابط [IDao] به شرح زیر درمیآید:
package exemples.android.dao;
import rx.Observable;
public interface IDao {
// عدد تصادفی
Observable<Integer> getAlea(int a, int b);
// URL از سرویس وب
void setUrlServiceWebJson(String url);
// حداکثر زمان انتظار (میلیثانیه) برای پاسخ سرور
void setTimeout(int timeout);
// زمان انتظار کلاینت به میلیثانیه قبل از ارسال درخواست
void setDelay(int delay);
// حالت اشکالزدایی
void setDebugMode(boolean isDebugEnabled);
}
- خط ۸: متد [getAlea] اکنون از کتابخانه RxJava (خط ۳) نوع [Observable] را برمیگرداند. اصل به شرح زیر است:
یک جریان از عناصر از نوع Observable<T> توسط یک یا چند مشترک (ناظر، مصرفکننده) از نوع Subscriber<T> مشاهده میشود. کتابخانه RxJava به جریان Observable<T> اجازه میدهد تا در یک نخ T1 و ناظر Subscriber<T> آن در یک نخ T2 اجرا شود، بدون اینکه توسعهدهندهنیازی به نگرانی در مورد مدیریت چرخه عمر این نخها یا رسیدگی به مسائل ذاتاً دشوار، مانند اشتراکگذاری دادهها بین نخها و همگامسازی آنها برای اجرای یک وظیفه کلی، نخواهد داشت. بنابراین، این امر برنامهنویسی ناهمزمان را تسهیل میکند.
1.17.5. کلاس [AbstractDao]
ما کلاس [Dao] را از کلاس زیر [AbstractDao] مشتق خواهیم کرد:
package exemples.android.dao;
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 interface IRequest<T> {
Response<T> getResponse();
}
// درخواست عمومی
protected <T> Observable<T> getResponse(final IRequest<T> request) {
//اجرای سرویس
return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
//اجرای سرویس
try {
// درخواست همزمان انجام میشود و پاسخ به مشترک ارسال میشود
Response<T> response = request.getResponse();
// خطا؟
int status = response.getStatus();
if (status != 0) {
// استثناء ثبت میشود
ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
} else {
// ارسال پاسخ
subscriber.onNext(response.getBody());
// سیگنال پایان مشاهدهپذیر را ارسال کنید
subscriber.onCompleted();
}
} catch (JsonProcessingException | RuntimeException e) {
// استثناء ثبت میشود
ex = new DaoException(e, 100);
}
// استثناء؟
if (ex != null) {
// استثناء بالا برده میشود
subscriber.onError(ex);
}
}
});
}
}
- کلاس [AbstractDao] عنصر اصلی آن یک متد عمومی، [getResponse] است که برای دریافت یک نوع [Response<T>] از سرور استفاده میشود، جایی که T نوع نتیجه مورد نظر کلاینت HTTP است. (در اینجا، `Integer`);
- خط ۲۰: تنها پارامتر متد عمومی [getResponse] یک نمونه از رابط عمومی [IRequest<T>] از خطوط ۱۵–۱۷ است. این رابط تنها یک متد دارد، [getResponse]، و این متد است که پاسخ مورد نظر را فراهم میکند، [Response<T>];
- به لطف دو عنصر قبلی، کلاس [AbstractDao] میتواند به عنوان کلاس والد برای هر لایه کلاینت [Dao] از یک سرور که پاسخهایی از نوع [Response<T>] ارسال میکند، عمل کند؛
- خط ۲۰: متد عمومی [getResponse] یک نوع [Observable<T>] را بازمیگرداند که نماینده نتیجه مورد انتظار واقعی توسط کلاینت HTTP است (در این مورد، یک نوع Observable<Integer>);
- خطوط 22–51: متد استاتیک [rx.Observable.create] یک نوع [Observable] ایجاد میکند؛
- خط ۲۲: تنها پارامتر این متد یک نمونه از نوع [rx.Observable.OnSubscribe<T>] است، یک رابط که متدهای زیر را دارد:
- [onNext(T element)]: امکان ارسال یک عنصر از نوع T به یک ناظر را فراهم میکند؛
- [onError(Throwable th)]: امکان ارسال یک استثنا به یک ناظر را فراهم میکند؛
- [onCompleted]: به یک ناظر اطلاع میدهد که انتشارها پایان یافتهاند؛
نوع [Observable<T>] تابع محدودیتهای خاصی است:
- این نوع، عناصر خود را با استفاده از روش [onNext(T element)] ارسال میکند؛
- متد [onCompleted] باید تنها یک بار، به محض اینکه دیگر عنصری برای ارسال به ناظر وجود نداشته باشد، فراخوانی شود؛
- روش [onCompleted] در صورتی که روش [onError(Throwable th)] فراخوانی شده باشد، فراخوانی نمیشود؛
در مثال ما:
- ناظر، قطعه [Vue1Fragment] خواهد بود. این قطعه است که عناصر صادرشده توسط [Observable<T>] (عناصر یا استثناها) را مصرف میکند؛
- نوع [Observable<T>] ایجاد شده تنها یک عنصر را منتشر خواهد کرد (خط ۳۷);
- خط ۲۹: یک درخواست همزمان HTTP به سرور ارسال میکند و نوع [Response<T>] را دریافت میکند. این درخواست HTTP توسط نوع [IRequest] که بهعنوان پارامتر به متد عمومی [getResponse] ارسال شده است، پردازش میشود؛
- خط ۳۱: status از پاسخ بازیابی میشود؛
- خطوط ۳۲–۳۴: اگر این status نشاندهنده یک خطا باشد، یک استثنا آماده میشود؛
- خطوط ۳۶–۳۹: اگر این status نشاندهنده خطا نباشد، آنگاه پاسخ واقعی مورد انتظار کلاینت ارسال میشود (خط ۳۷) و به ناظر اطلاع داده میشود که پاسخ دیگری در کار نخواهد بود (خط ۳۹);
- خطوط ۴۱–۴۴: اگر درخواست HTTP منجر به یک استثنا شود، آن را ثبت میکنیم؛
- خطوط ۴۶–۴۹: اگر استثنای [ex] با null متفاوت باشد، آنگاه به ناظر ارسال میشود. در اینجا نیازی به فراخوانی متد [onCompleted] برای اطلاعرسانی به ناظر مبنی بر اینکه هیچ عنصر دیگری ارسال نخواهد شد، نیست. این امر ضمنی است؛
نکته کلیدی از این توضیحات این است که:
- متد عمومی [<T> Observable<T> getResponse(final IRequest<T> request)] نوع [Observable<T>] را برمیگرداند که یا یک عنصر از نوع T یا یک استثنا را منتشر میکند؛
- این متد به عنوان تنها پارامتر خود، یک نوع [IRequest<T>] را میپذیرد، که تنها متد آن [getResponse()] دسترسی HTTP را پیادهسازی میکند، که نوع [Response<T>] را برمیگرداند؛
1.17.6. کلاس [Dao]
کلاس [Dao] به شرح زیر تکامل مییابد:
@EBean
public class Dao extends AbstractDao implements IDao {
// کلاینت سرویس REST
@RestService
protected WebClient webClient;
// زمان انتظار قبل از اجرای درخواست
private int delay;
// حالت اشکالزدایی
private boolean isDebugEnabled;
// نام کلاس
private String className;
// سازنده
public Dao() {
// نام کلاس
className = getClass().getSimpleName();
}
// رابط IDao -------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// لاگ
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// اجرای کلاینت وب
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// انتظار
waitSomeTime(delay);
// فراخوانی همزمان به HTTP
return webClient.getAlea(a, b);
}
});
}
...
- خط ۲: کلاس [Dao] از کلاس [AbstractDao] ارث میبرد؛
- خط ۲۴: متد [getAlea] اکنون یک نوع [Observable<Integer>] را برمیگرداند؛
- خط ۳۰: فراخوانی متد عمومی [getResponse] از کلاس والد. یک پارامتر از نوع [IRequest<Integer>] به آن ارسال میشود؛
- خطوط ۳۲–۳۷: پیادهسازی رابط [IRequest<Integer>];
- خط ۳۶: پرسوجوی HTTP از طریق رابطهای AA و [webClient] اجرا میشود، همانطور که قبلاً انجام شد. ما میدانیم که یک نوع [Response<Integer>] را بازیابی خواهیم کرد، که در واقع همان نوعی است که متد [IRequest<Integer>.getReponse()] باید بازگرداند؛
- خط ۳۶: در اینجا از خاصیتی به نام closure استفاده میکنیم: قابلیت جاسازی مقادیری که خارج از نمونه هستند در درون خود هنگام ایجاد؛ در این مورد، مقادیر [a, b] از خط ۲۴. این همان چیزی است که به متد [IRequest<Integer>.getReponse()] اجازه میدهد پارامتر نداشته باشد. این مقادیر بهصورت سختکد در داخل بدنه متد قرار گرفتهاند. و در حالی که معمولاً پارامترهای متد (a, b) → (x, y) تغییر داده میشوند، در اینجا یک نمونه جدید از [IRequest<Integer>] ایجاد میشود که مقادیر x و y را در خود جای داده است؛
1.17.7. کلاس [MainActivity]
کلاس [MainActivity] که رابط [IDao] را پیادهسازی میکند، به شرح زیر بهروزرسانی میشود:
// پیادهسازی IDao --------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(int a, int b) {
//اجرا
return dao.getAlea(a, b);
}
1.17.8. کلاس [Vue1Fragment]
کلاس [Vue1Fragment] به شرح زیر تکامل مییابد:
@Click(R.id.btn_Executer)
protected void doExecuter() {
// پاسخهای قبلی پاک میشوند
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// شمارنده پاسخ را روی 0 تنظیم مجدد میکند
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
//اعتبار ورودیها را بررسی میکند
if (!isPageValid()) {
return;
}
// ابتدای فعالیت
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// درخواست اعداد تصادفی
getAleasInBackground(a, b);
// شروع انتظار
beginWaiting();
}
- خط ۱۸: اعداد تصادفی از متد [getAleasInBackground] درخواست میشوند، که به این نام نامگذاری شده است زیرا اعداد در یک نخ (thread) متفاوت از نخ رابط کاربری (UI) درخواست خواهند شد؛
private int nbReponses = 0;
// اشتراکها در مشاهداتپذیرها
private List<Subscription> abonnements;
// توضیح [Background] غیرضروری است
void getAleasInBackground(int a, int b) {
//در ابتدا، هیچ پاسخی و هیچ اشتراکپذیری وجود ندارد
nbReponses = 0;
abonnements.clear();
//آمادهسازی مشاهدهپذیر
Observable<Integer> response = Observable.empty();
// ادغام نتایج فراخوانیهای مختلف HTTP
// آنها روی یک نخ ورودی/خروجی اجرا میشوند
for (int i = 0; i < nbAleas; i++) {
response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
}
// مشاهدهپذیر انباشتهشده روی نخ UI مشاهده خواهد شد
response = response.observeOn(AndroidSchedulers.mainThread());
try {
// مشاهدهپذیر اجرا میشود
abonnements.add(response.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
// اطلاعات به فهرست پاسخها اضافه میشود
showInfo(alea);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// پیام خطا
showAlert(th);
//پایان انتظار
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
//پایان انتظار
cancelWaiting();
}
}));
} catch (RuntimeException e) {
// استثناء در UiThread نمایش داده میشود
showAlert(e);
}
}
- خط ۳: یک مشاهدهپذیر دارای مشترکین است. ارتباط بین یک مشترک و فرایندی که مشاهده میکند، اشتراک نامیده میشود. در اینجا، ما فقط یک فرآیند مشاهدهشده و یک مشترک خواهیم داشت. بنابراین، فقط یک اشتراک خواهیم داشت. از نظر اصول، ما این موضوع را طوری در نظر میگیریم که گویی میتوانیم چندین فرآیند مشاهدهشده داشته باشیم که توسط مشاهدهگران مختلف مشاهده میشوند، که این امر منجر به چندین اشتراک خواهد شد؛
- خطوط ۱۱–۱۸: ما فرآیند مشاهدهشده (قابل مشاهده) را پیکربندی میکنیم. مهم است که درک کنیم این صرفاً پیکربندی است: فرآیند اجرا نمیشود؛
- خط ۱۱: ما با یک observable خالی شروع میکنیم، observableای که هیچ چیزی منتشر نمیکند؛
- خطوط 14–16: به این observable خالی، observableهای [nbAleas] را اضافه میکنیم که اینها queriesهای [nbAleas] خواهند بود که اعداد تصادفی HTTP را بازمیگردانند؛
- خط ۱۵: همانند قبل، عدد تصادفی شمارهٔ i از کلاس [MainActivity] درخواست میشود. مهم است که درک کنیم، در این مرحله، هنوز هیچ درخواست HTTP اجرا نشده است. متد [mainActivity.getAlea(a, b)] اجرا شده و یک نوع [Observable<Integer>] را بازمیگرداند. این فرآیندی است که پس از راهاندازی آن مشاهده خواهد شد؛
- خط ۱۵: متد [subscribeOn(Schedulers.io())] مشخص میکند که فرآیند باید (وقتی اجرا میشود) روی یک نخ I/O اجرا شود. کتابخانه RxJava انواع مختلفی از نخ را ارائه میدهد. نخ I/O برای فراخوانیهای HTTP مناسب است؛
- خط ۱۵: قابل مشاهده شماره i با قابل مشاهده اولیه از خط ۱۱ ادغام میشود: از آنجایی که هر یک از قابل مشاهدههای [nbAleas] یک عنصر را منتشر میکنند، یک قابل مشاهده ایجاد میشود که عناصر [nbAleas] را منتشر خواهد کرد. این قابل مشاهده است که مورد مشاهده قرار میگیرد. این مشاهدهگر پس از اینکه تمام مشاهدهگرهای تشکیلدهندهاش اعلان خود یعنی [onCompleted] را ارسال کردند، اعلان [onCompleted] را ارسال میکند. این کار ما را از شمارش پاسخها، همانطور که در نسخه قبلی انجام میدادیم، برای بررسی اینکه آیا تمام اعداد مورد انتظار را دریافت کردهایم یا خیر، بینیاز میکند؛
- خط ۱۸: در این مرحله، ما یک مشاهدهپذیر را پیکربندی کردهایم که ترکیبی از مشاهدهپذیرهای [nbAleas] است، که هر کدام روی یک نخ I/O اجرا میشوند؛
- خط ۱۸: متد [observeOn(AndroidSchedulers.mainThread())] برای مشخص کردن اینکه مقادیر صادر شده توسط ناظر (observable) باید در کدام نخ (thread) مشاهده شوند، استفاده میشود. در اینجا، نخ [AndroidSchedulers.mainThread())] متعلق به کتابخانه RxAndroid است و نه به RxJava. این به نخ رابط کاربری (UI thread) اشاره دارد که به حلقه رویداد (event loop) نیز معروف است. این مهم است: در یک برنامه اندروید، یک مؤلفه رابط کاربری فقط میتواند روی نخ رابط کاربری تغییر داده شود؛ در غیر این صورت، یک استثنا پرتاب میشود؛
- خطوط ۱۹–۴۵: اکنون که فرآیند مورد مشاهده پیکربندی شده است، اجرا میشود؛
- خط ۲۱: این عملیات [Observable.subscribe] است که اجرای فرآیند مورد مشاهده را آغاز میکند. این عملیات، فرآیندهای ناهمزمان [nbAleas] را که قبلاً پیکربندی شدهاند، راهاندازی خواهد کرد. نتایج این فرآیندها بهطور خودکار در نخ رابط کاربری در دسترس ناظر قرار خواهند گرفت؛
- یادآوری میکنیم که قابل مشاهده سه نوع رویداد صادر میکند:
- [onNext]: زمانی که یک عنصر را منتشر میکند؛
- [onError]: زمانی که با یک استثنا مواجه شده است؛
- [onCompleted]: زمانی که سیگنال میدهد که دیگر رویداد منتشر نخواهد کرد؛
متد [Observable.subscribe] سه شیء از نوع [Action1<Integer>, Action1<Throwable>, Action0] را بهعنوان پارامتر میپذیرد که متدهای [call] آنها برای رسیدگی به هر یک از این سه رویداد استفاده میشوند؛
- خطوط ۲۱–۲۷: پارامتر اول، از نوع [Action1<Integer>]، برای رسیدگی به رویداد [onNext] استفاده میشود. متد آن، [call]، عنصری را که توسط قابل مشاهده (observable) منتشر شده است (خط ۲۳) دریافت میکند؛
- خط ۲۵: متد [showInfo] از مثال قبلی مجدداً استفاده میشود؛
- خطوط ۲۷–۳۵: پارامتر دوم از نوع [Action1<Throwable>] برای رسیدگی به رویداد [onError] استفاده میشود. متد آن [call]، استثنایی را که توسط قابل مشاهده (observable) پرتاب شده است، دریافت میکند (خط ۲۹);
- خط ۳۱: متد [showAlert] از مثال قبلی مجدداً استفاده میشود؛
- خط ۳۳: فرآیند لغو درخواست کاربر آغاز میشود. این شامل لغو تمام قابل مشاهدههایی (observables) است که در حال حاضر در حال اجرا هستند؛
- خطوط ۳۵–۴۱: پارامتر سوم از نوع [Action0] برای رسیدگی به رویداد [onCompleted] استفاده میشود. متد آن [call] هیچ پارامتری ندارد؛
- خط ۳۹: انتظار لغو میشود؛
متد [showInfo] به شرح زیر تغییر میکند:
// [UiThread] توضیح لازم نیست
protected void showInfo(int alea) {
// لاگ
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
if (!hasBeenCanceled) {
// اطلاعات اضافی
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// اطلاعات به فهرست پاسخها اضافه میشود
reponses.add(0, String.valueOf(alea));
// نمایش پاسخها
adapterReponses.notifyDataSetChanged();
}
}
این متد دو تغییر دارد:
- خط ۱: تفسیر AA [@UiThread] حذف شده است؛
- ما دیگر پاسخها را برای تعیین اینکه آیا باید منتظر بمانیم یا خیر، شمارش نمیکنیم. اکنون این رویداد [onCompleted] از مشاهدهگر است که این اطلاعات را فراهم میکند؛
روش [showAlert] به شرح زیر تغییر یافته است:
// توضیح [UiThread] غیرضروری
protected void showAlert(Throwable th) {
// لاگ
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
if (!hasBeenCanceled) {
//همه چیز لغو شده است
doAnnuler();
// نمایش داده میشود
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
- تنها تغییر در خط ۱ است: ما حاشیهنویسی AA [@UiThread] را حذف کردهایم؛
در نهایت، متد [doAnnuler] به شرح زیر تغییر مییابد:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// لاگ
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
//حافظه
hasBeenCanceled = true;
//لغو وظایف ناهمزمان
if (abonnements != null) {
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
}
//پایان انتظار
cancelWaiting();
}
- خط ۱۲: اشتراک را لغو میکند و در نتیجه نظارت بر فرآیند مرتبط را متوقف میسازد؛
1.17.9. اجرا
سرویس وب را راهاندازی کنید (بخش 1.16.1.7)، کلاینت اندروید را راهاندازی کنید و آزمایشهایی را که با مثال قبلی انجام دادید (بخش 1.16.2.8) تکرار کنید.
1.17.10. مدیریت لغو
ما همان آزمایشها را مانند مثال قبلی (بخش 1.16.2.9) تکرار میکنیم.
آزمون ۱
ما در حالی که سرور راهاندازی نشده است، ۵ عدد درخواست میکنیم. لاگهای زیر را دریافت میکنیم:
پس از خط ۷، هیچ لاگ دیگری وجود ندارد که نشان میدهد ناظر (Vue1Fragment) دیگر اعلانیهای از فرآیند مورد مشاهده دریافت نمیکند.
آزمون ۲
حالا سرور را راهاندازی کرده و با تأخیر ۵ ثانیهای ۵ عدد را درخواست میکنیم، سپس قبل از پایان آن تأخیر روی [Annuler] کلیک میکنیم. لاگها به شرح زیر است:
پس از خط ۶، هیچ لاگ دیگری وجود ندارد، که نشان میدهد ناظر (Vue1Fragment) دیگر اعلانیهای از فرآیند مشاهدهشده دریافت نمیکند.
این رفتار مورد انتظار پس از لغو است. بنابراین میتوانیم متغیر بولین [hasBeenCanceled] را از کد [Vue1Fragment] حذف کنیم، متغیری که در مثال قبلی به دلیل رفتار نامطمئن لغو، معرفی کرده بودیم.
اینکه ناظر پس از لغو قابلیت مشاهدهپذیر، دیگر اعلانیهای دریافت نمیکند، به این معنا نیست که خود درخواستهای HTTP لغو شدهاند. برای اثبات این موضوع، کلاس [Dao] را به شرح زیر اصلاح میکنیم:
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// لاگ
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// اجرای کلاینت وب
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// انتظار
waitSomeTime(delay);
// فراخوانی همزمان به HTTP
Response<Integer> response= webClient.getAlea(a, b);
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
} catch (JsonProcessingException e) {
Log.d(String.format("%s", className),"erreur désérialisation jSON");
}
}
return response;
}
});
}
- خطوط ۱۵–۲۱: ما نتیجه پرسوجوی HTTP از خط ۱۴ را ثبت میکنیم؛
لاگهای مربوط به تست شمارهٔ ۲ به شرح زیر است:
- خطوط ۱–۵: پنج درخواست ارسال شد؛
- خط ۶: کاربر لغو کرد؛
- خطوط ۷–۱۱: ما در واقع پاسخهای پنج درخواست HTTP را دریافت میکنیم. با این حال، از آنجا که مشاهدهپذیر لغو شده است، این عناصر به ناظر ارسال نمیشوند؛
1.17.11. نتیجهگیری
در ادامه این سند، برنامههای کلاینت/سرور با استفاده از کتابخانه RxAndroid به جای کتابخانه AA توسعه داده خواهند شد به دلایل زیر:
- RxAndroid را میتوان در یک برنامه اندروید که از AA استفاده نمیکند، بهکار برد؛
- RxAndroid فراتر از صرفاً تسهیل عملیات غیرهمزمان عمل میکند. این کتابخانه طیف گستردهای از متدها را برای ایجاد یک مشاهدهپذیر جدید از روی دیگری ارائه میدهد. این متدها در AA معادلی ندارند؛
- به محض اینکه کسی بخواهد کلاسی را که با AA انوتیت شده است، مانند یک فرگمنت، مشتق کند، با مشکلات جدی مواجه میشود. در این صورت مجبور میشود AA را رها کرده و از راهحل ۱ برای برنامهنویسی ناهمزمان استفاده کند؛
خوانندگانی که علاقهمند به بررسی عمیقتر قابلیتهای کتابخانه RxAndroid هستند، میتوانند به سند [Introduction à RxJava. Application aux environnements Swing et Android] مراجعه کنند. این سند از RxAndroid بدون کتابخانه AA استفاده میکند.
1.18. مثال ۱۷: کامپوننتهای ورود داده
ما قصد داریم یک پروژه جدید بنویسیم تا برخی از اجزای رایج مورد استفاده در فرمهای ورود داده را نشان دهیم.
1.18.1. ایجاد پروژه
ما پروژه [Exemple-13] را به عنوان [Exemple-17] کپی میکنیم:
![]() | ![]() |
پروژه جدید تنها یک نما خواهد داشت: [vue1.xml]. بنابراین نمای [vue2.xml] و قطعات مرتبط با آن [Vue2Fragment] و [2] را حذف میکنیم. ما این تغییر را در مدیر قطعات برای [Mainactivity] منعکس میکنیم:
// پردازشگر قطعه ما باید برای هر برنامه مجدداً تعریف شود
//باید روشهای زیر را تعریف کند: getItem، getCount، getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// قطعات
private final Fragment[] fragments = {new Vue1Fragment_()};
....
}
پروژه را مجدداً اجرا کنید. باید نما شمارهٔ ۱ مانند قبل نمایش داده شود. ما از این پروژه کار خواهیم کرد.
1.18.2. نما XML از فرم
![]() |
نما تولید شده توسط فایل [vue1.xml] به شرح زیر است:

متن XML برای نما به شرح زیر است:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="30dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<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="30dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
<Button
android:id="@+id/formulaireButtonValider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/TextViewFormulaireCombo"
android:layout_below="@+id/TextViewFormulaireCombo"
android:layout_marginTop="30dp"
android:text="@string/formulaire_valider"/>
<TextView
android:id="@+id/textViewFormulaireCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTitre"
android:layout_below="@+id/textViewFormulaireTitre"
android:layout_marginTop="30dp"
android:text="@string/formulaire_checkbox"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireCheckBox"
android:layout_below="@+id/textViewFormulaireCheckBox"
android:layout_marginTop="30dp"
android:text="@string/formulaire_radioButton"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireRadioButton"
android:layout_below="@+id/textViewFormulaireRadioButton"
android:layout_marginTop="30dp"
android:text="@string/formulaire_seekBar"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireEdtText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireSeekBar"
android:layout_below="@+id/textViewFormulaireSeekBar"
android:layout_marginTop="30dp"
android:text="@string/formulaire_saisie"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireBool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireEdtText"
android:layout_below="@+id/textViewFormulaireEdtText"
android:layout_marginTop="30dp"
android:text="@string/formulaire_bool"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireDate"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_alignLeft="@+id/textViewFormulaireBool"
android:layout_below="@+id/textViewFormulaireBool"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@string/formulaire_date"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireMultilignes"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center"
android:layout_alignBaseline="@+id/textViewFormulaireTitre"
android:layout_alignParentTop="true"
android:layout_marginLeft="400dp"
android:layout_toRightOf="@+id/textViewFormulaireTitre"
android:text="@string/formulaire_multilignes"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireTime"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:gravity="center"
android:layout_alignLeft="@+id/textViewFormulaireMultilignes"
android:layout_below="@+id/textViewFormulaireMultilignes"
android:layout_marginTop="30dp"
android:text="@string/formulaire_time"
android:textSize="20sp"/>
<TextView
android:id="@+id/TextViewFormulaireCombo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTime"
android:layout_below="@+id/textViewFormulaireTime"
android:layout_marginTop="30dp"
android:text="@string/formulaire_combo"
android:textSize="20sp"/>
<CheckBox
android:id="@+id/formulaireCheckBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"
android:layout_marginLeft="100dp"
android:layout_toRightOf="@+id/textViewFormulaireCheckBox"
android:text="@string/formulaire_checkbox1"/>
<RadioGroup
android:id="@+id/formulaireRadioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:orientation="horizontal">
<RadioButton
android:id="@+id/formulaireRadioButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton1"/>
<RadioButton
android:id="@+id/formulaireRadioButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radionbutton2"/>
<RadioButton
android:id="@+id/formulaireRadionButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton3"/>
</RadioGroup>
<SeekBar
android:id="@+id/formulaireSeekBar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_alignLeft="@+id/formulaireCheckBox1"/>
<EditText
android:id="@+id/formulaireEditText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:ems="10"
android:inputType="text">
</EditText>
<Switch
android:id="@+id/formulaireSwitch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireBool"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:text="@string/formulaire_switch"
android:textOff="Non"
android:textOn="Oui"/>
<TimePicker
android:id="@+id/formulaireTimePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireTime"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes"
android:timePickerMode="spinner"
/>
<EditText
android:id="@+id/formulaireEditTextMultiLignes"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"
android:layout_alignBottom="@+id/textViewFormulaireMultilignes"
android:layout_marginLeft="50dp"
android:layout_toRightOf="@+id/textViewFormulaireMultilignes"
android:ems="10"
android:inputType="textMultiLine">
</EditText>
<Spinner
android:id="@+id/formulaireDropDownList"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_alignBottom="@+id/TextViewFormulaireCombo"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes">
</Spinner>
<DatePicker
android:id="@+id/formulaireDatePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireDate"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:datePickerMode="spinner"
android:calendarViewShown="false">
</DatePicker>
<TextView
android:id="@+id/textViewSeekBarValue"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@+id/formulaireSeekBar"
android:text=""/>
</RelativeLayout>
اجزای اصلی فرم به شرح زیر هستند:
| |
| |
| |
| |
| |
| |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
|
1.18.3. رشتههای فرم
رشتههای کاراکتری فرم در فایل زیر تعریف شدهاند: [res / values / strings.xml]:
![]() |
<resources>
<string name="app_name">Exemple-17</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!--نمای 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="formulaire_checkbox">Cases à cocher</string>
<string name="formulaire_radioButton">Boutons Radio</string>
<string name="formulaire_seekBar">Seek Bar</string>
<string name="formulaire_saisie">Champ de saisie</string>
<string name="formulaire_bool">Booléen</string>
<string name="formulaire_date">Date</string>
<string name="formulaire_time">Heure</string>
<string name="formulaire_multilignes">Champ de saisie multilignes</string>
<string name="formulaire_listview">Liste</string>
<string name="formulaire_combo">Liste déroulante</string>
<string name="formulaire_checkbox1">1</string>
<string name="formulaire_checkbox2">2</string>
<string name="formulaire_radiobutton1">1</string>
<string name="formulaire_radionbutton2">2</string>
<string name="formulaire_radiobutton3">3</string>
<string name="formulaire_switch"></string>
<string name="formulaire_valider">Valider</string>
</resources>
1.18.4. قطعه فرم
![]() |
کلاس [Vue1Fragment] به شرح زیر است:
package exemples.android.fragments;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
//یک قطعه نمایی است که توسط یک محفظهٔ قطعه نمایش داده میشود
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// میدانهای نمای نمایشدادهشده توسط قطعه
@ViewById(R.id.formulaireDropDownList)
Spinner dropDownList;
@ViewById(R.id.formulaireButtonValider)
Button buttonValider;
@ViewById(R.id.formulaireCheckBox1)
CheckBox checkBox1;
@ViewById(R.id.formulaireRadioGroup)
RadioGroup radioGroup;
@ViewById(R.id.formulaireSeekBar)
SeekBar seekBar;
@ViewById(R.id.formulaireEditText1)
EditText saisie;
@ViewById(R.id.formulaireSwitch1)
Switch switch1;
@ViewById(R.id.formulaireDatePicker1)
DatePicker datePicker1;
@ViewById(R.id.formulaireTimePicker1)
TimePicker timePicker1;
@ViewById(R.id.formulaireEditTextMultiLignes)
EditText multiLignes;
@ViewById(R.id.formulaireRadioButton1)
RadioButton radioButton1;
@ViewById(R.id.formulaireRadioButton2)
RadioButton radioButton2;
@ViewById(R.id.formulaireRadionButton3)
RadioButton radioButton3;
@ViewById(R.id.textViewSeekBarValue)
TextView seekBarValue;
// لیست کشویی
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");
}
@SuppressLint("DefaultLocale")
@Click(R.id.formulaireButtonValider)
protected void doValider() {
...
}
@Override
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);
}
}
- ردههای ۲۲–۴۹: ارجاعات برای تمام اجزای فرم بازیابی میشوند XML [vue1] (ردهی ۱۸);
- خط ۵۸: متد [setChecked] برای انتخاب یک دکمه رادیویی یا تیک زدن یک چکباکس استفاده میشود؛
- خط ۶۰: بهطور پیشفرض، کامپوننت [DatePicker] هم یک فیلد ورودی تاریخ و هم یک تقویم را نمایش میدهد. خط ۶۰ تقویم را حذف میکند؛
- خط ۶۲: [SeekBar].setMax() به شما امکان میدهد حداکثر مقدار اسلایدر را تنظیم کنید. حداقل مقدار ۰ است؛
- خطوط ۶۳–۷۴: رویدادهای اسلایدر مدیریت میشوند. هر زمان که کاربر تغییری ایجاد کند، مقدار اسلایدر در متغیر [TextView] در خط ۴۹ نمایش داده میشود؛
- خط ۷۱: پارامتر [progress] نمایانگر مقدار اسلایدر است؛
- خطوط ۷۶–۷۹: فهرستی از مقادیر [String] که باید با لیست کشویی مرتبط شوند؛
- خط ۹۰: متد [updateFragment] قطعه. هنگام اجرا، متغیر [activity] کلاس والد مقداردهی اولیه شده است؛
- خط ۹۲: منبع داده [list] با آداپتور لیست کشویی مرتبط است؛
- خطوط ۹۳–۹۴: اداپتور [dataAdapter] با لیست کشویی [dropDownList] مرتبط است؛
- خط ۸۴: متد [doValider] با کلیک روی دکمه [Valider] مرتبط است؛
هدف متد [doValider] نمایش مقادیر واردشده توسط کاربر است. کد آن به شرح زیر است:
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// فهرست پیامهای نمایشی
List<String> messages = new ArrayList<>();
// تیکباکس
boolean isChecked = checkBox1.isChecked();
messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
// دکمههای رادیویی
int id = radioGroup.getCheckedRadioButtonId();
String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
// SeekBar
int progress = seekBar.getProgress();
messages.add(String.format("SeekBar [value=%d]", progress));
// میدان ورودی
String texte = String.valueOf(saisie.getText());
messages.add(String.format("Saisie simple [value=%s]", texte));
// سوئیچ دوحالته
boolean état = switch1.isChecked();
messages.add(String.format("Switch [value=%s]", état));
// تاریخ
int an = datePicker1.getYear();
int mois = datePicker1.getMonth() + 1;
int jour = datePicker1.getDayOfMonth();
messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
// متن چندخطی
String lignes = String.valueOf(multiLignes.getText());
messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
//زمان
int heure = timePicker1.getHour();
int minutes = timePicker1.getMinute();
messages.add(String.format("Heure [%d, %d]", heure, minutes));
// لیست کشویی
int position = dropDownList.getSelectedItemPosition();
String selectedItem = String.valueOf(dropDownList.getSelectedItem());
messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
// نمایش
doAfficher(messages);
}
- خط ۴: مقادیر وارد شده به یک لیست پیامها اضافه خواهند شد؛
- خط ۶: متد [CheckBox].isCkecked() مشخص میکند که آیا یک چکباکس تیک خورده است یا خیر؛
- خط ۹: متد [RadioGroup].getCheckedButtonId() شناسه دکمه رادیویی انتخابشده را بازمیگرداند، یا در صورت عدم انتخاب هیچکدام، مقدار -1 را برمیگرداند؛
- خط ۱۰: کد [activity.findViewById(id)] به شما امکان میدهد دکمه رادیویی انتخابشده را شناسایی کرده و بدین ترتیب برچسب آن را بازیابی کنید؛
- خط ۱۳: متد [SeekBar].getProgress() مقدار یک اسلایدر را بازیابی میکند؛
- خط ۱۹: متد [Switch].isChecked() مشخص میکند که یک سوئیچ On (true) است یا Off (false);
- خط ۲۲: متد [DatePicker].getYear() سال انتخابشده را به صورت یک شیء [DatePicker] برمیگرداند؛
- خط ۲۳: متد [DatePicker].getMonth() ماه انتخابشده را با استفاده از یک شیء [DatePicker] در محدوده [0,11] بازمیگرداند؛
- خط 24: متد [DatePicker].getDayOfMonh() روز انتخابشده ماه را با یک شیء [DatePicker] در محدوده [1,31] بازمیگرداند؛
- خط ۳۰: متد [TimePicker].getHour() ساعت انتخابشده را به صورت یک شیء [TimePicker] بازمیگرداند؛
- خط ۳۱: متد [TimePicker].getMinute() دقایق انتخابشده را بهعنوان یک شیء [TimePicker] بازمیگرداند؛
- خط ۳۴: متد [Spinner].getSelectedItemPosition() موقعیت آیتم انتخابشده را در یک لیست کشویی بازمیگرداند؛
- خط ۳۵: متد [Spinner].getSelectedItem() شیء انتخابشده از یک لیست کشویی را بازمیگرداند؛
متد [doAfficher] که فهرست مقادیر واردشده را نمایش میدهد، به شرح زیر است:
private void doAfficher(List<String> messages) {
// ساختاردهی متن برای نمایش
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
//آن را نمایش دهد
new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
- خط ۱: این متد یک لیست از پیامهایی را که باید نمایش داده شوند دریافت میکند؛
- خطوط ۳–۶: یک شیء [StringBuilder] از این پیامها ساخته میشود. برای الحاق رشتهها، نوع [StringBuilder] کارآمدتر از نوع [String] است؛
- خط ۸: یک کادر گفتگو متن خط ۳ را نمایش میدهد:

1.18.5. اجرای پروژه
پروژه را اجرا کرده و اجزای ورودی مختلف را آزمایش کنید.
1.19. مثال ۱۸: استفاده از قالب نما
1.19.1. ایجاد پروژه
ما پروژهٔ جدیدی به نام [Exemple-18] را با کپی کردن پروژهٔ [Exemple-13] ایجاد میکنیم.
![]() | ![]() |
1.19.2. قالب نما
ما میخواهیم دو نما را از پروژه برداشته و در یک قالب بگنجانیم:
![]() |

هر یک از این دو نما به یک شکل ساختاردهی خواهند شد:
- در [1]، یک سربرگ؛
- در [2]، یک ستون سمت چپ که میتواند حاوی پیوندها باشد؛
- در [3]، یک پاورقی؛
- در [4]، محتوای اصلی.
این امر با اصلاح نمای پایه فعالیت [activity_main.xml] انجام میشود؛
![]() | ![]() |
کد XML برای نما [main] به شرح زیر است:
<?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>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="75dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/lavenderblushh2">
<TextView
android:id="@+id/textViewHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:text="@string/txt_header"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="0.8"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/left"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@color/lightcyan2">
<TextView
android:id="@+id/txt_left"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_left"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/floral_white"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</LinearLayout>
<LinearLayout
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/wheat1">
<TextView
android:id="@+id/textViewBottom"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_bottom"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
- سربرگ [1] از خطوط ۳۸–۵۴ استخراج میشود؛
- ستون سمت چپ [2] از خطوط 56–84 به دست میآید؛
- پاورقی [3] از خطوط 86–101 استخراج میشود؛
- بدنه [4] از خطوط 78–84 تولید میشود؛
نما XML [main] از اطلاعاتی که در فایلهای [res / values / colors.xml] و [res / values / strings.xml] یافت میشود، استفاده میکند:
![]() |
فایل [colors.xml] به شرح زیر است:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="red">#FF0000</color>
<color name="blue">#0000FF</colour>
<color name="wheat">#FFEFD5</colour>
<color name="floral_white">#FFFAF0</color>
<color name="lavenderblushh2">#EEE0E5</color>
<color name="lightcyan2">#D1EEEE</color>
<color name="wheat1">#FFE7BA</color>
</resources>
و فایل [strings.xml] به شرح زیر است:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">exemple-12</string>
<string name="action_settings">Settings</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="textView_nom">Quel est votre nom :</string>
<string name="btn_Valider">Validez</string>
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
<string name="textView_bonjour">"Bonjour "</string>
<string name="txt_header">Header</string>
<string name="txt_left">Left</string>
<string name="txt_bottom">Bottom</string>
</resources>
یک زمینهٔ زمان اجرا برای این پروژه ایجاد کرده و آن را اجرا کنید.
1.20. مثال ۱۹: کامپوننت [ListView]
کامپوننت [ListView] امکان تکرار یک نمای مشخص را برای هر آیتم در یک لیست فراهم میکند. نمای تکراری میتواند از هر پیچیدگیای برخوردار باشد، از یک رشته ساده از کاراکترها تا نمایی که امکان وارد کردن اطلاعات برای هر آیتم در لیست را فراهم میکند. ما [ListView] زیر را ایجاد خواهیم کرد:

هر نما در لیست سه مؤلفه دارد:
- یک [TextView] اطلاعاتی؛
- یک [CheckBox];
- یک [TextView] قابل کلیک؛
1.20.1. ایجاد پروژه
ما یک پروژه جدید، [Exemple-19]، با کپی کردن پروژه [Exemple-18] ایجاد میکنیم.
![]() | ![]() |
![]() |
ما پروژه را همانطور که در [3] تشریح شده است، توسعه خواهیم داد.
1.20.2. جلسه
![]() |
جلسه دادههایی را که بین فعالیت و قطعات مشترک است ذخیره میکند:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// فهرستی از دادهها
private List<Data> liste=new ArrayList<>();
// گیرندهها و تنظیمکنندهها
...
}
- خط ۱۱: لیست دادههای مورد استفاده توسط هر دو نما؛
کلاس [Data] به شرح زیر است:
package exemples.android.architecture;
public class Data {
// دادهها
private String texte;
private boolean isChecked;
// سازنده
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۶: متنی که برای هر مورد در لیست، اولین [TextView] را پر خواهد کرد؛
- خط ۷: مقدار بولی که برای تیکدار کردن یا از تیکدار بودن [checkBox] برای هر مورد در لیست استفاده میشود؛
1.20.3. فعالیت [MainActivity]
کد متد [@AfterInject] به شرح زیر است:
//تزریق جلسه
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// ایجاد یک لیست از دادهها
List<Data> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new Data("Texte n° " + i, false));
}
}
- خطوط ۱۲–۱۵: инициалиزاسيون لیست دادههای موجود در جلسه؛
1.20.4. نمایهٔ اولیه [Vue1]
![]() | ![]() |
نما XML [vue1.xml] ناحیه [1] را که در بالا نشان داده شده است، نمایش میدهد. کد آن به شرح زیر است:
<?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:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue1"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView_titre"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:text="@string/btn_vue2" />
<ListView
android:id="@+id/listView1"
android:layout_width="600dp"
android:layout_height="200dp"
android:layout_alignParentLeft="true"
android:layout_below="@+id/button_vue2"
android:layout_marginLeft="30dp"
android:layout_marginTop="50dp" >
</ListView>
</RelativeLayout>
- خطوط ۷–۱۶: کامپوننت [TextView] [2];
- خطوط ۲۷–۳۵: مؤلفه [ListView] [4];
- خطوط ۱۸–۲۵: کامپوننت [Button] [3];
1.20.5. نما توسط [ListView] تکرار شده است
![]() |
نمایی که توسط [ListView] تکرار شده است، نمای زیر است، [list_data]:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Libellé"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:text="@string/txt_dummy" />
<CheckBox
android:id="@+id/checkBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="37dp"
android:layout_toRightOf="@+id/txt_Libellé"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/textViewRetirer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Libellé"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="68dp"
android:layout_toRightOf="@+id/checkBox1"
android:text="@string/txt_retirer"
android:textColor="@color/blue"
android:textSize="20sp" />
</RelativeLayout>
- خطوط ۸–۱۴: کامپوننت [TextView] [1];
- خطوط ۱۶–۲۳: کامپوننت [CheckBox] [2];
- خطوط ۲۵–۳۵: مؤلفه [TextView] [3];
1.20.6. قطعه [Vue1Fragment]
![]() |
قطعه [Vue1Fragment] نما XML [vue1] را مدیریت میکند. کد آن به شرح زیر است:
package exemples.android.fragments;
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// fields of the view displayed by the fragment
@ViewById(R.id.listView1)
protected ListView listView;
// آداپتور لیست
private ListAdapter adapter;
// ابتداییسازی کامل
private boolean initDone = false;
@AfterViews
void afterViews() {
// حافظه
afterViewsDone = true;
}
@Click(R.id.button_vue2)
void navigateToView2() {
// حرکت به نمای ۲
mainActivity.navigateToView(1);
}
public void doRetirer(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
//دادهها به [ListView] متصل شدهاند
adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
initDone = true;
}
// جایی که قطعه (باز) تولید شده است – در این صورت، ListView باید دوباره به آداپتور خود متصل شود
listView.setAdapter(adapter);
//در مواردی که سایر قطعات منبع داده را تغییر دادهاند – در این صورت، ListView باید دوباره تازه شود
adapter.notifyDataSetChanged();
}
}
- خط ۱۵: نمای XML [vue1] با قطعه مرتبط است؛
- خطوط 26–30: متد [@AfterViews] هیچ کاری انجام نمیدهد. با این حال، لازم است متغیر [afterViewsDone] را روی true تنظیم کرد، زیرا مورد دوم توسط کلاس والد [AbstractFragment] استفاده میشود؛
- خطوط ۴۲–۵۳: متد [updateFragment]، که هر بار که قطعه قرار است نمایش داده شود، فراخوانی میشود. این متد اینجا طوری نوشته شده است که گویی قطعه میتواند از همجواری با قطعه نمایشدادهشده خارج شود و در نتیجه چرخه عمر خود را ریست کند. این در اینجا صادق نیست، اما اگر برنامه سه قطعه با همجواری ۱ داشت، چنین میشد؛
- خط ۴۴: آداپتور [ListView] تنها یکبار نیاز به инициалиزهشدن دارد؛
- خط ۴۶: ما یک آداپتور از نوع [ListAdapter] را با این [ListView] مرتبط میکنیم. ما این کلاس را ایجاد میکنیم. این کلاس از کلاس [ArrayAdapter] مشتق شده است، که قبلاً برای مرتبط کردن دادهها با [ListView] استفاده کردهایم. ما اطلاعات مختلفی را به سازنده [ListAdapter] ارسال میکنیم:
- یک مرجع به فعالیت جاری،
- شناسهی نمایی که برای هر مورد در لیست نمونهسازی خواهد شد،
- یک منبع داده برای پر کردن لیست،
- یک مرجع به فرگمنت. این برای رسیدگی به کلیک روی یک لینک [Retirer] در داخل [ListView] از طریق متد [doRetirer] در خط ۳۸ استفاده خواهد شد؛
- خط ۵۰: آداپتور با [ListView] مرتبط است. در عین حال، منبع داده [listes] با [ListView] مرتبط است. این عملیات هر بار که نما شماره ۱ نمایش داده میشود، انجام میشود. در واقع، این عملیات تنها پس از اجرای متد [@AfterViews] باید انجام شود. در اینجا، دستور بیش از حد اجرا میشود. ما به یک متغیر بولی نیاز داریم تا نشان دهد که متد [@AfterViews] به تازگی اجرا شده است و در نتیجه، [ListView] باید دوباره با اداپتر خود مرتبط شود؛
- خط ۵۲: [ListView] تازه میشود. در این مثال، این کار فایدهای ندارد زیرا تنها نما شماره ۱ میتواند منبع داده [ListView] را تغییر دهد. بیایید یک مورد کلیتر را در نظر بگیریم که در آن نما شماره ۲ نیز میتواند منبع داده [ListView] را تغییر دهد. ما بعداً در این سند با نمونههای مشابهی مواجه خواهیم شد. در این مورد، هنگام تغییر از نمای شماره ۲ به نمای شماره ۱، [ListView] در نمای شماره ۱ باید تازه شود؛
1.20.7. آداپتور [ListAdapter] برای [ListView]
![]() |
کلاس [ListAdapter]
- منبع داده را برای [ListView] پیکربندی میکند؛
- نمایش عناصر مختلف [ListView] را مدیریت میکند؛
- رویدادهای این عناصر را مدیریت میکند؛
کد آن به شرح زیر است:
package exemples.android.fragments;
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
// زمینهٔ اجرا
private Context context;
//شناسهٔ طرحبندی نمایش برای یک سطر در لیست
private int layoutResourceId;
// دادههای لیست
private List<Data> data;
// قطعه نمایشدهنده [ListView]
private Vue1Fragment fragment;
// آداپتور
final ListAdapter adapter = this;
// تولیدکننده
public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
super(context, layoutResourceId, data);
// اطلاعات ذخیره میشود
this.context = context;
this.layoutResourceId = layoutResourceId;
this.data = data;
this.fragment = fragment;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
}
- خط ۵: کلاس [ListAdapter] از کلاس [ArrayAdapter] ارث میبرد؛
- خط ۱۹: سازنده؛
- خط ۲۰: فراموش نکنید که سازنده کلاس والد [ArrayAdapter] را با سه پارامتر اول فراخوانی کنید؛
- خطوط 22–25: اطلاعات از سازنده ذخیره میشود؛
- خط ۲۹: متد [getView] توسط [ListView] به طور مکرر فراخوانی میشود تا نما را برای عنصر شماره [position] تولید کند. نتیجه، [View]، یک مرجع به ویوی ایجادشده است.
کد متد [getView] به شرح زیر است:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// خط فعلی ListView ایجاد شده است
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// متن
TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
textView.setText(data.get(position).getTexte());
// تیکباکس
CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
checkBox.setChecked(data.get(position).isChecked());
// لینک [Retirer]
TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
txtRetirer.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
fragment.doRetirer(position);
}
});
// پردازش کلیک روی چکباکس
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.get(position).setChecked(isChecked);
}
});
// خط را نمایش دهید
return row;
}
- خط ۲: این متد سه پارامتر میگیرد. ما فقط از اولی استفاده خواهیم کرد؛
- خط ۴: ما نما را برای عنصر شماره [position] ایجاد میکنیم. این نما [list_data] است که ID آن بهعنوان پارامتر دوم به سازنده ارسال شده بود. سپس ارجاعات به کامپوننتهای نمایی را که بهتازگی نمونهسازی کردهایم، بازیابی میکنیم؛
- خط ۶: ما مرجع شماره ۱ از [TextView] را بازیابی میکنیم؛
- خط ۷: متنی از منبع داده که بهعنوان سومین پارامتر به سازنده ارسال شده است، به آن اختصاص داده میشود؛
- خط ۹: مرجع برای [CheckBox] شمارهٔ ۲ بازیابی میشود؛
- خط ۱۰: ما آن را با استفاده از مقداری از منبع داده [ListView] تیک میزنیم یا تیک آن را برمیداریم؛
- خط ۱۲: مرجع برای [TextView] شمارهٔ ۳ بازیابی میشود؛
- خطوط ۱۳–۱۸: کلیک روی لینک [Retirer] را مدیریت کنید؛
- خط 16: متد [Vue1Fragment].doRetirer این کلیک را مدیریت خواهد کرد. در واقع منطقیتر به نظر میرسد که این رویداد توسط قطعهای که [ListView] را نمایش میدهد، مدیریت شود. این کلاس دید کلیتری دارد که کلاس [ListAdapter] فاقد آن است. ارجاع به قطعه [Vue1Fragment] بهعنوان چهارمین پارامتر به سازنده کلاس ارسال شده است؛
- خطوط ۲۰–۲۵: کلیک روی تیکباکس پردازش میشود. عملی که روی آن انجام میشود در دادههایی که نمایش میدهد منعکس میشود. دلیل این امر به شرح زیر است. [ListView] یک لیست است که تنها زیرمجموعهای از عناصر خود را نمایش میدهد. بنابراین، یک عنصر در لیست گاهی مخفی و گاهی نمایش داده میشود. وقتی آیتم شماره i قرار است نمایش داده شود، متد [getView] در خط 2 بالا برای موقعیت شماره i فراخوانی میشود. خط 10 وضعیت تیکباکس را بر اساس دادهای که به آن متصل است، مجدداً محاسبه میکند. بنابراین باید وضعیت تیکباکس را در طول زمان پیگیری کند؛
1.20.8. حذف یک آیتم از لیست
کلیک روی لینک [Retirer] در قطعه [Vue1Fragment] توسط متد زیر [doRetirer] مدیریت میشود:
public void doRetirer(int position) {
// حذف آیتم شماره [position] از لیست
List<Data> liste = mainActivity.getListe();
liste.remove(position);
// موقعیت پیمایش را ثبت کنید تا به آن بازگردید
// خواندن
// [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
//موقعیت عنصر اول: کاملاً قابل مشاهده یا خیر
int firstPosition = listView.getFirstVisiblePosition();
// آفست Y این عنصر نسبت به بالای ListView
// ارتفاع هر بخش پنهان را اندازهگیری میکند
View v = listView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// تازه کردن [ListView]
adapter.notifyDataSetChanged();
//موقعیت خود را در مکان صحیح روی ListView قرار دهید
listView.setSelectionFromTop(firstPosition, top);
}
- خط ۱: موقعیت لینک [Retirer] که کلیک شده است در داخل [ListView] دریافت میشود؛
- خط ۳: فهرست دادهها بازیابی میشود؛
- خط ۴: آیتم با شناسه [position] حذف میشود؛
- خط ۱۵: ما [ListView] را تازه میکنیم. بدون این، از نظر بصری هیچ تغییری رخ نمیدهد.
- خطوط ۵–۱۳ و ۱۷: یک فرآیند نسبتاً پیچیده. بدون آن، موارد زیر رخ میدهد:
- [ListView] خطوط ۱۵ تا ۱۸ از فهرست دادهها را نمایش میدهد،
- خط 16 حذف میشود،
- خط ۱۵ بالا آن را کاملاً ریست میکند و سپس [ListView] خطوط ۰–۳ از فهرست دادهها را نمایش میدهد؛
با خطوط بالا، حذف انجام میشود و [ListView] در خط بعدی پس از خط حذفشده باقی میماند.
1.20.9. نما XML [Vue2]
![]() | ![]() |
کد 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:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue2"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textViewResultats"
android:layout_marginTop="25dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="@string/btn_vue1" />
<TextView
android:id="@+id/textViewResultats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="" />
</RelativeLayout>
- خطوط ۶–۱۵: کامپوننت [TextView] شماره ۱؛
- خطوط ۲۶–۳۳: کامپوننت [TextView] شمارهٔ ۲؛
- خطوط ۱۷–۲۴: مؤلفه [Button]، شماره ۳؛
1.20.10. قطعه [Vue2Fragment]
![]() | 123 ![]() |
قطعه [Vue2Fragment] نمای XML [vue2] را مدیریت میکند. کد آن به شرح زیر است:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// میدانها در نما
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@AfterViews
void initFragment(){
//حافظه
afterViewsDone=true;
}
@Click(R.id.button_vue1)
void navigateToView1() {
// ما به نمای 1 ناوبری میکنیم
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
//موارد انتخابشده در نمای 1 را در لیست نمایش میدهد
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (Data data : mainActivity.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
}
}
کد مربوطه در متد [updateFragment] در خط ۳۲ قرار دارد:
- خط ۳۴: متنی که باید در [TextView] شماره ۲ نمایش داده شود، محاسبه میشود؛
- خطوط ۳۵–۳۹: لیست دادههای نمایشدادهشده توسط [ListView] بهصورت بازگشتی بررسی میشود. این لیست در فعالیت ذخیره میشود؛
- خط ۳۶: اگر آیتم داده شماره i تیک خورده باشد، برچسب مربوطه به نوع [StringBuilder] اضافه میشود؛
- خط ۴۱: [TextView] متن محاسبه شده را نمایش میدهد؛
1.20.11. اجرا
یک پیکربندی اجرا برای این پروژه ایجاد کنید و آن را اجرا کنید.
1.20.12. بهبود
در مثال قبلی، ما از یک منبع داده List<Data> استفاده کردیم که در آن کلاس [Data] به شرح زیر بود:
package exemples.android.fragments;
public class Data {
// دادهها
private String texte;
private boolean isChecked;
// تولیدکننده
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
...
}
در خط ۷، از یک بولین برای کنترل تیکباکس عناصر در [ListView] استفاده کردیم. اغلب، کلاس [ListView] نیاز دارد دادههایی را نمایش دهد که میتوان با تیک زدن یک کادر آنها را انتخاب کرد، حتی اگر عنصر منبع داده فیلد بولی متناظری برای آن کادر نداشته باشد. در این صورت میتوانیم به شرح زیر عمل کنیم:
کلاس [Data] به صورت زیر در میآید:
package exemples.android.fragments;
public class Data {
// دادهها
private String texte;
// سازنده
public Data(String texte) {
this.texte = texte;
}
// گیرنده و تنظیمکننده
...
}
یک کلاس [CheckedData] مشتقشده از کلاس قبلی ایجاد کنید:
package exemples.android.fragments;
public class CheckedData extends Data {
// عنصر بررسیشده
private boolean isChecked;
// سازنده
public CheckedData(String text, boolean isChecked) {
// والد
super(text);
// محلی
this.isChecked = isChecked;
}
// گیرنده و تنظیمکننده
...
}
سپس کافی است نوع MainActivity، ListAdapter، Vue1Fragment و Vue2Fragment را جایگزین کنید نوع [Data] را با نوع [CheckedData] جایگزین کنید. برای مثال، در [MainActivity]:
@AfterInject
protected void afterInject() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// ایجاد یک لیست از دادهها
List<CheckedData> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new CheckedData("Texte n° " + i, false));
}
}
پروژه این نسخه تحت نام [Exemple-19B] در اختیار شما قرار میگیرد.
1.21. مثال ۲۰: استفاده از منو
1.21.1. ایجاد پروژه
ما پروژه [Exemple-19B] را در پروژه [Exemple-20] کپی میکنیم:
![]() | ![]() |
![]() | 3 ![]() |
ما دکمهها را از نماهای ۱ و ۲ حذف کرده و آنها را با گزینههای منو [1-2] جایگزین میکنیم.
1.21.2. تعریف XML برای منوها
![]() |
فایل [res / menu / menu_vue1] منوی نما شماره ۱ را تعریف میکند:
<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/menuOptions"
app:showAsAction="ifRoom"
android:title="@string/menuOptions">
<menu>
<item
android:id="@+id/actionCacherMontrerTout"
android:title="@string/actionCacherMontrerTout"/>
<item
android:id="@+id/actionCacherMontrerActions"
android:title="@string/actionCacherMontrerActions"/>
<item
android:id="@+id/actionCacherMontrerActionsValider"
android:title="@string/actionCacherMontrerActionsValider"/>
</menu>
</item>
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue2"
android:title="@string/navigationVue2"/>
</menu>
</item>
</menu>
موارد منو با اطلاعات زیر تعریف میشوند:
- android:id: شناسهی عنصر؛
- android:title: برچسب آیتم؛
- app:showsAsAction: نشان میدهد که آیا آیتم منو میتواند در نوار اقدامی فعالیت قرار گیرد. [ifRoom] نشان میدهد که اگر فضای کافی باشد، آیتم باید در نوار اقدامی قرار گیرد؛
- یک گزینهٔ منو ممکن است خود یک زیرمنو باشد (برچسب <menu>، خطوط ۲۵ و ۲۹)؛
فایل [res / menu / menu_vue2] منوی مربوط به نما شمارهٔ ۲ را تعریف میکند:
<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/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue1"
android:title="@string/navigationVue1"/>
</menu>
</item>
</menu>
1.21.3. پردازش منو در کلاس انتزاعی [AbstractFragment]
ما مدیریت منو را برای هر دو نما به کلاس والد [AbstractFragment] منتقل خواهیم کرد:
package exemples.android.architecture;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// دادهها قابل دسترسی برای کلاسهای فرزند
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
protected String className;
// فعالیت
protected IMainActivity mainActivity;
protected Activity activity;
// جلسه
protected Session session;
// منو
private Menu menu;
private int[] menuOptions;
private boolean initDone;
// سازنده
public AbstractFragment() {
// ابتدایی
className = getClass().getSimpleName();
// گزارش
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// حافظه
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
// گزینههای منوی # را بازیابی میکند اگر این کار قبلاً انجام نشده باشد
if (!initDone) {
// بازیابی گزینههای منوی #
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// انتقال لیست گزینهها به یک آرایه
menuOptions = new int[menuOptionsIds.size()];
for (int i = 0; i < menuOptions.length; i++) {
menuOptions[i] = menuOptionsIds.get(i);
}
// فعالیت
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
//حافظه
initDone = true;
}
// به قطعهٔ فرعی دستور داده میشود که مقداردهی اولیه انجام دهد
updateFragment();
}
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
...
}
// نمایش گزینههای منو -----------------------------------
protected void setAllMenuOptions(boolean isVisible) {
....
}
protected void setMenuOptions(MenuItemState[] menuItemStates) {
...
}
// بهروزرسانی کلاس فرزند
protected abstract void updateFragment();
}
- خط ۴۲: لاگها نشان میدهند که متد [onCreateOptionsMenu] هر بار که قطعه نمایش داده میشود، فراخوانی میشود. این متد خیلی دیر فراخوانی میشود، به طور خاص پس از فراخوانی متد [updateFragment]. این نشان میدهد که میتوان از آن برای بهروزرسانی قطعه استفاده کرد. کاری که در اینجا انجام خواهیم داد همین است (خط ۶۳)؛
- خط ۴۲: این متد دو پارامتر دارد:
- [menu]: که یک منوی خالی است؛
- [inflater]: ابزاری که امکان ایجاد منو از توضیحات اولیه آن را فراهم میکند. ما در اینجا از این گزینه استفاده نخواهیم کرد، زیرا از یک حاشیهنویسی AA استفاده خواهیم کرد که این کار را برای ما انجام میدهد؛
- خط ۴۴: منو را ذخیره میکنیم. بعداً به آن نیاز خواهیم داشت؛
- خطوط 52–53: ما شناسههای تمام موارد منو را در آرایه در خط 28 ذخیره میکنیم؛
- خطوط ۵۵–۵۷: لاگها نشان میدهند که وقتی متد [onCreateOptionsMenu] فراخوانی میشود، متد [Fragment.getActivity()] فعالیت مرتبط با فراگمنت را بازمیگرداند؛
- خط ۵۵: ما فعالیت را بهعنوان نمونهای از کلاس Android به نام [Activity] ذخیره میکنیم؛
- خط ۵۶: ما اکتیویتی را به عنوان یک نمونه از رابط [IMainActivity] ذخیره میکنیم؛
- خط ۵۷: ما جلسه را ذخیره میکنیم؛
- خط ۵۹: ما متوجه میشویم که کلاس قبلاً инициалиزه شده است، بنابراین نیازی به انجام مجدد آن نداریم (خط ۵۰)؛
- خط ۶۳: ما به فرگمنت فرزند دستور میدهیم که خود را بهروزرسانی کند. این کار ممکن است زیرا فرگمنت هم قابل مشاهده است و هم با نما و منوی خود مرتبط است ؛
متد [getMenuOptions]، که شناسههای عناصر در یک منو را بازیابی میکند، به شرح زیر است:
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);
}
}
}
متد [setAllMenuOptions] به شما امکان میدهد تمام گزینههای منو را پنهان یا نمایش دهید؛
protected void setAllMenuOptions(boolean isVisible) {
// تمام گزینههای منو را بهروزرسانی میکند
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
روش [setMenuOptions] به شما امکان میدهد برخی از گزینههای منو را پنهان یا نمایش دهید؛
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// بهروزرسانی برخی گزینههای منو
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
کلاس [MenuItemState] به شرح زیر است:
![]() |
package exemples.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;
}
// گیرنده و تنظیمکننده
...
}
1.21.4. مدیریت منو در قطعه [Vue1Fragment]
کلاس [Vue1Fragment] به صورت زیر درمیآید:
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
...
@OptionsItem(R.id.navigationVue2)
void navigateToView2() {
// حرکت به نمای ۲
mainActivity.navigateToView(1);
}
@OptionsItem(R.id.actionValider)
void valider() {
// نمایش یک پیام
Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
}
private boolean actionCacherMontrerTout = true;
@OptionsItem(R.id.actionCacherMontrerTout)
void cacherMontrerTout() {
//تغییر وضعیت
actionCacherMontrerTout = !actionCacherMontrerTout;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionCacherMontrerTout), new MenuItemState(R.id.menuActions, actionCacherMontrerTout)});
}
private boolean actionCacherMontrerActions = true;
@OptionsItem(R.id.actionCacherMontrerActions)
void actionCacherMontrerActions() {
//تغییر وضعیت
actionCacherMontrerActions = !actionCacherMontrerActions;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
}
private boolean actionCacherMontrerActionsValider = true;
@OptionsItem(R.id.actionCacherMontrerActionsValider)
void actionCacherMontrerActionsValider() {
// تغییر وضعیت
actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
}
...
@Override
protected void updateFragment() {
....
// منو بهروزرسانی شد
//setMenuOptions(...)
}
}
- خط ۲: منوی [res / menu / menu_vue1.xml] با قطعه مرتبط است؛
- خط ۴۸: هنگامی که متد [updateFragment] اجرا میشود، منو نیز میتواند برای منعکس کردن وضعیت جدید قطعه بهروزرسانی شود؛
- خط ۷: حاشیهنویسی [@OptionsItem(R.id.navigationVue2)] متدی را مشخص میکند که هنگام کلیک بر روی گزینه منوی [Navigation / Vue 2] اجرا شود؛
- خطوط ۱۹–۲۵: برای پنهان کردن یک شاخه از منو، کافی است گزینهٔ ریشهٔ آن را پنهان کنید؛
- خط ۲۴: گزینههای ریشه [menuNavigation, menuActions] نمایش داده میشوند یا پنهان میگردند؛
- خط ۴۰: برای نمایش یک گزینه در یک شاخه منو، شما باید نه تنها آن گزینه بلکه تمام گزینههایی را که هنگام حرکت به سمت بالا از گزینه برگ (leaf) به ریشه منو با آنها مواجه میشوید، نمایش دهید؛
1.21.5. مدیریت منو در قطعه [Vue2Fragment]
کد مشابهی را میتوان در قطعه مربوط به نما شماره ۲ یافت:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {
// fields in the view
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// به نمای 1 بروید
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// نمایش آیتمهای فهرستی که در نمای 1 انتخاب شدهاند
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (CheckedData data : session.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
// بهروزرسانی منو
//setMenuOptions(...)
}
}
- خط ۳۵: گزینه [Navigation / Vue 1] نمایش داده میشود؛
- خطوط 17–20: وقتی گزینه [Navigation / Vue1] کلیک میشود، متد [navigateToView1] فراخوانی میشود؛
1.21.6. اجرا
یک زمینهٔ اجرا برای این پروژه ایجاد کنید و آن را اجرا کنید.
1.22. مثال ۲۱: بازسازی کلاس انتزاعی [AbstractFragment]
مثال قبلی به ما نشان داد که وقتی قطعه دارای منو است، متد [onCreateOptionsMenu] آن مکان مناسبی برای دستور دادن به قطعه برای بهروزرسانی خود است:
- این متد دقیقاً یک بار زمانی که قرار است قطعه نمایش داده شود، فراخوانی میشود؛
- وقتی این متد فراخوانی میشود، ارتباطات قطعه با فعالیت، نما و منوی آن برقرار میگردد؛
برای نشان دادن این موضوع، بیایید به مثال ۱۲ بازگردیم که شامل چندین قطعه است که ترتیب کنار هم قرار گرفتن آنها قابل تغییر است. در آن مثال، قطعات منو نداشتند. اکنون یک منوی خالی را به آنها اختصاص خواهیم داد.
1.22.1. ایجاد پروژه
ما پروژه [Exemple-12] را در پروژه [Exemple-21] کپی میکنیم:
![]() | ![]() |
1.22.2. منوی قطعات
![]() |
منوی اضافه شده برای قطعات خالی خواهد بود:
<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">
</menu>
آنچه باید در اینجا درک کنید این است که فعالیت از قبل دارای منوی خود است [menu_main]:
<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>
وقتی یک فعالیت از قبل منو داشته باشد، منوی مربوط به قطعات به منوی فعالیت اضافه میشود: بنابراین شما گزینههای هر دو منو را در اختیار دارید. در این حالت، منوی قطعات خالی خواهد بود. در نتیجه، فقط منوی فعالیت قابل مشاهده خواهد بود.
1.22.3. قطعات
![]() |
ما کلاس انتزاعی [AbstractFragment] را از مثال قبلی (به بخش 1.21.3 مراجعه کنید) مجدداً استفاده میکنیم. ما منوی [menu_fragment] را با دو قطعه مرتبط میکنیم:
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
در دو قطعه [PlaceholderFragment] و [Vue1Fragment]، هرگونه ارجاع به کلاس انتزاعی قدیمی [AbstractFragment] را حذف میکنیم.
1.22.4. اجرا
اپلیکیشن را اجرا کنید و بررسی کنید که کار میکند. لاگها را زیر نظر داشته باشید تا ببینید متد [onCreateOptionsMenu] از کلاس [AbstractFragment] چه زمانی اجرا میشود. اکنون این متد است که متد [updateFragment] را در فرگمنتهای فرزند فراخوانی میکند.
1.23. مثال ۲۲: ذخیره و بازیابی وضعیت فعالیت و قطعات
1.23.1. مشکل
در اینجا به مسئله چرخاندن دستگاه اندروید (عمودی <--> افقی) میپردازیم. برای نشان دادن این موضوع، مثال ۲۱ قبلی را مجدداً بررسی میکنیم:

اگر دستگاه را بچرخانیم [1]، نمای جدید زیر را دریافت میکنیم:

میتوانیم ببینیم که:
- در [1]، برگه [Fragment n° 3] ناپدید شده است؛
- در [2]، متنی که نمایش داده میشود در واقع مربوط به قطعه شماره ۳ است، اما شمارنده بازدید نادرست است؛
در طول این چرخش، لاگها به شرح زیر است:
- خط ۱: میتوان دید که فعالیت بهطور کامل بازسازی شده است؛
- خطوط ۳–۷: همین امر در مورد پنج قطعهای که توسط فعالیت مدیریت میشوند نیز صدق میکند؛
- خط ۲۱: قطعه شماره ۳ در حال نمایش است. میبینیم که قبل از افزایش، تعداد بازدیدها ۰ است؛
بنابراین نتیجهٔ حاصل پس از چرخش را میتوان به شرح زیر توضیح داد:
- کلاس [MainActivity] در ابتدا یک نوار تب با یک تب واحد به نام [Vue 1] ایجاد میکند. این تبی است که قابل مشاهده است؛
- پس از چرخش دستگاه، مدیر صفحه [mViewPager] همان قطعه را مجدداً نمایش میدهد، در این مورد قطعه شماره ۳. مهم است که به خاطر داشته باشیم که برگهها و قطعات مفاهیم متفاوتی هستند و چرخههای عمر متفاوتی دارند. متد [updateFragment] برای قطعه شماره ۳ اجرا خواهد شد:
public void updateFragment() {
// لاگ
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// افزایش شماره بازدید
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// متن اصلاحشده
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- خط ۷: شماره آخرین بازدید از جلسه خوانده میشود. با این حال، این جلسه، مانند همه چیزهای دیگر، بازسازی شده و شماره بازدید به صفر ریست شده است. این موضوع نتیجه نمایش داده شده در قطعه شماره ۳ را توضیح میدهد؛
1.23.2. روشهای ذخیره و بازیابی فعالیت و قطعات
1.23.2.1. راه حل ۱: ذخیره دستی
هنگامی که دستگاه چرخانده میشود، دو متد از اکتیویتی فراخوانی میشوند:
// مدیریت پشتیبانگیری/بازیابی فعالیت ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// والد
super.onSaveInstanceState(outState);
// نسخهٔ پشتیبان وضعیت فعالیت
// ....
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// والد
super.onCreate(savedInstanceState);
// بازیابی فعالیت
// ...
}
- خطوط ۲–۸: متد [onSaveInstanceState] توسط سیستم در حین چرخش فراخوانی میشود. اینجاست که فعالیت میتواند ذخیره شود. اگر هیچ اقدامی انجام نشود، هیچ چیزی ذخیره نمیشود. وضعیت فعالیت باید در پارامتر [Bundle outState] که به متد ارسال میشود، ذخیره شود. کلاس [Bundle] شبیه یک فرهنگ لغت است. این کلاس متدهای [putString, putInt, putLong, putBoolean, putChar, ...] را با دو پارامتر دارد: void putT(String key, T value);
- خطوط ۱۰–۱۶: متد [onCreate] هنگام ایجاد فعالیت فراخوانی میشود. اگر وضعیت فعالیت ذخیره شده باشد، این وضعیت ذخیره شده از طریق پارامتر [Bundle savedInstanceState] به آن ارسال میشود. برای بازیابی مقادیر ذخیرهشده، متدهایی مانند [getString, getInt, getLong, geBoolean, getChar, ...] با یک پارامتر در دسترس هستند: T getT(String key);
قطعات (Fragments) نیز همین دو متد را برای ذخیره وضعیت خود دارند.
ما از این اطلاعات برای ذخیره و بازیابی وضعیت مثال ۲۱ استفاده خواهیم کرد. برای این کار، پروژه [Exemple-21] را در [Exemple-22] کپی میکنیم.
1.23.2.2. راه حل ۲: ذخیره خودکار
مستندات اندروید بیان میکند که هنگامی که دستگاه چرخانده میشود، میتوانید با استفاده از عبارت: [Fragment].setRetainInstance(true). از نابودی یک فرگمنت جلوگیری کنید. چندین مقاله در [StackOverflow] توصیه میکنند که این دستور را تنها برای فرکمنتهایی که رابط کاربری ندارند [http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, http://stackoverflow.com/questions/12640316/further-understanding-setretaininstancetrue, http://stackoverflow.com/questions/21203948/setretaininstancetrue-in-oncreate-fragment-in-android] استفاده کنید. من این دستور را روی دو مثال آزمایش کردم: مثال-۱۷ (بخش ۱.۱۸ – یک برنامه با یک فرگمنت که یک فرم را نمایش میدهد) و مثال-۲۱ (بخش ۱.۲۲)، یک برنامه با پنج فرگمنت. در هر دو مورد، اعمال این دستور واحد بر تمام فرگمنتهای برنامه برای بازسازی صحیح نمای نمایش داده شده هنگام چرخش دستگاه ناکافی بود. به جای ساختن دو مدل، یکی بر اساس [setRetainInstance(true)] و دیگری بر اساس [setRetainInstance(false)] (که مقدار پیشفرض است)، تصمیم گرفتم توصیههای [StackOverflow] را دنبال کنم و مقدار پیشفرض false را برای متد [setRetainInstance(boolean )] حفظ کنم. دستور [Fragment].setRetainInstance(true) هرگز در بقیه این سند استفاده نشد.
1.23.3. روش پشتیبانگیری و بازیابی پروژه [Exemple-22]
پروژه [Exemple-22] به شرح زیر تکامل مییابد:
![]() |
دو کلاس جدید در اینجا ظاهر میشوند:
- [PlaceHolderFragmentState]، که وضعیت یک قطعه از نوع [PlaceHolderFragment] را ذخیره خواهد کرد؛
- [Vue1FragmentState]، که وضعیت یک قطعه از نوع [Vue1Fragment] را ذخیره خواهد کرد؛
این کلاسها به شرح زیر هستند:
package exemples.android;
public class Vue1FragmentState {
//وضعیت Vue1Fragment
private boolean hasBeenVisited=false;
// گیرندهها و تنظیمکنندهها
...
}
- خط ۵: بولین [hasBeenVisited] زمانی درست است که قطعه [Vue1Fragment] حداقل یک بار بازدید (نمایش) شده باشد. این فیلد برای مثال ایجاد شده است زیرا قطعه [Vue1Fragment] چیزی برای ذخیره ندارد؛
کلاس [PlaceHolderFragmentState] به شرح زیر است:
package exemples.android;
public class PlaceHolderFragmentState {
//وضعیت بازدید شده یا نشده
private boolean hasBeenVisited;
// متن نمایش داده شده
private String text;
// گیرندهها و تنظیمکنندهها
...
}
- خط ۵: بولی [hasBeenVisited] ظاهر میشود؛
- خط ۷: متنی که توسط فرگمنت در لحظهای که قرار است ذخیره شود نمایش داده میشود. ما دیدهایم که این متن در حین چرخش از دست رفت؛
وضعیت قطعات در جلسه ذخیره خواهد شد و این فعالیت است که مسئول ذخیره و بازیابی این جلسه خواهد بود. جلسه به شرح زیر تحول مییابد:
package exemples.android;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// تعداد قطعات بازدید شده
private int numVisit;
//شناسه قطعه از نوع [PlaceholderFragment] نمایش داده شده در زبانه دوم
private int numFragment = -1;
// شماره زبانهٔ انتخابشده
private int selectedTab = 0;
// شماره نمای فعلی
private int currentView;
// ذخیرهٔ قطعه ---------------
private Vue1FragmentState vue1FragmentState;
private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
// سازنده
public Session() {
for (int i = 0; i < placeHolderFragmentStates.length; i++) {
placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
}
vue1FragmentState = new Vue1FragmentState();
}
// گیرنده و تنظیمکننده
...
}
- خط ۱۸: وضعیت قطعه [Vue1Fragment];
- خط ۱۹: وضعیت تکههای از نوع [PlaceHolderFragment];
- خطوط ۲۲–۲۷: در سازنده جلسه، فیلدهای خطوط ۱۸ و ۱۹ مقداردهی اولیه میشوند؛
- خطوط ۱۲–۱۵: دو فیلد جدید ظاهر میشوند:
- خط ۱۳: شماره آخرین زبانه انتخابشده؛
- خط ۱۵: شماره آخرین قطعه نمایش داده شده؛
فعالیت جلسه را به شرح زیر ذخیره و بازیابی میکند:
// مدیریت ذخیره/بازیابی فعالیت ----------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// والد
super.onSaveInstanceState(outState);
// ذخیره جلسه
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();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// والد
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
//بازیابی جلسه
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// لاگ
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
}
- خط ۸: جلسه به صورت رشته jSON ذخیره میشود؛
- خط ۲۹: جلسه از رشتهٔ jSON بازیابی میشود؛
برای مدیریت ذخیره و بازیابی قطعات، کلاس انتزاعی [AbstractFragment] به شرح زیر اصلاح میشود:
// مدیریت پشتیبانگیری/بازیابی -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// والد
super.setUserVisibleHint(isVisibleToUser);
// نسخهٔ پشتیبان؟
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// قطعه در آستانه پنهان شدن است – در حال پشتیبانگیری از آن هستیم
saveFragment();
saveFragmentDone = true;
}
//حافظه
this.isVisibleToUser = isVisibleToUser;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// والد
super.onActivityCreated(savedInstanceState);
// لاگ
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
//قطعه باید بازیابی شود
fragmentHasToBeInitialized = true;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// log
if (isDebugEnabled) {
Log.d(className, "onSaveInstanceState");
}
// والد
super.onSaveInstanceState(outState);
// قطعه را فقط در صورتی ذخیره کن که قابل مشاهده باشد
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// کلاسهای فرزند
protected abstract void updateFragment();
protected abstract void saveFragment();
- تصمیم گرفته شده است که وضعیت قطعات را در جلسه در دو نقطه زمانی ذخیره کند:
- خطوط ۲–۱۴: زمانی که قطعه از حالت قابل مشاهده به پنهان تغییر میکند؛
- خطوط ۲۹–۴۲: زمانی که سیستم نشان میدهد که قطعه باید ذخیره شود و قطعه قابل مشاهده است (خط ۳۸);
این مکانیزم از ذخیره کردن بیش از حد لزوم جلوگیری میکند. در واقع، از آنجا که ما وضعیت قطعه i را زمانی که از حالت قابل مشاهده به پنهان تغییر کرد ذخیره کردهایم، وقتی قطعه j نمایش داده میشود و چرخشی انجام میگیرد، نیازی به ذخیره مجدد قطعه i نیست. اگر از آخرین باری که ذخیره شده دوباره نمایش داده نشده باشد، پس وضعیت آن تغییر نکرده است. تنها وضعیت قطعه j نیاز به ذخیره شدن دارد. این مکانیزم یک مزیت دیگر نیز دارد: نیازی نیست که فقط هنگام چرخش دستگاه، وضعیت یک قطعه را ذخیره کنیم. مورد دیگری نیز وجود دارد، یعنی ناوبری ساده بین قطعات، برای مثال در یک سیستم تبدار. در چنین مواردی، میخواهیم یک قطعه را به حالتی بازگردانیم که در آخرین نمایش خود در آن حالت بوده است. این حالت ممکن است تا حدی از بین رفته باشد اگر قطعه در مقطعی دیگر مجاور قطعات نمایش داده شده نباشد. خودِ قطعه به طور کامل بازسازی نمیشود، اما نمای مرتبط با آن بازسازی میشود. پشتیبانگیری که هنگام پنهان شدن قطعه انجام شده است، برای بازیابی آخرین وضعیت این نما استفاده خواهد شد؛
- خطوط ۱۰، ۴۰: برای جلوگیری از ایجاد دو پشتیبان متوالی، متغیر بولهای [saveFragmentDone] برای نشان دادن اینکه یک پشتیبان ایجاد شده است، استفاده میشود؛
- خطوط ۹، ۳۹: به قطعهٔ فرزند دستور داده میشود تا وضعیت خود را ذخیره کند. متد [saveFragment] انتزاعی است (خط ۴۷). بنابراین، این وظیفه بر عهدهٔ کلاسهای فرزند است که آن را پیادهسازی کنند؛
- خطوط 16–26: متد [onActivityCreated] برای مقداردهی به بولین [fragmentHasToBeInitialized] به مقدار true استفاده میشود. این به آن دلیل است که قطعه فرزند باید بداند که باید وضعیت قطعه را از حالتی که در جلسه پیدا خواهد کرد، به طور کامل ریست کند؛
هنوز در کلاس [AbstractFragment]، متد [onCreateOptionsMenu] به شرح زیر تغییر میکند:
// بهروزرسانی قطعه
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// حافظه
this.menu = menu;
// لاگ
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
...
// به قطعهٔ فرعی دستور داده میشود که خود را بهروزرسانی کند
updateFragment();
//پشتیبانگیری باید انجام شود
saveFragmentDone = false;
}
- خط ۱۴: دیدیم که بولین [saveFragmentDone] هنگام انجام عملیات ذخیره روی vrai تنظیم شد. در مقطعی، باید روی faux ریست شود. وقتی متد [updateFragment] (خط ۱۲) از قطعه فرعی اجرا میشود، قطعه قابل مشاهده خواهد شد. با این حال، دقیقاً زمانی که یک قطعه قابل مشاهده است که باید ذخیره شود، بهطور مشخص در لحظهای که از حالت قابل مشاهده به حالت پنهان تغییر میکند. سپس مقدار بولین [saveFragmentDone] را روی false تنظیم میکنیم تا ذخیره انجام شود؛
1.23.4. ذخیره قطعه [Vue1Fragment]
قطعات در متد [saveFragment] ذخیره میشوند که توسط کلاس والد [AbstractFragment] فراخوانی میشود:
//وضعیت قطعه ذخیره شود
@Override
public void saveFragment() {
// لاگ
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// ذخیره وضعیت قطعه در طول جلسه
Vue1FragmentState state = new Vue1FragmentState();
state.setHasBeenVisited(true);
session.setVue1FragmentState(state);
// لاگ
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- خطوط ۹–۱۱: ذخیره وضعیت قطعه در جلسه. هنگامی که متد [saveFragment] فراخوانی میشود، قطعه قابل مشاهده است. بنابراین باید بولین [hasBeenVisited] روی vrai (خط ۱۰) تنظیم شود؛
1.23.5. ذخیرهٔ قطعه [PlaceHolderFragment]
قطعات در متد [saveFragment] ذخیره میشوند، که توسط کلاس والد [AbstractFragment] فراخوانی میشود:
@Override
public void saveFragment() {
//ذخیره وضعیت قطعه در جلسه
PlaceHolderFragmentState state = new PlaceHolderFragmentState();
state.setText(textViewInfo.getText().toString());
state.setHasBeenVisited(true);
session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- خطوط ۴–۷: وضعیت قطعه را در طول جلسه ذخیره میکند؛
- خط ۵: متنی که در حال حاضر توسط [TextView] textViewInfo نمایش داده میشود، ذخیره میشود؛
- خط ۶: مقدار بولی قطعه [hasBeenVisited] روی vrai تنظیم میشود؛
- خط ۷: وضعیت قطعه در آرایه [placeHolderFragmentStates] ذخیره میشود. شماره عنصری که باید مقداردهی اولیه شود، برابر است با شماره بخش قطعه منهای یک؛
1.23.6. بازیابی قطعه [Vue1Fragment]
قطعات در متد [updateFragment] بازیابی میشوند:
@Override
protected void updateFragment() {
//ثبت
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
}
// احیا؟
if (fragmentHasToBeInitialized) {
//بازیابی وضعیت
hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
fragmentHasToBeInitialized = false;
}
// لاگ
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
}
// ناوبری؟
boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
if (navigation) {
// افزایش شماره بازدید
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
//نمایش شماره بازدید
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
//تغییر شماره نمای فعلی
session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
}
- خطوط ۸–۱۲: بازیابی وضعیت قطعه. متغیر بولی [fragmentHasToBeInitialized] توسط کلاس والد [AbstractFragment] مقداردهی اولیه شده است. وقتی روی vrai تنظیم میشود، قطعه تازه بازسازی شده و باید دوباره تنظیم شود. اینجاست که این کار انجام میشود. در این مثال خاص، کاری برای انجام دادن وجود ندارد. ما صرفاً نشان دادهایم که مقدار بولین [hasBeenVisited] را میتوان از وضعیت ذخیرهشدهی قطعه بازیابی کرد (خط ۱۰)؛
- خط ۱۱: فراموش نکنید که [fragmentHasToBeInitialized] را روی faux ریست کنید، تا زمانی که بعداً به این قطعه بازگردیم—به شرطی که دستگاه چرخانده نشده باشد—ابتدایسازی غیرضروری قطعه را انجام ندهیم؛
- خطوط ۱۸–۲۶: شمارنده بازدید را افزایش دهید. در اینجا یک مشکل وجود دارد: هنگام بازیابی قطعه، نمیخواهیم این شمارنده را افزایش دهیم. باید در اینجا بین موارد زیر تمایز قائل شویم:
- یک اقدام ناوبری ساده که کاربر را به برگه [Vue 1] بازمیگرداند؛
- یک بازیابی زمانی که کاربر دستگاه خود را در حالی که برگه [Vue 1] نمایش داده شده است، میچرخاند؛
ما بین این دو حالت با استفاده از شماره نما که در جلسه ذخیره شده است، تمایز قائل میشویم. این شماره، شماره آخرین نمای نمایشدادهشده است (خط ۲۸).
- خط ۱۸: در صورتی که شماره آخرین نما با شماره نمای فعلی متفاوت باشد، ناوبری انجام میشود نه بازیابی؛
- خطوط ۲۱–۲۵: شمارشگر بازدید افزایش یافته و نمایش داده میشود؛
1.23.7. بازیابی قطعه [PlaceHolderFragment]
قطعات در روش [updateFragment] بازیابی میشوند:
// دادهها
private String text;
private int numVisit;
private String newText;
private boolean hasBeenVisited = false;
private ObjectMapper jsonMapper = new ObjectMapper();
...
public void updateFragment() {
// لاگ
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// این کدام قطعه است؟
int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
int numView = numSection - 1;
// آیا باید قطعه را مقداردهی اولیه کرد؟
if (fragmentHasToBeInitialized) {
// متن اولیه
text = getString(R.string.section_format, numSection);
fragmentHasToBeInitialized = false;
}
// ناوبری؟
boolean navigation = session.getCurrentView() != numView;
if (navigation) {
// افزایش شمارش بازدید
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
//متن اصلاحشده
newText = String.format("%s, visite %s", text, numVisit);
} else {
//این یک بازسازی است
PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
newText = state.getText();
}
// نمایش متن
textViewInfo.setText(newText);
// نمايش فعلی
session.setCurrentView(numView);
}
- خطوط ۱۵–۱۶: شماره نمایی که در حال حاضر در حال بهروزرسانی است، تعیین میشود؛
- خطوط ۱۸–۲۲: این مورد، حالتی را پوشش میدهد که قطعه پس از تغییر جهت دستگاه در یک چرخهٔ ذخیره/بازیابی قرار دارد. باید در اینجا بازیابی شود. این معمولاً شامل بازیابی برخی از فیلدهای قطعه است؛
- خط ۲۰: فیلد [text] در خط ۲ باید حاوی متن اولیه نمایشدادهشده توسط قطعه باشد: [Hello world from section i]. این فیلد باید در اینجا مجدداً تولید شود؛
- خط 21: توجه کنید که قطعه اولیه شده است؛
- خطوط ۲۴–۳۶: همانند مورد قبلی برای قطعه [Vue1Fragment]، شمارنده بازدید نباید در حین بازیابی افزایش یابد. همانند قبل، باید بین ناوبری و بازیابی تمایز قائل شد؛
- خطوط ۳۲–۳۶: مورد بازیابی؛
- خط ۳۴: وضعیت قطعه قبل از چرخش دستگاه از جلسه بازیابی میشود؛
- خط ۳۵: متنی که در آن زمان نمایش داده شده بود بازیابی میشود؛
- خط ۳۸: این متن دوباره نمایش داده میشود؛
- خط ۴۰: شماره نمای جدید نمایشدادهشده در جلسه ثبت میشود؛
1.23.8. مدیریت تب
بخشهای قبلی به مدیریت تبها نپرداختهاند. با این حال، در مثال ۲۱ هنگام چرخاندن دستگاه با مشکلی مواجه شدیم: تنها تب اول، [Vue 1]، حفظ شد و تب دوم از بین رفت.
ما این مشکل را در کلاس [MainActivity] به شرح زیر حل میکنیم:
@AfterViews
protected void afterViews() {
// لاگ
if (IS_DEBUG_ENABLED) {
Log.d(className, "afterViews");
}
//نوار ابزار
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
// زبانهی اول
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);
}
// کدام برگه را باید انتخاب کنم؟
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- خطوط ۱۴–۱۶: ایجاد زبانه اول؛
- خطوط ۱۸–۲۳: ایجاد زبانه دوم. برای تعیین اینکه آیا نیاز به ایجاد آن است، شماره قطعه نمایش داده شده در زبانه ۲ را در جلسه بررسی میکنیم. اگر این شماره با -۱ (مقدار اولیه آن) برابر نباشد، زبانه دوم ایجاد میشود. در این مرحله، دو زبانه وجود دارد که زبانه اول به صورت پیشفرض انتخاب شده است؛
- خط ۲۶: ما شماره زبانه انتخابشده قبل از عملیات ذخیره/بازیابی را از جلسه بازیابی کرده و آن را مجدداً انتخاب میکنیم. اگر فیلد [selectedTab] هنوز توسط کد مقداردهی اولیه نشده باشد، از مقدار اولیه ۰ آن استفاده میشود؛











































































































































































































































































































































