Skip to content

1. Навчання програмуванню для Android

PDF цього документа доступний за посиланням |ICI|.

Приклади з документа доступні за посиланням |ICI|.

1.1. Introduction

1.1.1. Зміст

Цей документ є переробленою версією кількох існуючих документів:

  1. «Android для розробників» J2EE;
  1. Вступ до програмування планшетів Android на прикладах;
  2. Керування Arduino за допомогою планшета на базі Android;
  3. Вступ до програмування планшетів Android на прикладах — версія 2

та містить такі нововведення:

  • у документі 1 було представлено архітектуру під назвою AVAT (Активність-Види-Дії-Завдання) для спрощення асинхронного програмування в додатку Android. У цьому документі для управління асинхронними діями використовується стандартна бібліотека RxJava;
  • у документі 2 використовувався Eclipse з плагіном Android під назвою IDE. У цьому документі використовується Android Studio;
  • документ 3 наведено без змін;
  • у документі 4 використовувалася бібліотека [Android Annotations] (AA) разом із IDE IntelliJ IDEA Community Edition. Цей документ повністю повторює документ 4 з такими відмінностями:
    • IDE тепер — це Android Studio;
    • системою збірки для всіх клієнтських та серверних проєктів є Gradle (у документі 4 іноді використовувався Maven);
    • асинхронне програмування здійснюється за допомогою бібліотеки RxJava (у документі 4 використовувалася бібліотека AA);
  • у цьому документі розглядаються теми, які не розглядалися або розглядалися лише частково в попередніх документах:
    • поняття суміжності фрагментів;
    • збереження/відновлення активності та її фрагментів;
    • життєвий цикл фрагментів;

Нарешті, у ньому представлено скелет клієнта для Android, що взаємодіє з веб-сервісом / jSON, у якому виокремлено велику кількість елементів, що регулярно зустрічаються в клієнтах такого типу. Цей скелет використовується у всіх прикладах, починаючи з розділу 2. Це справді інноваційна частина документа.

Наводяться такі приклади:

Exemple
Характер
1
Імпорт існуючого проєкту Android
2
Базовий проект Android
3
Базовий проект [Android Annotations]
4
Погляди та події
5
Навігація між поданнями
6
Навігація за вкладками
7
Використання бібліотеки [Android Annotations] із Gradle
8 à 12
Управління фрагментами в додатку для Android
13
Перегляд навігації між вікнами
14
Двошарова архітектура
15
Архітектура «клієнт-сервер»
16
Управління асинхронністю за допомогою RxJava
17, 17B
Компоненти введення даних
18
Використання шаблону переглядів
19
Компонент ListView
20
Використання меню
21
Використання батьківського класу для фрагментів
22, 22B
Збереження та відновлення стану активності та фрагментів
23
Погодний клієнт
Chap 2
Скелет клієнта Android, що взаємодіє з веб-сервісом / jSON. У ньому виокремлено велику кількість елементів, які регулярно зустрічаються в клієнтах Android такого типу.
Chap 3
Управління зустрічами в медичному кабінеті
Chap 4
Практичне завдання — управління базовою системою нарахування заробітної плати
Chap 5
Практичне завдання — керування платами Arduino

Цей документ використовувався на останньому курсі інженерної школи IstiA при університеті Анже [istia.univ-angers.fr]. Це пояснює іноді дещо особливий тон тексту. Обидва практичні завдання — це тексти з TP, для яких наведено лише основні положення розв’язання. Розв’язання має побудувати сам читач.

Вихідний код прикладів доступний |ICI|. Щоб виконати ці приклади, слід дотримуватися процедури, описаної в розділі 6.12.

Цей документ є вступним посібником з програмування для Android. Він не претендує на вичерпність. Він призначений переважно для початківців.

Основний ресурс з програмування для Android знаходиться за адресою URL [http://developer.android.com/guide/components/index.html]. Саме там слід шукати загальну інформацію про програмування для Android.

1.1.2. Необхідні знання

Для оптимального використання цього документа необхідне добре володіння мовою Java.

1.1.3. Використані інструменти

Наведені нижче приклади було протестовано в такому середовищі:

  • комп’ютер із Windows 10 Pro 64-біт;
  • JDK 1.8;
  • Android SDK API 23;
  • Android Studio, версія 2.1;
  • емулятор Genymotion, версія 2.6.0;

Щоб скористатися цим документом, вам потрібно встановити:

  • JDK (див. розділ 6.8);
  • менеджер емуляторів Android Genymotion (див. розділ 6.9);
  • менеджер залежностей Maven (див. розділ 6.10);
  • IDE та [Android Studio] (див. розділ 6.11);

1.2. Приклад-01: імпорт прикладу Android

1.2.1. Створення проєкту

Створимо за допомогою Android Studio перший проект для Android. Спочатку створимо порожню папку [exemples], куди будуть розміщені всі наші проекти:

  

потім створимо проєкт за допомогою Android Studio. Спочатку імпортуємо один із прикладів, що входять до складу IDE [1-5]:

 

Image

Імпорт проекту може призвести до помилок через невідповідність між середовищем, яке використовувалося під час створення проекту, та тим, що використовується тут для його виконання. Це нагода дізнатися, як вирішувати такі помилки. У цьому випадку ми маємо таку помилку:

Імпортований проект налаштований за допомогою таких файлів: [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'] // шаблонний код, що генерується під час створення шаблону прикладу

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"
    }
}
  • Повідомлена помилка пов’язана з рядками 31, 34–35: у нас немає SDK версії 21. Ми замінюємо цю версію на версію 23, яка у нас є.

У файлі [build.gradle] Android Studio пропонує такі зміни:

 

Щоб прийняти пропозиції, потрібно натиснути [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]:


#Середа, 10 квітня 15:27:10 PDT 2013
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

У рядку 6, наведеному вище, слід замінити 2,8 на 2,10.

Щоб отримати доступ до файлу [<projet>/gradle/wrapper/gradle-wrapper.properties], слід скористатися перспективою проекту:

Виправивши це, можна скомпілювати додаток [1], запустити емулятор Genymotion [2], а потім запустити проект [3]:

 

Image

Зупинимо програму:

  

Тепер можна закрити проект. Створимо новий.

  

1.2.2. Кілька зауважень щодо IDE

1.2.2.1. Перспективи

IDE 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 року) цей кеш часто не враховував щойно внесені зміни в код. У такому випадку потрібно очистити цей кеш:

У версії Android 2.1 (травень 2016 р.) попередню операцію доводилося виконувати багато разів, і іноді цього було недостатньо для усунення виявленої аномалії. Рішенням стало відключення технології [Instant Run]:

  • у [3-4], все було вимкнено;

У подальшій роботі ми використовували саме цю конфігурацію кешу і не стикалися з жодними проблемами.

1.2.2.4. Управління журналами

Під час виконання проєкту в моніторі Android відображаються журнали:

У вкладці [Android Monitor] [1] журнали відображаються у вкладці [logcat] [2]. Кнопка [3] дозволяє видалити журнали. Ця кнопка корисна, коли потрібно переглянути журнали певної дії:

  • видаляємо журнали;
  • на пристрої Android виконується дія, для якої потрібні журнали;
  • журнали, що з’являться після цього, стосуватимуться саме цієї дії;

Існує кілька рівнів журналів [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] (рядок 9). Вони викликаються на різних етапах життєвого циклу додатка. Іноді вони виконуються кілька разів. Навіть під час ознайомлення з документацією іноді важко визначити, чи такий метод життєвого циклу буде виконано до чи після методу, який ми написали самостійно. Однак ця інформація часто є важливою. У такому разі можна додати записи в журнал, як показано нижче:


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()) {
      ...
  }
}
  • у рядках 7, 14 та 21 використовується клас [Log]. Цей клас дозволяє записувати логи в консоль Android [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], який створює активність Android, виконується раніше, ніж метод [onCreateOptionsMenu], який створює меню додатка.

Тепер, якщо натиснути на пункт меню в емуляторі Android [1]:

  

у консолі журналів додається такий запис:


05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected

Далі ми часто додаватимемо в код Android інструкції для логів. Здебільшого ми не будемо їх коментувати. Вони є лише для того, щоб заохотити читача переглянути консоль логів, щоб поступово зрозуміти життєвий цикл Android-додатка.

1.2.2.5. Управління емулятором [Genymotion]

Іноді емулятор Genymotion вилітає, і його неможливо перезапустити. Це відбувається через те, що процеси VirtualBox залишаються активними в диспетчері завдань. Відкрийте його [Ctrl-Alt-Supp] і видаліть усі наявні завдання VirtualBox:

Після цього перезапустіть емулятор Genymotion з Android Studio.

1.2.2.6. Управління створеним бінарним файлом APK

Компіляція проєкту створює бінарний файл із розширенням .apk:

Існує дві версії: одна називається [debug], а інша — [debug-unaligned]. Слід використовувати першу, оскільки друга є проміжною версією. Бінарний файл .pak, створений за допомогою [4], можна безпосередньо перенести на емулятор або пристрій Android. Щоб перенести його на емулятор, достатньо перетягнути файл на емулятор за допомогою миші.

1.3. Приклад-02: простий проект для Android

Створимо за допомогою Android Studio новий проект Android [1-12]:

 

У [13] запускається додаток. У результаті на емуляторі Genymotion з’являється екран [14].

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 із використанням елементів його конфігурації. Це мінімальний файл, який ми поступово доповнюватимемо.

  • рядки 3–12: характеристики додатка для Android;
  • рядки 22–25: його залежності. Саме тут ми вноситимемо зміни відповідно до розглянутих прикладів;

1.3.2. Маніфест додатка

  

Файл [AndroidManifest.xml] [1] визначає характеристики бінарного файлу додатка для Android. Його вміст виглядає так:


<?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>
  • рядок 3: пакет проекту Android;
  • рядок 10: назва активності;

Ці дві відомості походять із даних, введених під час створення проєкту:

  • рядок 3 маніфесту (пакет) походить із введеного вище значення [4]. У цьому пакеті автоматично генерується певна кількість класів;
  • рядок 10 маніфесту (назва активності) походить із введеного вище запису [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>
  • рядок 10: основна активність додатка. Вона посилається на клас [1], наведений вище;
  • рядок 6: піктограма додатка [2]. Її можна змінити;
  • рядок 7: назва додатка. Вона міститься у файлі [strings.xml] [3]:

<resources>
  <string name="app_name">Exemple-02</string>
</resources>

Файл [strings.xml] містить символьні рядки, що використовуються додатком. У рядку 2 назва додатка походить із даних, введених під час створення проєкту [4]:

 
  • рядок 10: тег активності. Додаток для Android може мати кілька активностей;
  • рядок 12: ця активність позначена як основна;
  • рядок 13: і вона має з’являтися у списку додатків, які можна запустити на пристрої Android.

1.3.3. Головна активність

 

Додаток для Android базується на одній або декількох активностях. Тут було згенеровано активність [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);
  }
}
  • рядок 6: клас [MyActivity] успадковує клас Android [AppCompatActivity]. Так буде для всіх майбутніх активностей;
  • рядок 9: метод [onCreate] виконується під час створення активності. Це відбувається до відображення виду, пов’язаного з цією активністю;
  • рядок 10: викликається метод [onCreate] батьківського класу. Це потрібно робити завжди;
  • рядок 11: файл [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: текст, що відображається. Не рекомендується вказувати текст безпосередньо у видах. Краще перемістити цей текст у файл [res/values/strings.xml] [3]:

Отже, текст, що відображається, буде [Hello World!]. Де він буде відображатися? Контейнер [RelativeLayout] заповнить екран. [TextView], який є його єдиним елементом, буде відображатися у верхньому лівому куті цього контейнера, тобто у верхньому лівому куті екрана;

Що означає [R.layout.activity_main] у рядку 11? Кожному ресурсу Android (видам, фрагментам, компонентам тощо) присвоюється ідентифікатор. Таким чином, вид [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];
  • рядок 7: атрибут [R.string.app_name] є ідентифікатором рядка [app_name] у файлі [res / values / string.xml]:
  • рядок 19: атрибут [R.mipmap.ic_launcher] є ідентифікатором зображення [res / mipmap / ic_launcher];

Отже, слід пам’ятати, що коли в коді згадується [R.layout.activity_main], мається на увазі атрибут класу [R]. IDE допомагає нам дізнатися про різні елементи цього класу:

1.3.4. Запуск додатка

Щоб запустити додаток для Android, нам потрібно створити конфігурацію виконання:

  • у [1] виберіть [Edit Configurations];
  • проект було створено з конфігурацією [app], яку ми видалимо, щоб створити нову — [2];
  • у [3] створити нову конфігурацію виконання;
  
  • у [4] вибрати [Android Application];

Image

  • у [5] у випадаючому списку вибрати модуль [app];
  • [6-8] — залишити значення, запропоновані за замовчуванням;
  • у [7] дія за замовчуванням — це та, що визначена у файлі [AndroidManifest.xml] (рядок 1 нижче):

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

Через деякий час програмний емулятор відобразить таке вікно:

Image

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] у рядках 8–12 належить до методів, які можуть викликатися протягом життєвого циклу активності. У документації Android наведено їхній перелік:

 
  • [1]: метод [onCreate] викликається під час запуску активності. Саме в цьому методі активность пов’язується з представленням та отримуються посилання на її компоненти;
  • [2-3]: потім викликаються методи [onStart, onResume]. Бачимо, що метод [onResume] є останнім методом, який виконується перед переходом до стану [4] активності, що виконується;

1.4. Приклад-03: переписання проєкту [Exemple-02] із використанням бібліотеки [Android Annotations]

Тепер ми введемо бібліотеку [Android Annotations], яка спрощує розробку додатків для Android. Для цього скопіюємо приклад [Exemple-02] у [Exemple-03], дотримуючись процедури [1-16].

  • у [1], перейдіть до перспективи [Project], щоб переглянути весь проект Android;

Примітка: між [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. Ця бібліотека вводить нові класи для анотації вихідного коду Android. Ці анотації використовуватиме процесор, який створюватиме в модулі нові класи Java, що братимуть участь у його компіляції нарівні з класами, написаними розробником. Таким чином, ми маємо такий ланцюжок компіляції:

Спочатку ми додамо у файл [build.gradle] залежності від компілятора анотацій AA (процесор, згаданий вище):


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'])
}
  • у рядках 4–5 додаються дві залежності, що утворюють бібліотеку AA;

Файл [build.gradle] знову змінюється для використання плагіна під назвою [android-apt], який змінює процес компіляції на двоступеневий:

  • обробка анотацій Android, що призводить до створення нових класів;
  • компіляція всіх класів проєкту;

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    // Починаючи з версії 0.11 плагіна Gradle для Android, необхідно використовувати android-apt версії 1.3 або вище
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'
  • рядок 8: версія плагіна [android-apt], яку буде шукано в центральному репозиторії Maven (рядок 3);
  • рядок 13: активація цього плагіна;

На цьому етапі переконайтеся, що конфігурація виконання [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);
  }
}
  • рядок 7: анотація [@EActivity] є анотацією AA (рядок 3). Її параметром є вид, пов’язаний з активністю;

Ця анотація створить клас [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>
  • рядок 11: нова активність;

Зробивши це, ми можемо скомпілювати проект [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);
    }
...
  • рядки 24–25: клас [MainActivity_] успадковує клас [MainActivity];

Ми не будемо намагатися пояснити код класів, згенерованих AA. Вони керують тією складністю, яку анотації намагаються приховати. Але іноді може бути корисно розглянути його, коли потрібно зрозуміти, як «перекладаються» анотації, які ми використовуємо.

Тепер можна знову запустити конфігурацію [app]. Ми отримаємо той самий результат, що й раніше. Далі ми будемо працювати з цим проєктом, який продублюємо, щоб продемонструвати важливі поняття програмування для Android.

1.5. Приклад-04: представлення та події

1.5.1. Створення проєкту

Ми дотримуватимемося процедури, описаної для дублювання [Exemple-02] у [Exemple-03] у розділі 1.4:

Ми:

  • дублюємо проект [Exemple-03] у [Exemple-04] (після видалення папки [app / build] з [Exemple-03]);
  • завантажуємо проект [Exemple-04];
  • змінимо назву проєкту у файлі [app / res / values / strings.xml] (перспектива Android);
  • видаляємо файл [Exemple-04 / Exemple-04.iml] (перспектива Project);
  • скомпілюємо, а потім запустимо проект;
 

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>
  • рядок 2: порожній контейнер [RelativeLayout], який займатиме всю ширину планшета (рядок 3) і всю його висоту (рядок 4);
  • у [1] виберіть вкладку [Design] у вікні [vue1.xml], що відображається;
  • у [2-4] перейдіть у режим планшета;
  • у [5] перейдіть до масштабу 1 для планшета;
  • у [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>
  • зміни, внесені в графічному інтерфейсі, знаходяться в рядках 10, 11 та 14. Інші атрибути [TextView] є значеннями за замовчуванням або випливають із розташування компонента у вигляді;
  • рядки 7–8: розмір компонента відповідає розміру тексту, який він містить (wrap_content), як за висотою, так і за шириною;
  • рядок 13: верхня межа компонента вирівняна з верхньою межею вікна (рядок 13), на 50 пікселів нижче (рядок 13);
  • рядок 12: лівий край компонента вирівняно з лівим краєм вікна (рядок 13), на 213 пікселів правіше (рядок 12);

Як правило, точні розміри лівого, правого, верхнього та нижнього полів задаються безпосередньо у файлі XML.

Діючи таким самим чином, створіть наступний вигляд [1]:

 

Компоненти такі:

Id
Type
Rôle
1
textViewTitreVue1
TextView
Titre de la vue
2
textView1
TextView
une question
3
editTextNom
EditText
saisie d'un nom
4
buttonValider
Button
pour valider la saisie
5
buttonVue2
Button
pour passer à la vue n° 2

Розміщення компонентів один відносно одного може виявитися досить складним завданням, оскільки реакції графічного редактора іноді бувають непередбачуваними. Краще скористатися властивостями компонентів:

Компонент [textView1] слід розмістити на відстані 50 пікселів нижче заголовка та на відстані 50 пікселів від лівого краю контейнера:

  • у [1] верхній край (top) компонента вирівняно відносно нижнього краю (bottom) компонента [textViewTitreVue1] на відстані 50 пікселів від [3] (top);
  • у [2] лівий край (left) компонента вирівнюється відносно лівого краю контейнера на відстані 50 пікселів від [3] (left);

Компонент [editTextNom] має бути розміщений на відстані 60 пікселів праворуч від компонента [textView1] і вирівняний по нижньому краю щодо цього самого компонента;

 
  • у [1] лівий край (left) компонента вирівняно щодо правого краю (right) компонента [textView1] на відстані 60 пікселів від [2] (left). Він вирівняний по нижньому краю (bottom:bottom) компонента [textView1] [1];

Компонент [buttonValider] має бути розміщений на відстані 60 пікселів праворуч від компонента [editTextNom] і вирівняний по нижньому краю цього самого компонента;

 
  • у [1] лівий край (left) компонента вирівняно щодо правого краю (right) компонента [editTextNom] на відстані 60 пікселів від [2] (left). Він вирівняний по нижньому краю компонента (bottom:bottom) [editTextNom] [1];

Компонент [buttonVue2] має бути розміщений на відстані 50 пікселів нижче компонента [textView1] і вирівняний по лівому краю щодо цього самого компонента;

 
  • у [1] лівий край (left) компонента вирівняно щодо лівого краю (left) компонента [textView1] і розміщено нижче (top:bottom) на відстані 50 пікселів від [2] (top);

Створений файл 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>

У ньому міститься все, що було зроблено за допомогою графічного інтерфейсу. Отже, іншим способом створення подання є безпосереднє редагування цього файлу. Коли звикнеш, це може бути швидше, ніж використання графічного редактора.

  • У рядку 38 міститься інформація, яку ми не показали. Вона надається через властивості компонента [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);
  }
}
  • рядок 7: тепер активність відображає погляд [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>
  • рядок 12: цей рядок конфігурації запобігає появі клавіатури одразу після відображення виду [vue1]. Справа в тому, що цей вид містить поле введення, яке отримує фокус під час відображення виду. Цей фокус за замовчуванням викликає появу віртуальної клавіатури;

Запустіть додаток і переконайтеся, що відображається саме вікно [vue1.xml]:

Image

1.5.3. Обробка подій

Тепер обробимо клік на кнопці [Valider] у вікні [Vue1]:

Image

Код [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] пов’язується з ідентифікаційним компонентом [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 не обробляє їх належним чином.

  • рядок 32: анотація [@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"/>
  • рядок 35: відображає введене ім'я:
    • Toast.makeText(...).show() : виводить текст на екран,
    • перший параметр функції makeText — це активність,
    • другий параметр — це текст, який має відобразитися у вікні, що з’явиться завдяки makeText,
    • третій параметр — час існування вікна: Toast.LENGTH_LONG або Toast.LENGTH_SHORT;
  • у рядку 26 анотація [@AfterViews] позначає метод, який слід виконати, коли всі поля, позначені анотацією [@ViewById], будуть ініціалізовані. Важливо знати, коли ці поля ініціалізуються. Наприклад, чи можна в методі [onCreate] використовувати посилання з рядка 18? Щоб відповісти на це питання, ми встановили журнали;

Запустіть проект [Exemple-04] і переконайтеся, що щось відбувається, коли ви натискаєте кнопку [Valider]. Ми отримуємо такі записи журналу:

05-28 09:06:23.751 571-571/exemples.android D/MainActivity: onCreate
05-28 09:06:23.841 571-571/exemples.android D/MainActivity: afterViews

З цього можна зробити висновок, що під час виконання методу [onCreate] поля, позначені [@ViewById], ще не ініціалізовані. Ще раз наголошуємо, що початківцям рекомендується додавати такі журнали до методів, які керують життєвим циклом додатка.

1.6. Приклад-05: навігація між поданнями

У попередньому проєкті кнопка [Vue n° 2] не використовувалася. Ми пропонуємо використати її, створивши другий вигляд і продемонструвавши, як переходити від одного вигляду до іншого. Існує кілька способів вирішення цієї задачі. Пропонований тут спосіб полягає в тому, щоб пов’язати кожен вигляд з активною діяльністю. Інший метод полягає у створенні єдиної активності типу [AppCompatActivity], яка відображає види типу [Fragment]. Саме цей метод буде використовуватися в майбутніх додатках.

1.6.1. Створення проєкту

Дублюємо проект [Exemple-04] у [Exemple-05]. Для цього дотримуємося процедури, описаної для дублювання [Exemple-02] у [Exemple-03] у розділі 1.4 та наведеної у розділі 1.5.

1.6.2. Додавання другої активності

Щоб керувати другим видом, ми створимо другу активність. Саме вона керуватиме видом № 2. Тут ми маємо модель «один вид = одна активність». Існують й інші можливі моделі.

123

Image

  • у [1-4] створюємо нову активність;

Image

  • у [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);
  }
}
  • рядок 11: операція пов’язана з поданням [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] (рядок 2). У рядку 11 видно, що він був пов’язаний із новою активністю.

Маніфест модуля Android [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>

У рядку 20 зареєстровано другий актив.

1.6.3. Перехід від виду № 1 до виду № 2

Повернемося до коду класу [MainActivity], який відображає вікно № 1. Перехід до вікна № 2 наразі не реалізовано:

  

Ми реалізуємо його таким чином:


  // перехід до вікна № 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // перехід до подання № 2 з передачею імені, введеного в поданні № 1
    // створюється Intent
    Intent intent = new Intent();
    // прив’язуємо цей Intent до активності
    intent.setClass(this, SecondActivity.class);
    // до цього Intent додаємо інформацію
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // запускаємо активність типу [SecondActivity], передаючи їй Intent
    startActivity(intent);
}
  • рядки 2–3: метод [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"/>

Коментарі описують кроки, які потрібно виконати для зміни подання:

  1. рядок 6: створити об’єкт типу [Intent]. Цей об’єкт дозволить вказати як активність, яку потрібно запустити, так і інформацію, яку потрібно їй передати;
  2. рядок 8: пов’язати Intent з активністю, у даному випадку з активністю типу [SecondActivity], яка відповідатиме за відображення виду № 2. Слід пам’ятати, що активність [MainActivity] відображає вид № 1. Отже, один вид відповідає одній активності. Нам потрібно буде визначити тип [SecondActivity];
  3. рядок 10: за бажанням, вкажіть інформацію в об’єкті [Intent]. Ця інформація призначена для активності [SecondActivity], яка буде запущена. Параметри [Intent.putExtra] — це (ключовий об’єкт, значення об’єкта). Слід зауважити, що метод [EditText.getText()], який повертає текст, введений у поле введення, повертає не тип [String], а тип [Editable]. Щоб отримати введений текст, слід використовувати метод [toString];
  4. рядок 12: запустити активність, визначену об’єктом [Intent].

Запустіть проект [Exemple-05] і переконайтеся, що ви бачите вікно № 2 (наразі порожнє):

1.6.4. Створення виду № 2

 
  • у [1-2] видаляємо вигляд [main.xml], який нам більше не потрібен, а потім змінюємо вигляд [vue2.xml] наступним чином:
 

Компоненти такі:

Id
Type
Rôle
1
textViewTitreVue2
TextView
Titre de la vue
2
textViewBonjour
TextView
un texte
5
btn_vue1
Button
pour passer à la vue n° 1

Файл 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] ми написали такий код:


    // перейти до екрану № 2
    protected void navigateToView2() {
        // перехід до екрану № 2 з передачею імені, введеного на екрані № 1
        // створюється Intent
        Intent intent = new Intent();
        // прив’язуємо цей Intent до активності
        intent.setClass(this, SecondActivity.class);
        // до цього Intent додаємо інформацію
        intent.putExtra("NOM", edtNom.getText().toString().trim());
        // запускаємо активність типу [SecondActivity], передаючи їй Intent
        startActivity(intent);
}

У рядку 9 ми вказали для [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 = 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));
        }
      }
    }
  }

}
  • рядок 11: використовується анотація [@EActivity], щоб вказати, що клас [SecondActivity] є діяльністю, пов’язаною з поданням [vue2.xml];
  • рядки 15–16: отримується посилання на компонент [TextView], ідентифікований за [R.id.textViewBonjour]. Тут не вказано [@ViewById(R.id.textViewBonjour)]. У цьому випадку AA припускає, що ідентифікатор компонента збігається з анотованим полем, у даному випадку — з полем [textViewBonjour];
  • рядок 23: анотація [@AfterViews] позначає метод, який має бути виконаний після того, як поля, анотовані [@ViewById], будуть ініціалізовані. У методі [OnCreate] (рядок 19) ці поля використовувати не можна, оскільки вони ще не ініціалізовані. У проєкті [Exemple-05] відбувається перехід від однієї активності до іншої, і спочатку було незрозуміло, чи метод, позначений анотацією [@AfterViews], буде виконуватися один раз під час початкової інстанціації активності чи кожного разу, коли активність запускається. Тестування показало, що підтвердилася друга гіпотеза;
  • рядок 26: клас [AppCompatActivity] має метод [getIntent], який повертає об’єкт [Intent], пов’язаний з активністю;
  • рядок 28: метод [Intent.getExtras] повертає тип [Bundle], який є своєрідним словником, що містить інформацію, пов’язану з об’єктом [Intent] активності;
  • рядок 31: отримуємо ім’я, розміщене в об’єкті [Intent] цієї активності;
  • рядок 34: його виводимо на екран.

Нагадування: поля, позначені анотацією [@ViewById], не повинні містити символів з діакритичними знаками.

Повернемося до класу [SecondActivity]. Оскільки ми написали:


@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {

AA згенерує клас [SecondActivity_], похідний від [SecondActivity], і саме цей клас буде справжньою операцією. Це змушує нас внести зміни в:

[MainActivity]


  // перехід до екрану № 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // прив’язуємо цей Intent до активності
    intent.setClass(this, SecondActivity_.class);
    ...
}
  • у рядку 6 потрібно замінити [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>
  • у рядку 20 потрібно замінити [SecondActivity] на [SecondActivity_];

Перевірте цю нову версію. Введіть ім’я у вікні № 1 і переконайтеся, що воно правильно відображається у вікні № 2.

1.6.6. Перехід з вікна № 2 до вікна № 1

Щоб перейти з виду № 2 до виду № 1, ми будемо дотримуватися процедури, описаної раніше:

  • вставимо код переходу в активність [SecondActivity], яка відображає вигляд № 2;
  • написати метод [@AfterViews] в активності [MainActivity], яка відображає вигляд № 1;

Код [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);
    }
}
  • рядки 1–2: прив’язуємо метод [navigateToView1] до натискання кнопки [btn_vue1];
  • рядок 4: створюється новий [Intent];
  • рядок 5: пов’язується з активністю [MainActivity_];
  • рядок 7: отримуємо Intent, пов'язаний з [SecondActivity];
  • рядок 9: отримуємо інформацію з цього Intent;
  • рядок 12: ключ [NOM] отримується з [intent2] для розміщення в [intent1] з тим самим пов’язаним значенням;
  • рядок 15: запускається активність [MainActivity_].

У код [MainActivity] додається наступний метод [@AfterViews]:


  @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) {
          // виводимо його
          editTextNom.setText(nom);
        }
      }
    }
}

Внесіть ці зміни та протестуйте свою програму. Тепер, коли ви повертаєтеся з екрану № 2 до екрану № 1, має відображатися ім’я, введене спочатку, чого досі не відбувалося.

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");
    ...
    }
}
  • рядки 2–4: ми хочемо дізнатися, чи клас [MainActivity] інстанціюється один або кілька разів;
  • рядок 8: ми хочемо дізнатися, чи метод [onCreate] викликається один або кілька разів;
  • рядок 14: ми хочемо дізнатися, чи метод [afterViews] викликається один або кілька разів;

Те саме робимо в класі [SecondActivity].

Після запуску програми ми отримуємо такі записи в журналі:

1
2
3
05-28 09:38:09.429 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:38:09.449 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:38:09.600 26711-26711/exemples.android D/MainActivity: afterViews

Методи [onCreate, afterViews] першої активності були виконані в такому порядку. При натисканні кнопки [Vue n° 2] з’являються такі нові записи в журналі:

1
2
3
05-28 09:39:26.607 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:39:26.608 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:39:26.617 26711-26711/exemples.android D/SecondActivity: afterViews

Методи [onCreate, afterViews] другої активності були виконані в такому порядку. При натисканні на кнопку [Vue n° 1] з’являються такі нові записи журналу:

1
2
3
05-28 09:39:56.393 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:39:56.394 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:39:56.400 26711-26711/exemples.android D/MainActivity: afterViews

Отже, клас [MainActivity] знову інстанціюється. При натисканні на кнопку [Vue n° 2] з’являються такі нові записи в журналі:

1
2
3
05-28 09:40:59.099 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:40:59.102 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:40:59.113 26711-26711/exemples.android D/SecondActivity: afterViews

Отже, клас [SecondActivity] знову інстанціюється.

Отже, обидві активності систематично створюються заново при зміні активності.

Тепер ми розглянемо архітектуру з єдиною активністю, яка може керувати кількома видами, що називаються фрагментами. Активність та види будуть інстанційовані лише один раз, на відміну від попереднього методу, де активність могла інстанційовуватися кілька разів.

1.7. Приклад-06: навігація за допомогою вкладок

Тут ми розглянемо інтерфейси з вкладками. Приклад є складним, але в ньому представлені всі елементи, які ми будемо використовувати надалі: єдина активність, менеджер фрагментів (видів), контейнер фрагментів, навігація між фрагментами. Поняття «вкладок» відрізняється від поняття «фрагментів» і відіграє другорядну роль у тому, що ми хочемо продемонструвати в цьому прикладі.

1.7.1. Створення проєкту

Створюємо новий проєкт:

 
  • у [7] вибираємо активність із вкладками (Tabbed Activity);
  • у [10-14] зберігаємо значення, запропоновані за замовчуванням;
  • у [15] вибираємо вкладки з панеллю заголовків;

У результаті створюється такий проект:

 
  • у [1] — активність;
  • у [2] — перегляди;

Автоматично створено конфігурацію виконання [app], що має назву модуля, [2b]:

 

Її можна запустити. Після цього з’явиться вікно з трьома вкладками [3-6]:

Image

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'
}

Є одна новинка порівняно з тим, що вже зустрічалося раніше: рядок 25. Ця бібліотека необхідна для нових компонентів, які використовує згенерований додаток.

1.7.3. Вигляд [activity_main]

  

Вигляд [activity_main] пов'язаний з діяльністю [MainActivity] у рамках проєкту. У режимі [design] вигляд має такий вигляд:

Image

Вона містить такі компоненти:

  
  • [main_content] — це весь вигляд;
  • [appbar] (червона рамка, 1) — це панель додатка. Вона містить два компоненти:
    • [toolbar] (жовта рамка 4) — панель інструментів;
    • [tabs] (помаранчева рамка 5) — це панель заголовків вкладок;
  • [container] (зелена рамка, 2) може вміщувати різні фрагменти. Фрагмент — це представлення. Таким чином, одна й та сама активність зможе відображати кілька представлень (фрагментів) у цьому контейнері;
  • [fab] (компонент 3) називається плаваючим компонентом;

У режимі [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>

Тут ми бачимо елементи, описані раніше:

  • рядки 2–49: визначення компонента [main_content] (рядок 5), який становить весь вигляд. Бачимо, що це layout (менеджер розміщення компонентів) типу [CoordinatorLayout] (рядок 2);
  • рядки 11–33: контейнер [appbar] (рядок 12). Це layout типу [AppBarLayout] (рядок 11);
  • рядки 18–24: компонент [toolbar] (рядок 19) типу [Toolbar] (рядок 18);
  • рядки 28–31: контейнер [tabs] (рядок 29). Це layout типу [TabLayout] (рядок 28). Він відображатиме назви вкладок;
  • рядки 35–39: компонент [container] (рядок 36). Саме цей контейнер відображає різні види діяльності;
  • рядки 41–47: компонент [fab] (рядок 42) типу [FloatingActionButton] (рядок 41). Це кнопка, на яку можна натиснути. За замовчуванням вона розміщується внизу праворуч загального виду;

Ми не будемо намагатися розібратися у значенні всіх атрибутів цих компонентів. Ми будемо використовувати їх такими, якими вони є. Їхню роль можна зрозуміти лише з досвідом і, найчастіше, у режимі [design]. У цьому режимі виявляється, що компоненти мають кілька десятків атрибутів. Зазвичай ініціалізуються лише деякі з них, а інші зберігають значення за замовчуванням.

Проте уточнимо кілька моментів. Більшість значень, що налаштовують різні види, зібрані в папці [res / values]:

  

Ці значення вказані у рядках 15–16, 23, 39, 46 файлу [activity_main.xml]. Розглянемо приклад:

  • рядок 15:

    android:paddingTop="@dimen/appbar_padding_top"

Анотація [@dimen] посилається на файл [res / values / dimens.xml]:


<resources>
  <!-- Стандартні поля екрана відповідно до рекомендацій Android Design. -->
  <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>

Рядок 15 файлу [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 {
...
  }
}
  • рядок 28: Android надає контейнер представлень типу [android.support.v4.view.ViewPager] (рядок 12). Цьому контейнеру потрібно надати менеджер представлень або фрагментів. Його надає розробник;
  • рядок 25: обробник фрагментів, що використовується в цьому прикладі. Його реалізація наведена у рядках 61–63;
  • рядок 31: метод, що виконується під час створення активності;
  • рядок 35: вигляд [activity_main.xml] пов’язано з активністю;
  • рядок 37: отримуємо посилання на компонент [toolbar] з подання за його ідентифікатором;
  • рядок 38: ця панель інструментів стає панеллю дій (поняття Android) активності;
  • рядок 40: створюється екземпляр менеджера фрагментів. Параметром конструктора є клас Android [android.support.v4.app.FragmentManager] (рядок 10);
  • рядок 44: у представленні [activity_main.xml] отримується посилання на контейнер фрагментів за його ідентифікатором;
  • рядок 45: менеджер фрагментів пов'язується з контейнером фрагментів. Це означає, що коли контейнеру фрагментів буде наказано відобразити фрагмент № i, його буде запрошено у менеджера фрагментів;
  • рядок 48: отримуємо посилання на панель вкладок за її ідентифікатором;
  • рядок 49: менеджер вкладок пов'язаний із контейнером фрагментів. Це означає, що при натисканні на вкладку № 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;
    }
  }
}
  • фрагменти, що відображаються додатком, залежать від нього. Обробник фрагментів визначається розробником;
  • рядок 5: менеджер фрагментів успадковує клас Android [android.support.v4.app.FragmentPagerAdapter]. Конструктор задано заздалегідь. Ми повинні визначити принаймні два наступні методи:
    • int getCount(): повертає кількість фрагментів, які потрібно обробляти;
    • Fragment getItem(i): повертає фрагмент № i;

Метод CharSequence getPageTitle(i), який формує заголовок фрагмента № i, є необов’язковим. Оскільки менеджер вкладок був пов'язаний з менеджером фрагментів, заголовок вкладки № i буде заголовком фрагмента № i. Таким чином, заголовки рядків 27–33 стануть заголовками вкладок;

  • рядки 18–21: getCount визначає кількість фрагментів, що керуються, у даному випадку три;
  • рядки 11–15: getItem(i) повертає фрагмент № i. У цьому випадку всі фрагменти будуть однаковими, типу [PlaceholderFragment];
  • рядки 24–35: 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>
  • рядки 1–16: layout типу [RelativeLayout];
  • рядки 11–14: єдиний компонент подання (фрагмент): [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;
    }
  }
  • рядок 2: клас [PlaceholderFragment] успадковує клас Android [Fragment]. Зазвичай це завжди так;
  • рядок 2: клас [PlaceholderFragment] є статичним. Його метод [newInstance] (рядок 10) дозволяє отримувати екземпляри типу [PlaceholderFragment];
  • рядки 10–19: метод [newInstance] створює та повертає об’єкт типу [PlaceholderFragment];
  • рядки 14–16: фрагмент створюється з одним аргументом;

Фрагмент повинен визначати метод [onCreateView] у рядку 22. Цей метод повинен повертати представлення, пов’язане з фрагментом.

  • рядок 25: представлення [fragment_main.xml] пов’язане з фрагментом;
  • рядок 27: цей вигляд містить компонент [TextView], посилання на який отримується за його ідентифікатором;
  • рядок 29: у [TextView] відображається текст;
    • [getString] — це метод батьківського класу [AppCompatActivity];
    • перший аргумент — це номер компонента. [R.string.section_format] позначає номер компонента, ідентифікованого за допомогою [section_format] у файлі [res / values / strings.xml] (рядок 4 нижче):

<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 (%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) вище;
  • рядок 31: повертається створений таким чином вигляд;

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>
  • рядки 1–9: меню;
  • рядки 5–8: елемент меню, позначений як [action_settings] (рядок 5);
  • рядок 6: мітка пункту меню. Вона міститься у файлі [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>

Наведений вище код відповідає такому зображенню (меню розташоване у правому верхньому куті вікна виконання Android):

 

Це меню обробляється наступним чином у активності [MainActivity]:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Розгортає меню; це додає елементи до панелі дій, якщо вона присутня.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Обробляє тут кліки на елементах панелі дій. Панель дій буде
    // автоматично оброблятиме натискання на кнопку «Додому/Вгору», якщо
    // ви вказали батьківську активність у AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      return true;
    }

    return super.onOptionsItemSelected(item);
}
  • рядки 1–6: цей метод викликається, коли система готова до створення меню додатка. Вхідним параметром [Menu menu] є порожнє меню, яке ще не містить опцій;
  • рядок 4: використовується файл [res / menu / menu_main.xml]. Об’єкту [Menu menu], переданому як параметр, присвоюються опції меню, визначені в цьому файлі;
  • рядок 5: вказується, що меню було створено;
  • рядки 8–21: метод [onOptionsItemSelected] виконується, щойно натискається пункт меню;
  • рядок 13: посилання на натиснутий пункт меню;
  • рядки 16–18: якщо натиснута опція має ідентифікатор [action_settings], ніяких дій не виконується, і вказується, що подія була оброблена (рядок 17);
  • рядок 20: подія передається до батьківського класу;

Щоб краще зрозуміти, що відбувається з цим меню, додамо записи журналу до попереднього коду:


  @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");
    // Тут обробляються кліки на елементах панелі дій. Панель дій
    // автоматично оброблятиме натискання кнопки «Додому/Вгору», якщо
    // ви вказали батьківську активність у 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"/>

У рядку 7 згадується зображення, надане підтримкою Android, — зображення конверта.

Цей компонент обробляється у класі [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();
      }
});
  • рядок 2: отримуємо посилання на плаваючу кнопку у вікні, пов’язаному з активністю (activity_main);
  • рядки 3–9: йому прив’язується обробник для обробки кліка по ньому;
  • рядок 6: клас [Snackbar] дозволяє відображати тимчасові повідомлення у вікні за допомогою методу [Snackbar.make]. Перший аргумент — це вікно, з якого [Snackbar] шукатиме батьківське вікно, у якому потрібно відобразити повідомлення. У даному випадку [view] — це вікно конверта, на яке натиснули (рядок 5). Батьківським видом, який буде знайдено, стане вид [activity_main]. Другий аргумент — це повідомлення, яке потрібно відобразити. Третій аргумент — тривалість відображення (SHORT або LONG);
  • рядок 7: можна натиснути на відображене повідомлення і таким чином запустити дію. Тут до натискання на повідомлення не прив’язано жодної дії. Нарешті, метод [show] відображає повідомлення;

Клік на плаваючу кнопку дає такий візуальний результат:

 

1.7.5. Виконання проекту

Тепер, коли ми розібрали деталі згенерованого коду, ми можемо краще зрозуміти його виконання:

Image

При натисканні на вкладку № i у контейнері переглядів відображається фрагмент № i. Це видно з тексту, що відображається в [4]. Також можна помітити, що переходити від однієї вкладки до іншої можна, перетягуючи перегляд вправо або вліво за допомогою миші (прокрутка). Ми побачимо, що цю поведінку можна контролювати.

При натисканні на пункт меню в [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)));
      ...
    }
  }


}

Ми знову запускаємо проект. Ось перші записи журналу:

1
2
3
4
5
6
7
8
9
05-28 10:44:32.622 29371-29371/exemples.android D/MainActivity: constructor
05-28 10:44:32.626 29371-29371/exemples.android D/MainActivity: onCreate
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 1
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 2
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 10:44:32.760 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 10:44:33.349 29371-29371/exemples.android D/menu: création menu en cours
  • рядок 1: створення активності;
  • рядок 2: виконання її методу [onCreate];
  • рядки 3–4: створення екземпляра фрагмента № 1;
  • рядки 5–6: створення екземпляра фрагмента № 2;
  • рядок 7: ініціалізація фрагмента № 2;
  • рядок 8: ініціалізація фрагмента № 1;
  • рядок 9: створення меню активності;

Тут слід згадати код, що відповідає за створення фрагментів:


  // менеджер фрагментів
  // саме до нього звертаються з запитом щодо фрагментів, які потрібно відобразити в головному вікні
  // повинен визначити методи [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);
    }
...
  • рядки 11–15: фрагмент інстанціюється за допомогою [newInstance] щоразу, коли контейнер фрагментів запитує його;

Наведені вище логи показують, що перші два фрагменти були інстанційовані та ініціалізовані.

Тепер натиснімо на вкладку № 2. Нові записи у журналі такі:

1
2
3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: newInstance 3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 3
  • рядки 1–3: фрагмент № 3 інстанціюється та ініціалізується. Нагадаємо, що відображається саме фрагмент № 2;

Тепер натиснімо на вкладку № 3. Тут немає жодних записів у журналі. Ймовірно, це тому, що фрагмент № 3, який потрібно відобразити, вже був інстанційований. Тепер повернімося до вкладки № 1. Записи у журналі мають такий вигляд:

05-28 10:48:26.630 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1

Фрагмент № 1 не інстанціюється знову, але його метод [onCreateView] знову виконується. Така поведінка повторюється для двох інших фрагментів.

З цих логів можна зробити такі висновки:

  • активність була інстанційована, а потім ініціалізована один раз;
  • кожен фрагмент був інстанційований один раз;
  • метод [onCreateView] кожного фрагмента виконувався кілька разів;

Важливо знати, і це підтверджують журнали, що за замовчуванням, коли відображається фрагмент № i, фрагменти i-1 та i+1 інстанціюються, якщо вони ще не інстанційовані. Саме це пояснює, наприклад, що під час запуску, коли потрібно відобразити фрагмент № 1, інстанціюються та ініціалізуються фрагменти 1 і 2. Журнали також показують, що метод [getItem(i)] викликається лише один раз, навіть якщо фрагмент № i відображається кілька разів. Отже, схоже, що контейнер фрагментів [ViewPager], який має відобразити фрагмент № i, запитує його один раз у менеджера фрагментів [SectionsPagerAdapter]. Потім він більше не запитує його і продовжує використовувати той, який отримав.

Нарешті, журнали містять відомості про метод фрагментів [onCreateView]:

  • під час запуску були створені екземпляри фрагментів 1 і 2, і їхній метод [onCreateView] було виконано;
  • під час переходу від фрагмента 1 до фрагмента 2 метод [onCreateView] фрагмента 2 не виконується повторно. Отже, його не можна використовувати для оновлення фрагмента 2. Однак користувач міг, працюючи з фрагментом 1, виконати операцію, результат якої мав би відображатися у фрагменті 2. Бачимо, що метод [onCreateView] не можна буде використати для оновлення фрагмента 2. Доведеться знайти інше рішення;

1.8. Приклад-07: Приклад-06, переписаний із використанням бібліотеки [AA]

1.8.1. Створення проєкту

Ми скопіюємо проект [Exemple-06] у [Exemple-07], щоб додати до останнього анотації Android. Для цього дотримуйтесь процедури, описаної в розділі 1.4. Ми отримуємо такий результат:

1.8.2. Налаштування Gradle

 

Ми модифікуємо файл [build.gradle] наступним чином:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Починаючи з версії 0.11 плагіна Gradle для Android, потрібно використовувати 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());

    // контейнер фрагментів пов'язаний з менеджером фрагментів
    // тобто фрагмент № 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();
      }
    });
  }
  • рядок 1: анотація [@EActivity] перетворює [MainActivity] на клас, що керується AA. Його параметр [R.layout.activity_main] є ідентифікатором подання [activity_main.xml], пов’язаного з цією активністю;
  • рядки 11–12: компонент, ідентифікований за [R.id.tabs], вставляється в поле [tabLayout]. Це менеджер вкладок;
  • рядки 14–15: компонент з ідентифікатором [R.id.fab] вставляється в поле [fab]. Це плаваюча кнопка;
  • рядки 23–50: код, який раніше містився в методі [onCreate], переміщується в метод з довільною назвою, але з анотацією [@AfterViews] (рядок 23). У методі, позначеному таким чином, гарантується, що всі компоненти візуального інтерфейсу, позначені [@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>
  • рядок 12: нова активність.

На цьому етапі знову запустіть проект і переконайтеся, що ви як і раніше отримуєте інтерфейс із вкладками.

1.8.4. Переписування фрагментів

Ми розглянемо управління фрагментами проекту. Наразі клас [PlaceholderFragment] є внутрішнім статичним класом активності [MainActivity]. Ми повернемося до більш типового випадку використання, коли фрагменти визначаються у зовнішніх класах. Крім того, ми вводимо анотації 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)));
    }
  }
}
  • рядок 15: фрагмент має анотацію [@EFragment], параметром якої є ідентифікатор подання XML, пов’язаного з фрагментом, у даному випадку — подання [fragment_main.xml];
  • рядки 19–20: вставляють у поле [textViewInfo] посилання на компонент [fragment_main.xml], ідентифікований як [R.id.section_label], який є типом [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>
  • рядки 42–52: метод [onResume] виконується перед відображенням подання, пов’язаного з фрагментом. Його можна використовувати для оновлення візуального інтерфейсу, який буде відображатися;
  • рядок 47: необхідно викликати однойменний метод батьківського класу;
  • рядок 49: виникає складність у визначенні того, чи можна виконати метод [onResume] до ініціалізації поля у рядку 20. Про це нам повідомлять журнали, встановлені для відстеження життєвого циклу фрагмента. Наразі, з міркувань обережності, ми виконуємо перевірку на нульове значення;
  • рядок 51: оновлюємо інформацію поля [textViewInfo] за допомогою цілочисельного аргументу, переданого фрагменту під час його створення;

Клас [MainActivity] втрачає свій внутрішній клас [PlaceholderFragment], а його менеджер фрагментів змінюється наступним чином:


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));
    }
  }
  • рядок 4: фрагменти поміщаються в масив;
  • рядки 16–23: ініціалізація масиву фрагментів відбувається у конструкторі. Вони мають тип [PlaceholderFragment_] (рядок 18), а не [PlaceholderFragment]. Клас [PlaceholderFragment], дійсно, був анотований анотацією AA і породжуватиме клас [PlaceholderFragment_], похідний від [PlaceholderFragment], і саме цей клас має використовувати активність. Кожному створеному фрагменту передається цілочисельний аргумент, який буде відображатися фрагментом;
  • рядки 42–45: ми змінили назви фрагментів. Оскільки вони також є назвами вкладок, ми повинні побачити зміни на панелі вкладок;

Скомпілюємо [Make] та [1] у цьому проєкті:

 
  • у [2], бачимо, що класи, згенеровані бібліотекою AA, знаходяться у папці [app / build / generated / source / apt / debug] (щоб побачити [2], потрібно перебувати у перспективі [Project]);

Запустіть проект [Exemple-07] і переконайтеся, що він як і раніше працює.

1.8.5. Перегляд журналів

Під час запуску програми з’являються такі журнали:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours
  • рядок 1: створення єдиної активності;
  • рядок 2: метод [afterViews] активності: її поля, позначені [@ViewById], ініціалізуються;
  • рядки 3–5: створення трьох фрагментів;
  • рядки 6–7: контейнер фрагментів [ViewPager] запитує перші два фрагменти;
  • рядки 8–9: методи фрагмента 2;
  • рядки 10–11: методи фрагмента 1;
  • рядки 12–13: метод [onResume] фрагмента 1;
  • рядки 14–15: метод [onResume] фрагмента 2;
  • рядок 16: створення меню активності;

Слід зауважити, що ми отримали відповідь на поставлене раніше питання: наприклад, метод [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)));
}

Тепер перейдемо з вкладки 1 на вкладку 2. Нові записи журналу такі:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3
  • рядок 1: контейнер фрагментів [ViewPager] запитує фрагмент № 3;
  • рядки 2–3: методи фрагмента № 3. Нагадаємо, що цей фрагмент був інстанційований одразу після запуску програми;
  • рядки 4–5: виконується метод [onResume] фрагмента № 3. Нагадаємо, що відображається саме фрагмент № 2;

Тепер перейдемо з вкладки 2 на вкладку 3. Жодних записів у журналі немає. Отже, жоден із методів [onCreateView, afterViews, onResume] фрагмента № 3 не виконується. Текст [Hello World from section:3] відображається правильно лише тому, що цей текст уже було створено на попередньому етапі під час відображення фрагмента № 2. Справді, слід згадати, що на цьому етапі було виконано метод [onResume] з фрагмента № 3. Тут ми бачимо, що, так само як і метод [onCreateView], метод [onResume] також не може бути використаний для оновлення фрагмента № 3. Якби потрібно було змінити текст, що відображається фрагментом, жоден із цих двох методів не зміг би цього зробити.

Тепер повернімося з вкладки № 3 до вкладки № 1. Логи виглядають наступним чином:

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

Бачимо, що всі методи фрагмента 1 були виконані. Бачимо, що метод getItem не був викликаний. Як уже зазначалося, цей метод викликається лише один раз для кожного фрагмента;

Тепер перейдемо з вкладки 1 на сусідню вкладку 2. Отримуємо такі записи журналу:

1
2
3
4
05-28 14:12:59.526 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Дивно, чи не так? Усі методи фрагмента № 3 виконуються повторно.

Щоб зрозуміти ці явища, слід пам’ятати, що за замовчуванням, коли контейнер фрагментів збирається відобразити фрагмент i, він ініціалізує фрагменти i-1, i та i+1. Давайте ще раз переглянемо логи з огляду на цю інформацію.

Спочатку — журнали запуску програми:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours

Оскільки контейнер фрагментів відображатиме фрагмент 1, фрагменти 1 і 2 ініціалізуються (рядки 8–15).

Тепер переходимо з вкладки 1 на вкладку 2:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Оскільки контейнер фрагментів відображатиме фрагмент 2, фрагменти 1, 2 та 3 мають бути ініціалізовані. Фрагменти 1 та 2 вже ініціалізовані на попередньому етапі. Фрагмент 3 ініціалізується у рядках 2–5.

Переходимо з вкладки 2 на вкладку 3. Журналів немає. Оскільки контейнер фрагментів буде відображати фрагмент 3, фрагменти 2 та 3 мають бути ініціалізовані. Однак з попереднього етапу вони вже ініціалізовані. Що тут не видно, так це те, що фрагмент 1, який не межує з фрагментом 3, втрачає свій стан, який не зберігається в пам’яті.

Переходимо з вкладки 3 на вкладку 1. Журнали мають такий вигляд:

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

Оскільки контейнер фрагментів відображатиме фрагмент 1, фрагмент 2 також має бути ініціалізований. Він уже ініціалізований з попереднього етапу. На цьому ж етапі стан фрагмента 1 було втрачено. Тому його переініціалізують у рядках 1–4. Що тут не видно, так це те, що фрагмент 3, який не є сусіднім до фрагмента 1, втрачає свій стан, який тоді не зберігається в пам’яті.

Під час переходу з вкладки 1 на сусідню вкладку 2 ми отримуємо такі записи журналу:

1
2
3
4
05-28 14:12:59.526 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Оскільки контейнер фрагментів відображатиме фрагмент 2, фрагменти 1, 2 та 3 мають бути ініціалізовані. Фрагменти 1 та 2 вже ініціалізовані на попередньому етапі. Фрагмент 3 ініціалізується до рядків 1–4.

Що ми дізналися?

  • що стандартне управління фрагментами є дуже специфічним, і його потрібно знати, якщо не хочете з’їхати з глузду. Цей режим управління можна змінити, і ми це зробимо трохи пізніше;
  • що при такому типовому управлінні жоден із методів [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();
  }

}

Запустимо додаток. Перші записи в журналі мають такий вигляд:

06-03 02:45:42.163 2346-2346/exemples.android D/MainActivity: constructor
06-03 02:45:42.331 2346-2346/exemples.android D/MainActivity: afterViews
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.515 2346-2346/exemples.android D/MainActivity: getItem[0]
06-03 02:45:42.516 2346-2346/exemples.android D/MainActivity: getItem[1]
06-03 02:45:42.517 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 02:45:42.520 2346-2346/exemples.android D/PlaceholderFragment: afterViews 2
06-03 02:45:42.523 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume 2
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 02:45:44.596 2346-2346/exemples.android D/menu: création menu en cours
  • рядок 1: побудова єдиної активності;
  • рядок 2: метод [afterViews] активності: її поля, позначені [@ViewById], ініціалізовано;
  • рядки 3–5: створення трьох фрагментів;
  • рядки 6–7: контейнер фрагментів [ViewPager] запитує перші два фрагменти;
  • рядки 8–9: створюється представлення фрагмента 2 (не обов’язково стає видимим);
  • рядки 10–11: створюється представлення фрагмента 1 (не обов’язково стає видимим);
  • рядки 12–13: метод [onResume] фрагмента 1;
  • рядки 14–15: метод [onResume] фрагмента 2;
  • рядок 16: створення меню активності;

Перейдемо з вкладки 1 на вкладку 3:


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
  • рядок 1: контейнер фрагментів запитує третій фрагмент;
  • рядки 2–3: створюється представлення фрагмента 3 (не обов’язково відображається);
  • рядки 4–5: виконується метод [onResume] фрагмента 3;
  • рядок 6: виконується метод [onDestroyView] фрагмента 1. Це означає, що коли користувач повернеться до фрагмента 1 або до сусіднього фрагмента, життєвий цикл цього фрагмента буде виконано знову;

Повертаємося з вкладки 3 на вкладку 1:


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
  • рядки 1–4: життєвий цикл фрагмента 1 виконується повторно, оскільки він зазнав дії [onDestroyView];
  • рядок 5: тепер для фрагмента 3 виконується його метод [onDestroyView]. І знову ж таки, коли користувач повернеться до фрагмента 3 або до сусіднього фрагмента, життєвий цикл цього фрагмента буде виконано повторно;

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
  • записи в рядках 7, 9–10 показують, що видимим стає лише фрагмент 1. Також видно, що він стає видимим до виконання його методу [onCreateView];

Перейдемо з вкладки 1 на вкладку 2:


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
  • фрагмент 1 приховано (рядок 3), фрагмент 2 відображено (рядок 4);

Перейдемо з вкладки 2 на вкладку 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
  • фрагмент 2 приховано (рядок 1), фрагмент 3 відображено (рядок 2);

Повернемося до вкладки 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
  • фрагмент 3 приховано (рядок 2), фрагмент 1 відображено (рядок 3);

Що ми дізналися?

  • метод [setUserVisibleHint] виконується один раз із властивістю [isVisibleToUser] до true для фрагмента, який буде відображено;
  • неможливо визначити, коли саме цей метод буде виконано у контексті життєвого циклу фрагмента. Так, для фрагмента 1 метод [setUserVisibleHint, true] було виконано раніше, ніж метод [onCreateView] на початку життєвого циклу цього фрагмента, тоді як для фрагментів 2 і 3 сталося навпаки;

1.8.8. setOffscreenPageLimit

Попередні журнали показують, що коли контейнер фрагментів [ViewPager] готується до відображення фрагмента № i, він виконує, якщо це ще не зроблено, життєвий цикл сусідніх фрагментів i-1 та i+1. Цю роботу можна контролювати за допомогою методу [ViewPager].setOffscreenPageLimit:

// зсув фрагментів
    [ViewPager].setOffscreenPageLimit(n);

За допомогою наведеної вище інструкції,

  1. коли контейнер фрагментів [ViewPager] готується відобразити фрагмент № i, він виконує, якщо це ще не зроблено, життєвий цикл сусідніх фрагментів з інтервалу [i-n, i+n];
  2. якщо потім відображається фрагмент j:
    • те саме явище повторюється для сусідніх фрагментів інтервалу [j-n, j+n];
    • фрагменти, ініціалізовані на етапі 1 і які більше не знаходяться у сусідстві [j-n, j+n] з новим фрагментом, можуть тоді зазнати операції [onDestroyView]. Проте я помітив у інших додатках, зокрема в тому, що розглядається у розділі 3, що це не завжди так;

Ми змінюємо метод [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();
      }
    });
}
  • рядок 20: ми встановлюємо кількість сусідніх фрагментів, які потрібно ініціалізувати, рівною загальній кількості фрагментів мінус 1. Таким чином, під час запуску, коли контейнер фрагментів відображатиме фрагмент № 1, він одночасно ініціалізує фрагменти 2, 3, ..., n, де n = 1 + mSectionsPagerAdapter.getCount() - 1=mSectionsPagerAdapter.getCount(). Отже, будуть ініціалізовані всі фрагменти. Коли вікно відображення переміститься на інший фрагмент, контейнер фрагментів:
    • виявить, що всі фрагменти, суміжні з новим фрагментом, вже ініціалізовані, і тому не буде їх ініціалізувати;
    • оскільки сусідні фрагменти нового фрагмента також охоплюють усі інші фрагменти, жоден із них не буде «деініціалізований» контейнером фрагментів;

Загалом, ми повинні бачити, що всі фрагменти інстанціюються та ініціалізуються під час запуску додатка, а потім більше ніколи. Саме це ми зараз перевіряємо, переглядаючи логи.

Під час запуску ми маємо такі журнали:

06-03 03:30:55.411 10344-10344/exemples.android W/System: ClassLoader referenced unknown path: /data/app/exemples.android-1/lib/x86
06-03 03:30:55.417 10344-10344/exemples.android D/MainActivity: constructor
06-03 03:30:55.460 10344-10344/exemples.android D/MainActivity: afterViews
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.559 10344-10344/exemples.android D/MainActivity: getItem[0]
06-03 03:30:55.559 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[1]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[2]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 03:30:56.798 10344-10344/exemples.android D/menu: création menu en cours
  • рядки 4–6: створення трьох фрагментів;
  • рядки 7, 9, 11: контейнер фрагментів запитує три фрагменти. У попередній версії він запитував два;
  • рядки 14–25: виконується життєвий цикл трьох фрагментів;

Тепер перейдемо з вкладки 1 на вкладку 2:

06-03 03:34:03.388 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:34:03.388 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true

Перейдемо з вкладки 2 на вкладку 3:

06-03 03:34:43.292 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:34:43.292 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true

Потім з вкладки 3 на вкладку 1:

06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true

Журнали підтверджують цю теорію. Усі фрагменти були інстанційовані та ініціалізовані під час запуску. Після цього методи їхнього життєвого циклу більше не виконуються. Ми маємо тут дуже передбачувану роботу фрагментів, що значно полегшує їхнє використання.

Нам потрібно знайти спосіб оновлення фрагмента, який буде відображатися, незалежно від того, яке сусідство фрагментів обрав розробник. Журнали показали нам дві речі:

  • метод [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] видається тут найбільш підходящим для оновлення фрагмента 1;

Коли ми перейшли з вкладки 1 на вкладку 2, журнали мали такий вигляд:


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

Цього разу для оновлення фрагмента 2 використовується лише метод [setUserVisibleHint, true] у рядку 4;

Коли ми перейшли з вкладки 2 на вкладку 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

Тут для оновлення фрагмента 3 використовується лише метод [setUserVisibleHint, true] із рядка 2;

Коли ми перейшли з вкладки 3 на вкладку 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

Тут потрібно використати метод [onResume] з фрагмента 1 (рядки 6–7) для оновлення фрагмента 1.

Отже, на цьому прикладі ми бачимо, що для оновлення фрагмента, який буде відображатися, у нас є два методи: [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>

Просто скопіюємо та вставимо перший пункт меню й адаптуємо результат (рядки 9 та 10). Назву цього нового пункту додаємо до файлу [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");
    // Тут обробляються кліки на елементах панелі дій. Панель дій буде
    // автоматично оброблятиме натискання на кнопку «Головна/Вгору», доки
    // ви вказали батьківську активність у 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);
}
  • рядки 14–19: копіюємо та вставляємо рядки 10–13 і адаптуємо код до нової опції;
  • рядок 17: активність завершується програмною дією;

Тепер запустимо цю нову версію, а потім, як тільки з’явиться перший екран, клацнемо на пункт меню [Terminate]. Журнали матимуть такий вигляд:

1
2
3
4
5
6
7
8
9
06-04 12:35:32.996 15994-15994/exemples.android D/menu: onOptionsItemSelected
06-04 12:35:32.996 15994-15994/exemples.android D/menu: action_terminate selected
06-04 12:35:33.561 15994-15994/exemples.android D/MainActivity: onDestroy
06-04 12:35:33.561 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 3
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 3
  • рядки 1–2: клік на опцію [Terminate];
  • рядок 4: викликано метод [onDestroy] активності;
  • рядки 4–5: викликається метод [onDestroyView] фрагмента 1, а потім — його метод [onDestroy];
  • рядки 6–9: ця операція повторюється для двох інших фрагментів;

Отже, слід пам’ятати, що метод [onDestroy] активності та фрагментів викликається, коли активність має бути видалена системою, розробником або користувачем. Цей метод можна використовувати для збереження інформації, наприклад, локально на планшеті, щоб відновити її, коли користувач знову запустить додаток.

1.9. Приклад-08: оновлення фрагмента зі змінною сукупністю суміжних фрагментів

1.9.1. Створення проєкту

Дублюємо проект [Exemple-07] у [Exemple-08]. Для цього дотримуємося процедури, описаної для дублювання [Exemple-02] у [Exemple-03] у розділі 1.4.

1.9.2. Переписування фрагмента [PlaceholderFragment]

Новий код фрагмента [PlaceholderFragment] виглядає наступним чином. Він працює незалежно від заданої суміжності фрагментів (1, часткова, повна):


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);
  }
}
  • рядки 34–48: метод [@AfterViews] може виконуватися кілька разів. Раніше його використовували для ініціалізації тексту фрагмента (рядок 42). Ми продовжуємо це робити, але щоб виконати цю операцію лише один раз, використовуємо булеве значення [initDone] (рядок 44), яке вказує, що ініціалізація вже відбулася і її не потрібно повторювати;
  • рядки 56–59: ми вводимо метод [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;
    }
}
  • рядок 14: зберігається інформація про те, чи є фрагмент видимим;
  • рядки 22–25: якщо фрагмент видимий і метод [@AfterViews] було виконано, виконується метод [update], а булеве значення [updateDone] передається до true;
  • рядки 26–28: якщо фрагмент буде приховано, булеве значення [updateDone] скидається до false. Нам дійсно потрібна подія, щоб скинути булеве значення [updateDone], встановлене на true, до false, щойно викликається метод [update], щоб можна було виконати нові оновлення. Для цього ми використовуємо той факт, що фрагмент більше не є видимим. Коли він знову стане видимим, оновлення фрагмента доведеться виконати заново;
  • рядки 32–42: журнали показують, що залежно від обраної суміжності фрагментів метод [onResume] може виконуватися, навіть коли фрагмент не видимий. Якщо він не видимий, оновлення не виконується (рядок 39), і, як це було зроблено для [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] виконується, коли завершується життєвий цикл фрагмента. Пізніше може розпочатися інший цикл.

  • рядок 6: метод [onDestroyView] видаляє всі зв’язки з видом, прив’язаним до фрагмента. Він буде відтворений під час наступного життєвого циклу фрагмента. Наразі нам потрібно змінити значення логічного параметра [afterViews] на false, щоб вказати, що зв’язок із видом більше не існує;

Ми запустимо додаток із 5 фрагментами, що мають сусідність 2. Зміни внесено в [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
  • рядки 8, 10, 12: контейнер фрагментів запитує всі фрагменти, суміжні з фрагментом 1;
  • рядки 9, 11, 13: метод [setUserVisibleHint] цих фрагментів виконується разом із [visibleToUser] до false;
  • рядок 14: метод [setUserVisibleHint] фрагмента 1 виконується з [visibleToUser] до true;
  • рядки 15–17: викликається метод [afterViews] для 3 сусідніх сегментів. Отже, тут ми бачимо випадок, коли цей метод викликається після того, як фрагмент став видимим (фрагмент 1, рядок 14);
  • рядки 18–20: викликається метод [onResume] для 3 сусідніх сегментів;

Перехід з вкладки 1 на вкладку 2:


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
  • оскільки сусідство фрагментів зміщується на одну позицію вправо, фрагмент 4 запитується контейнером фрагментів;
  • рядок 2: викликається метод [setUserVisibleHint] фрагмента 4 з [visibleToUser] до false;
  • рядок 3: метод [setUserVisibleHint] фрагмента 1 викликається з [visibleToUser] до false. Адже фрагмент 1 тепер прихований;
  • рядок 4: метод [setUserVisibleHint] фрагмента 2 викликається з [visibleToUser] до true. Фрагмент 2 тепер видимий;
  • рядки 5–6: життєвий цикл фрагмента 4 триває;

Перехід з вкладки 2 на вкладку 3:


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
  • оскільки сусідство фрагментів зміщено на одну позицію вправо, фрагмент 5 запитується контейнером фрагментів;
  • рядок 2: метод [setUserVisibleHint] фрагмента 5 викликається з [visibleToUser] до false;
  • рядок 3: метод [setUserVisibleHint] фрагмента 2 викликається з [visibleToUser] до false. Адже фрагмент 2 тепер прихований;
  • рядок 4: метод [setUserVisibleHint] з фрагмента 3 викликається з [visibleToUser] до true. Фрагмент 3 тепер відображається;
  • рядки 5–6: життєвий цикл фрагмента 5 триває;

Перехід від вкладки 3 до вкладки 4:


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
  • рядок 1: фрагмент 3 тепер прихований;
  • рядок 2: фрагмент 4 тепер видимий. Зазначимо, що цикл життя фрагмента 4 не виконується. Це вже було зроблено двома кроками раніше;
  • рядок 3: фрагмент 1 виходить із сусідства з відображеним фрагментом 4. Виконується його метод [onDestroyView]. Наступного разу, коли він буде відображений, його цикл перегляду [onCreateView, afterViews, onResume] буде виконано знову;

Перехід від вкладки 4 до вкладки 5:


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
  • рядок 1: фрагмент 4 тепер прихований;
  • рядок 2: фрагмент 5 тепер видимий. Зауважимо, що цикл життя фрагмента 5 не виконується. Це вже було зроблено на 2 кроки раніше;
  • рядок 3: фрагмент 2 виходить із сусідства з відображеним фрагментом 5. Виконується його метод [onDestroyView];

Перехід з вкладки 5 на вкладку 1:


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
  • рядки 1, 4, 5, 6: життєвий цикл фрагмента 1 виконується повторно. Адже він втратив зв’язок зі своїм вікном;
  • рядки 2, 5, 8, 9: з тієї ж причини цикл життя фрагмента 2 виконується повторно;
  • рядки 10–11: фрагменти 4 і 5 виходять із сусідства з відображеним фрагментом;
  • рядок 7: фрагмент 1 оновлюється;
 

У логах ніколи не фіксувалося, що методи [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
  • журнали показують, що цикл життя 5 фрагментів виконано;
  • фрагмент 1 відображається у рядку 18;

Перехід з вкладки 1 на вкладку 2:


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
  • рядок 1: фрагмент 1 приховано;
  • рядок 2: фрагмент 2 відображається;

Перехід з вкладки 2 на вкладку 3:


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
  • рядок 1: фрагмент 2 приховано;
  • рядок 2: фрагмент 3 відображається;

Перехід з вкладки 3 на вкладку 4:


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
  • рядок 1: фрагмент 3 приховано;
  • рядок 2: фрагмент 4 відображається;

Перехід з вкладки 4 на вкладку 5:


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
  • рядок 1: фрагмент 4 приховано;
  • рядок 2: фрагмент 5 відображається;

Перехід з вкладки 5 на вкладку 1:


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
  • рядок 1: фрагмент 5 приховано;
  • рядок 2: фрагмент 1 відображається;
  • рядок 3: фрагмент 1 оновлюється;

Перехід з вкладки 1 на вкладку 4:


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
  • рядок 1: фрагмент 1 приховано;
  • рядок 2: фрагмент 4 відображається;
  • рядок 3: фрагмент 4 оновлюється;

Можна помітити, що при повній суміжності поведінка фрагментів є набагато передбачуванішою.

Тепер встановимо нульову суміжність і подивимося, що відбудеться. Клас [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
  • у рядках 8 і 10 видно, що контейнер фрагментів затребував 2 фрагменти, № 1 і № 2. Отже, все відбувається так, ніби сусідність дорівнює 1. Таким чином, сусідність 0 була проігнорована.

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] відображає частину стану додатка;

Ми запускаємо додаток із сусідністю фрагментів, рівною 2. Журнали під час запуску додатка:


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);
  • рядок 19: бачимо, що коли потік виконання досягає методу [update] фрагмента 1, метод [getActivity] дійсно повертає активність;

Коли сусідність фрагментів встановлено на 4 (повна сусідність), журнали мають такий вигляд:


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. Приклад-09: взаємодія між фрагментами, свайп та прокрутка

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;
  }
}
  • рядок 8: сесія інкапсулює кількість відвіданих фрагментів;
  • рядок 5: анотація [EBean] є анотацією AA. Атрибут [scope] визначає область дії (або термін існування) класу, що анотується таким чином. У цьому випадку атрибут [scope = EBean.Scope.Singleton] робить клас [Session] синглтоном: його екземпляр буде створено один раз і тільки один раз під час запуску програми. Посилання на клас, позначений анотацією [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);
  }

...
  • рядки 7–8: ін’єкція посилання на синглтон сесії за допомогою анотації [@Bean]. Параметром анотації є клас біна, який потрібно ін’єктувати. Поле, анотоване таким чином, не може мати область дії [private];
  • рядок 15: анотація [@AfterInject] слугує для позначення методу, який слід викликати після завершення всіх ін’єкцій класу. Отже, коли виконується метод [afterInject] у рядку 16, посилання з рядка 8 вже ініціалізовано;
  • рядок 20: лічильник відвідувань обнуляється;

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));
  }
  • рядок 7: сесія;
  • рядки 35–37: ми знаємо, що коли ми потрапляємо в метод [update], метод [getActivity] дійсно повертає активність. Ми користуємося цим, щоб отримати сесію та зберегти її локально (рядок 36);
  • рядки 39–41: щоб збільшити номер відвідування, ми отримуємо його з сесії. Цей код можна було б розмістити в методі [setUserVisibleHint], починаючи з рядка 19, оскільки ми знаємо, що саме тоді метод [getActivity] повертає активність. Тут вирішено не надавати цьому методу особливої ролі та перенести код, що стосується конкретного фрагмента, у метод [update], який призначений саме для цього;
  • рядок 43: відображає номер відвідування;

Коли запускати цю програму з 5 фрагментами та сусідством у 2 фрагменти, перші записи у журналі мають такий вигляд:


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
...
  • рядки 2–3: бачимо, що метод [afterInject] цієї активності виконується раніше, ніж її метод [afterViews];

Пропонуємо читачеві випробувати цей новий додаток.

1.10.5. Вимкнення функції «Swipe» або «Прокрутка»

У попередньому додатку, коли ми прокручуємо емулятор Android мишкою вліво або вправо, поточний вигляд замінюється видом праворуч або ліворуч, залежно від ситуації. Така поведінка за замовчуванням не завжди є бажаною. Ми навчимося вимикати прокрутку виглядів (swipe).

Повернемося до головного вікна 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"/>

Рядок 1 позначає клас, який керує сторінками активності. Цей клас міститься в активності [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;
...

У рядку 12 контейнер фрагментів має тип [android.support.v4.view.ViewPager] (рядок 1). Щоб вимкнути сканування, цей клас потрібно похідно створити наступним чином:

  

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

  // сетер
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

}
  • рядок 8: клас [MyPager] розширює клас Android [ViewPager] (рядок 4);
  • при прокручуванні рукою можуть викликатися обробники подій у рядках 24 та 34. Обидва вони повертають логічне значення. Для блокування прокручування їм достатньо повернути логічне значення [false];
  • рядок 11: логічне значення, яке вказує, чи приймається жест прокрутки рукою.

Після цього потрібно використовувати наш новий обробник сторінок. Це робиться у поданнях 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"/>

У рядку 1 використовується новий клас. У [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);
    // панель вкладок також пов’язана з контейнером фрагментів
...
  • рядок 12: тип менеджера сторінок тепер — [MyPager];
  • рядок 23: вмикається або вимикається прокрутка рукою.

Випробуйте цю нову версію. Увімкніть або вимкніть прокрутку та зверніть увагу на різницю в поведінці вікон, коли ви перетягуєте їх мишкою вправо або вліво. У всіх майбутніх додатках прокрутка буде вимкнена. Ми більше не будемо нагадувати про це.

1.10.6. Вимкнення прокрутки між фрагментами

Продовжимо з удосконаленням менеджера вкладок. Коли переходимо з вкладки 1 на вкладку 4, бачимо, як прокручуються дві проміжні вкладки 2 і 3. У жаргоні Android це називається 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] — номер фрагмента, який потрібно відобразити;

  • рядок 10: викликається метод [setCurrentItem] батьківського класу. Другий аргумент методу [false] вимагає негайного переходу між старим і новим фрагментом (без прокрутки), а аргумент методу [true] — переходу через scrolling. Тут другим аргументом є значення поля в рядку 4, яке розробник може встановити за допомогою методу, описаного в рядках 16–18;

Якщо потрібно вимкнути прокрутку, клас [MainActivity] матиме такий вигляд:


...
    // зсув фрагментів
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // забороняється прокрутка між фрагментами
    mViewPager.setSwipeEnabled(false);

    // без прокрутки
    mViewPager.setScrollingEnabled(false);
...

Запустіть проект ще раз і переконайтеся, що, наприклад, між вкладками 1 і 4 більше немає 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>
  <!-- вид 1 -->
  <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>
  • вище додано рядки 6–9;

Тепер створюємо клас [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();
  }
}

  • рядок 10: анотація [@EFragment] означає, що фрагмент, який використовується активністю, насправді буде класом [Vue1Fragment_]. Про це слід пам’ятати. Фрагмент пов’язаний з поданням [vue1.xml];
  • рядки 14–15: компонент, ідентифікований як [R.id.editTextNom], вставляється у поле [editTextNom] у рядку 15;
  • рядки 18–20: метод [doValider] обробляє подію «click» на кнопці, ідентифікованій за кодом [R.id.buttonValider];
  • рядок 21: перший параметр [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_();
    }

 ...
  }
  • рядок 13: є [FRAGMENTS_COUNT] фрагменти: [FRAGMENTS_COUNT-1] фрагменти типу [PlaceholderFragment] (рядки 14–21) та один фрагмент типу [Vue1Fragment_], рядок 23 (зверніть увагу на підкреслення);

Скомпілюйте, а потім запустіть проект [Exemple-09]. Вкладка № 5 має виглядати інакше:

1.10.8. Зробити всі фрагменти похідними від одного й того самого абстрактного класу

Новий фрагмент [Vue1Fragment] також потребує оновлення під час відображення. Для цього нам доведеться створити код, подібний до того, що був створений для фрагмента [PlaceholderFragment]. Щоб уникнути повторень, ми винесемо все, що можна, в абстрактний клас, від якого успадковуватимуть усі фрагменти додатка.

Для цього ми створюємо новий проєкт.

1.11. Приклад-10: успадкування всіх фрагментів від абстрактного класу

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() {
    // батьківський
    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();
}
  • рядок 7: клас [AbstractFragment] успадковує клас Android [Fragment];
  • кожен фрагмент повинен мати можливість оновлюватися. Саме тому батьківський клас [AbstractFragment] вимагає від своїх дочірніх класів наявності методу [updateFragment] (рядок 68), який він викликає (рядок 65);
  • рядок 19: клас зберігатиме посилання на активність додатка;
  • рядок 22: клас зберігатиме посилання на сесію, у якій зібрані дані, спільні для фрагментів та активності;
  • рядки 25–33: конструктор абстрактного класу;
  • рядок 27: створення копії константи [MainActivity.IS_DEBUG_ENABLED] у полі рядка 16;
  • рядок 28: зберігається ім’я інстанційованого класу, тобто ім’я дочірнього класу;
  • рядки 15–22: ці поля мають атрибут [protected], щоб дочірні класи мали до них доступ. Слід зауважити, що дочірні класи ігнорують існування булевих значень [isVisibleToUser] та [updateDone] (рядки 10–11);
  • рядок 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), що надає доступ до сесії.

  • рядки 4–10: ініціалізуються активність та сесія, якщо це ще не було зроблено;
  • рядок 12: викликається метод [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() {
  ...
  }

}
  • рядок 10: клас [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);
  }
  • рядки 7 і 23: у логах ми виводимо інформацію про батьківський клас за допомогою успадкованого методу [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();
  }
}
  • рядок 9: клас [Vue1Fragment] успадковує клас [AbstractFragment];
  • рядки 18–26: метод [@AfterViews] не виконує жодних цікавих дій. Проте його все одно потрібно написати, щоб змінити значення логічного параметра [afterViewsDone] на true, оскільки ця інформація використовується батьківським класом;
  • рядки 42–49: метод [updateFragment] полягає у виведенні короткого повідомлення з номером відвідування (рядок 48) та інкрементуванні цього номера в сесії (рядки 44–46);

Пропонуємо читачеві протестувати цей новий проєкт.

У всіх майбутніх проектах ми будемо використовувати таку архітектуру:

  • одна активність та n фрагментів;
  • усі фрагменти успадковують клас [AbstractFragment];
  • дані, які мають обмінюватися між фрагментами, а також між фрагментами та активністю, розміщуються у класі [Session];

1.11.6. Зв’язок між вкладками та фрагментами

У класі [MainActivity], який керує вкладками, написано:


// панель вкладок також пов'язана з контейнером фрагментів
// тобто вкладка № i відображає фрагмент № i з контейнера
tabLayout.setupWithViewPager(mViewPager);

У рядку 3 менеджер вкладок пов'язується з контейнером фрагментів. Ми вже бачили наслідок цього зв'язку: коли користувач натискає на вкладку № 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>
  • рядки 9–28: п’ять нових опцій меню;
  • назви опцій (рядки 10, 14, 18, 22, 26) визначені у файлі [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>
  <!-- вид 1 -->
  <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);
    }
  }
  • рядок 2: метод [onOptionsItemSelected] викликається при натисканні на один із пунктів меню;
  • рядок 8: отримується ідентифікатор опції, на яку натиснули;
  • рядки 9–36: різні випадки обробляються методом switch;
  • рядки 16–36: клік на опції [Fragment i] перенаправляє до методу [showFragment(i-1)] у рядках 41–45;
  • рядок 43: до контейнера фрагментів надсилається запит на відображення запитуваного фрагмента;
  • рядок 42: попередньо перевіряється, чи це можливо (умова 1) і чи це необхідно (умова 2);

Читачеві пропонується протестувати цю нову версію. Можна помітити, що при запиті на відображення фрагмента № i він успішно відображається, а вкладка № i сама по собі вибирається.

Тепер, коли ми розглянули, як працює зв’язок між вкладками та фрагментами, перейдемо до іншого випадку: коли управління вкладками відокремлене від управління фрагментами. Це, наприклад, той випадок, коли вкладок менше, ніж фрагментів. Щоб проілюструвати цей новий випадок використання, створимо новий проєкт.

1.12. Приклад-11: вкладки, відокремлені від фрагментів

1.12.1. Створення проєкту

Дублюємо проєкт [Exemple-10] у [Exemple-11]:

1.12.2. Цілі

Новий додаток матиме дві вкладки:

  • на першій вкладці завжди відображатиметься фрагмент [Vue1];
  • на другій вкладці відображатиметься фрагмент, обраний у меню;

Image

  • у [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;

  // методи getter та setter
...
}
  • рядок 10: ми самі будемо обробляти кліки на вкладках. При кліці на вкладку потрібно відтворити фрагмент, який вона відображала під час останнього вибору. Поле [numFragment] запам'ятає номер цього фрагмента для вкладки № 2 — число в полі [0, Fragments_COUNT-2]. Коли буде натиснуто на вкладку № 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>

На вкладці № 2 відображатиметься один із чотирьох фрагментів рядків 9–24. П’ятий фрагмент — це фрагмент [Vue1Fragment], який завжди відображатиметься на вкладці № 1.

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

    // відображення Vue1
    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]. За своєю структурою це буде останній фрагмент контейнера;
  • рядки 20–22: оскільки ми не встановили зв’язку між вкладками та контейнером фрагментів, нам доводиться самостійно керувати вкладками. Спочатку панель вкладок [tabLayout] у рядку 3 не містить жодної вкладки;
  • рядок 20: створюємо першу вкладку;
  • рядок 21: надаємо їй заголовок. У попередніх прикладах заголовки вкладок збігалися із заголовками фрагментів. Тепер це вже не так. Тому видаляємо метод [getPageTitle] з менеджера фрагментів. Він нам більше не потрібен:

    // опціонально — присвоює назви керованим фрагментам
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
}
  • рядок 22: створену вкладку додають до панелі вкладок. Наша панель вкладок тепер має одну вкладку. Що відображає ця вкладка? Слід розуміти, що вкладки та фрагменти — це два незалежні поняття. Відображується завжди той фрагмент, який обрав контейнер фрагментів. Якщо змінити вкладку і не вимагати від контейнера змінити відображуваний фрагмент, нічого не відбудеться: відображається той самий фрагмент, але вибрана вкладка змінилася. Отже, тут відображується фрагмент, обраний у рядку 17: фрагмент [Vue1Fragment];
  • рядки 26–30: метод, який потрібно написати для обробки зміни вкладки користувачем;

Метод [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);
}
  • рядок 8: отримуємо позицію вкладки, на яку натиснули. Тут ми отримаємо число 0 або 1;
  • рядки 12–15: якщо було натиснуто першу вкладку, готуємося до відображення фрагмента [Vue1Fragment];
  • рядки 16–18: в інших випадках (натиснуто вкладку № 2) готуємося до повторного відображення фрагмента, який відображався востаннє, коли була вибрана вкладка № 2. Номер цього фрагмента тоді було збережено в сесії додатка;
  • рядок 21: до контейнера фрагментів надсилається запит на відображення потрібного фрагмента;

Тепер розглянемо управління опціями меню (також у [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;
}
  • рядки 16–31: управління 4 пунктами меню. Кожен менеджер викликає метод [showFragment] із номером фрагмента, який потрібно відобразити;

Метод [showFragment] виглядає наступним чином:


  // вкладка № 2
  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);
      // вибираємо вкладку 2 — нічого не робимо, якщо вона вже вибрана
      tab2.select();
    }
}
  • Нагадаємо, що на початку роботи програми є лише одна вкладка;
  • рядок 2: посилання на вкладку № 2, null на початку;
  • рядок 5: умови відображення не змінилися порівняно з попередньою версією;
  • рядки 7–10: якщо вкладка № 2 ще не існує, її створюють (рядок 8) і додають до панелі вкладок (рядок 9);
  • рядок 12: у заголовок другої вкладки вноситься номер фрагмента, який буде відображатися, з нумерацією, що починається з 1;
  • рядок 14: відображається потрібний фрагмент;
  • рядок 16: його номер записується в сесію;
  • рядок 18: вибирається вкладка № 2. Якщо вона вже була вибрана, нічого не відбудеться: метод [onTabSelected] не буде виконано. Якщо вона ще не була вибрана, запуститься метод [onTabSelected]. Тоді цей метод вимагає від контейнера фрагментів відобразити фрагмент, який вже відображався у рядку 14. Простий тест у методі [onTabSelected] дозволяє уникнути цього випадку:

        // фрагмент відображається лише за потреби
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

Пропонуємо читачам випробувати цю нову версію.

1.12.6. Удосконалення

Тепер ми добре розуміємо, що таке фрагменти, їхній життєвий цикл, поняття суміжності фрагментів та їхній зв’язок із панеллю вкладок. Крім того, ми маємо надійну архітектуру, яка щойно пройшла тест із прикладу 11:

  • одна активність і n фрагментів;
  • усі фрагменти є підкласами класу [AbstractFragment];
  • дані, які мають обмінюватися між фрагментами, а також між фрагментами та активністю, розміщені в класі [Session];

У новому проєкті ми уточнимо взаємозв’язки між активністю та фрагментами шляхом додавання інтерфейсу.

1.13. Приклад-12: кодування взаємозв’язків між активністю та фрагментами

У цьому прикладі ми хочемо визначити мінімальні взаємозв’язки між активністю та фрагментами. Для цього ми використаємо:

  • інтерфейс [IMainActivity], який визначатиме, що фрагменти можуть вимагати від активності;
  • абстрактний клас [AbstractFragment], який визначатиме стан та методи, що повинен мати кожен фрагмент;

1.13.1. Створення проєкту

Ми дублюємо проект [Exemple-11] у [Exemple-12], дотримуючись процедури, описаної в пункті 1.4. Отримуємо такий результат:

1.13.2. Інтерфейс [IMainActivity]

З наведених прикладів випливає, що фрагменти потребують доступу до сесії, ініційованої активністю. Крім того, хоча це й не видно з цих прикладів, але можна передбачити: обробники подій фрагментів іноді завершуються зміною виду. Ми попросимо активність здійснити цю зміну. Інтерфейс [IMainActivity] може виглядати наступним чином:

  

package exemples.android;

public interface IMainActivity {

  // доступ до сеансу
  Session getSession();

  // зміна виду
  void navigateToView(int position);

  // режим налагодження
  boolean IS_DEBUG_ENABLED = true;
}

У рядку 12 зверніть увагу на наявність константи, яка раніше була в класі [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();
}
  • рядки 6 і 7: підтримуються два типи посилань на активність:
    • рядок 6: посилання на активність, що реалізує інтерфейс [IMainActivity];
    • рядок 7: посилання на активність, що успадковує клас Android [Activity]. Так відбувається з кожною активністю;

Ці дві посилання, звісно, вказують на один і той самий об’єкт. Але він розглядається як два різні типи. Це дозволить уникнути приведення типів під час виконання;

  • рядок 14: отримуємо посилання на активність за допомогою методу [getActivity];
  • рядок 15: якщо вона не дорівнює нулю, то можна отримати доступ до сесії;
  • рядки 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);
    }
  }

  • рядки 10–12: метод [getSession] вже існував;
  • рядки 15–22: метод [navigateToView] відображає фрагмент № [position];
  • рядок 17: перевіряється, чи є що робити;
  • рядок 19: відображається фрагмент № [position];

На цьому етапі запустіть програму. Вона має працювати.

1.13.6. Зміна відображення фрагментів у [MainActivity]

Наразі клас [MainActivity] відображає фрагмент за допомогою інструкції:


    // відображення Vue1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

Оскільки метод [navigateToView] виконує те саме, цей тип інструкції скрізь (у 2 місцях) замінюють на:

navigateToView(...);

Потім запустіть додаток. Він має працювати як і раніше.

1.13.7. Висновок

Відтепер ми завжди будемо використовувати попередню архітектуру:

  • активність, що реалізує інтерфейс [IMainActivity];
  • фрагменти, що успадковують клас [AbstractFragment], що зобов’язує їх реалізовувати метод [updateFragment]. Вони також повинні мати метод [@AfterViews], у якому вони встановлюють значення логічного оператора [afterViewsDone] на true;
  • сесію, що інкапсулює дані, які мають бути спільними для фрагментів та активності;

1.14. Приклад-13: Приклад-05 із фрагментами

У проєкті [Exemple-05] ми впровадили навігацію між видами. Тоді йшлося про навігацію між активностями: 1 вид = 1 активність. Тут ми пропонуємо мати одну активність із кількома видами типу [AbstractFragment].

1.14.1. Створення проєкту

Ми дублюємо попередній проєкт [Exemple-12] у [Exemple-13], дотримуючись процедури, описаної в розділі 1.4. Отримуємо такий результат:

1.14.2. Структурування проєкту

Ми почнемо використовувати пакети для впорядкування коду. Наразі можна виділити дві окремі області:

  • управління діяльністю;
  • управління фрагментами;

Для них ми створюємо два пакети [exemples.android.activity] та [exemples.android.fragments]:

 

Аналогічним чином створюємо пакет [exemples.android.fragments]:

У [8] створюємо третій пакет під назвою [architecture], у який помістимо сутності [IMainActivity, AbstractFragment, Session, MyPager], що є базовими елементами архітектури нашого додатка. Це робиться для того, щоб нагадати собі, що ми зробили конкретний вибір архітектури. Далі перемістіть існуючі елементи проєкту, як зазначено в [9]. Кожне переміщення потрібно підтвердити, натиснувши кнопку [Refactor].

На цьому етапі скомпілюйте додаток. У [MainActivity] з’являються такі помилки:

 

Під час переміщення класів у пакети Android Studio внесло необхідні зміни в код додатка (наприклад, рядки 18–21). Класи, про які йдеться в рядках 15 і 17, не були переміщені. Вони генеруються бібліотекою 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>

Рядки 3 і 12 вказують, що вказаною активністю є [exemples.android.MainActivity_]. Однак, оскільки активність була переміщена до пакета [activity], рядок 12 тепер має виглядати так:


      android:name=".activity.MainActivity_"

Зверніть увагу на символ «.» перед [activity]. Знову ж таки, Android Studio не зміг оновити маніфест, оскільки він посилається на клас 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;

  // методи getter та setter

  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];
  • також видаляємо панель інструментів у рядках 18–24;

Код меню [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>
  • видаляємо рядки 9–24. Таким чином залишаємо опцію, яку не будемо використовувати. Просто для того, щоб мати приклад оголошення опції меню, який можна буде відтворити за допомогою копіювання/вставлення;

У класі [MainActivity] видаляємо все, що стосується вкладок, плаваючої кнопки, панелі інструментів та меню. Щоб знайти ці посилання, найпростіше видалити їхні оголошення:


  // менеджер вкладок
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // плаваюча кнопка
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

і перекомпілювати додаток. Помилковими є ті рядки, які посилаються на елементи, що зникли. Видаліть усі ці рядки. Крім того, змініть менеджер фрагментів так, щоб він більше не посилався на фрагмент [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;
    }
}
  • рядки 7–10: ми видалили всю генерацію фрагментів;

На цьому етапі помилок компіляції більше не повинно бути. У класі [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);

    // відображення «Вид 1»
    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;
    }
  }
}

Залишилося внести кілька змін:

  • видаліть рядок 31, який більше не потрібен;
  • у рядку 33 вкажіть 1 як суміжність фрагментів;
  • у рядку 76: перейдіть до подання 0. Саме воно буде відображатися першим;
  • рядок 108: ініціалізуйте масив фрагментом [Vue1Fragment_]:

    // фрагменти
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Отже, у нас є лише один фрагмент. Запустіть додаток. Ви повинні отримати такий результат:

Image

Кнопка [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>
  <!-- вид 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- вид 2 -->
  <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() {
  }
}
  • рядок 9: фрагмент пов'язаний з поданням [res / layout / vue2.xml];
  • рядок 10: клас успадковує абстрактний клас [AbstractFragment];
  • рядки 12–20: обов’язковий метод [@AfterViews];
  • рядки 23–25: обов’язковий метод [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() {
  }
}
  • рядки 37–40: метод [showVue2] обробляє подію «натискання» на кнопку [Vue n° 2];
  • рядок 39: навігація здійснюється за допомогою методу [navigateToView] активності. Нагадаємо, що активність була збережена в батьківському класі у вигляді:

  // діяльність
protected IMainActivity mainActivity;

і що ця активність вже була ініціалізована при переході до будь-якого обробника подій.

  • рядок 34: інструкція використовує змінну [activity] батьківського класу, яка є посиланням на активність як екземпляр типу Android [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() {
  }
}
  • рядки 24–27: метод [showVue1] обробляє подію «натискання» на кнопці [Vue n° 1];

Запустіть проект і переконайтеся, що навігація між видами працює.

1.14.6. Визначення сесії

Робота додатка відбувається наступним чином:

  • введення імені у вікні № 1;
  • відображення цього імені у вікні № 2;

Щоб вікно № 1 могло передати введене ім’я до вікна № 2, ми використаємо таку сесію:


package exemples.android.architecture;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // ім'я
  private String nom;

  // методи отримання та встановлення
...
}
  • рядок 8: введене ім’я;

Клас [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());
    // перехід до екрану № 2
    mainActivity.navigateToView(1);
  }

  // оновлення фрагмента
  @Override
  protected void updateFragment() {

  }
}
  • рядки: 31–37: обробляємо натискання кнопки [Vue n° 2];
  • рядок 34: перед переходом до подання № 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));
  }
}

Коли відображається вікно № 2, потрібно відобразити ім’я, введене у вікні № 1. Відомо, що одразу після його відображення буде виконано його метод [updateFragment]. Отже, саме в цьому методі (рядки 36–42) можна розмістити код для відображення імені.

  • рядки 16–17: оголошення єдиного візуального компонента вікна;
  • рядок 39: ім’я, введене у вікні № 1, отримується із сесії;
  • рядок 41: змінюється мітка [textViewBonjour];

Запустіть проект і переконайтеся, що він працює.

1.14.8. Управління життєвим циклом фрагментів

У фрагменті [Vue1Fragment] метод [@AfterViews] виглядає так:


  @AfterViews
  protected void afterViews() {
    // пам'ять
    afterViewsDone = true;
    // журнал
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
}

Цей метод є неповним. Адже завжди слід передбачати випадок, коли фрагмент повторно використовується після операції [onDestroyView]. У цьому випадку вікно фрагмента 1 оновлюється, і ім’я, яке, можливо, було введено раніше, зникне з вікна. Цього не потрібно. Наразі введена назва залишається на екрані, оскільки сусідство фрагментів 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);
        // перехід до подання № 2
        activity.navigateToView(1);
}
  • рядок 27: перед переходом з подання 1 до подання 2 запам'ятовується введене ім'я;
  • рядок 17: під час кожного нового виконання життєвого циклу фрагмента останнє введене ім’я відображається знову;

Для фрагмента [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));
}
  • єдиний візуальний компонент вікна (рядок 3) оновлюється щоразу, коли вікно відображається (рядок 21). Отже, метод [@AfterViews] нічого не додає;

1.14.9. Висновок

На цьому етапі ми знову продемонстрували доцільність нашої архітектури:

  • активність, що реалізує інтерфейс [IMainActivity];
  • фрагменти, що успадковують клас [AbstractFragment], що зобов’язує їх реалізувати метод [updateFragment]. Вони також повинні мати метод [@AfterViews], у якому вони встановлюють булеве значення [afterViewsDone] на true;
  • сесію, що інкапсулює дані, які мають бути спільними для фрагментів та активності;

1.15. Приклад-14: двошарова архітектура

Ми створимо одновіконний додаток із такою архітектурою:

1.15.1. Створення проєкту

Ми дублюємо попередній проєкт [Exemple-12] у [Exemple-13], дотримуючись процедури, описаної в розділі 1.4. Отримуємо такий результат:

1.15.2. Вигляд [vue1]

У додатку буде лише один вигляд [vue1.xml]. Тому ми видаляємо інший вигляд [vue2.xml], а також пов’язаний з ним фрагмент:

 

Скомпілюйте додаток. У [MainActivity] з’являються помилки:

 

Виправте рядок 4 нижче в менеджері фрагментів [SectionsPagerAdapter]


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // фрагменти
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...

Рядок 4 вище стає таким:


    // фрагменти
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Видаліть імпорти, які стали непотрібними: [Ctrl-Shift-O]. Помилок компіляції більше не повинно бути. Запустіть проект: має з’явитися вікно № 1. Тепер ми його змінимо.

Ми створимо вигляд [vue1.xml], який дозволить генерувати випадкові числа:

 

Його компоненти такі:

Id
Type
Rôle
1
edtNbAleas
EditText
nombre de nombres aléatoires à générer dans l'intervalle entier [a,b]
2
edtA
EditText
valeur de a
2
edtB
EditText
valeur de b
4
btnExécuter
Button
lance la génération des nombres
5
ListView
lstReponses
liste des nombres générés dans l'ordre inverse de leur génération. On voit d'abord le dernier généré ;

Його код 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>
  <!-- вид 1 -->
  <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</color>
  <color name="colorAccent">#FF4081</color>
  <!-- кольори додатка -->
  <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<>();
  // адаптер списку
  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] (рядки 46–57) та [udateFragment] (рядки 75–81) будуть виконані лише один раз під час запуску додатка;
  • рядки 55–56: приховуються два повідомлення про помилки з подання (наведені нижче) [1-2];
 
  • рядки 59–60: метод, що виконується при натисканні кнопки [Exécuter];
  • рядки 71–73: перевіряється правильність введених даних;

Метод [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
    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);
  }

  • рядки 2–4: ці три поля ініціалізуються методом [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++) {
            // генерується випадкове виключення 1 раз із 3
            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;
    }
}
  • рядок 9: для класу [Metier] використовується анотація AA [@EBean], щоб мати змогу вставляти посилання на цей клас у шар [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);
    }

}
  • рядок 3: клас [AleaException] успадковує системний клас [RuntimeException], що робить його неконтрольованим винятком: його не обов’язково обробляти в блоці 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;

}
  • рядок 5: інтерфейс [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);
}
  • рядки 11–12: шар [métier] вводиться в активність. Для цього використовується анотація AA [@Bean], параметром якої є клас з анотацією AA [@EBean];
  • рядок 2: активність реалізує інтерфейс [IMainActivity], а отже, і інтерфейс [IMetier] шару [métier];
  • рядки 16–19: реалізація єдиного методу інтерфейсу [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<>();
  // адаптер списку
  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);
  }
}
  • рядки 69–70: встановлюється адаптер компонента типу [ListView];

Компонент [ListView] призначений для відображення списку елементів. Він робить це за допомогою адаптера типу [ListAdapter], який, у свою чергу, підключений до джерела даних, що має забезпечувати [ListView]. Для визначення адаптера [ListView] використовується такий метод [ListView.setAdapter]:


public void setAdapter (ListAdapter adapter)

[ListAdapter] — це інтерфейс. Клас [ArrayAdapter] реалізує цей інтерфейс. Конструктор, використаний у рядку 69 вище, має такий вигляд:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] — це активність, яка відображає [ListView];
  • [resource] — це ціле число, що ідентифікує представлення, яке використовується для відображення елемента [ListView]. Це представлення може мати будь-яку складність. Розробник створює його відповідно до своїх потреб;
  • [textViewResourceId] — це ціле число, що ідентифікує компонент [TextView] у поданні [resource]. Рядок буде відображатися саме цим компонентом;
  • [objects] — список об’єктів, що відображаються компонентом [ListView]. Метод [toString] об’єктів використовується для відображення об’єкта в [TextView], ідентифікованого за [textViewResourceId], у поданні, ідентифікованому за [resource].

Завдання розробника полягає у створенні подання [resource], яке відображатиме кожен елемент [ListView]. Для простого випадку, коли потрібно відобразити лише простий рядок символів, як у цьому прикладі, Android надає представлення з ідентифікатором [android.R.layout.simple_list_item_1]. Воно містить компонент [TextView], ідентифікований як [android.R.id.text1]. Саме цей метод використовується в рядку 69 для створення адаптера [ListView]. Цей адаптер потрібно визначити лише один раз. Щоб забезпечити можливість його повторного використання, його визначено як екземплярну змінну класу (рядок 39). Повернімося до рядка 69:


adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);

Першим параметром конструктора [ArrayAdapter] є активність, отримана у фрагменті за допомогою [getActivity] і збережена тут у змінній [activity] батьківського класу. Це поле не завжди має значення. Так, журнали показують, що під час входу в метод [@AfterViews] він ще не був ініціалізований, а отже, рядки 69–70 не можна розмістити в цьому методі. У методі [updateFragment] це можливо, оскільки відомо, що під час виконання цього методу обов’язково виконується метод [activity!=null]. Адаптер тут пов’язаний із джерелом даних [reponses], визначеним у рядку 37;

Метод [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();
  }
  • рядки 7–8: потрібно очистити ListView. Для цього очищається джерело даних [reponses] і адаптер, пов’язаний із ListView, отримує запит на оновлення;
  • рядки 10–12: перед виконанням запитуваної дії перевіряється правильність введених значень;
  • рядок 14: у активності запитується список випадкових чисел. Отримується список об’єктів, кожен з яких має тип [Integer] або [AleaException];
  • рядки 16–22: на основі отриманого списку об’єктів оновлюється джерело даних [reponses], яке відображає ListView;
  • рядок 24: надсилається запит на оновлення адаптера ListView;

1.15.8. Виконання

Запустіть проект і перевірте його працездатність.

1.16. Приклад-15: архітектура «клієнт/сервер»

Ми розглянемо поширену архітектуру для додатка Android, в якій додаток Android взаємодіє з віддаленими веб-сервісами. Тепер ми матимемо таку архітектуру:

До додатка для Android додано рівень [DAO] для взаємодії з віддаленим сервером. Він буде взаємодіяти з сервером, який генерує випадкові числа, що відображаються на планшеті Android. Цей сервер матиме таку дворівневу архітектуру:

Клієнти надсилають запити на певні об’єкти URL з рівня [web / jSON] і отримують текстову відповідь у форматі jSON (JavaScript Object Notation). Тут наш веб-сервіс оброблятиме єдиний URL типу [/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/] (червень 2016 р.), який дозволить нам згенерувати проект Gradle з необхідними для нашого проекту залежностями. Цей проект не є проектом Android, і для його побудови Android Studio на даний момент не пропонує жодної допомоги:

  • у [1]: виберіть проект Gradle;
  • у [2-3]: характеристики залежності jar, згенерованої проектом (див. нижче);
  • у [4]: виберіть веб-залежність [5], щоб були доступні бінарні файли, необхідні для нашого веб-сервісу;
  • у [6]: згенеруйте проєкт. Після цього буде згенеровано архів ZIP із шаблоном проєкту Gradle, який буде запропоновано для завантаження;

Що вказати в [2-3]? Ми вже використовували залежності Gradle. Наприклад, у попередньому проєкті це було наступне:

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Починаючи з версії 0.11 плагіна Gradle для Android, необхідно використовувати 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
  ...
}
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'
}
  • рядок 22: залежність має вигляд [groupId:artifactId:version]. У формі на сайті потрібно вказати [http://start.spring.io/]:
    • у [2] — це [groupId];
    • у [3] — це [artifactId];

Розпакуйте у папку з іншими проектами отриманий zip-файл:

У програмі Android Studio відкрийте проект Gradle [server-01] [1-2]. Відкритий проект має назву [3] (перспектива «Project»).

1.16.1.2. Налаштування Gradle

  

Створений файл Gradle (червень 2016 р.) має такий вигляд:


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'
 }
}
  • рядки 14 та 34–38 призначені для Eclipse IDE. Ми їх видаляємо;
  • рядки 1–11 та 15 призначені для додавання плагіна під назвою [spring-boot] до нашого проєкту Gradle. Spring Boot — це проєкт екосистеми Spring [http://projects.spring.io/spring-boot/]. Цей плагін визначає версії залежностей, які найчастіше використовуються разом із Spring. Це дозволяє не вказувати їхні версії (рядки 30 та 31). У такому разі версія відповідає тій, що визначена використовуваною версією Spring Boot (рядок 3);
  • рядки 22–23: версія Java, яку слід використовувати, у даному випадку версія 1.8;
  • рядки 25–27: репозиторії бінарних файлів, які слід використовувати для завантаження залежностей;
  • рядок 26: вказує на центральний репозиторій Maven. Наразі це найбільший доступний репозиторій бінарних файлів з відкритим кодом;
  • рядки 29–32: залежності, необхідні для проєкту:
  • рядок 30: ця залежність містить усі бінарні файли, необхідні для побудови веб-сервісу Spring;
  • рядок 31: ця залежність містить усі бінарні файли, необхідні для тестування, зокрема для тестів 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'
}

// версії Java
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')
}
  • рядок 30: ми додали локальний репозиторій Maven робочої станції. Він створюється під час встановлення Maven (див. розділ 6.10). Якщо потрібна залежність уже є в локальному репозиторії Maven, її не буде запитуватися у центральному репозиторії Maven;
  • рядки 19–22: завдання Gradle, що дозволяє згенерувати бінарний файл проекту. Ми використаємо його, щоб побачити, що відбувається;
  • у [1-4] виконайте завдання [jar], визначене у файлі [build.gradle] ([1] знаходиться вгорі праворуч та поруч із IDE);

Попередня операція створює архів jar проекту та розміщує його в папці [build / libs] [5]:

  

Назва архіву походить безпосередньо з інформації, вказаної для завдання [jar] у файлі [build.gradle] (рядки 19–22).

Усі залежності проекту можна переглянути таким чином:

 

У [1] можна побачити, що єдина залежність проекту [compile('org.springframework.boot:spring-boot-starter-web')] принесла з собою десятки бінарних файлів. Spring Boot для веб-додатків включив залежності, які, ймовірно, знадобляться веб-додатку Spring MVC. Це означає, що деякі з них можуть виявитися непотрібними. Spring Boot ідеально підходить для навчального посібника:

  • він надає залежності, які нам, ймовірно, знадобляться;
  • він містить вбудований сервер Tomcat [1], що позбавляє нас необхідності розгортати додаток на зовнішньому веб-сервері;

На сайті екосистеми Spring можна знайти безліч прикладів використання Spring Boot [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'
    }
  }
}
  • рядок 10: імпортуємо плагін Gradle під назвою [maven-publish], який дозволяє опублікувати бінарний файл проєкту в репозиторії Maven відповідно до стандартів Maven;
  • рядок 11: завдання Gradle під назвою [publishing];
  • рядки 14–15: характеристики бінарного файлу Maven, який буде створено;
  • рядок 23: репозиторій Maven, у якому він буде опублікований, у даному випадку — локальний репозиторій Maven;

Додавання плагіна [maven-publish] створило нові завдання в проєкті Gradle:

Якщо в [2] виконати завдання [publish], бінарний файл проєкту буде створено та встановлено у папку, вказану в рядку 23 файлу [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
}
  • рядок 6: потрібно вказати повну назву класу, що виконується, проекту:
  

Код цього класу буде таким:


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 недостатньо. Також потрібно налаштувати сам проєкт. Оскільки це не проєкт Android, згенерований за допомогою IDE, тут необхідно виконати налаштування, яких ми досі не робили.

 
  • у [3-4]: візьміть JDK 1.8;

Для компіляції проєкту кнопка, доступна для проєктів Android, більше відсутня. Ми скористаємося опцією з меню [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);
}
  • рядок 5: метод, що генерує 1 випадкове число в [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] у рядку 8, яка змусить Spring створити єдиний екземпляр класу (синглтон) і зробити його посилання доступним для інших компонентів Spring. Для досягнення того самого ефекту тут можна було б використати й інші анотації Spring. Компоненти Spring мають імена за замовчуванням, які можна вказати як атрибут використовуваної анотації. Без цього атрибута, як у цьому випадку, компонент 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;
  }

  // гетери та сеттери
....
}
  • рядок 3: [AleaException] успадковує клас [RuntimeException]. Отже, це неконтрольоване виключення (немає обов’язку обробляти його за допомогою try / catch);
  • рядок 6: до класу [RuntimeException] додається код помилки;

1.16.1.5. Веб-сервіс / jSON

 
  

Веб-сервіс / jSON реалізовано за допомогою Spring MVC. Spring MVC реалізує архітектурну модель, відому як MVC (Модель – Вигляд – Контролер), наступним чином:

Обробка запиту клієнта відбувається наступним чином:

  1. запит — запитувані URL мають вигляд http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] — це клас Spring, який обробляє вхідні URL. Вона «направляє» URL до дії, яка має її обробити. Ці дії — це методи певних класів, що називаються [Contrôleurs]. Літера «C» у MVC тут є рядком [Dispatcher Servlet, Contrôleur, Action]. Якщо для обробки вхідного запиту URL не було налаштовано жодної дії, сервлет [Dispatcher Servlet] відповість, що запитуваний URL не знайдено (помилка 404 NOT FOUND);
  1. обробка
  • обрана дія може використовувати параметри parami, які їй передала сервлета [Dispatcher Servlet]. Вони можуть походити з кількох джерел:
    • з шляху [/param1/param2/...] сервлета URL,
    • з параметрів [p1=v1&p2=v2] сервлета URL,
    • з параметрів, надісланих браузером разом із запитом;
  • Під час обробки запиту користувача для виконання дії може знадобитися рівень [metier] [2b]. Після обробки запиту клієнта ця дія може викликати різні відповіді. Типовим прикладом є:
    • сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
    • сторінка підтвердження в іншому випадку
  • дія вимагає відображення певного виду [3]. Цей вид відображатиме дані, які називаються моделлю виду. Це «М» у MVC. Дія створить цю модель M [2c] і вимагатиме відображення певного виду V [3];
  1. відповідь — обраний вигляд V використовує модель M, створену дією, для ініціалізації динамічних частин відповіді HTML, яку він повинен надіслати клієнту, а потім надсилає цю відповідь.

Для веб-сервісу / jSON попередня архітектура дещо змінена:

  • у [4a] модель, яка є класом Java, перетворюється на рядок jSON за допомогою бібліотеки jSON;
  • у [4b] цей рядок jSON надсилається до браузера;

Приклад серіалізації об’єкта Java у рядок jSON та десеріалізації рядка jSON в об’єкт Java наведено в додатках до розділу 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;
    }

    // методи getter та setter
...
}
  • рядок 13: поле [T body] — це відповідь, на яку чекає клієнт. Ми вирішили використовувати тут загальну відповідь типу T, а не тип Integer з очікуваним випадковим числом. Ми хочемо мати можливість повторно використовувати цей клас в інших ситуаціях. Під час обробки запиту клієнта сервер може зіткнутися з проблемою, яка потім узагальнюється в двох інших полях;
    • рядок 8: код стану (0, якщо помилки немає);
    • рядок 9: якщо 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;
  }

}
  • рядок 17: анотація [@Controller] вказує, що клас є контролером MVC, методи якого обробляють запити для певних URL веб-додатку;
  • рядки 21–22: анотація [@Autowired] вимагає від Spring ввести в поле компонент типу [IMetier]. Це буде попередній клас [Metier]. Оскільки ми додали до нього анотацію [@Service], він обробляється як компонент Spring;
  • рядки 24–25: те саме ми робимо з маппером jSON, який ми визначимо пізніше. Наш веб-сервіс надсилатиме свою відповідь у вигляді рядка jSON. Саме цей маппер здійснюватиме серіалізацію відповіді у формат jSON;
  • рядок 30: метод, що генерує випадкове число. Його назва не має значення. Під час виконання його параметри ініціалізуються Spring MVC. Ми ще побачимо, як саме. До того ж, якщо він виконується, то це тому, що веб-сервер отримав запит HTTP GET щодо URL із рядка 28;
  • рядок 28: анотація [@RequestMapping] визначає певні властивості анотованого методу:
    • [value]: URL, що приймається методом;
    • [method]: метод HTTP, який приймає цей метод. В основному їх два: GET та POST. Метод [POST] використовується, коли клієнт хоче додати документ до свого запиту HTTP;
    • [produces]: встановлює один із заголовків відповіді HTTP, яка буде надіслана клієнту. У цьому випадку серед заголовків HTTP, надісланих разом із відповіддю клієнту, буде один, який повідомить йому, що відповідь надсилається у вигляді рядка jSON. Цей заголовок не є обов’язковим. Він надається клієнту в інформаційних цілях, якщо той очікує відповідей, які можуть мати різні форми;
    • [consumes]: тут відсутній. Він дозволяє вказати заголовки HTTP, які повинні супроводжувати запит HTTP клієнта, щоб він був прийнятий;
  • рядок 29: анотація [@ResponseBody] вказує, що результат, згенерований методом, має бути надісланий клієнту. Без цієї анотації відповідь методу розглядається як ключ, що дозволяє вибрати сторінку HTML для надсилання клієнту. У веб-сервісі /jSON немає сторінок HTML;
  • рядок 28: оброблений URL має вигляд /{a}/{b}, де {x} позначає змінну. Змінним {a} та {b} присвоюються значення параметрів методу в рядку 30. Це здійснюється за допомогою анотації @PathVariable("x"). Слід зауважити, що {a} та {b} є компонентами URL і, отже, мають тип String. Перетворення String у тип параметрів може завершитися невдачею. У такому разі Spring MVC генерує виняток. Підсумуємо: якщо за допомогою браузера я запитую URL /100/200, метод getAlea у рядку 30 виконається з цілочисельними параметрами a=100, b=200;
  • рядок 36: до шару [métier] надсилається запит на отримання випадкового числа в інтервалі [a,b]. Нагадаємо, що метод [metier].getAlea може викликати виняток;
  • рядок 37: помилки немає;
  • рядок 39: код помилки;
  • рядок 40: список повідомлень у відповіді відповідає списку у стеку винятків (рядки 46–57). У цьому випадку ми знаємо, що стек містить лише один виняток, але хотіли продемонструвати більш узагальнений метод;
  • рядок 43: відповідь типу [Response<Integer>] повертається у вигляді рядка jSON;

1.16.1.6. Налаштування проекту Spring

  

Існують різні способи налаштування Spring:

  • з файлами XML;
  • з кодом Java;
  • з поєднанням обох;

Ми вирішили налаштувати наш веб-додаток за допомогою коду Java. Цю конфігурацію забезпечує наступний клас [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();
  }

}
  • рядок 12: ми вказуємо Spring, у яких пакетах він знайде два компоненти, якими він повинен керувати:
    • компонент [Metier], анотований [@Service], у пакеті [exemples.android.server.metier];
    • компонент [WebController], позначений анотацією [@Controller], у пакеті [exemples.android.server.web];
  • рядок 13: анотація [@EnableWebMvc] дозволяє Spring Boot самостійно виконати низку стандартних налаштувань для Spring-додатка MVC. Це відповідно полегшує роботу розробника;
  • рядки 16, 22, 27 та 33: анотація [@Bean] також визначає компоненти (beans) Spring так само, як і дві згадані раніше анотації (@Service, @Controller). Тут анотація [@Bean] анотує метод, а не клас, і саме результат цього методу є компонентом Spring. За відсутності атрибута іменування в анотації [@Bean] створений компонент Spring має назву анотованого методу;
  • рядки 16–20: визначають бін [dispatcherServlet]. Це попередньо визначена Spring назва MVC, яка визначає фронт-контролер додатка MVC, об’єкт, через який проходять усі запити клієнтів і який розподіляє їх (звідси й назва) між різними [@Controller] додатка Spring MVC;
  • рядок 18: бін [dispatcherServlet] є екземпляром класу [DispatcherServlet], наданим Spring MVC;
  • рядки 22–25: бін [servletRegistrationBean] слугує для визначення, які URL приймаються додатком. У рядку 24 приймаються всі URL;
  • рядки 27–30: bean [embeddedServletContainerFactory] слугує для визначення вбудованого сервера у залежностях проєкту, який має розміщувати веб-додаток. У рядку 29 вказано, що це сервер Tomcat і що він працюватиме на порту 8080. За замовчуванням бінарні файли цього веб-сервера додаються через залежність [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] є виконуваним класом (рядки 7–10);
  • рядок 9: статичний метод [SpringApplication.run] є методом класу [spring Boot] (рядок 4), який запускає додаток. Його першим параметром є Java-клас, що налаштовує проект. У даному випадку це клас [Config], який ми щойно описали. Другим параметром є масив аргументів, що передається методу [main] (рядок 7);

Веб-додаток можна запустити різними способами, зокрема таким:

 

У консолі з’являється низка записів журналу:

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

2014-10-07 09:13:42.194  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Starting Application on Gportpers3 with PID 7408 (D:\data\istia-1415\android\dvp\exemples\exemple-10-server\target\classes started by ST in D:\data\istia-1415\android\dvp\exemples)
2014-10-07 09:13:42.294  INFO 7408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@23edbbb9: startup date [Tue Oct 07 09:13:42 CEST 2014]; root of context hierarchy
2014-10-07 09:13:42.996  INFO 7408 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-10-07 09:13:44.251  INFO 7408 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-10-07 09:13:44.665  INFO 7408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-10-07 09:13:44.666  INFO 7408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2575 ms
2014-10-07 09:13:45.748  INFO 7408 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-10-07 09:13:45.750  INFO 7408 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-10-07 09:13:46.802  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.946  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public android.exemples.server.web.AleaResponse android.exemples.server.web.AleaController.getAlea(int,int)
2014-10-07 09:13:46.950  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.951  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] на обробник типу [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] на обробник типу [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:47.294  INFO 7408 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-10-07 09:13:47.335  INFO 7408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-10-07 09:13:47.337  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Started Application in 6.081 seconds (JVM running for 6.897)
  • рядки 12–14: запускається вбудований сервер Tomcat;
  • рядки 15–19: завантажується та налаштовується сервлет [DispatcherServlet] з Spring MVC;
  • рядок 20: виявлено URL [/{a}/{b}] веб-сервера;

Тепер відкриємо браузер і протестуємо веб-сервіс /URL:

Кожного разу ми отримуємо представлення jSON об’єкта типу [Response<Integer>].

Замість стандартного браузера давайте тепер використаємо розширення [Advanced Rest Client] для браузера Chrome (див. додатки, параграф 6.13):

Image

  • у [1] замість запитуваного URL;
  • на [2] за допомогою GET;
  • у [3] надсилається запит;

Image

  • у [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>

Рівень журналів контролюється у рядку 12. Якщо тепер перегенерувати виконуваний архів і запустити його, ми отримаємо лише журнали рівня [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. Android-клієнт веб-сервера / jSON

Клієнт для Android матиме таку архітектуру:

Клієнт матиме два компоненти:

  1. рівень [Présentation] (представлення + активність), аналогічний тому, що ми розглядали у прикладі [Exemple-14];
  2. шар [DAO], який взаємодіє зі службою [web / jSON], яку ми розглядали раніше.

1.16.2.1. Створення проєкту

Ми дублюємо попередній проєкт [Exemple-14] у [Exemple-15], дотримуючись процедури, описаної в розділі 1.4. Отримуємо такий результат:

Далі читачеві пропонується створити наступний проєкт.

1.16.2.2. Налаштування Gradle

 

Файл [build.gradle] має такий вигляд:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Починаючи з версії 0.11 плагіна Gradle для Android, необхідно використовувати 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'
    }
  }

  // Параметри пакування, необхідні для створення 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'
  }
}

Ми коментуємо лише те, з чим ще не стикалися:

  • рядки 46–47: додавання плагіна AA. Плагін [rest-spring-api] дозволяє делегувати бібліотеці AA обмін даними між клієнтом і сервером;
  • рядок 50: бібліотека [spring-android-rest-template] — це бібліотека, яку використовує AA для забезпечення обміну даними між клієнтом і сервером. Версія [2.0.0.M3] є так званою «мілстоун-версією», якої немає у звичайних репозиторіях Maven. Тому в рядках 56–59 слід вказати репозиторій (рядок 58), який потрібно використовувати для пошуку бібліотеки;
  • рядок 51: бібліотека jSON;
  • рядки 33–39: без цього параметра під час генерації бінарного файлу APK проекту виникають помилки;

1.16.2.3. Маніфест додатка для Android

  

Файл [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>
  • рядок 5: доступ до Інтернету дозволено;

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

  // максимальний час очікування (мс) на відповідь сервера
  void setTimeout(int timeout);

  // час очікування клієнта в мілісекундах перед надсиланням запиту
  void setDelay(int delay);

}
  • рядок 6: метод веб-сервісу / jSON для отримання випадкового числа в інтервалі [a,b] цього веб-сервісу;
  • рядок 9: URL веб-сервісу / jSON для генерації випадкових чисел;
  • рядок 12: встановлюємо максимальний час очікування відповіді від сервера;
  • рядок 15: встановлюємо час очікування перед виконанням запиту до сервера, щоб дати користувачеві час скасувати свій запит;

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 {

  // 1 випадкове число в інтервалі [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
  • рядок 12: [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 {
  • рядок 11: анотація [@Rest] є анотацією AA. Значення атрибута [converters] — це масив конвертерів. Тут конвертер [MappingJackson2HttpMessageConverter.class] забезпечує, що коли сервер надсилає рядок jSON, він автоматично десеріалізується. Отже, у рядку (d) ми бачимо, що URL [/{a}/{b}] повертає тип String, який насправді є рядком jSON (рядок b). Використовуючи цю інформацію та дані про очікуваний тип у рядку 16, екземпляр [WebClient] клієнта десеріалізує отриманий рядок у тип [Response<Integer>];
  • рядок 15: анотація 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) {
  • рядок 2: вказується, що очікується запит HTTP POST і що тіло цього запиту (об’єкт, що надсилається) має бути передане у вигляді рядка jSON (атрибут consumes);
  • рядок 4: відправлене значення буде отримано в параметрі [@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;
    }

    // методи getter та setter
...
}

Це клас [Response], який вже використовувався на стороні сервера (параграф 1.16.1.5). Фактично, з точки зору програмування, все відбувається так, ніби клієнтський рівень [DAO] безпосередньо взаємодіє з контролером [WebController] веб-сервісу:

Мережева взаємодія між клієнтом і сервером, а також серіалізація/десеріалізація об’єктів Java на стороні клієнта є прозорими для програміста.

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

}
  • рядок 15: ми позначаємо клас [Dao] анотацією [@EBean], щоб перетворити його на bean AA, який можна буде ін'єктувати в інше місце;
  • рядки 19–20: ми вводимо реалізацію, яка буде створена для інтерфейсу [WebClient], який ми описали. Саме анотація [@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];
  • рядок 4: клас [SimpleClientHttpRequestFactory] надається залежністю [spring-android-rest-template]. Він дозволить нам встановити максимальний час очікування відповіді від сервера (рядки 5–6);
  • рядок 8: ми створюємо об’єкт типу [RestTemplate], який слугуватиме носієм комунікації з веб-сервісом. Як параметр ми передаємо йому об’єкт [factory], який щойно було створено;
  • рядок 10: діалог «клієнт-сервер» може набувати різних форм. Обмін даними відбувається у вигляді текстових рядків, і ми повинні вказати об’єкту типу [RestTemplate], що він має робити з цим текстовим рядком. Для цього ми надаємо йому конвертери — класи, здатні обробляти текстові рядки. Вибір конвертера зазвичай здійснюється за допомогою заголовків HTTP, які супроводжують текстовий рядок. У даному випадку ми знаємо, що отримуємо виключно текстові рядки у форматі jSON. Крім того, у розділі 1.16.1.7 ми бачили, що сервер надсилав заголовок HTTP:

Content-Type: application/json;charset=UTF-8 

У рядку 10 єдиним конвертером для [RestTemplate] буде конвертер jSON, реалізований за допомогою бібліотеки [Jackson]. Існує одна особливість щодо цих конвертерів: AA вимагає, щоб він також був зазначений у анотації веб-клієнта [WebClient]:


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

У рядку 1 ми змушені вказати конвертер, хоча ми вже вказуємо його програмно.

  • рядок 12: об’єкт [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();
    }
}
  • рядок 8: очікуємо [delay] мілісекунд;
  • рядок 10: просто викликається метод з такою самою сигнатурою у класі, що реалізує інтерфейс [WebClient];
  • рядок 11: аналізується отримана від сервера відповідь, перевіряючи її [status];
  • рядки 12–14: якщо на стороні сервера не сталося помилки (status=0), то повертаємо результат методу;
  • рядок 17: якщо на стороні сервера сталася помилка (status!=0), то готується виняток, але не запускається. Сервер надіслав список повідомлень про помилки. Ми створюємо виняток, єдиним повідомленням якого є рядок jSON зі списку повідомлень сервера;
  • рядки 19–22: інші випадки винятків;
  • рядок 24: коли ми доходимо до цього місця, це означає, що обов’язково сталося виключення. Тож ми його запускаємо;

Виняток [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;
  }

  // гетери та сеттери
...
}
  • рядок 6: виняток [DaoException] є неконтрольованим винятком;

Метод [setUrlServiceWebJson]

Метод [setUrlServiceWebJson] має такий вигляд:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // встановлюється URL сервісу REST
    webClient.setRootUrl(urlServiceWebJson);
}
  • рядок 4: 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;

}
  • рядок 5: інтерфейс [IMainActivity] розширює інтерфейс [IDao];
  • рядки 13–16: до методів, що вже були наведені в попередніх прикладах (рядки 7–11), ми додали два методи для управління зображенням очікування додатка (рядки 14, 16);
  • рядок 21: встановлюємо максимальний час очікування відповіді сервера — 1 секунду;

1.16.2.5.2. Клас [Utils]

У класі [Utils] зібрано статичні допоміжні методи, які можна викликати з різних місць архітектури додатка:


package exemples.android.architecture;

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

public class Utils {

  // список повідомлень про виняток — версія 1
  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;
  }

  // список повідомлень про виняток — версія 2
  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();
  }

}
  • рядки 9–18: створюють список повідомлень про помилки, що містяться у файлі Throwable;
  • рядки 21–32: на основі попереднього методу формує з отриманого списку повідомлень текст, який буде відображатися у сповіщенні Android;
  • рядки 27–28: повідомлення пронумеровані. Найменший номер (1) відповідає початковому виняткові, а найбільший — найновішому виняткові у стеку винятків;

1.16.2.5.3. Абстрактний клас [AbstractFragment]

Клас [AbstractFragment] має два призначення:

  1. забезпечити, щоб метод [updateFragments] дочірніх класів завжди викликався під час відображення фрагмента і лише один раз;
  2. виокремити стан та методи дочірніх класів, які можна виокремити;

Саме завдання 2 змушує нас розміщувати в цьому класі операції з управління зображенням очікування: усі фрагменти асинхронного додатка для Android мають вирішувати подібні проблеми:


  // управління очікуванням
  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] під час запиту чисел відображається індикатор очікування. Він зникає, коли всі числа отримано або операція скасована;

Image

  • у [6] перевіряється правильність введених даних;

Користувачеві пропонується завантажити файл [vue1.xml] із прикладів. Далі наводимо ідентифікатори нових компонентів:

Image

Type
Id
1
EditText
edt_nbaleas
2
TextView
txt_errorNbAleas
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorIntervalle
6
EditText
editTextUrlServiceWeb
7
TextView
textViewErreurUrl
8
EditText
editTextDelay
9
TextView
textViewErreurDelay
10
Button
btn_Executer
11
Button
btn_Annuler
12
TextView
txt_Reponses
13
ListView
lst_reponses

Кнопки [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<>();
  }
...
  • рядки 24–49: посилання на компоненти подання [vue1.xml] (рядок 20);
  • рядки 55–69: метод [@AfterViews], що виконується після ініціалізації посилань із рядків 24–49;
  • рядок 58: не забути — необхідно для життєвого циклу фрагмента;
  • рядки 60–63: повідомлення про помилки приховано;
  • рядки 65–66: приховується кнопка [Annuler] (рядок 65) і відображається кнопка [Exécuter] (рядок 66). Нагадуємо, що фізично вони розташовані одна над одною;
  • рядок 68: поле з рядка 52 міститиме список символьних рядків, які мають відображатися за допомогою 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);
}
  • рядки 4–5: створюється адаптер відповідей 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;
    // обнуляємо лічильник відповідей
    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;
  • рядок 19: булеве значення, яке допоможе нам визначити, чи скасував користувач свій запит;
  • рядки 21–22: виводимо нульовий лічильник кількості відповідей;
  • рядки 24–26: отримуємо введені дані з рядків [2-6] і перевіряємо їхню правильність. Якщо хоча б один із них є недійсним, метод припиняється (рядок 25), а користувач повертається до графічного інтерфейсу;
  • рядки 28–29: якщо всі введені дані є дійсними, то до активності передається URL веб-сервісу (рядок 28), а також час очікування перед кожним викликом сервісу (рядок 29). Ця інформація необхідна для рівня [DAO], і слід нагадати, що саме активність здійснює зв’язок із цим рівнем;
  • рядки 31–33: випадкові числа запитуються по одному у методі [getAlea] у рядку 39;
  • рядок 38: метод [getAlea] має анотацію AA [@Background], завдяки чому він буде виконуватися в іншому потоці (потоці виконання, процес), ніж той, у якому виконується візуальний інтерфейс. Дійсно, обов’язково виконувати будь-який інтернет-виклик у потоці, відмінному від потоку візуального інтерфейсу. Таким чином, у певний момент часу може існувати кілька потоків:
    • той, що відображає візуальний інтерфейс UI (User Interface) та обробляє його події,
    • потоки [nbAleas], кожен з яких запитує випадкове число у веб-сервісу. Ці потоки запускаються асинхронно: потік UI запускає потік [getAlea] (рядок 32), який запитує випадкове число у веб-сервісу й не чекає на завершення цього запиту. Про завершення йому буде повідомлено за допомогою події. Таким чином, потоки [nbAleas] запускатимуться паралельно. Можна налаштувати додаток так, щоб він запускав лише один потік за раз. У такому разі утворюється черга потоків, що підлягають виконанню;

У рядку 38 параметр [id] присвоює ім’я створеному потоку. Тут усі потоки [nbAleas] мають однакову назву — [alea]. Це дозволить нам скасувати їх усі одночасно. Цей параметр є необов’язковим, якщо скасування потоку не передбачено;

  • рядок 44: викликається метод [getAlea] активності. Отже, він буде викликаний у потоці, окремому від потоку UI. Цей метод здійснить виклик веб-сервісу і не чекатиме на відповідь. Пізніше він отримає повідомлення про те, що відповідь доступна, у вигляді події. Саме в цей момент у рядку 44 буде викликано метод [showInfo] із отриманою відповіддю як параметром;
  • рядки 45–47: під час виконання веб-запиту може виникнути виняток. У такому разі передбачено відображення повідомлень про помилки цього винятку у сповіщенні;
  • рядок 35: починається очікування результатів:
    • буде відображено індикатор очікування;
    • кнопка [Annuler] замінить кнопку [Exécuter]. Оскільки запущені потоки є асинхронними, потік UI не чекає на їх завершення, і рядок 35 виконується до їхнього завершення. Після завершення методу [beginWaiting] метод UI знову може реагувати на дії користувача, наприклад, на натискання кнопки [Annuler]. Якби запущені потоки були синхронними, до рядка 35 можна було б дійти лише після завершення всіх потоків. У такому разі їхнє скасування вже не мало б сенсу;

Метод [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] у рядку 1;
  • рядок 2: метод отримує випадкове число;
  • рядок 3: тіло методу виконується лише в тому випадку, якщо користувач не скасував свій запит;
  • рядки 5–6: збільшується лічильник відповідей і виводиться його значення;
  • рядки 8–11: якщо отримано всі очікувані відповіді, то очікування завершується (завершення сигналу очікування, кнопка [Exécuter] замінює кнопку [Annuler]);
  • рядки 12–15: отримане випадкове число додається до списку відповідей, що відображається компонентом [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]:

  • рядок 1: анотація [@UiThread] є обов’язковою;
  • рядок 2: метод отримує виняток, що виник;
  • рядок 3: метод виконується лише в тому випадку, якщо користувач не скасував свій запит;
  • рядок 5: запит користувача скасовується так, ніби він сам натиснув кнопку [Annuler];
  • рядок 7: відображається сповіщення за допомогою класу Android [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();
}
  • рядок 4: фіксується, що користувач скасував свій запит;
  • рядок 6: скасовує всі завдання, ідентифіковані рядком [alea]. Другий параметр [true] означає, що завдання мають бути скасовані, навіть якщо вони вже запущені. Ідентифікатор [alea] використовується для кваліфікації методу [getAlea] у фрагменті (рядок 1 нижче):

  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

Примітка: виявилося, що рядок 6 коду методу [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);
}
  • рядки 2–3: введення шару [DAO] за допомогою анотації AA;
  • рядки 5–13: код, що виконується після цього введення;
  • рядок 12: встановлюється 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), а потім запустіть клієнт для Android:

Image

Щоб дізнатися, що ввести в [1], виконайте наступні дії. Відкрийте командне вікно та введіть таку команду:


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 до вашого комп’ютера (рядки 10 та 18). Ці адреси є особливо зручними, оскільки їх не блокує брандмауер Windows. У рядку 30 вказано адресу IP вашого комп’ютера в локальній мережі. Щоб скористатися цією адресою, зазвичай потрібно вимкнути брандмауер Windows. Якщо ви підключені до мережі Wi-Fi, використовуйте Wi-Fi-адресу і, у цьому випадку, також вимкніть брандмауер, якщо він у вас є.

Перевірте роботу програми в таких випадках:

  • 100 випадкових чисел у проміжку [1000, 2000] без затримки;
  • 2000 випадкових чисел у діапазоні [10000, 20000] без затримки та скасуйте затримку до завершення генерації;
  • 5 випадкових чисел в інтервалі [100, 200] із затримкою 5000 мс та скасування затримки до завершення генерації;

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;
}
  • рядок 9: вказуємо назву класу;
  • рядки 16–18: записуємо запис у журнал щоразу, коли викликається метод [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() {
    // журнал
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
   ...
}

Кожного разу, коли фрагмент [Vue1Fragment] отримує інформацію від шару [DAO], генерується запис у журналі. Крім того, під час виклику методу [doAnnuler] ця подія фіксується у журналі.

Тест 1

Запитуємо 5 чисел, хоча сервер ще не запущено. Отримуємо такі записи в журналі:

06-06 08:48:51.571 15317-16201/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.576 15317-16202/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.585 15317-16204/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.586 15317-16203/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.593 15317-16205/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
  • рядки 1–5: метод [getAlea] класу [Dao] викликається п’ять разів. Нагадаємо, що це асинхронні виклики, які здійснює фрагмент [VueFragment], і що він не очікує на результат свого виклику;
  • рядок 7: відбувся перший запит HTTP, і фрагмент [VueFragment] отримав своє перше виключення;
  • рядок 8: після цього він вимагає скасування всіх запитів;
  • рядки 9–12: однак бачимо, що він отримує наступні чотири винятки. Отже, всі асинхронні запити, що перебували в стані очікування, були виконані;

Тест 2

Тепер запустимо сервер і надішлемо запит на 5 чисел із затримкою в 5 секунд, а потім натиснемо на [Annuler] до закінчення цієї затримки. Журнали мають такий вигляд:

06-06 09:12:38.360 4640-5054/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.360 4640-5055/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.361 4640-5056/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.362 4640-5057/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.363 4640-5058/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 09:12:39.895 4640-4640/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:30:00.150 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
06-06 09:30:00.151 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
  • рядки 1–5: метод [getAlea] класу [Dao] викликано п’ять разів;
  • рядок 7: користувач запросив скасування запитів;
  • рядок 8: бачимо, що [Vue1_Fragment] отримує 5 значень. Знову ж таки, усі асинхронні запити, що перебували в черзі, були виконані;

Саме тому нам довелося використовувати булеве значення [hasBeenCanceled], щоб уникнути відображення будь-чого в разі, коли було подано запит на скасування. У коді скасування:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // журнал
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // пам'ять
    hasBeenCanceled = true;
    // асинхронне завдання скасовано
    BackgroundExecutor.cancelAll("alea",true);
    // завершення очікування
    cancelWaiting();
}

код у рядку 10 не працює так, як очікується. Можливо, це пов’язано з тим, що асинхронні завдання використовують один і той самий метод з анотацією [@Background]:


  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

1.17. Приклад-16: управління асинхронністю за допомогою RxAndroid

Тепер ми пропонуємо керувати асинхронністю, необхідною для додатків Android, за допомогою бібліотеки під назвою RxJava [http://reactivex.io/] та її похідної версії для середовища Android [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);
}
  • рядок 8: метод [getAlea] тепер повертає тип [Observable] із бібліотеки RxJava (рядок 3). Принцип такий:

Потік елементів типу 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);
  • рядок 20: єдиним параметром генеричного методу [getResponse] є екземпляр генеричного інтерфейсу [IRequest<T>] із рядків 15–17. Цей інтерфейс має лише один метод [getResponse], і саме цей метод надає бажану відповідь [Response<T>];
  • завдяки двом попереднім елементам клас [AbstractDao] може слугувати батьківським класом для будь-якого клієнтського шару [Dao] сервера, що надсилає відповіді типу [Response<T>];
  • рядок 20: узагальнений метод [getResponse] повертає тип [Observable<T>], який представляє результат, на який насправді очікує клієнт HTTP (у даному випадку тип Observable<Integer>);
  • рядки 22–51: статичний метод [rx.Observable.create] створює тип [Observable];
  • рядок 22: єдиним параметром цього методу є екземпляр типу [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>] випромінюватиме лише один елемент (рядок 37);
  • рядок 29: надсилає синхронний запит HTTP до сервера та отримує тип [Response<T>]. Цей запит HTTP забезпечується типом [IRequest], переданим як параметр у генеричний метод [getResponse];
  • рядок 31: з відповіді отримується status;
  • рядки 32–34: якщо цей status є кодом помилки, готується виняток;
  • рядки 36–39: якщо цей status не пов’язаний з помилкою, то надсилається відповідь, яку насправді очікує клієнт (рядок 37), і спостерігачеві повідомляється, що більше ніяких повідомлень не буде (рядок 39);
  • рядки 41–44: якщо запит HTTP завершується винятком, його фіксують;
  • рядки 46–49: якщо виняток [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);
      }
    });
}
...
  • рядок 2: клас [Dao] є похідним від класу [AbstractDao];
  • рядок 24: метод [getAlea] тепер повертає тип [Observable<Integer>];
  • рядок 30: виклик генеричного методу [getResponse] батьківського класу. Йому передається параметр типу [IRequest<Integer>];
  • рядки 32–37: реалізація інтерфейсу [IRequest<Integer>];
  • рядок 36: виконується запит HTTP через інтерфейс AA [webClient], як це робилося раніше. Відомо, що ми отримаємо тип [Response<Integer>], який і є тим типом, що має повертати метод [IRequest<Integer>.getReponse()];
  • рядок 36: тут використовується властивість під назвою closure — здатність інкапсулювати в екземпляр зовнішні щодо нього значення під час його створення, в даному випадку значення [a, b] з рядка 24. Саме це дозволяє методу [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;
    // обнуляємо лічильник відповідей
    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();
}
  • рядок 18: випадкові числа запитуються у методі [getAleasInBackground], який називається так, оскільки числа будуть запитуватися в потоці, відмінному від потоку інтерфейсу користувача;

  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);
    }
}
  • рядок 3: об’єкт спостереження має підписників. Зв’язок між підписником та процесом, який він спостерігає, називається підпискою (Subscription). Тут ми матимемо лише один процес, що спостерігається, та одного підписника. Отже, ми матимемо лише одну підписку. Для початку ми робимо так, ніби можемо мати кілька процесів, що спостерігаються різними спостерігачами, що дало б кілька підписок;
  • рядки 11–18: налаштовуємо спостережуваний процес (observable). Слід розуміти, що це лише налаштування: процес не виконується;
  • рядок 11: ми починаємо з порожнього об’єкта спостереження, який нічого не генерує;
  • рядки 14–16: до цього порожнього об’єкта спостереження додаються об’єкти спостереження [nbAleas], які будуть запитами [nbAleas], що повернуть випадкові числа [nbAleas];
  • рядок 15: як і раніше, випадкове число № i запитується у класі [MainActivity]. Важливо зрозуміти, що на цьому етапі жоден запит HTTP ще не виконується. Виконується метод [mainActivity.getAlea(a, b)], який повертає тип [Observable<Integer>]. Цей процес можна буде спостерігати після його запуску;
  • рядок 15: метод [subscribeOn(Schedulers.io())] вимагає, щоб процес виконувався (коли він буде запущений) у потоці вводу-виводу. Бібліотека RxJava пропонує різні типи потоків. Потік вводу-виводу підходить для викликів HTTP;
  • рядок 15: спостережуваний об’єкт № i об’єднується з початковим спостережуваним об’єктом із рядка 11: із спостережуваних об’єктів [nbAleas], кожен з яких генерує один елемент, створюється спостережуваний об’єкт, який генеруватиме [nbAleas] елементів. Саме він буде об’єктом спостереження. Цей об’єкт спостереження надсилає сповіщення [onCompleted], коли всі об’єкти спостереження, з яких він складається, надішлють свої власні сповіщення [onCompleted]. Це позбавить нас необхідності підраховувати відповіді, як ми це робили в попередній версії, щоб дізнатися, чи отримано всі очікувані числа;
  • рядок 18: коли ми доходимо до цього місця, ми вже налаштували об’єкт спостереження, який є композицією об’єктів спостереження [nbAleas], кожен з яких виконується у власному потоці вводу-виводу;
  • рядок 18: метод [observeOn(AndroidSchedulers.mainThread())] використовується для визначення, на якому потоці має відбуватися спостереження за значеннями, що видаються об’єктом спостереження. Тут потік [AndroidSchedulers.mainThread())] належить до бібліотеки RxAndroid, а не до RxJava. Він позначає потік інтерфейсу користувача, який також називають циклом подій. Цей момент важливий: у додатку для Android зміна компонента інтерфейсу користувача може відбуватися лише в потоці інтерфейсу користувача, інакше виникає виняток;
  • рядки 19–45: тепер, коли процес, що підлягає спостереженню, налаштовано, його запускають;
  • рядок 21: саме операція [Observable.subscribe] запускає виконання спостережуваного процесу. Ця операція запустить асинхронні процеси [nbAleas], налаштовані раніше. Результати цих процесів автоматично стануть доступними для спостерігача у потоці інтерфейсу користувача;
  • Нагадаємо, що об’єкт спостереження генерує три типи подій:
    • [onNext]: коли він надсилає елемент;
    • [onError]: коли він зіткнувся з винятком;
    • [onCompleted]: коли він повідомляє, що більше не буде генерувати події;

Метод [Observable.subscribe] має в якості параметрів три об’єкти [Action1<Integer>, Action1<Throwable>, Action0], методи яких [call] призначені для обробки кожного з цих трьох подій;

  • рядки 21–27: перший параметр типу [Action1<Integer>] призначений для обробки події [onNext]. Його метод [call] отримує елемент, який був випущений спостережуваним об’єктом (рядок 23);
  • рядок 25: повторно використовується метод [showInfo] з попереднього прикладу;
  • рядки 27–35: другий параметр типу [Action1<Throwable>] призначений для обробки події [onError]. Його метод [call] отримує виняток, який був згенерований об’єктом спостереження (рядок 29);
  • рядок 31: повторно використовується метод [showAlert] з попереднього прикладу;
  • рядок 33: запускається процедура скасування запиту користувача. Вона полягає у скасуванні всіх об’єктів спостереження, що наразі виконуються;
  • рядки 35–41: третій параметр типу [Action0] слугує для обробки події [onCompleted]. Його метод [call] не приймає жодних параметрів;
  • рядок 39: скасовується очікування;

Метод [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();
    }
}

У методі внесено дві зміни:

  • рядок 1: видалено анотацію 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();
    }
}
  • єдина зміна — у рядку 1: видалено анотацію 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();
}
  • рядок 12: скасовує підписку і, відповідно, спостереження за пов’язаним процесом;

1.17.9. Виконання

Запустіть веб-сервіс (параграф 1.16.1.7), запустіть клієнт для Android і повторіть тести, які ви виконували з попереднім прикладом (параграф 1.16.2.8).

1.17.10. Управління скасуванням

Повторіть ті самі тести, що й у попередньому прикладі (параграф 1.16.2.9).

Тест 1

Запитуємо 5 чисел, хоча сервер не запущено. Отримуємо такі журнали:

1
2
3
4
5
6
7
06-07 05:48:09.790 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Exception reçue
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

Після рядка 7 записи більше відсутні, що свідчить про те, що спостерігач (Vue1Fragment) більше не отримує сповіщень від спостережуваного процесу.

Тест 2

Тепер запустимо сервер і запросимо 5 чисел з інтервалом у 5 секунд, а потім натиснемо на [Annuler] до закінчення цього інтервалу. Журнали мають такий вигляд:

1
2
3
4
5
6
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:23.485 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

Після рядка 6 записів у журналі більше немає, що свідчить про те, що спостерігач (Vue1Fragment) більше не отримує сповіщень від спостережуваного процесу.

Це є очікуваною поведінкою у разі скасування. Отже, у коді [Vue1Fragment] можна видалити булеву змінну [hasBeenCanceled], яку ми ввели в попередньому прикладі, оскільки скасування не працювало так, як ми очікували.

Те, що спостерігач більше не отримує сповіщень після скасування об’єкта спостереження, не означає, що самі запити 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;
      }
    });
}
  • рядки 15–21: ми записуємо в журнал результат запиту HTTP із рядка 14;

Журнали для тесту № 2 мають такий вигляд:

06-07 06:03:20.778 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.784 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:21.493 27085-27085/exemples.android D/Vue1Fragment_: Annulation demandée
06-07 06:03:21.636 27085-27440/exemples.android D/Dao_: response [{"body":176,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27442/exemples.android D/Dao_: response [{"body":145,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27439/exemples.android D/Dao_: response [{"body":197,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27438/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27441/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
  • рядки 1–5: було зроблено 5 запитів;
  • рядок 6: користувач скасував запит;
  • рядки 7–11: ми успішно отримуємо відповіді на п’ять запитів HTTP. Однак через скасування спостережуваного ці елементи не передаються спостерігачеві;

1.17.11. Висновок

У подальшому тексті цього документа клієнт-серверні додатки будуть реалізовані з використанням бібліотеки RxAndroid, а не бібліотеки AA, з таких причин:

  1. RxAndroid можна використовувати в додатку для Android, який не використовує AA;
  2. RxAndroid не лише спрощує асинхронні операції. Вона пропонує безліч методів для створення нового об’єкта спостереження на основі іншого. Ці методи не мають аналогів у AA;
  3. як тільки виникає потреба у похідному класі, анотованому AA, наприклад, фрагменті, виникають серйозні проблеми. У такому випадку доводиться відмовитися від AA і використовувати рішення 1 для асинхронного програмування;

Читач, зацікавлений у детальнішому вивченні можливостей бібліотеки RxAndroid, може ознайомитися з документом [Introduction à RxJava. Application aux environnements Swing et Android]. У ньому використовується RxAndroid без бібліотеки AA.

1.18. Приклад-17: компоненти введення даних

Ми створимо новий проєкт, щоб продемонструвати деякі типові компоненти, що використовуються у формах введення даних.

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, як і раніше. Ми будемо працювати на основі цього проекту.

1.18.2. Вигляд XML форми

  

Вигляд, створений файлом [vue1.xml], є таким:

Image

Текст 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>

Основні компоненти форми такі:

  • рядок 2: вертикальний макет [ScrollView]. Він дозволяє
  • відобразити форму, розмір якої перевищує розмір екрану
  • планшета. Повний вигляд форми можна побачити,
  • прокрутку;
 
  • рядки 125–132: прапорець
  • рядки 134–159: група з трьох перемикачів
  • рядки 161–166: рядок пошуку
  • рядки 16–176: поле введення
  • рядки 178–186: перемикач «так/ні»
  • рядки 188–195: поле введення часу
  • рядки 197–207: багаторядкове поле введення
  • рядки 209–215: випадаючий список
  • рядки 217–225: поле введення дати
  • усі інші компоненти — це [TextView], які відображають тексти.
 

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);
  }
}
  • рядки 22–49: отримуємо посилання на всі компоненти форми XML [vue1] (рядок 18);
  • рядок 58: метод [setChecked] дозволяє встановити прапорець або радіо-кнопку;
  • рядок 60: за замовчуванням компонент [DatePicker] відображає як поле введення дати, так і календар. Рядок 60 прибирає календар;
  • рядок 62: [SeekBar].setMax() дозволяє встановити максимальне значення повзунка. Мінімальне значення — 0;
  • рядки 63–74: обробка подій повзунка. При кожній зміні, здійсненій користувачем, потрібно відображати значення повзунка у [TextView] з рядка 49;
  • рядок 71: параметр [progress] представляє значення повзунка;
  • рядки 76–79: список [String], який буде пов’язано зі списком, що розгортається;
  • рядок 90: метод [updateFragment] фрагмента. Під час його виконання ініціалізується змінна [activity] батьківського класу;
  • рядок 92: джерело даних [list] пов’язане з адаптером випадаючого списку;
  • рядки 93–94: адаптер [dataAdapter] пов’язано зі списком, що розгортається, [dropDownList];
  • рядок 84: метод [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);
}
  • рядок 4: введені значення будуть накопичуватися у списку повідомлень;
  • рядок 6: метод [CheckBox].isCkecked() дозволяє визначити, чи встановлено прапорець;
  • рядок 9: метод [RadioGroup].getCheckedButtonId() дозволяє отримати ідентифікатор перемикача, який було обрано, або -1, якщо жоден не було обрано;
  • рядок 10: код [activity.findViewById(id)] дозволяє визначити, яка кнопка-перемикач позначена, і таким чином отримати її текст;
  • рядок 13: метод [SeekBar].getProgress() дозволяє отримати значення повзунка;
  • рядок 19: метод [Switch].isChecked() дозволяє визначити, чи перемикач має значення On (true) чи Off (false);
  • рядок 22: метод [DatePicker].getYear() дозволяє отримати вибраний рік за допомогою об’єкта [DatePicker];
  • рядок 23: метод [DatePicker].getMonth() дозволяє отримати вибраний місяць за допомогою об’єкта [DatePicker] у діапазоні [0,11];
  • рядок 24: метод [DatePicker].getDayOfMonh() дозволяє отримати вибраний день місяця з об’єктом [DatePicker] в діапазоні [1,31];
  • рядок 30: метод [TimePicker].getHour() дозволяє отримати обрану годину за допомогою об’єкта [TimePicker];
  • рядок 31: метод [TimePicker].getMinute() дозволяє отримати вибрані хвилини за допомогою об’єкта [TimePicker];
  • рядок 34: метод [Spinner].getSelectedItemPosition() дозволяє отримати позицію вибраного елемента у випадаючому списку;
  • рядок 35: метод [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();
}
  • рядок 1: метод отримує список повідомлень, які потрібно відобразити;
  • рядки 3–6: на основі цих повідомлень створюється об’єкт [StringBuilder]. Для об’єднання рядків тип [StringBuilder] є ефективнішим, ніж тип [String];
  • рядок 8: діалогове вікно відображає текст із рядка 3:

Image

1.18.5. Виконання проєкту

Запустіть проект і протестуйте різні компоненти введення даних.

1.19. Приклад-18: використання шаблону переглядів

1.19.1. Створення проекту

Створюємо новий проект [Exemple-18] шляхом копіювання проекту [Exemple-13].

1.19.2. Шаблон переглядів

Ми хочемо взяти обидва види з цього проекту та включити їх у шаблон:

  

Image

Кожен із цих двох виглядів матиме однакову структуру:

  • у [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] отримується з рядків 38–54;
  • ліва смуга [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</color>
    <color name="wheat">#FFEFD5</color>
    <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. Приклад-19: компонент [ListView]

Компонент [ListView] дозволяє повторювати певний вигляд для кожного елемента списку. Повторюваний вигляд може мати будь-яку складність — від простого рядка символів до виду, що дозволяє вводити інформацію для кожного елемента списку. Ми створимо такий компонент [ListView]:

Image

Кожен вигляд у списку має три компоненти:

  • інформаційний [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<>();

  // методи отримання та встановлення
...
}
  • рядок 11: список даних, що використовується обома поданнями;

Клас [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;
    }

    // методи getter та setter
    ...
}
  • рядок 6: текст, який буде заповнювати перший [TextView] кожного елемента списку;
  • рядок 7: логічне значення, яке використовується для позначення або відсутності позначки у [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));
    }
}
  • рядки 12–15: ініціалізація списку даних, наявних у сесії;

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>
  • рядки 7–16: компонент [TextView] [2];
  • рядки 27–35: компонент [ListView] [4];
  • рядки 18–25: компонент [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>
  • рядки 8–14: компонент [TextView] [1];
  • рядки 16–23: компонент [CheckBox] [2];
  • рядки 25–35: компонент [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 {

  // поля подання, що відображається фрагментом
  @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() {
    // перехід до подання 2
    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();
  }
}
  • рядок 15: представлення XML [vue1] пов’язане з фрагментом;
  • рядки 26–30: метод [@AfterViews] нічого не робить. Однак він необхідний для присвоєння змінній [afterViewsDone] значення true, оскільки ця змінна використовується батьківським класом [AbstractFragment];
  • рядки 42–53: метод [updateFragment], який викликається щоразу, коли фрагмент стає видимим. Метод було написано тут так, ніби фрагмент може вийти з сусідства з відображеним фрагментом і, отже, перезапустити свій життєвий цикл. У даному випадку це не так, але так було б, якби додаток мав 3 фрагменти з сусідством 1;
  • рядок 44: адаптер [ListView] потрібно ініціалізувати лише один раз;
  • рядок 46: до цього [ListView] прив’язується адаптер типу [ListAdapter]. Ми створимо цей клас. Він походить від класу [ArrayAdapter], який ми вже мали нагоду використовувати для прив’язування даних до [ListView]. Ми передаємо конструктору [ListAdapter] різну інформацію:
    • посилання на поточну активність,
    • ідентифікатор подання, яке буде інстанційовано для кожного елемента списку,
    • джерело даних для наповнення списку,
    • посилання на фрагмент. Воно буде використовуватися для обробки кліка на посилання [Retirer] у [ListView] за допомогою методу [doRetirer] у рядку 38;
  • рядок 50: адаптер пов’язаний із [ListView]. Одночасно джерело даних [listes] пов’язане із [ListView]. Ця операція тут виконуватиметься щоразу, коли відображається вікно № 1. Насправді її потрібно виконувати лише тоді, коли було виконано метод [@AfterViews]. Тут інструкція виконується занадто часто. Виникає потреба у булевому значенні, яке б вказувало, що метод [@AfterViews] щойно було виконано, а отже, [ListView] має бути знову пов’язаний зі своїм адаптером;
  • рядок 52: оновлюється [ListView]. У цьому прикладі це не має сенсу, оскільки лише подання № 1 може змінювати джерело даних [ListView]. Розглянемо більш загальний випадок, коли вид № 2 також може змінювати джерело даних [ListView]. Такі приклади ми розглянемо далі в цьому документі. У цьому випадку, при переході з подання № 2 до подання № 1, [ListView] у поданні № 1 має бути оновлено;

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) {
...
    }
}
  • рядок 5: клас [ListAdapter] успадковує клас [ArrayAdapter];
  • рядок 19: конструктор;
  • рядок 20: не забудьте викликати конструктор батьківського класу [ArrayAdapter] із трьома першими параметрами;
  • рядки 22–25: зберігаємо інформацію з конструктора;
  • рядок 29: метод [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;
}
  • рядок 2: метод отримує три параметри. Ми будемо використовувати лише перший;
  • рядок 4: створюється представлення елемента № [position]. Це представлення [list_data], ідентифікатор якого було передано як другий параметр конструктору. Далі отримуються посилання на компоненти щойно створеного представлення;
  • рядок 6: отримуємо посилання на [TextView] № 1;
  • рядок 7: йому присвоюється текст із джерела даних, яке було передано як третій параметр конструктору;
  • рядок 9: отримуємо посилання на [CheckBox] № 2;
  • рядок 10: встановлюється або знімається галочка на основі значення з джерела даних для [ListView];
  • рядок 12: отримуємо посилання на [TextView] № 3;
  • рядки 13–18: обробляється клік на посилання [Retirer];
  • рядок 16: саме метод [Vue1Fragment].doRetirer оброблятиме це натискання. Дійсно, логічніше, щоб цю подію обробляв фрагмент, який відображає [ListView]. Він має загальний огляд, якого не має клас [ListAdapter]. Посилання на фрагмент [Vue1Fragment] було передано як четвертий параметр у конструктор класу;
  • рядки 20–25: обробляється клік на прапорці. Дія, виконана над нею, позначається на даних, які вона відображає. Це пов’язано з наступною причиною. [ListView] — це список, який відображає лише частину цих елементів. Таким чином, елемент списку іноді прихований, іноді відображений. Коли елемент № i має бути відображений, для позиції № i викликається метод [getView] із рядка 2 вище. Рядок 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);
}
  • рядок 1: отримуємо позицію в [ListView] посилання [Retirer], на яке натиснули;
  • рядок 3: отримуємо список даних;
  • рядок 4: видаляється елемент № [position];
  • рядок 15: оновлюємо [ListView]. Без цього візуально нічого не зміниться.
  • рядки 5–13, 17: досить складні маніпуляції. Без них відбуватиметься наступне:
    • [ListView] відображає рядки 15–18 списку даних,
    • видаляється рядок 16,
    • рядок 15 вище повністю його скидає, і [ListView] тоді відображає рядки 0–3 зі списку даних;

З урахуванням наведених вище рядків видалення відбувається, і [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>
  • рядки 6–15: компонент [TextView] № 1;
  • рядки 26–33: компонент [TextView] № 2;
  • рядки 17–24: компонент [Button] № 3;

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] у рядку 32:

  • рядок 34: обчислюється текст, який потрібно відобразити у [TextView] № 2;
  • рядки 35–39: відбувається перебір списку даних, що відображаються методом [ListView]. Цей список зберігається в активності;
  • рядок 36: якщо дані № i були позначені галочкою, додається відповідний текст у тип [StringBuilder];
  • рядок 41: [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;
    }
...

}

У рядку 7 ми використовували логічне значення для управління прапорцем елементів [ListView]. Часто [ListView] має відображати дані, які можна вибрати, встановивши прапорець, навіть якщо елемент джерела даних не має булевого поля, що відповідає цьому прапорцю. У такому разі можна вчинити наступним чином:

Клас [Data] набуває такого вигляду:


package exemples.android.fragments;

public class Data {

    // дані
    private String texte;

    // конструктор
    public Data(String texte) {
        this.texte = texte;
    }

    // методи getter та setter
...
}

Створюємо клас [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. Приклад-20: використання меню

1.21.1. Створення проєкту

Ми дублюємо проект [Exemple-19B] у проект [Exemple-20]:

3

Ми видалимо кнопки з переглядів 1 і 2 та замінимо їх опціями меню [1-2].

1.21.2. Визначення XML меню

  

Файл [res / menu / menu_vue1] визначає меню виду № 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=".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>, рядки 25, 29);

Файл [res / menu / menu_vue2] визначає меню виду № 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=".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;
    // журнал
    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();
}
  • рядок 42: журнали показують, що метод [onCreateOptionsMenu] викликається щоразу, коли відображається фрагмент. Він викликається дуже пізно, зокрема після того, як було викликано метод [updateFragment]. Це наводить на думку, що його можна використовувати для оновлення фрагмента. Саме це ми й зробимо тут (рядок 63);
  • рядок 42: метод має два параметри:
    • [menu]: це порожнє меню;
    • [inflater] — інструмент, що дозволяє створити меню на основі його початкового опису. Ми не скористаємося цією можливістю тут, оскільки будемо використовувати анотацію AA, яка зробить це за нас;
  • рядок 44: ми зберігаємо меню. Воно знадобиться нам пізніше;
  • рядки 52–53: ми зберігаємо в масиві з рядка 28 ідентифікатори всіх елементів меню;
  • рядки 55–57: журнали показують, що під час виклику методу [onCreateOptionsMenu] метод [Fragment.getActivity()] повертає активність, пов’язану з фрагментом;
  • рядок 55: ми зберігаємо активність як екземпляр класу Android [Activity];
  • рядок 56: ми зберігаємо активність як екземпляр інтерфейсу [IMainActivity];
  • рядок 57: зберігаємо сесію;
  • рядок 59: відзначаємо, що ініціалізація класу вже відбулася, щоб не повторювати її (рядок 50);
  • рядок 63: запитуємо дочірній фрагмент про оновлення. Це можливо, оскільки фрагмент є одночасно видимим, пов’язаним зі своїм видом та своїм меню;

Метод [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;
  }

  // методи getter та setter
...
}

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() {
    // перехід до подання 2
    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(...)
  }
}
  • рядок 2: меню [res / menu / menu_vue1.xml] пов'язане з фрагментом;
  • рядок 48: під час виконання методу [updateFragment] меню також може бути оновлено, щоб відобразити новий стан фрагмента;
  • рядок 7: анотація [@OptionsItem(R.id.navigationVue2)] позначає метод, який має виконуватися при натисканні на пункт меню [Navigation / Vue 2];
  • рядки 19–25: щоб приховати гілку меню, достатньо приховати її кореневий пункт;
  • рядок 24: показуємо/приховуємо кореневі опції [menuNavigation, menuActions];
  • рядок 40: щоб показати пункт гілки меню, потрібно показати не тільки його, а й усі пункти, які зустрічаються на шляху від кінцевого пункту до кореня меню;

1.21.5. Управління меню у фрагменті [Vue2Fragment]

Подібний код можна знайти у фрагменті подання № 2:


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 {

  // поля подання
  @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(...)
  }
}
  • рядок 35: відображається пункт [Navigation / Vue 1];
  • рядки 17–20: при натисканні на опцію [Navigation / Vue1] викликається метод [navigateToView1];

1.21.6. Виконання

Створіть контекст виконання для цього проєкту та запустіть його.

1.22. Приклад-21: рефакторинг абстрактного класу [AbstractFragment]

Попередній приклад показав, що коли фрагмент має меню, його метод [onCreateOptionsMenu] є підходящим місцем для запиту до фрагмента про оновлення:

  • він викликається рівно один раз, коли фрагмент збирається відобразитися;
  • під час її виклику встановлюються зв’язки фрагмента з його активністю, видом та меню;

Щоб продемонструвати це, повернемося до прикладу 12, який характеризується великою кількістю фрагментів, сусідство яких можна змінювати. У цьому прикладі фрагменти не мали меню. Ми пов’яжемо з ними порожнє меню.

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. Приклад-22: збереження/відновлення стану активності та фрагментів

1.23.1. Проблема

Тут ми розглянемо проблему повороту пристрою Android (вертикальний <--> горизонтальний режим). Для ілюстрації повернемося до попереднього прикладу 21:

Image

Якщо ми повернемо пристрій [1], отримаємо такий новий вигляд:

Image

Бачимо, що:

  • у [1] вкладка [Fragment n° 3] зникла;
  • у [2] відображається текст фрагмента № 3, але лічильник відвідувань показує неправильні дані;

Під час цієї ротації журнали мають такий вигляд:

07-13 04:08:27.188 1677-1677/exemples.android D/MainActivity: constructor
07-13 04:08:27.189 1677-1677/exemples.android D/MainActivity: afterInject
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.194 1677-1677/exemples.android D/MainActivity: afterViews
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.203 1677-1677/exemples.android D/PlaceholderFragment: afterViews 4 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.204 1677-1677/exemples.android D/PlaceholderFragment: afterViews 3 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.208 1677-1677/exemples.android D/Vue1Fragment: afterViews Vue1Fragment_ - numVisit=0
07-13 04:08:27.208 1677-1677/exemples.android D/PlaceholderFragment: afterViews 2 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.209 1677-1677/exemples.android D/PlaceholderFragment: afterViews 1 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.351 1677-1677/exemples.android D/menu: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment_: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment: update 3 - PlaceholderFragment_ - numVisit=0, initDone=true, getActivity()==null:false
  • рядок 1: бачимо, що активність повністю відновлено;
  • рядки 3–7: те саме стосується п’яти фрагментів, що обробляються цією активністю;
  • рядок 21: буде відображено фрагмент № 3. Бачимо, що перед інкрементом номер відвідування дорівнює 0;

Отже, результат, отриманий після ротації, можна пояснити таким чином:

  • клас [MainActivity] спочатку створює панель вкладок з однією вкладкою, названою [Vue 1]. Саме цю вкладку ми бачимо;
  • після повороту пристрою менеджер сторінок [mViewPager] знову відображає той самий фрагмент, тобто в даному випадку фрагмент № 3. Тут слід пам’ятати, що вкладки та фрагменти — це різні поняття, які мають різний життєвий цикл. Буде виконано метод [updateFragment] фрагмента № 3:

  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));
}
  • рядок 7: останній номер відвідування зчитується із сесії. Однак ця сесія, як і все інше, була відновлена, а номер відвідування скинуто до нуля. Це пояснює результат, що відображається у фрагменті № 3;

1.23.2. Методи збереження/відновлення активності та фрагментів

1.23.2.1. Рішення 1: ручне збереження

Під час повороту пристрою викликаються два методи активності:


// управління збереженням/відновленням активності ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // батьківський елемент
    super.onSaveInstanceState(outState);
    // збереження стану активності
    // ....
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // батьківський елемент
    super.onCreate(savedInstanceState);
     // відновлення активності
    // ...
  }
  • рядки 2–8: метод [onSaveInstanceState] викликається системою під час повороту. Саме тут можна виконати збереження активності. Якщо нічого не робити, нічого не зберігається. Збереження стану активності має відбуватися в параметрі [Bundle outState], що передається методу. Клас [Bundle] схожий на словник. Він має методи [putString, putInt, putLong, putBoolean, putChar, ...] з двома параметрами: void putT(String key, T value);
  • рядки 10–16: метод [onCreate] викликається під час створення активності. Якщо стан активності було збережено, ця копія передається йому в параметрі [Bundle savedInstanceState]. Для отримання збережених значень використовуються методи, такі як [getString, getInt, getLong, geBoolean, getChar, ...] з одним параметром: T getT(String key);

Фрагменти мають ці два методи для збереження свого стану.

Ми використаємо цю інформацію для збереження та відновлення стану з прикладу 21. Для цього ми дублюємо проект [Exemple-21] у [Exemple-22].

1.23.2.2. Рішення 2: автоматичне збереження

У документації Android зазначено, що під час повороту пристрою можна уникнути знищення фрагмента, використовуючи інструкцію: [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]. Я протестував цю інструкцію на двох прикладах: Приклад-17 (параграф 1.18 — додаток із одним фрагментом, що відображає форму) та Приклад-21 (параграф 1.22 — додаток із п’ятьма фрагментами). В обох випадках застосування лише цієї однієї інструкції до всіх фрагментів додатка виявилося недостатнім для правильного відновлення вигляду, що відображався під час повороту пристрою. Замість того, щоб створювати дві моделі — одну на основі [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;
  // методи getter та setter
...
}
  • рядок 5: логічне значення [hasBeenVisited] дорівнює «true», якщо фрагмент [Vue1Fragment] було відвідано (відображено) хоча б один раз. Це поле було створено для прикладу, оскільки фрагмент [Vue1Fragment] не має чого зберігати;

Клас [PlaceHolderFragmentState] має такий вигляд:


package exemples.android;

public class PlaceHolderFragmentState {
  // статус «відвідано» чи «не відвідано»
  private boolean hasBeenVisited;
  // текст, що відображається
  private String text;

  // методи getter та setter
...
}
  • рядок 5: тут міститься логічне значення [hasBeenVisited];
  • рядок 7: текст, який відображає фрагмент у момент, коли його потрібно зберегти. Ми бачили, що цей текст було втрачено під час обертання;

Стан фрагментів буде збережено в сесії, і саме активність відповідатиме за збереження/відновлення цієї сесії. Сесія розвивається наступним чином:


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();
  }
  // методи getter та setter
...
}
  • рядок 18: стан фрагмента [Vue1Fragment];
  • рядок 19: стан фрагментів типу [PlaceHolderFragment];
  • рядки 22–27: у конструкторі сесії ініціалізуються поля рядків 18 і 19;
  • рядки 12–15: з’являються два нових поля:
    • рядок 13: номер останньої вибраної вкладки;
    • рядок 15: номер останнього відображеного фрагмента;

Активність зберігає/відновлює сесію наступним чином:


  // управління збереженням/відновленням активності ----------------------------
  @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();
        }
      }
    }
}
  • рядок 8: сесія зберігається у вигляді її рядка jSON;
  • рядок 29: сесія відновлюється на основі її рядка 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) {
    // журнал
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // батьківський елемент
    super.onSaveInstanceState(outState);
    // збереження фрагмента лише в тому випадку, якщо він видимий
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }

  // дочірні класи
  protected abstract void updateFragment();

  protected abstract void saveFragment();
  • вирішено зберігати стан фрагментів у сесії у два моменти:
    • рядки 2–14: коли фрагмент переходить зі стану «видимий» у стан «прихований»;
    • рядки 29–42: коли система вказує, що потрібно зберегти фрагмент, і цей фрагмент є видимим (рядок 38);

Цей механізм дозволяє уникнути збереження стану частіше, ніж це необхідно. Адже, оскільки стан фрагмента i було збережено, коли він перейшов із видимого стану в прихований, то коли фрагмент j відображається і виконується поворот, немає потреби знову зберігати стан фрагмента i. Якщо він не відображався з моменту останнього збереження, то його стан не змінився. Зберегти потрібно лише стан фрагмента j. Цей механізм має ще одну перевагу: збереження стану фрагмента потрібне не лише під час обертання пристрою. Існує також випадок простої навігації між фрагментами, наприклад, у системі з вкладками. У такому разі ми хочемо відновити фрагмент у тому стані, в якому він перебував, коли його востаннє відображали. Цей стан може частково зникнути, якщо фрагмент у певний момент вийшов із сукупності відображуваних фрагментів. У такому разі фрагмент не відтворюється повністю, але пов’язаний з ним вигляд — так. Збереження, яке було зроблено, коли фрагмент став прихованим, послужить для відновлення останнього стану цього виду;

  • рядки 10, 40: щоб уникнути двох послідовних збережень, використовується булеве значення [saveFragmentDone] для позначення того, що збереження вже відбулося;
  • рядки 9, 39: дочірньому фрагменту надається запит зберегти свій стан. Метод [saveFragment] є абстрактним (рядок 47). Отже, його мають реалізувати дочірні класи;
  • рядки 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;
  }
  • рядок 14: ми бачили, що булеве значення [saveFragmentDone] змінювалося на vrai після збереження. У певний момент воно має повернутися до faux. Коли виконується метод [updateFragment] (рядок 12) дочірнього фрагмента, він стає видимим. Саме тоді, коли фрагмент є видимим, його потрібно зберегти — у той конкретний момент, коли він перейде зі стану «видимий» у стан «прихований». Тоді ми встановлюємо значення булевого параметра [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();
      }
    }
}
  • рядки 9–11: збереження стану фрагмента в сесії. Коли викликається метод [saveFragment], фрагмент є видимим. Тому потрібно встановити значення логічного параметра [hasBeenVisited] на vrai (рядок 10);

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;
    // журнал
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • рядки 4–7: збереження стану фрагмента в сесії;
  • рядок 5: зберігається текст, який наразі відображається [TextView] textViewInfo;
  • рядок 6: булеве значення [hasBeenVisited] фрагмента змінюється на vrai;
  • рядок 7: стан фрагмента записується в масив [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);
  }
  • рядки 8–12: відновлення стану фрагмента. Булеве значення [fragmentHasToBeInitialized] було ініціалізовано батьківським класом [AbstractFragment]. Коли воно дорівнює vrai, фрагмент щойно відновлено, і його потрібно переініціалізувати. Саме тут це відбувається. У цьому конкретному прикладі нічого робити не потрібно. Ми просто показали, що можна знайти значення булевого значення [hasBeenVisited] у збереженому стані фрагмента (рядок 10);
  • рядок 11: не слід забувати змінити значення [fragmentHasToBeInitialized] на faux, щоб при подальшому поверненні до цього фрагмента без обертання пристрою не відбувалося зайвої ініціалізації фрагмента;
  • рядки 18–26: інкремент лічильника відвідувань. Тут виникає складність: під час відновлення фрагмента ми не хочемо збільшувати цей лічильник. Тут потрібно розрізняти:
    • простої навігації, яка повертає користувача на вкладку [Vue 1];
    • відновленням, коли користувач повертає свій пристрій під час відображення вкладки [Vue 1];

Ці два випадки розрізняються за номером перегляду, збереженим у сесії. Цей номер відповідає останньому відображеному перегляду (рядок 28).

  • рядок 18: відбувається перехід, а не відновлення, якщо номер останнього перегляду відрізняється від номера поточного перегляду;
  • рядки 21–25: інкрементування лічильника відвідувань та його відображення;

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);
}
  • рядки 15–16: визначається номер вікна, яке саме оновлюється;
  • рядки 18–22: випадок, коли фрагмент перебуває в циклі збереження/відновлення після зміни орієнтації пристрою. Тут його потрібно відновити. Зазвичай це відновлення певних полів фрагмента;
  • рядок 20: поле [text] у рядку 2 має містити початковий текст, що відображається фрагментом: [Hello world from section i]. Тут його потрібно відтворити;
  • рядок 21: бачимо, що ініціалізація фрагмента відбулася;
  • рядки 24–36: як і раніше для фрагмента [Vue1Fragment], під час відновлення лічильник відвідувань не повинен збільшуватися. Як і раніше, нам потрібно розрізняти навігацію та відновлення;
  • рядки 32–36: випадок відновлення;
  • рядок 34: стан фрагмента до повороту пристрою відновлюється із сесії;
  • рядок 35: тут відновлюється текст, який тоді відображався;
  • рядок 38: цей текст знову відображається;
  • рядок 40: у сесії фіксується номер нового відображеного вікна;

1.23.8. Управління вкладками

У попередніх параграфах не розглядалося управління вкладками. Однак у прикладі 21 під час повороту пристрою ми зіткнулися з проблемою: зберігалася лише перша вкладка [Vue 1]. Друга вкладка була втрачена.

Ми вирішуємо цю проблему у класі [MainActivity] наступним чином:


@AfterViews
  protected void afterViews() {
    // журнал
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // панель інструментів
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

 ...

    // 1-ша вкладка
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2-га вкладка?
    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();

...

  }
  • рядки 14–16: створення першої вкладки;
  • рядки 18–23: створення другої вкладки. Щоб дізнатися, чи потрібно її створювати, перевіряємо в сесії номер фрагмента, що відображався у вкладці 2. Якщо цей номер відрізняється від -1 (його початкового значення), то створюється друга вкладка. На цьому етапі маємо дві вкладки, з яких за замовчуванням вибрана перша;
  • рядок 26: у сесії шукаємо номер вкладки, яка була вибрана перед збереженням/відновленням, і вибираємо її знову. Якщо поле [selectedTab] ще не було ініціалізовано кодом, то використовується його початкове значення 0;