Skip to content

1. Nauka programowania na Androida

Plik PDF z tego dokumentu jest dostępny |TUTAJ|.

Przykłady z dokumentu są dostępne |TUTAJ|.

1.1. Introduction

1.1.1. Spis treści

Niniejszy dokument stanowi przeróbkę kilku istniejących dokumentów:

  1. Android dla programistów J2EE;
  1. Wprowadzenie do programowania tabletów z systemem Android na przykładach;
  2. Wprowadzenie do programowania tabletów z systemem Android na przykładach – wersja 2

i wprowadza następujące nowości:

  • w dokumencie 1 przedstawiono architekturę o nazwie AVAT (Aktywność-Widoki-Akcje-Zadania) w celu ułatwienia programowania asynchronicznego w aplikacji na Androida. W niniejszym dokumencie do zarządzania akcjami asynchronicznymi wykorzystano standardową bibliotekę RxJava;
  • w dokumencie 2 wykorzystywano środowisko Eclipse z wtyczką dla Androida o nazwie IDE. W niniejszym dokumencie wykorzystuje się środowisko Android Studio;
  • dokument 3 został przejęty bez zmian;
  • w dokumencie 4 wykorzystano bibliotekę [Android Annotations] (AA) wraz z IDE IntelliJ IDEA Community Edition. Niniejszy dokument zawiera całość dokumentu 4 z następującymi różnicami:
    • IDE to teraz Android Studio;
    • systemem kompilacji jest Gradle dla wszystkich projektów klienckich i serwerowych (w dokumencie 4 czasami używano Mavena)
    • programowanie asynchroniczne realizowane jest za pomocą biblioteki RxJava (w dokumencie 4 wykorzystywano bibliotekę AA);
  • niniejszy dokument porusza zagadnienia, które nie zostały poruszone lub zostały poruszone w niewielkim stopniu w poprzednich dokumentach:
    • pojęcie sąsiedztwa fragmentów;
    • zapisywanie / przywracanie aktywności i jej fragmentów;
    • cykl życia fragmentów;

Wreszcie przedstawia szkielet klienta na Androida komunikującego się z serwisem internetowym / jSON, w którym wyodrębniono wiele elementów, które regularnie pojawiają się w tego typu klientach. Szkielet ten jest wykorzystywany we wszystkich przykładach począwszy od rozdziału 2. Jest to naprawdę innowacyjna część dokumentu.

Przedstawiono następujące przykłady:

Exemple
Charakter
1
Importowanie istniejącego projektu Android
2
Podstawowy projekt na Androida
3
Podstawowy projekt [Android Annotations]
4
Widoki i zdarzenia
5
Nawigacja między widokami
6
Nawigacja za pomocą zakładek
7
Korzystanie z biblioteki [Android Annotations] w Gradle
8 à 12
Zarządzanie fragmentami aplikacji na Androida
13
Nowa nawigacja między widokami
14
Architektura dwuwarstwowa
15
Architektura klient-serwer
16
Zarządzanie asynchronicznością za pomocą RxJava
17, 17B
Komponenty do wprowadzania danych
18
Wykorzystanie szablonu widoków
19
Komponent ListView
20
Korzystanie z menu
21
Korzystanie z klasy nadrzędnej dla fragmentów
22, 22B
Zapisywanie i przywracanie stanu aktywności i fragmentów
23
Aplikacja pogodowa
Chap 2
Szkielet klienta na Androida komunikującego się z usługą internetową / jSON. Zawiera on wiele elementów, które często pojawiają się w tego typu klientach na Androida.
Chap 3
Zarządzanie wizytami w gabinecie lekarskim
Chap 4
Ćwiczenie praktyczne – Zarządzanie podstawową listą płac
Chap 5
Ćwiczenie praktyczne – sterowanie płytkami Arduino

Niniejszy dokument był wykorzystywany na ostatnim roku studiów inżynierskich IstiA na Uniwersytecie w Angers [istia.univ-angers.fr]. To wyjaśnia nieco specyficzny ton tekstu. Oba ćwiczenia praktyczne pochodzą z tekstów TP, w których podano jedynie ogólne zarysy rozwiązania. Rozwiązanie należy opracować samodzielnie.

Kod źródłowy przykładów jest dostępny |ICI|. Aby uruchomić te przykłady, należy postępować zgodnie z procedurą opisaną w paragrafie 6.12.

Niniejszy dokument stanowi wprowadzenie do programowania na platformie Android. Nie ma on charakteru wyczerpującego. Jest skierowany przede wszystkim do początkujących.

Strona referencyjna poświęcona programowaniu dla systemu Android znajduje się pod adresem URL [http://developer.android.com/guide/components/index.html]. To właśnie tam należy się udać, aby uzyskać ogólny przegląd programowania dla systemu Android.

1.1.2. Wymagania wstępne

Warunkiem optymalnego wykorzystania niniejszego dokumentu jest dobra znajomość języka Java.

1.1.3. Wykorzystane narzędzia

Poniższe przykłady zostały przetestowane w następującym środowisku:

  • komputer z systemem Windows 10 Pro 64-bitowym;
  • JDK 1.8;
  • Android SDK API 23;
  • Android Studio, wersja 2.1;
  • emulator Genymotion, wersja 2.6.0;

Aby skorzystać z niniejszego dokumentu, należy zainstalować:

  • JDK (patrz punkt 6.8);
  • menedżer emulatorów Android Genymotion (patrz punkt 6.9);
  • menedżer zależności Maven (patrz punkt 6.10);
  • IDE [Android Studio] (patrz punkt 6.11);

1.2. Przykład 01: importowanie przykładowego projektu Android

1.2.1. Tworzenie projektu

Stwórzmy w Android Studio pierwszy projekt na Androida. Najpierw utwórzmy pusty folder [exemples], w którym będą umieszczone wszystkie nasze projekty:

  

następnie utwórzmy projekt w Android Studio. Najpierw zaimportujemy jeden z przykładów dostarczonych wraz z IDE [1-5]:

 

Image

Import projektu może zakończyć się błędami wynikającymi z rozbieżności między środowiskiem używanym podczas tworzenia projektu a tym, które jest tutaj wykorzystywane do jego uruchomienia. Jest to okazja, aby zapoznać się z metodami rozwiązywania tego typu błędów. W tym przypadku mamy do czynienia z następującym błędem:

Zimportowany projekt jest skonfigurowany przez następujący plik: [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"
}

// Przykładowa kompilacja wykorzystuje wiele katalogów, aby
// oddzielenia kodu szablonowego i wspólnego od
// głównego kodu przykładowego.
List<String> dirs = [
    'main',     // głównego kodu przykładowego; tutaj znajdziesz interesujące elementy.
    'common',   // komponenty ponownie wykorzystywane przez wiele przykładów
    'template'] // kod szablonowy generowany przez proces tworzenia szablonów przykładów

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"
    }
}
  • Zgłoszony błąd wynika z wierszy 31, 34–35: nie mamy pliku SDK w wersji 21. Zastępujemy tę wersję wersją 23, którą posiadamy.

W pliku [build.gradle] program Android Studio wyświetla następujące sugestie:

 

Aby zaakceptować sugestie, należy kliknąć [alt-entrée] na danej propozycji:

 

Może również pojawić się błąd dotyczący wersji Gradle:

 

Błąd ten wynika z niezgodności wersji Gradle wymaganej przez plik [build.gradle] projektu (wersja 2.10, wiersz 6 poniżej):


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}

a wersją podaną w pliku [<projet>/gradle/wrapper/gradle-wrapper.properties]:


#śr., 10 kwietnia, godz. 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

W wierszu 6 powyżej należy zastąpić wartość 2,8 wartością 2,10.

Aby uzyskać dostęp do pliku [<projet>/gradle/wrapper/gradle-wrapper.properties], należy skorzystać z perspektywy projektu:

Po wprowadzeniu tej poprawki można skompilować aplikację [1], uruchomić emulator Genymotion [2], a następnie uruchomić projekt [3]:

 

Image

Zatrzymajmy aplikację:

  

Teraz możemy zamknąć projekt. Utworzymy nowy.

  

1.2.2. Kilka uwag na temat IDE

1.2.2.1. Perspektywy

IDE Android Studio (AS) oferuje różne widoki do pracy z projektem. Będziemy korzystać głównie z dwóch:

  • perspektywa [Android] [1]:
  • perspektywa [Project] [4];
 
  

W większości przypadków będziemy pracować w perspektywie [Android]. Gdy będziemy duplikować projekt w innym projekcie, będziemy potrzebować perspektywy [Project].

1.2.2.2. Zarządzanie uruchamianiem

Istnieje kilka sposobów na uruchomienie, zatrzymanie lub ponowne uruchomienie projektu AS. Przede wszystkim są to przyciski na pasku narzędzi:

Przycisk [Rerun] [3] zatrzymuje wykonywanie projektu [2], a następnie ponownie go uruchamia [1].

1.2.2.3. Zarządzanie pamięcią podręczną

Android Studio utrzymuje pamięć podręczną projektów, którymi zarządza, aby zapewnić jak największą responsywność środowiska IDE. W wersji Android 2.1 (maj 2016 r.) pamięć ta często nie uwzględniała właśnie wprowadzonych zmian w kodzie. W takim przypadku należy unieważnić tę pamięć podręczną:

W systemie Android 2.1 (maj 2016 r.) powyższą operację trzeba było wykonywać wielokrotnie, a czasami nawet to nie wystarczało do usunięcia wykrytej anomalii. Rozwiązaniem było wyłączenie technologii [Instant Run]:

  • w [3-4] wszystko zostało wyłączone;

W dalszej części pracowano z tą konfiguracją pamięci podręcznej i nie napotkano żadnych problemów.

1.2.2.4. Zarządzanie logami

Podczas wykonywania projektu w monitorze systemu Android wyświetlane są logi:

W zakładce [Android Monitor] [1] logi wyświetlają się w zakładce [logcat] [2]. Przycisk [3] służy do usuwania logów. Przycisk ten jest przydatny, gdy chcemy wyświetlić logi dotyczące konkretnej czynności:

  • usuwamy logi;
  • na urządzeniu z systemem Android wykonuje się czynność, której logi chcemy wyświetlić;
  • logi, które się wtedy pojawią, będą dotyczyły właśnie tej czynności;

Istnieje kilka poziomów logów [4]. Domyślnie wybrany jest tryb [Verbose]. Oznacza to, że wyświetlane są logi wszystkich poziomów. Za pomocą [4] można wybrać konkretny poziom.

Logi są bardzo przydatne, aby dowiedzieć się, w których momentach realizacji projektu wywoływane są określone metody. Będziemy z nich często korzystać. Weźmy na przykład kod klasy [MainActivity] z projektu [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);
    }
}

Powyżej metody [onCreate, ligne 14] i [onCreateOptionsMenu, ligne 26] są metodami klasy nadrzędnej [Activity] (wiersz 9). Są one wywoływane w różnych momentach cyklu życia aplikacji. Czasami są one wykonywane wielokrotnie. Nawet po zapoznaniu się z dokumentacją czasami trudno jest stwierdzić, czy dana metoda cyklu życia zostanie wykonana przed czy po metodzie, którą sami napisaliśmy. A przecież często warto znać tę informację. Można wówczas umieścić logi, jak poniżej:


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()) {
      ...
  }
}
  • W wierszach 7, 14 i 21 używana jest klasa [Log]. Klasa ta umożliwia zapisywanie wpisów logowania na konsoli Androida [logcat]. Wpisy logowania są klasyfikowane według różnych poziomów (info, warning, debug, verbose, error). [Log.d] wyświetla logi poziomu [debug]. Jego pierwszym argumentem jest źródło komunikatu logowego. Różne źródła mogą bowiem wysyłać komunikaty do konsoli logów. Aby móc je rozróżnić, stosuje się ten pierwszy argument. Drugim argumentem jest komunikat, który ma zostać zapisany w konsoli logów;

Jeśli ponownie uruchomimy projekt [Exemple-01], otrzymamy następujące logi:


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

W ten sposób dowiadujemy się, że metoda [onCreate], która tworzy aktywność Androida, jest wykonywana przed metodą [onCreateOptionsMenu], która tworzy menu aplikacji.

Teraz, jeśli klikniemy opcję menu w emulatorze Androida [1]:

  

w konsoli logów pojawia się następujący wpis:


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

W dalszej części często będziemy dodawać do kodu Androida instrukcje logowania. W większości przypadków nie będziemy ich komentować. Znajdują się one tam wyłącznie po to, aby zachęcić czytelnika do śledzenia konsoli logów w celu stopniowego zrozumienia cyklu życia aplikacji na Androida.

1.2.2.5. Obsługa emulatora [Genymotion]

Czasami emulator Genymotion ulega awarii i nie da się go ponownie uruchomić. Wynika to z faktu, że procesy VirtualBox pozostały aktywne w menedżerze zadań. Otwórz menedżera zadań [Ctrl-Alt-Supp] i usuń wszystkie obecne zadania VirtualBox:

Po wykonaniu tej czynności uruchom ponownie emulator Genymotion z poziomu Android Studio.

1.2.2.6. Zarządzanie utworzonym plikiem binarnym APK

Kompilacja projektu generuje plik binarny z rozszerzeniem .apk:

Istnieją dwie wersje: jedna o nazwie [debug], a druga o nazwie [debug-unaligned]. Należy użyć tej pierwszej, ponieważ druga jest wersją pośrednią. Plik binarny .pak wygenerowany w [4] można przenieść bezpośrednio do emulatora lub urządzenia z systemem Android. Aby przenieść go do emulatora, wystarczy przeciągnąć i upuścić go myszką na emulator.

1.3. Przykład 02: podstawowy projekt na Androida

Utwórzmy w Android Studio nowy projekt na Androida o nazwie [1-12]:

 

W pliku [13] uruchamia się aplikację. W rezultacie na emulatorze Genymotion wyświetla się obraz [14].

1.3.1. Konfiguracja Gradle

Utworzony projekt jest skonfigurowany za pomocą następującego pliku [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'
}

Plik ten został wygenerowany przez IDE na podstawie elementów jego konfiguracji. Jest to plik minimalny, który będziemy stopniowo rozbudowywać.

  • wiersze 3–12: charakterystyka aplikacji na Androida;
  • wiersze 22–25: jej zależności. To właśnie w tym miejscu będziemy wprowadzać zmiany zgodnie z omówionymi przykładami;

1.3.2. Manifest aplikacji

  

Plik [AndroidManifest.xml] [1] określa parametry pliku binarnego aplikacji na Androida. Jego zawartość wygląda następująco:


<?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>
  • wiersz 3: pakiet projektu Android;
  • wiersz 10: nazwa aktywności;

Te dwie informacje pochodzą z danych wprowadzonych podczas tworzenia projektu:

  • wiersz 3 manifestu (pakiet) pochodzi z powyższego wpisu [4]. W tym pakiecie automatycznie generowanych jest pewna liczba klas;
  • wiersz 10 manifestu (nazwa aktywności) pochodzi z powyższego wpisu [1];

Wróćmy do manifestu:


<?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>
  • wiersz 10: główna aktywność aplikacji. Odwołuje się do klasy [1] wymienionej powyżej;
  • wiersz 6: ikona aplikacji [2]. Można ją zmienić;
  • wiersz 7: nazwa aplikacji. Znajduje się w pliku [strings.xml] [3]:

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

Plik [strings.xml] zawiera ciągi znaków używane przez aplikację. Wiersz 2 – nazwa aplikacji pochodzi z danych wprowadzonych podczas tworzenia projektu [4]:

 
  • wiersz 10: znacznik aktywności. Aplikacja na Androida może mieć wiele aktywności;
  • wiersz 12: aktywność ta została oznaczona jako główna;
  • wiersz 13: i musi ona pojawić się na liście aplikacji, które można uruchomić na urządzeniu z systemem Android.

1.3.3. Główna aktywność

 

Aplikacja na Androida opiera się na jednej lub kilku aktywnościach. W tym przypadku wygenerowano aktywność [1]: [MainActivity]. Aktywność może wyświetlać jeden lub kilka widoków w zależności od swojego typu. Wygenerowana klasa [MainActivity] wygląda następująco:


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);
  }
}
  • wiersz 6: klasa [MyActivity] rozszerza klasę Androida [AppCompatActivity]. Tak będzie w przypadku wszystkich przyszłych aktywności;
  • wiersz 9: metoda [onCreate] jest wykonywana podczas tworzenia aktywności. Dzieje się to przed wyświetleniem widoku powiązanego z aktywnością;
  • wiersz 10: wywoływana jest metoda [onCreate] klasy nadrzędnej. Należy to zawsze wykonać;
  • wiersz 11: plik [activity_main.xml] [2] jest widokiem powiązanym z aktywnością. Definicja XML tego widoku jest następująca:

<?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>
  • wiersze b–k: menedżer formatowania. Domyślnie wybrano typ [RelativeLayout]. W tym typie kontenera komponenty są rozmieszczane względem siebie (na prawo od, na lewo od, poniżej, powyżej);
  • wiersze m–p: komponent typu [TextView] służący do wyświetlania tekstu;
  • wiersz n: wyświetlany tekst. Nie zaleca się umieszczania tekstu na stałe w widokach. Lepiej jest przenieść te teksty do pliku [res/values/strings.xml] [3]:

Wyświetlany tekst będzie zatem brzmiał: [Hello World!]. Gdzie zostanie wyświetlony? Kontener [RelativeLayout] wypełni ekran. Element [TextView], który jest jego jedynym elementem, zostanie wyświetlony w lewym górnym rogu tego kontenera, a więc w lewym górnym rogu ekranu;

Co oznacza [R.layout.activity_main] w wierszu 11? Każdemu zasobowi systemu Android (widokom, fragmentom, komponentom itp.) przypisywany jest identyfikator. Tak więc widok o nazwie [V.xml], znajdujący się w folderze [res / layout], będzie identyfikowany jako [R.layout.V]. R to klasa wygenerowana w folderze [app / build / generated] [1-3]:

 

Klasa [R] ma następujący wygląd:


...............
    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;
}
  • wiersz 14: atrybut [R.layout.activity_main] jest identyfikatorem widoku [res / layout / activity_main.xml];
  • wiersz 7: atrybut [R.string.app_name] jest identyfikatorem ciągu znaków [app_name] w pliku [res / values / string.xml]:
  • wiersz 19: atrybut [R.mipmap.ic_launcher] jest identyfikatorem obrazu [res / mipmap / ic_launcher];

Należy zatem pamiętać, że odwołując się w kodzie do [R.layout.activity_main], odwołujemy się do atrybutu klasy [R]. IDE pomaga nam poznać różne elementy tej klasy:

1.3.4. Uruchamianie aplikacji

Aby uruchomić aplikację na Androida, musimy utworzyć konfigurację uruchomienia:

  • w [1] należy wybrać [Edit Configurations];
  • projekt został utworzony z konfiguracją [app], którą usuniemy, aby utworzyć ją ponownie jako [2];
  • w [3] należy utworzyć nową konfigurację wykonania;
  
  • w [4] należy wybrać [Android Application];

Image

  • w [5], z listy rozwijanej wybierz moduł [app];
  • w [6-8] należy zachować wartości domyślne;
  • w [7] domyślną aktywnością jest ta zdefiniowana w pliku [AndroidManifest.xml] (wiersz 1 poniżej):

    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
</activity>
  • w pliku [8] należy wybrać plik [Show Chooser Dialog], który umożliwia wybór urządzenia, na którym aplikacja ma być uruchamiana (emulator, tablet);
  • w polu [9] należy zaznaczyć, że ten wybór ma zostać zapamiętany;
  • zatwierdzić konfigurację;
  
  • w [11] uruchomić menedżera emulatorów [Genymotion] (patrz punkt 6.9);
  • w [12], wybierz emulator tabletu i uruchom [13];
  • w [14], uruchom konfigurację wykonania [app];
  • w [15] wyświetla się formularz wyboru urządzenia wykonawczego. Dostępne jest tu tylko jedno: uruchomiony wcześniej emulator [Genymotion];

Po chwili emulator programowy wyświetla następujący widok:

Image

1.3.5. Cykl życia aktywności

Wróćmy do kodu aktywności [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);
  }
}

Metoda [onCreate] w wierszach 8–12 należy do metod, które mogą być wywoływane w trakcie cyklu życia aktywności. Dokumentacja systemu Android zawiera listę tych metod:

 
  • [1]: metoda [onCreate] jest wywoływana podczas uruchamiania aktywności. To właśnie w tej metodzie przypisuje się aktywność do widoku i pobiera odwołania do jej komponentów;
  • [2-3]: następnie wywoływane są metody [onStart, onResume]. Widać, że metoda [onResume] jest ostatnią metodą wykonywaną przed przejściem do stanu [4] aktualnie wykonywanej aktywności;

1.4. Przykład-03: przeróbka projektu [Exemple-02] z wykorzystaniem biblioteki [Android Annotations]

Teraz wprowadzimy bibliotekę [Android Annotations], która ułatwia tworzenie aplikacji na Androida. W tym celu skopiujemy przykład [Exemple-02] do [Exemple-03], postępując zgodnie z procedurą [1-16].

  • na [1], przejdź do widoku [Project], aby zobaczyć cały projekt dla systemu Android;

Uwaga: między plikami [14] a [15] nastąpiło przejście z perspektywy [Android] do perspektywy [Project] (patrz punkt 1.2.2.1).

Następnie modyfikujemy plik [res / values / strings.xml] [17]:

 

Plik [strings.xml] zostaje zmodyfikowany w następujący sposób:


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

Teraz uruchamiamy nową aplikację, która przejęła całą konfigurację z pliku [Exemple-02]:

 

W przypadku [19] uzyskujemy ten sam wynik, co w przypadku [Exemple-02], ale z nową nazwą.

Teraz wprowadzimy bibliotekę [Android Annotations], którą dla wygody nazwiemy AA. Biblioteka ta wprowadza nowe klasy służące do adnotowania kodu źródłowego Androida. Adnotacje te będą wykorzystywane przez procesor, który utworzy w module nowe klasy Java, które wezmą udział w jego kompilacji na równi z klasami napisanymi przez programistę. Otrzymujemy zatem następujący łańcuch kompilacji:

Najpierw dodamy do pliku [build.gradle] zależności od kompilatora adnotacji AA (procesor wspomniany powyżej):


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'])
}
  • w wierszach 4–5 dodano dwie zależności, które tworzą bibliotekę AA;

Plik [build.gradle] zostaje ponownie zmodyfikowany w celu wykorzystania wtyczki o nazwie [android-apt], która zmienia proces kompilacji na dwuetapowy:

  • przetwarzanie adnotacji Androida, co prowadzi do powstania nowych klas;
  • kompilacja wszystkich klas projektu;

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    // Od wersji 0.11 wtyczki Gradle dla systemu Android należy używać android-apt w wersji >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'
  • wiersz 8: wersja wtyczki [android-apt], która zostanie wyszukana w centralnym repozytorium Maven (wiersz 3);
  • wiersz 13: aktywacja tej wtyczki;

Na tym etapie sprawdź, czy konfiguracja uruchomienia [app] nadal działa.

Teraz dodamy pierwszą adnotację biblioteki AA w klasie [MainActivity]:

  

Klasa [MainActivity] wygląda na razie następująco:


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

Kod ten wyjaśniliśmy już w punkcie 1.3.3. Modyfikujemy go w następujący sposób:


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);
  }
}
  • wiersz 7: adnotacja [@EActivity] jest adnotacją AA (wiersz 3). Jej parametrem jest widok powiązany z aktywnością;

Ta adnotacja spowoduje utworzenie klasy [MainActivity_] wywodzącej się z klasy [MainActivity] i to właśnie ta klasa będzie stanowić rzeczywistą aktywność. Musimy zatem zmodyfikować manifest projektu [AndroidManifest.xml] w następujący sposób:


<?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>
  • wiersz 11: nowa aktywność;

Po wykonaniu tych czynności możemy skompilować projekt [1]:

 
  • w pliku [2] widoczna jest klasa [MainActivity_] wygenerowana w folderze [app / build / generated / source / apt / debug];

Wygenerowana klasa [MainActivity_] ma następujący wygląd:


//
// DO NOT EDIT THIS FILE.
// Wygenerowano przy użyciu AndroidAnnotations 4.0.0.
// 
// Możesz stworzyć większe dzieło zawierające ten plik i rozpowszechniać je na wybranych przez siebie warunkach.
//


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);
    }
...
  • wiersze 24–25: klasa [MainActivity_] rozszerza klasę [MainActivity];

Nie będziemy próbować wyjaśniać kodu klas wygenerowanych przez AA. Zarządzają one złożonością, którą adnotacje starają się ukryć. Czasami jednak warto się z nim zapoznać, gdy chcemy zrozumieć, w jaki sposób „tłumaczone” są stosowane przez nas adnotacje.

Teraz możemy ponownie uruchomić konfigurację [app]. Otrzymujemy ten sam wynik, co poprzednio. Będziemy teraz opierać się na tym projekcie, który skopiujemy, aby przedstawić ważne pojęcia związane z programowaniem dla systemu Android.

1.5. Przykład 04: widoki i zdarzenia

1.5.1. Tworzenie projektu

Postępujemy zgodnie z procedurą opisaną w punkcie 1.4, aby skopiować projekt [Exemple-02] do projektu [Exemple-03]:

My:

  • duplikujemy projekt [Exemple-03] do [Exemple-04] (po usunięciu folderu [app / build] z [Exemple-03]);
  • wczytujemy projekt [Exemple-04];
  • zmieńmy nazwę projektu w pliku [app / res / values / strings.xml] (perspektywa Android);
  • usuńmy plik [Exemple-04 / Exemple-04.iml] (perspektywa Project);
  • skompilujmy, a następnie uruchommy projekt;
 

1.5.2. Tworzenie widoku

Teraz za pomocą edytora graficznego zmodyfikujemy widok wyświetlany przez projekt [Exemple-04]:

  • w projekcie [1-4] utwórz nowy widok o nazwie XML;
  • w [5] nadaj nazwę widoku;
  • w [6] należy określić tag główny widoku. W tym przypadku wybieramy kontener o nazwie [RelativeLayout]. W tym kontenerze komponenty są rozmieszczane względem siebie: „po prawej stronie”, „po lewej stronie”, „poniżej”, „powyżej”;
  

Plik [vue1.xml] wygenerowany przez [7] wygląda następująco:


<?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>
  • wiersz 2: pusty kontener [RelativeLayout], który zajmie całą szerokość tabletu (wiersz 3) i całą jego wysokość (wiersz 4);
  • w [1] wybierz zakładkę [Design] w wyświetlonym widoku [vue1.xml];
  • w [2-4] przejdź do trybu tabletu;
  • w widoku [5] ustaw skalę tabletu na 1;
  • w pliku [6] wybierz tryb „poziomy” dla tabletu;
  • zrzut ekranu [7] przedstawia dokonane wybory.
  • w pliku [1] należy wybrać plik [Large Text] i przeciągnąć go na widok [2];
  • w widoku [3] kliknij dwukrotnie na komponent;
  • w [4], zmień wyświetlany tekst. Zamiast umieszczać go na stałe w widoku XML, przeniesiemy go do pliku [res / values / string.xml]
  • w pliku [5] dodajemy nową wartość do pliku [strings.xml];
  • w pliku [8] nadaje się identyfikator łańcuchowi;
  • w [9] podaje się wartość ciągu znaków;
  • w [10] – nowy widok po zatwierdzeniu poprzedniego etapu;
  • po dwukrotnym kliknięciu na komponent zmienia się jego identyfikator [11];
  • na [12]; w właściwościach komponentu zmienia się rozmiar czcionki z [50sp];
  • na [13], nowy widok;

Plik [vue1.xml] uległ następującym zmianom:


<?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>
  • zmiany wprowadzone w interfejsie graficznym znajdują się w wierszach 10, 11 i 14. Pozostałe atrybuty pliku [TextView] to wartości domyślne lub wynikają z położenia komponentu w widoku;
  • wiersze 7–8: rozmiar komponentu odpowiada wysokości i szerokości zawartego w nim tekstu (wrap_content);
  • wiersz 13: górna krawędź komponentu jest wyrównana do górnej krawędzi widoku (wiersz 13), 50 pikseli poniżej (wiersz 13);
  • wiersz 12: lewa krawędź komponentu jest wyrównana do lewej krawędzi widoku (wiersz 13), 213 pikseli w prawo (wiersz 12);

Zazwyczaj dokładne rozmiary marginesów lewego, prawego, górnego i dolnego są ustalane bezpośrednio w pliku XML.

Postępując w ten sam sposób, utwórz następujący widok [1]:

 

Składniki są następujące:

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

Umieszczanie komponentów względem siebie może być frustrujące, ponieważ reakcje edytora graficznego bywają nieprzewidywalne. Lepiej jest skorzystać z właściwości komponentów:

Komponent [textView1] należy umieścić 50 pikseli poniżej tytułu i 50 pikseli od lewej krawędzi kontenera:

  • w przypadku [1] górna krawędź (top) komponentu jest wyrównana względem dolnej krawędzi (bottom) komponentu [textViewTitreVue1] w odległości 50 pikseli od [3] (top);
  • w elemencie [2] lewa krawędź (left) komponentu jest wyrównana względem lewej krawędzi kontenera w odległości 50 pikseli od [3] (left);

Komponent [editTextNom] musi być umieszczony 60 pikseli na prawo od komponentu [textView1] i wyrównany od dołu do tego samego komponentu;

 
  • w przypadku [1] lewa krawędź (left) komponentu jest wyrównana względem prawej krawędzi (right) komponentu [textView1] w odległości 60 pikseli od komponentu [2] (left). Jest on wyrównany do dolnej krawędzi (bottom:bottom) komponentu [textView1] [1];

Komponent [buttonValider] powinien być umieszczony 60 pikseli na prawo od komponentu [editTextNom] i wyrównany od dołu do tego samego komponentu;

 
  • w przypadku komponentu [1] lewa krawędź (left) komponentu jest wyrównana względem prawej krawędzi (right) komponentu [editTextNom] w odległości 60 pikseli od komponentu [2] (left). Jest on wyrównany do dolnej krawędzi komponentu (bottom:bottom) [editTextNom] [1];

Komponent [buttonVue2] należy umieścić 50 pikseli poniżej komponentu [textView1] i wyrównać do lewej krawędzi tego samego komponentu;

 
  • w przypadku komponentu [1] lewa krawędź (left) komponentu jest wyrównana względem lewej krawędzi (left) komponentu [textView1] i umieszczona poniżej (top:bottom) w odległości 50 pikseli od komponentu [2] (top);

Wygenerowany plik XML wygląda następująco:


<?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>

Znajduje się w nim wszystko, co zostało zrobione graficznie. Innym sposobem na utworzenie widoku jest więc bezpośrednie napisanie tego pliku. Gdy już się do tego przyzwyczaimy, może to być szybsze niż korzystanie z edytora graficznego.

  • W wierszu 38 znajduje się informacja, której nie pokazaliśmy. Jest ona podana poprzez właściwości komponentu [editTextNom] [1]:
 

Wszystkie teksty pochodzą z następującego pliku [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>

Teraz zmodyfikujmy aktywność [MainActivity], aby ten widok był wyświetlany przy uruchomieniu aplikacji:


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);
  }
}
  • wiersz 7: aktywność wyświetla teraz widok [vue1.xml];

Zmodyfikuj plik [AndroidManifest.xml] w następujący sposób:


<?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>
  • wiersz 12: ten wiersz konfiguracji zapobiega wyświetlaniu klawiatury po wyświetleniu widoku [vue1]. Widok ten zawiera bowiem pole wprowadzania danych, które ma fokus w momencie wyświetlenia widoku. Ten fokus powoduje domyślne wyświetlenie klawiatury wirtualnej;

Uruchom aplikację i sprawdź, czy wyświetlany jest rzeczywiście widok [vue1.xml]:

Image

1.5.3. Obsługa zdarzeń

Zajmijmy się teraz obsługą kliknięcia przycisku [Valider] w widoku [Vue1]:

Image

Kod [MainActivity] zmienia się w następujący sposób:


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 {

  // elementy interfejsu graficznego
  @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");
  }

  // menedżer zdarzeń
  @Click(R.id.buttonValider)
  protected void doValider() {
    // wyświetlana jest wpisana nazwa
    Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

}
  • wiersze 17–18: przypisuje się pole [protected EditText editTextNom] do komponentu identyfikatora [R.id.editTextNom] interfejsu wizualnego. Pole powiązane z komponentem musi być dostępne w klasie pochodnej [MainActivity_] i z tego powodu nie może należeć do zakresu [private]. Pole oznaczone identyfikatorem [R.id.editTextNom] pochodzi z widoku [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"/>

Uwaga: nie należy używać znaków akcentowanych w identyfikatorach typu [id]. AA nie obsługuje ich poprawnie.

  • wiersz 32: adnotacja [@Click(R.id.buttonValider)] określa metodę obsługującą zdarzenie „Click” na przycisku o identyfikatorze [R.id.buttonValider]. Identyfikator ten pochodzi również z widoku [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"/>
  • linia 35: wyświetla wprowadzoną nazwę:
    • Toast.makeText(...).show(): wyświetla tekst na ekranie,
    • pierwszym parametrem funkcji makeText jest aktywność,
    • drugi parametr to tekst, który ma zostać wyświetlony w okienku wyświetlanym przez makeText,
    • trzecim parametrem jest czas wyświetlania okienka: Toast.LENGTH_LONG lub Toast.LENGTH_SHORT;
  • w wierszu 26 adnotacja [@AfterViews] określa metodę, która ma zostać wykonana po zainicjowaniu wszystkich pól oznaczonych adnotacją [@ViewById]. Ważne jest, aby wiedzieć, kiedy te pola są inicjowane. Na przykład, czy w metodzie [onCreate] można wykorzystać odwołanie z wiersza 18? Aby odpowiedzieć na to pytanie, umieściliśmy logi;

Uruchom projekt [Exemple-04] i sprawdź, czy po kliknięciu przycisku [Valider] dzieje się coś. Otrzymujemy następujące wpisy w dzienniku:

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

Wnioskujemy z tego, że podczas wykonywania metody [onCreate] pola oznaczone przez [@ViewById] nie są jeszcze zainicjowane. Ponownie zachęcamy początkujących czytelników do umieszczania tego typu logów w metodach zarządzających cyklem życia aplikacji.

1.6. Przykład 05: nawigacja między widokami

W poprzednim projekcie przycisk [Vue n° 2] nie został wykorzystany. Proponujemy wykorzystać go poprzez utworzenie drugiego widoku i pokazanie, jak przechodzić między widokami. Istnieje kilka sposobów rozwiązania tego problemu. Proponowanym tutaj rozwiązaniem jest powiązanie każdego widoku z aktywnością. Inną metodą jest posiadanie jednej aktywności typu [AppCompatActivity], która wyświetla widoki typu [Fragment]. Będzie to metoda stosowana w przyszłych aplikacjach.

1.6.1. Tworzenie projektu

Duplikujemy projekt [Exemple-04] do [Exemple-05]. W tym celu należy postępować zgodnie z procedurą opisaną w punkcie 1.4, dotyczącą powielania projektu [Exemple-02] do projektu [Exemple-03], która została powtórzona w punkcie 1.5.

1.6.2. Dodanie drugiej aktywności

Aby zarządzać drugim widokiem, utworzymy drugą aktywność. To właśnie ona będzie zarządzać widokiem nr 2. Mamy tu do czynienia z modelem „jeden widok = jedna aktywność”. Istnieją również inne możliwe modele.

123

Image

  • w [1-4] tworzymy nową aktywność;

Image

  • w [5] – nazwa klasy, która zostanie wygenerowana;
  • w [6] – nazwa widoku (vue2.xml) powiązanego z nową aktywnością;
  
  • w [7-8] – pliki, na które ma wpływ poprzednia konfiguracja;

Czynność [SecondActivity] wygląda następująco:


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);
  }
}
  • wiersz 11: aktywność jest powiązana z widokiem [vue2.xml];

Widok [vue2.xml] wygląda następująco:


<?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>

Jest to na razie pusty widok z menedżerem układu typu [RelativeLayout] (wiersz 2). W wierszu 11 widać, że został on powiązany z nową aktywnością.

Manifest modułu Android [AndroidManifest.xml] uległ następującym zmianom:


<?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>

W wierszu 20 zarejestrowano drugą aktywność.

1.6.3. Przejście z widoku nr 1 do widoku nr 2

Wróćmy do kodu klasy [MainActivity], która wyświetla widok nr 1. Przejście do widoku nr 2 nie jest obecnie obsługiwane:

  

Obsługujemy to w następujący sposób:


  // przejdź do widoku nr 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // przechodzimy do widoku nr 2, przekazując mu nazwę wprowadzoną w widoku nr 1
    // tworzy się Intent
    Intent intent = new Intent();
    // przypisuje się ten Intent do aktywności
    intent.setClass(this, SecondActivity.class);
    // przypisuje się informacje do tego Intentu
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // uruchamia się aktywność typu [SecondActivity], przekazując jej Intent
    startActivity(intent);
}
  • wiersze 2–3: metoda [navigateToView2] obsługuje „kliknięcie” przycisku oznaczonego jako [R.id.buttonVue2], zdefiniowanego w widoku [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"/>

Komentarze opisują kroki, które należy wykonać, aby zmienić widok:

  1. wiersz 6: utworzyć obiekt typu [Intent]. Obiekt ten pozwoli określić zarówno aktywność, która ma zostać uruchomiona, jak i informacje, które należy jej przekazać;
  2. wiersz 8: powiązanie Intent z aktywnością, w tym przypadku aktywnością typu [SecondActivity], która będzie odpowiedzialna za wyświetlenie widoku nr 2. Należy pamiętać, że aktywność [MainActivity] wyświetla widok nr 1. Zatem jeden widok odpowiada jednej aktywności. Będziemy musieli zdefiniować typ [SecondActivity];
  3. wiersz 10: opcjonalnie można wprowadzić informacje do obiektu [Intent]. Są one przeznaczone dla aktywności [SecondActivity], która zostanie uruchomiona. Parametry [Intent.putExtra] to (klucz obiektu, wartość obiektu). Należy zauważyć, że metoda [EditText.getText()], która zwraca tekst wprowadzony w polu wprowadzania danych, nie zwraca typu [String], lecz typu [Editable]. Aby uzyskać wprowadzony tekst, należy użyć metody [toString];
  4. wiersz 12: uruchom aktywność zdefiniowaną przez obiekt [Intent].

Uruchom projekt [Exemple-05] i sprawdź, czy wyświetla się widok nr 2 (na razie pusty):

1.6.4. Tworzenie widoku nr 2

 
  • w [1-2] usuwamy widok [main.xml], który nie jest już potrzebny, a następnie modyfikujemy widok [vue2.xml] w następujący sposób:
 

Składniki są następujące:

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

Plik XML [vue2.xml] ma następującą treść:


<?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>

Uruchom projekt [Exemple-05] i sprawdź, czy po kliknięciu przycisku [Vue n° 2] wyświetla się nowy widok.

1.6.5. Aktywność [SecondActivity]

W [MainActivity] napisaliśmy następujący kod:


    // przechodzimy do widoku nr 2
    protected void navigateToView2() {
        // przechodzimy do widoku nr 2, przekazując mu nazwę wprowadzoną w widoku nr 1
        // tworzymy Intent
        Intent intent = new Intent();
        // przypisujemy ten Intent do aktywności
        intent.setClass(this, SecondActivity.class);
        // przypisujemy informacje do tego Intentu
        intent.putExtra("NOM", edtNom.getText().toString().trim());
        // uruchamia się aktywność typu [SecondActivity], przekazując jej Intent
        startActivity(intent);
}

W wierszu 9 umieściliśmy dla [SecondActivity] informacje, które nie zostały wykorzystane. Wykorzystujemy je teraz, a odbywa się to w kodzie [SecondActivity]:

  

Kod [SecondActivity] zmienia się w następujący sposób:


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 {

  // elementy interfejsu wizualnego
  @ViewById
  protected TextView textViewBonjour;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  @AfterViews
  protected void afterViews() {
    // pobieramy intencję, jeśli istnieje
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // pobieramy nazwę
        String nom = extras.getString("NOM");
        if (nom != null) {
          // wyświetla się ją
          textViewBonjour.setText(String.format("Bonjour %s !", nom));
        }
      }
    }
  }

}
  • wiersz 11: używamy adnotacji [@EActivity], aby wskazać, że klasa [SecondActivity] jest aktywnością powiązaną z widokiem [vue2.xml];
  • wiersze 15–16: pobierane jest odwołanie do komponentu [TextView] identyfikowanego przez [R.id.textViewBonjour]. W tym miejscu nie wpisano [@ViewById(R.id.textViewBonjour)]. W takim przypadku AA zakłada, że identyfikator komponentu jest identyczny z poliem opatrzonym adnotacją, w tym przypadku polem [textViewBonjour];
  • wiersz 23: adnotacja [@AfterViews] odnosi się do metody, która musi zostać wykonana po zainicjowaniu pól oznaczonych adnotacją [@ViewById]. W metodzie [OnCreate] (wiersz 19) nie można korzystać z tych pól, ponieważ nie zostały one jeszcze zainicjowane. W projekcie [Exemple-05] następuje przejście z jednej aktywności do drugiej i z góry nie było jasne, czy metoda oznaczona adnotacją [@AfterViews] zostanie wykonana jednokrotnie podczas początkowej instancji aktywności, czy też za każdym razem, gdy aktywność zostanie uruchomiona. Testy wykazały, że druga hipoteza była prawdziwa;
  • wiersz 26: klasa [AppCompatActivity] posiada metodę [getIntent], która zwraca obiekt [Intent] powiązany z aktywnością;
  • wiersz 28: metoda [Intent.getExtras] zwraca typ [Bundle], który jest rodzajem słownika zawierającego informacje powiązane z obiektem [Intent] danej aktywności;
  • wiersz 31: pobierana jest nazwa umieszczona w obiekcie [Intent] danej czynności;
  • wiersz 34: wyświetla się tę nazwę.

Przypomnienie: pola oznaczone adnotacją [@ViewById] nie mogą zawierać znaków akcentowanych.

Wróćmy do klasy [SecondActivity]. Ponieważ napisaliśmy:


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

AA wygeneruje klasę [SecondActivity_] wywodzącą się z [SecondActivity] i to właśnie ta klasa będzie stanowić rzeczywistą aktywność. To prowadzi nas do wprowadzenia zmian w:

[MainActivity]


  // przechodzimy do widoku nr 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // powiązanie tego Intentu z aktywnością
    intent.setClass(this, SecondActivity_.class);
    ...
}
  • w wierszu 6 należy zastąpić [SecondActivity] na [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>
  • w wierszu 20 należy zastąpić [SecondActivity] na [SecondActivity_];

Przetestuj tę nową wersję. Wpisz nazwę w widoku nr 1 i sprawdź, czy widok nr 2 prawidłowo ją wyświetla.

1.6.6. Przechodzenie z widoku nr 2 do widoku nr 1

Aby przejść z widoku nr 2 do widoku nr 1, zastosujemy procedurę opisaną wcześniej:

  • umieścić kod nawigacyjny w aktywności [SecondActivity], która wyświetla widok nr 2;
  • napisać metodę [@AfterViews] w aktywności [MainActivity], która wyświetla widok nr 1;

Kod aktywności [SecondActivity] zmienia się w następujący sposób:


  @Click(R.id.buttonVue1)
  protected void navigateToView1() {
    // tworzymy Intent dla aktywności [MainActivity]
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity_.class);
    // pobieramy Intent z bieżącej aktywności [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // umieszczamy nazwę w intentcie [MainActivity]
        intent1.putExtra("NOM", extras2.getString("NOM"));
      }
      // uruchamia się [MainActivity]
      startActivity(intent1);
    }
}
  • wiersze 1–2: przypisujemy metodę [navigateToView1] do kliknięcia przycisku [btn_vue1];
  • wiersz 4: tworzy się nowy kod [Intent];
  • wiersz 5: przypisuje się do działania [MainActivity_];
  • wiersz 7: pobieramy Intent powiązany z [SecondActivity];
  • wiersz 9: pobierane są informacje z tego Intentu;
  • wiersz 12: klucz [NOM] jest pobierany z [intent2] w celu umieszczenia go w [intent1] z tą samą przypisaną wartością;
  • wiersz 15: uruchamiana jest aktywność [MainActivity_].

W kodzie [MainActivity] dodaje się następującą metodę [@AfterViews]:


  @AfterViews
  protected void afterViews() {
    // pobieramy intent, jeśli istnieje
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // pobieramy nazwę
        String nom = extras.getString("NOM");
        if (nom != null) {
          // wyświetla się
          editTextNom.setText(nom);
        }
      }
    }
}

Wprowadź te zmiany i przetestuj aplikację. Teraz, po powrocie z widoku nr 2 do widoku nr 1, powinna pojawić się nazwa wprowadzona początkowo, co do tej pory nie miało miejsca.

1.6.7. Cykl życia aktywności

W punkcie 1.3.5 przedstawiliśmy cykl życia aktywności. Mamy tutaj dwie aktywności i przełączamy się między nimi podczas wykonywania. Aktywności te zawierają dwie metody, co do których nie wiadomo dokładnie, kiedy są wywoływane jedna względem drugiej: [onCreate] i [afterViews]. Ważne jest, aby to wiedzieć. W tym celu dodajemy logi do obu aktywności:

W klasie [MainActivity] zapisujemy zatem:


  // producent
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity", "onCreate");
    ...
  }

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
    ...
    }
}
  • wiersze 2–4: chcemy wiedzieć, czy klasa [MainActivity] jest instancjonowana jeden lub więcej razy;
  • wiersz 8: chcemy sprawdzić, czy metoda [onCreate] jest wywoływana raz czy więcej razy;
  • wiersz 14: chcemy sprawdzić, czy metoda [afterViews] jest wywoływana raz czy więcej razy;

Dokładnie to samo robimy w klasie [SecondActivity].

Po uruchomieniu aplikacji otrzymujemy następujące wpisy w dzienniku:

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

Metody [onCreate, afterViews] z pierwszej aktywności zostały wykonane w tej kolejności. Po kliknięciu przycisku [Vue n° 2] pojawiają się następujące nowe wpisy w dzienniku:

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

Metody [onCreate, afterViews] z drugiej czynności zostały wykonane w tej kolejności. Po kliknięciu przycisku [Vue n° 1] pojawiają się następujące nowe logi:

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

Klasa [MainActivity] została zatem ponownie zainicjowana. Po kliknięciu przycisku [Vue n° 2] pojawiają się następujące nowe wpisy w dzienniku:

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

W związku z tym ponownie tworzona jest instancja klasy [SecondActivity].

Obie aktywności są zatem systematycznie odtwarzane przy każdej zmianie aktywności.

Teraz zapoznamy się z architekturą opartą na jednej aktywności, która może obsługiwać wiele widoków zwanych fragmentami. Aktywność i widoki będą instancjonowane tylko raz, w przeciwieństwie do poprzedniej metody, w której aktywność mogła być instancjonowana wielokrotnie.

1.7. Przykład 06: nawigacja za pomocą zakładek

W tym miejscu przyjrzymy się interfejsom z zakładkami. Przykład ten jest złożony, ale wprowadza wszystkie elementy, z których będziemy korzystać w dalszej części: pojedynczą aktywność, menedżera fragmentów (widoków), kontener fragmentów oraz nawigację między fragmentami. Pojęcie zakładek różni się od pojęcia fragmentów i ma drugorzędne znaczenie w kontekście tego, co chcemy pokazać w tym przykładzie.

1.7.1. Tworzenie projektu

Tworzymy nowy projekt:

 
  • w [7] wybieramy aktywność z zakładkami (Tabbed Activity);
  • w [10-14] należy zachować wartości domyślne;
  • w [15] wybieramy zakładki z paskiem tytułu;

Utworzony projekt wygląda następująco:

 
  • w pliku [1] – aktywność;
  • w [2] – widoki;

Automatycznie utworzono konfigurację wykonania [app], o nazwie modułu, [2b]:

 

Można ją uruchomić. Pojawi się wtedy okno z trzema zakładkami o nazwie [3-6]:

Image

1.7.2. Konfiguracja Gradle

Projekt [Exemple-06] został wygenerowany wraz z następującym plikiem [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'
}

W porównaniu z tym, co już znamy, pojawiła się nowość: wiersz 25. Biblioteka ta jest niezbędna dla nowych komponentów wykorzystywanych przez wygenerowaną aplikację.

1.7.3. Widok [activity_main]

  

Widok [activity_main] jest widokiem powiązanym z aktywnością [MainActivity] w projekcie. W trybie [design] widok wygląda następująco:

Image

Zawiera następujące komponenty:

  
  • [main_content] stanowi całość widoku;
  • [appbar] (ramka czerwona, 1) to pasek aplikacji. Zawiera on dwa komponenty:
    • [toolbar] (żółta ramka 4) to pasek narzędzi;
    • [tabs] (pomarańczowa ramka 5) to pasek tytułów kart;
  • [container] (zielona ramka, 2) może pomieścić różne fragmenty. Fragment to widok. W ten sposób ta sama aktywność będzie mogła wyświetlać kilka widoków (fragmentów) w tym kontenerze;
  • [fab] (komponent 3) nazywany jest komponentem pływającym;

W trybie [text] kod wygląda następująco:


<?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>

Znajdujemy tu elementy opisane wcześniej:

  • wiersze 2–49: definicja komponentu [main_content] (wiersz 5), który stanowi całość widoku. Widać, że jest to layout (menedżer rozmieszczenia komponentów) typu [CoordinatorLayout] (wiersz 2);
  • wiersze 11–33: kontener [appbar] (wiersz 12). Jest to layout typu [AppBarLayout] (wiersz 11);
  • wiersze 18–24: komponent [toolbar] (wiersz 19) typu [Toolbar] (wiersz 18);
  • wiersze 28–31: kontener [tabs] (wiersz 29). Jest to layout typu [TabLayout] (wiersz 28). Będzie on wyświetlał tytuły zakładek;
  • wiersze 35–39: komponent [container] (wiersz 36). To właśnie ten kontener wyświetla różne widoki aktywności;
  • wiersze 41–47: komponent [fab] (wiersz 42) typu [FloatingActionButton] (wiersz 41). Jest to przycisk, który można kliknąć. Domyślnie znajduje się on w prawym dolnym rogu widoku ogólnego;

Nie będziemy próbować zrozumieć znaczenia wszystkich atrybutów tych komponentów. Będziemy z nich korzystać takimi, jakie są. Ich rolę odkrywa się wraz z nabieraniem doświadczenia, często w trybie [design]. W tym trybie okazuje się, że komponenty mają kilkadziesiąt atrybutów. Zazwyczaj tylko niektóre z nich są inicjowane, podczas gdy pozostałe zachowują wartości domyślne.

Wyjaśnijmy jednak kilka kwestii. Większość wartości konfigurujących różne widoki znajduje się w folderze [res / values]:

  

Wartości te są odwołane w wierszach 15–16, 23, 39, 46 pliku [activity_main.xml]. Weźmy przykład:

  • wiersz 15:

    android:paddingTop="@dimen/appbar_padding_top"

Adnotacja [@dimen] odnosi się do pliku [res / values / dimens.xml]:


<resources>
  <!-- Domyślne marginesy ekranu, zgodnie z wytycznymi projektowymi systemu Android. -->
  <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>

Wiersz 15 pliku [activity_main.xml] odnosi się do wiersza (f) powyżej;

Analogicznie adnotacja:

  • [@string] odnosi się do pliku zasobów [res / values / strings.xml];
  • [@color] odnosi się do pliku zasobów [res / values / colors.xml];
  • [@style] odnosi się do pliku zasobów [res / values / styles.xml];

1.7.4. Czynność

  

Kod wygenerowany dla tej aktywności jest zgodny z opisem przedstawionym powyżej: jest złożony. Przeanalizujemy go w kilku etapach.

1.7.4.1. Zarządzanie fragmentami i zakładkami

Kod [MainActivity] dotyczący fragmentów i zakładek wygląda następująco:


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 {

  // menedżer fragmentów
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // kontener fragmentów 
  private ViewPager mViewPager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // element nadrzędny
    super.onCreate(savedInstanceState);
    // widok
    setContentView(R.layout.activity_main);
    // pasek narzędzi
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // menedżer fragmentów
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // kontener fragmentów jest powiązany z menedżerem fragmentów
    // czyli fragment nr i z kontenera fragmentów jest fragmentem nr i dostarczonym przez menedżera fragmentów
    mViewPager = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // pasek zakładek jest również powiązany z kontenerem fragmentów
    // czyli zakładka nr i wyświetla fragment nr i z kontenera
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
   }


  // fragment
  public static class PlaceholderFragment extends Fragment {
 ...
  }

  // menedżer fragmentów
  // to właśnie do niego kierowane są żądania dotyczące fragmentów, które mają być wyświetlane w widoku głównym
  // musi definiować metody [getItem] i [getCount] – pozostałe są opcjonalne
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • wiersz 28: system Android udostępnia kontener widoków typu [android.support.v4.view.ViewPager] (wiersz 12). Należy dostarczyć do tego kontenera menedżera widoków lub fragmentów. Dostarcza go programista;
  • wiersz 25: menedżer fragmentów używany w tym przykładzie. Jego implementacja znajduje się w wierszach 61–63;
  • wiersz 31: metoda wykonywana podczas tworzenia aktywności;
  • wiersz 35: widok [activity_main.xml] jest powiązany z aktywnością;
  • wiersz 37: pobieramy odwołanie do komponentu [toolbar] z widoku na podstawie jego identyfikatora;
  • wiersz 38: ten pasek narzędzi staje się paskiem akcji (pojęcie z systemu Android) aktywności;
  • wiersz 40: tworzona jest instancja menedżera fragmentów. Parametrem konstruktora jest klasa Android [android.support.v4.app.FragmentManager] (wiersz 10);
  • wiersz 44: w widoku [activity_main.xml] pobieramy odwołanie do kontenera fragmentów na podstawie jego identyfikatora;
  • wiersz 45: menedżer fragmentów jest powiązany z kontenerem fragmentów. Oznacza to, że gdy kontener fragmentów otrzyma żądanie wyświetlenia fragmentu nr i, zostanie ono przekazane do menedżera fragmentów;
  • wiersz 48: pobieramy odwołanie do paska zakładek na podstawie jego identyfikatora;
  • wiersz 49: menedżer zakładek jest powiązany z kontenerem fragmentów. Oznacza to, że po kliknięciu zakładki nr i kontener wyświetli fragment nr i. Powiązanie między menedżerem zakładek a kontenerem fragmentów pozwala nam uniknąć konieczności zarządzania zakładkami. Dzięki temu nie musimy definiować menedżera zdarzeń dla kliknięcia na zakładkę. Powiązanie z kontenerem fragmentów zapewnia to domyślnie. Przyjrzymy się przykładowi, w którym będzie więcej fragmentów niż zakładek. W takim przypadku nie tworzymy tego powiązania.

Obsługa fragmentów [SectionsPagerAdapter] wygląda następująco:


// menedżer fragmentów
  // to do niego kierowane są żądania dotyczące fragmentów, które mają być wyświetlane w widoku głównym
  // musi zdefiniować metody [getItem] i [getCount] – pozostałe są opcjonalne
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }

    // numer fragmentu i pozycja
    @Override
    public Fragment getItem(int position) {
      // tworzy instancję fragmentu [PlaceHolder] i zwraca ją
      return PlaceholderFragment.newInstance(position + 1);
    }

    // zwraca liczbę zarządzanych fragmentów
    @Override
    public int getCount() {
      return 3;
    }

    // opcjonalne – nadaje tytuł zarządzanym fragmentom
    @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;
    }
  }
}
  • fragmenty wyświetlane przez aplikację zależą od niej samej. Zarządzacz fragmentów jest definiowany przez programistę;
  • wiersz 5: menedżer fragmentów rozszerza klasę Android [android.support.v4.app.FragmentPagerAdapter]. Konstruktor jest narzucony. Musimy zdefiniować co najmniej dwie następujące metody:
    • int getCount(): zwraca liczbę fragmentów do obsługi;
    • Fragment getItem(i): zwraca fragment nr i;

Metoda CharSequence getPageTitle(i), która generuje tytuł fragmentu nr i, jest opcjonalna. Ponieważ menedżer zakładek został powiązany z menedżerem fragmentów, tytuł zakładki nr i będzie tytułem fragmentu nr i. W ten sposób tytuły w wierszach 27–33 będą tytułami zakładek;

  • wiersze 18–21: getCount określa liczbę zarządzanych fragmentów, w tym przypadku trzy;
  • wiersze 11–15: getItem(i) zwraca fragment nr i. W tym przypadku wszystkie fragmenty będą identyczne i będą miały postać [PlaceholderFragment];
  • wiersze 24–35: getPageTitle(int i) zwraca tytuł fragmentu nr i;

1.7.4.2. Wyświetlane fragmenty

  

Fragmenty tej aktywności mają tutaj ten sam typ i wszystkie są powiązane z następującym widokiem: 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>
  • wiersze 1–16: element layout typu [RelativeLayout];
  • wiersze 11–14: jedyny element widoku (fragment): [TextView] identyfikowany przez [section_label];

W [MainActivity] obsługiwane fragmenty są typu [PlaceholderFragment], a mianowicie:


// fragment
  public static class PlaceholderFragment extends Fragment {
      // tekst wyświetlany w fragmencie
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
    }

    // zwraca fragment z informacją: numer fragmentu przekazany jako parametr
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // informacja wbudowana
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // wynik
      return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // widok [fragment_main] został zainicjowany
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // znaleziono plik [TextView]
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // jego zawartość została zmodyfikowana
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // zwracany jest widok
      return rootView;
    }
  }
  • wiersz 2: klasa [PlaceholderFragment] rozszerza klasę Androida [Fragment]. Zazwyczaj tak właśnie jest;
  • wiersz 2: klasa [PlaceholderFragment] jest statyczna. Jej metoda [newInstance] (wiersz 10) pozwala uzyskać instancje typu [PlaceholderFragment];
  • wiersze 10–19: metoda [newInstance] tworzy i zwraca obiekt typu [PlaceholderFragment];
  • wiersze 14–16: fragment jest tworzony z jednym argumentem;

Fragment musi definiować metodę [onCreateView] z wiersza 22. Metoda ta musi zwracać widok powiązany z fragmentem.

  • wiersz 25: widok [fragment_main.xml] jest powiązany z fragmentem;
  • wiersz 27: ten widok zawiera komponent [TextView], którego odwołanie pobieramy za pomocą jego identyfikatora;
  • wiersz 29: wyświetla się tekst w komponencie [TextView];
    • [getString] jest metodą klasy nadrzędnej [AppCompatActivity];
    • pierwszym argumentem jest numer komponentu. [R.string.section_format] oznacza numer komponentu zidentyfikowanego przez [section_format] w pliku [res / values / strings.xml] (wiersz 4 poniżej):

<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>
  • (ciąg dalszy)
    • wiersz (d) powyżej %1$d wskazuje, że argument nr 1 (%1) musi być sformatowany jako liczba całkowita ($d);
    • drugi argument [getString] to wartość, którą należy przypisać do argumentu $1 w wierszu (d) powyżej;
    • [getArguments] podaje odniesienie do pakietu argumentów fragmentu. Należy tutaj pamiętać, że każdy argument został utworzony przy użyciu następującego pakietu (wiersze f–h):

    // zwraca fragment zawierający informację: numer fragmentu przekazany jako parametr
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // informacja dołączona
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // wynik
      return fragment;
}
  • (ciąg dalszy)
    • getArguments().getInt(ARG_SECTION_NUMBER) zwróci zatem wartość [sectionNumber] z wierszy (g) i (b) powyżej;
  • wiersz 31: zwracamy tak utworzony widok;

1.7.4.3. Obsługa menu

W wygenerowanej aplikacji znajduje się menu:

  

Zawartość pliku [menu_main.xml] jest następująca:


<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>
  • wiersze 1–9: menu;
  • wiersze 5–8: element menu oznaczony jako [action_settings] (wiersz 5);
  • wiersz 6: etykieta opcji menu. Znajduje się ona w pliku [res / values / strings.xml] (wiersz (c) poniżej):

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

Powyższy kod odpowiada poniższemu widokowi (menu znajduje się w prawym górnym rogu okna uruchomieniowego systemu Android):

 

Menu to jest obsługiwane w następujący sposób w aktywności [MainActivity]:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Rozwiń menu; spowoduje to dodanie pozycji do paska akcji, jeśli jest on obecny.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Obsługuj tutaj kliknięcia elementów paska akcji. Pasek akcji będzie
    // automatycznie obsłuży kliknięcia przycisku Strona główna/W górę, o ile
    // jak tylko określisz aktywność nadrzędną w AndroidManifest.xml.
    int id = item.getItemId();

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

    return super.onOptionsItemSelected(item);
}
  • wiersze 1–6: ta metoda jest wywoływana, gdy system jest gotowy do utworzenia menu aplikacji. Parametr wejściowy [Menu menu] to puste menu, które nie zawiera jeszcze żadnych opcji;
  • wiersz 4: wykorzystywany jest plik [res / menu / menu_main.xml]. Obiektowi [Menu menu], przekazanemu jako parametr, przypisywane są opcje menu zdefiniowane w tym pliku;
  • wiersz 5: wskazano, że menu zostało utworzone;
  • wiersze 8–21: metoda [onOptionsItemSelected] jest wykonywana po kliknięciu dowolnej opcji menu;
  • wiersz 13: odwołanie do klikniętej opcji menu;
  • wiersze 16–18: jeśli kliknięta opcja ma identyfikator [action_settings], nie wykonuje się żadnych czynności i sygnalizuje się, że zdarzenie zostało przetworzone (wiersz 17);
  • wiersz 20: zdarzenie jest przekazywane do klasy nadrzędnej;

Aby lepiej zrozumieć, co dzieje się z tym menu, dodajemy logi do powyższego kodu:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("menu", "création menu en cours");
    // Rozwiń menu; spowoduje to dodanie pozycji do paska akcji, jeśli jest on obecny.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Obsługuj tutaj kliknięcia elementów paska akcji. Pasek akcji będzie
    // automatycznie obsłuży kliknięcia przycisku Strona główna/W górę, o ile
    // jak tylko określisz aktywność nadrzędną w AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    // nadrzędna
    return super.onOptionsItemSelected(item);
}

1.7.4.4. Pływający przycisk

Wygenerowany widok zawiera pływający przycisk:

  

Ten komponent jest zdefiniowany w widoku głównym [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"/>

Wiersz 7 odwołuje się do obrazu dostarczonego przez środowisko Android, przedstawiającego kopertę.

Komponent ten jest obsługiwany w klasie [MainActivity] w następujący sposób:


    // pływający przycisk
    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();
      }
});
  • wiersz 2: pobieramy odwołanie do pływającego przycisku w widoku powiązanym z aktywnością (activity_main);
  • wiersze 3–9: przypisuje się do niego procedurę obsługi kliknięcia;
  • wiersz 6: klasa [Snackbar] umożliwia wyświetlanie komunikatów tymczasowych w widoku za pomocą metody [Snackbar.make]. Pierwszym argumentem jest widok, z którego metoda [Snackbar] wyszuka widok nadrzędny, w którym ma zostać wyświetlony komunikat. W tym przypadku [view] jest widokiem klikniętej koperty (wiersz 5). Znalezionym widokiem nadrzędnym będzie widok [activity_main]. Drugim argumentem jest komunikat do wyświetlenia. Trzecim argumentem jest czas wyświetlania (SHORT lub LONG);
  • wiersz 7: można kliknąć wyświetlony komunikat i w ten sposób uruchomić akcję. W tym przypadku z kliknięciem komunikatu nie jest powiązana żadna akcja. Na koniec metoda [show] wyświetla komunikat;

Kliknięcie pływającego przycisku daje następujący efekt wizualny:

 

1.7.5. Wykonanie projektu

Teraz, gdy wyjaśniliśmy szczegóły wygenerowanego kodu, możemy lepiej zrozumieć jego działanie:

Image

Po kliknięciu zakładki nr i w kontenerze widoków wyświetlany jest fragment nr i. Widać to po tekście wyświetlonym w [4]. Można również zauważyć, że można przechodzić między zakładkami, przesuwając widok w prawo lub w lewo za pomocą myszy (przesunięcie). Zobaczymy, że zachowanie to można kontrolować.

Po kliknięciu opcji menu w [6] otrzymujemy następujące logi:

 

1.7.6. Cykl życia fragmentów

  • w [1] widać, że metoda [onCreateView] i kolejne są wykonywane podczas pierwszego wyświetlenia fragmentu oraz za każdym razem, gdy aktywność musi go ponownie wyświetlić;

Aby śledzić cykl życia aktywności i fragmentów, dodajemy następujące logi do kodu [MainActivity]:


// konstruktor
  public MainActivity(){
    Log.d("MainActivity","constructor");
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
      // element nadrzędny
    super.onCreate(savedInstanceState);
...
  }

  // fragment
  public static class PlaceholderFragment extends Fragment {
    // tekst wyświetlany we fragmencie
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
      Log.d("PlaceholderFragment", "constructor");
    }

    // zwraca fragment z informacją: numer fragmentu przekazany jako parametr
    public static PlaceholderFragment newInstance(int sectionNumber) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
      // fragment
      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)));
      ...
    }
  }


}

Ponownie uruchamiamy projekt. Pierwsze wpisy w dzienniku są następujące:

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
  • wiersz 1: utworzenie aktywności;
  • wiersz 2: wykonanie jej metody [onCreate];
  • wiersze 3–4: instancjonowanie fragmentu nr 1;
  • wiersze 5–6: utworzenie instancji fragmentu nr 2;
  • wiersz 7: inicjalizacja fragmentu nr 2;
  • wiersz 8: inicjalizacja fragmentu nr 1;
  • wiersz 9: utworzenie menu aktywności;

W tym miejscu należy przypomnieć sobie kod odpowiedzialny za tworzenie fragmentów:


  // menedżer fragmentów
  // to do niego kierowane są żądania dotyczące fragmentów, które mają być wyświetlane w widoku głównym
  // musi zdefiniować metody [getItem] i [getCount] – pozostałe są opcjonalne
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }

    // numer fragmentu i pozycja
    @Override
    public Fragment getItem(int position) {
      // tworzy się instancję fragmentu [PlaceHolder] i zwraca się ją
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • wiersze 11–15: fragment jest instancjonowany przez [newInstance] za każdym razem, gdy kontener fragmentów tego wymaga;

Powyższe logi pokazują, że dwa pierwsze fragmenty zostały zainicjowane i zainicjowane.

Teraz kliknijmy na zakładkę nr 2. Nowe logi wyglądają następująco:

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
  • wiersze 1–3: fragment nr 3 został zainicjowany i zainicjowany. Przypominamy, że wyświetlany jest fragment nr 2;

Teraz kliknijmy na zakładkę nr 3. Nie ma tam żadnych wpisów w dzienniku. Prawdopodobnie dlatego, że fragment nr 3, który miał zostać wyświetlony, został już wcześniej zainicjowany. Teraz wróćmy do zakładki nr 1. Wpisy w dzienniku wyglądają następująco:

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

Fragment nr 1 nie jest ponownie instancjonowany, ale jego metoda [onCreateView] jest ponownie wykonywana. Takie zachowanie powtarza się w przypadku pozostałych dwóch fragmentów.

Z tych logów wynika, że:

  • aktywność została instancjonowana, a następnie zainicjowana jeden raz;
  • każdy fragment został instancjonowany raz;
  • że metoda [onCreateView] każdego fragmentu została wykonana wielokrotnie;

Należy pamiętać, co potwierdzają logi, że domyślnie podczas wyświetlania fragmentu nr i instancjonowane są fragmenty i-1 i i+1, o ile nie zostały one jeszcze instancjonowane. To właśnie wyjaśnia na przykład, że podczas uruchamiania, gdy należy wyświetlić fragment nr 1, to właśnie fragmenty 1 i 2 zostały zainicjowane i zainicjowane. Z logów wynika również, że metoda [getItem(i)] jest wywoływana tylko raz, nawet jeśli fragment nr i jest wyświetlany wielokrotnie. Wydaje się zatem, że kontener fragmentów [ViewPager], który ma wyświetlić fragment nr i, żąda go raz od menedżera fragmentów [SectionsPagerAdapter]. Następnie nie żąda go ponownie i nadal korzysta z tego, który otrzymał.

Wreszcie logi dostarczają informacji na temat metody fragmentów [onCreateView]:

  • podczas uruchamiania utworzono instancje fragmentów 1 i 2 oraz wykonano ich metodę [onCreateView];
  • podczas przechodzenia z fragmentu 1 do fragmentu 2 metoda [onCreateView] fragmentu 2 nie jest ponownie wykonywana. Nie można więc jej użyć do aktualizacji fragmentu 2. Użytkownik mógł jednak wykonać w ramach fragmentu 1 operację, której wynik powinien zostać wyświetlony przez fragment 2. Widać, że metoda [onCreateView] nie będzie mogła zostać wykorzystana do aktualizacji fragmentu 2. Trzeba znaleźć inne rozwiązanie;

1.8. Przykład-07: Przykład-06 przepisany z wykorzystaniem biblioteki [AA]

1.8.1. Tworzenie projektu

Skopiujemy projekt [Exemple-06] do [Exemple-07], aby wprowadzić do niego adnotacje Androida. W tym celu należy postępować zgodnie z procedurą opisaną w punkcie 1.4. Otrzymujemy następujący wynik:

1.8.2. Konfiguracja Gradle

 

Modyfikujemy plik [build.gradle] w następujący sposób:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Od wersji 0.11 wtyczki Gradle dla systemu Android należy używać biblioteki android-apt w wersji >= 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'
}

Dodaliśmy konfigurację niezbędną do korzystania z biblioteki [Android Annotations] (patrz punkt 1.4).

1.8.3. Dodanie pierwszych adnotacji AA

Utworzymy adnotacje AA w pliku [MainActivity]:

  

Klasa [MainActivity] zmienia się w następujący sposób:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // menedżer fragmentów
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // kontener fragmentów
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // menedżer kart
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // pływający przycisk
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;


  // konstruktor
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    // pasek narzędzi
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // menedżer fragmentów
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // kontener fragmentów jest powiązany z menedżerem fragmentów
    // czyli fragment nr i z kontenera fragmentów jest fragmentem nr i dostarczonym przez menedżera fragmentów
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // pasek zakładek jest również powiązany z kontenerem fragmentów
    // czyli zakładka nr i wyświetla fragment nr i z kontenera
    tabLayout.setupWithViewPager(mViewPager);

    // przycisk pływający
    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();
      }
    });
  }
  • wiersz 1: adnotacja [@EActivity] sprawia, że [MainActivity] staje się klasą zarządzaną przez AA. Jej parametr [R.layout.activity_main] stanowi identyfikator widoku [activity_main.xml] powiązanego z tą aktywnością;
  • wiersze 11–12: komponent o identyfikatorze [R.id.tabs] jest wstrzykiwany do pola [tabLayout]. Jest to menedżer zakładek;
  • wiersze 14–15: komponent o identyfikatorze [R.id.fab] jest wstawiany do pola [fab]. Jest to pływający przycisk;
  • wiersze 23–50: kod, który wcześniej znajdował się w metodzie [onCreate], zostaje przeniesiony do metody o dowolnej nazwie, ale opatrzonej adnotacją [@AfterViews] (wiersz 23). W tak oznaczonej metodzie mamy pewność, że wszystkie komponenty interfejsu wizualnego oznaczone jako [@ViewById] zostały zainicjowane;
  • ponadto dodano logi, aby śledzić cykl życia aktywności;

Należy pamiętać, że adnotacja [@EActivity] wygeneruje klasę [MainActivity_], która będzie stanowić rzeczywistą aktywność projektu. Należy zatem zmodyfikować plik [AndroidManifest.xml] w następujący sposób:


<?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>
  • wiersz 12: nowa aktywność.

W tym momencie należy ponownie uruchomić projekt i sprawdzić, czy nadal wyświetla się interfejs z zakładkami.

1.8.4. Przepisywanie fragmentów

Przyjrzymy się teraz zarządzaniu fragmentami w projekcie. Na razie klasa [PlaceholderFragment] jest statyczną klasą wewnętrzną aktywności [MainActivity]. Powrócimy do bardziej typowego przypadku użycia, w którym fragmenty są definiowane w klasach zewnętrznych. Ponadto wprowadzamy adnotacje AA dla fragmentów.

Projekt [Exemple-07] zmienia się w następujący sposób:

  

Powyżej widoczna jest klasa [PlaceholderFragment], która została wyodrębniona poza klasę [MainActivity]. Została ona przepisana w następujący sposób:


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;

// fragment to widok wyświetlany przez kontener fragmentów
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // element interfejsu wizualnego
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;

  // numer fragmentu
  private static final String ARG_SECTION_NUMBER = "section_number";

  // konstruktor
  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)));
    // element nadrzędny
    super.onResume();
    // wyświetlanie
    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)));
    }
  }
}
  • wiersz 15: fragment jest opatrzony adnotacją [@EFragment], której parametrem jest identyfikator widoku XML powiązanego z fragmentem, w tym przypadku widoku [fragment_main.xml];
  • wiersze 19–20: wstawiają do pola [textViewInfo] odniesienie do komponentu [fragment_main.xml] zidentyfikowanego przez [R.id.section_label], który jest typem [TextView] (wiersz (l) poniżej):

<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>
  • wiersze 42–52: metoda [onResume] jest wykonywana przed wyświetleniem widoku powiązanego z fragmentem. Można jej użyć do aktualizacji interfejsu wizualnego, który ma zostać wyświetlony;
  • wiersz 47: należy wywołać metodę o tej samej nazwie z klasy nadrzędnej;
  • wiersz 49: pojawia się trudność w ustaleniu, czy metoda [onResume] może zostać wykonana przed zainicjowaniem pola z wiersza 20. Informacje na ten temat dostarczą nam logi utworzone w celu śledzenia cyklu życia fragmentu. Na razie, dla pewności, przeprowadzamy test na null;
  • wiersz 51: aktualizujemy informacje w polu [textViewInfo] za pomocą argumentu całkowitoliczbowego przekazanego do fragmentu podczas jego tworzenia;

Klasa [MainActivity] traci swoją klasę wewnętrzną [PlaceholderFragment], a jej menedżer fragmentów zmienia się w następujący sposób:


public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmenty
    private Fragment[] fragments;
    // liczba fragmentów
    private static final int FRAGMENTS_COUNT = 3;
    // nr fragmentu
    private static final String ARG_SECTION_NUMBER = "section_number";

    // producent
    public SectionsPagerAdapter(FragmentManager fm) {
      // element nadrzędny
      super(fm);
      // inicjalizacja tablicy fragmentów
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; i++) {
        // tworzy się fragment
        fragments[i] = new PlaceholderFragment_();
        // do fragmentu można przekazać argumenty
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
    }

    // pozycja fragmentu nr
    @Override
    public Fragment getItem(int position) {
        Log.d("MainActivity", String.format("getItem[%s]", position));      
      return fragments[position];
    }

    // zwraca liczbę zarządzanych fragmentów
    @Override
    public int getCount() {
      return fragments.length;
    }

    // opcjonalnie – nadaje tytuł zarządzanym fragmentom
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
    }
  }
  • wiersz 4: fragmenty są umieszczane w tablicy;
  • wiersze 16–23: inicjalizacja tablicy fragmentów odbywa się w konstruktorze. Są one typu [PlaceholderFragment_] (wiersz 18), a nie [PlaceholderFragment]. Klasa [PlaceholderFragment] została bowiem opatrzona adnotacją AA i spowoduje powstanie klasy [PlaceholderFragment_], pochodnej klasy [PlaceholderFragment], i to właśnie tej klasy musi używać aktywność. Każdy utworzony fragment otrzymuje argument typu całkowitego, który zostanie wyświetlony przez ten fragment;
  • wiersze 42–45: zmieniono tytuły fragmentów. Ponieważ są to również tytuły zakładek, powinna być widoczna zmiana na pasku zakładek;

Skompilujmy ten projekt: [Make] [1]:

 
  • w [2], widać, że klasy wygenerowane przez bibliotekę AA znajdują się w folderze [app / build / generated / source / apt / debug] (aby zobaczyć [2], należy przejść do perspektywy [Project]);

Uruchom projekt [Exemple-07] i sprawdź, czy nadal działa.

1.8.5. Analiza logów

Po uruchomieniu aplikacji pojawiają się następujące logi:

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
  • wiersz 1: tworzenie pojedynczej aktywności;
  • wiersz 2: metoda [afterViews] aktywności: jej pola oznaczone przez [@ViewById] są inicjowane;
  • wiersze 3–5: tworzenie trzech fragmentów;
  • wiersze 6–7: kontener fragmentów [ViewPager] żąda dwóch pierwszych fragmentów;
  • wiersze 8–9: metody fragmentu 2;
  • wiersze 10–11: metody fragmentu 1;
  • wiersze 12–13: metoda [onResume] fragmentu 1;
  • wiersze 14–15: metoda [onResume] z fragmentu 2;
  • wiersz 16: utworzenie menu działania;

Warto zauważyć, że otrzymujemy odpowiedź na zadane wcześniej pytanie: na przykład metoda [onResume] z fragmentu 1 (wiersz 12) jest wykonywana po metodzie [afterViews] z tego samego fragmentu (wiersz 11). Zatem gdy metoda [onResume] jest wykonywana, może ona korzystać z pól oznaczonych przez metodę [@ViewById]. Możemy zatem teraz zapisać metodę [onResume] w następujący sposób:


  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // element nadrzędny
    super.onResume();
    // wyświetlanie
    textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}

Przejdźmy teraz z zakładki 1 do zakładki 2. Nowe wpisy w dzienniku są następujące:

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
  • wiersz 1: kontener fragmentów [ViewPager] żąda fragmentu nr 3;
  • wiersze 2–3: metody fragmentu nr 3. Przypominamy, że fragment ten został zainicjowany już przy uruchomieniu aplikacji;
  • wiersze 4–5: wykonywana jest metoda [onResume] fragmentu nr 3. Przypominamy, że wyświetlany jest fragment nr 2;

Teraz przejdźmy z zakładki 2 do zakładki 3. Nie ma żadnego wpisu w dzienniku. Zatem żadna z metod [onCreateView, afterViews, onResume] fragmentu nr 3 nie jest wykonywana. Tekst [Hello World from section:3] wyświetla się poprawnie wyłącznie dlatego, że został on już utworzony na poprzednim etapie podczas wyświetlania fragmentu nr 2. Przypomnijmy bowiem, że na tym etapie została wykonana metoda [onResume] z fragmentu nr 3. Można tu zauważyć, że podobnie jak metoda [onCreateView], również metoda [onResume] nie może zostać wykorzystana do aktualizacji fragmentu nr 3. Gdyby konieczna była zmiana tekstu wyświetlanego przez ten fragment, żadna z tych dwóch metod nie byłaby w stanie tego zrobić.

Teraz wróćmy z zakładki nr 3 do zakładki nr 1. Logi wyglądają wówczas następująco:

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

Widać, że wszystkie metody fragmentu 1 zostały wykonane. Widać, że metoda getItem nie została wywołana. Jak już wspomniano, metoda ta jest wywoływana tylko raz dla każdego fragmentu;

Teraz przejdźmy z zakładki 1 do sąsiedniej zakładki 2. Otrzymujemy następujące zapisy:

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

Zaskakujące, prawda? Wszystkie metody fragmentu nr 3 są ponownie wykonywane.

Aby zrozumieć te zjawiska, należy pamiętać, że domyślnie, gdy kontener fragmentów wyświetla fragment i, inicjuje on fragmenty i-1, i oraz i+1. Przyjrzyjmy się ponownie logom w świetle tej informacji.

Po pierwsze, logi z momentu uruchomienia aplikacji:

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

Ponieważ kontener fragmentów wyświetli fragment 1, fragmenty 1 i 2 są inicjowane (wiersze 8–15).

Przechodzimy teraz z zakładki 1 do zakładki 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

Ponieważ kontener fragmentów ma wyświetlić fragment 2, fragmenty 1, 2 i 3 muszą zostać zainicjowane. Fragmenty 1 i 2 są już zainicjowane z poprzedniego etapu. Fragment 3 jest inicjowany w wierszach 2–5.

Przechodzimy z zakładki 2 do zakładki 3. Nie ma żadnych wpisów w logach. Ponieważ kontener fragmentów wyświetli fragment 3, fragmenty 2 i 3 muszą zostać zainicjowane. Jednak od poprzedniego etapu są one już zainicjowane. Nie widać tutaj, że fragment 1, który nie sąsiaduje z fragmentem 3, traci swój stan, który nie jest zachowywany w pamięci.

Przechodzimy z zakładki 3 do zakładki 1. Logi są następujące:

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

Ponieważ kontener fragmentów wyświetli fragment 1, fragment 2 również musi zostać zainicjowany. Jest on zainicjowany od poprzedniego etapu. W tym samym etapie stan fragmentu 1 został utracony. Zostaje on zatem zresetowany w wierszach 1–4. Nie widać tu jednak, że fragment 3, który nie sąsiaduje z fragmentem 1, traci swój stan, który nie jest wówczas zachowywany w pamięci.

Podczas przechodzenia z zakładki 1 do sąsiedniej zakładki 2 pojawiają się następujące wpisy w dzienniku:

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

Ponieważ kontener fragmentów wyświetli fragment 2, fragmenty 1, 2 i 3 muszą zostać zainicjowane. Fragmenty 1 i 2 są już zainicjowane z poprzedniego etapu. Fragment 3 jest zainicjowany w wierszach 1–4.

Czego się nauczyliśmy?

  • że domyślne zarządzanie fragmentami jest bardzo specyficzne i trzeba je znać, jeśli nie chcemy stracić włosów. Można zmienić ten tryb zarządzania i zrobimy to nieco później;
  • że przy tym domyślnym sposobie zarządzania żadna z metod [onCreateView, onResume] nie może być wykorzystana do aktualizacji fragmentu, który ma zostać wyświetlony, ponieważ nie ma pewności, że zostaną one wykonane;

1.8.6. onDestroyView

Metoda [onDestroyView] stanowi część cyklu życia fragmentów (patrz punkt 1.7.6):

Widać, że w cyklu życia fragmentu:

  • metoda [onCreateView] może zostać wykonana wielokrotnie;
  • zanim nastąpi powrót do metody [onCreateView], konieczne jest przejście przez metodę [onDestroyView] [2];

Wstawimy te metody do fragmentów, aby lepiej śledzić ich cykl życia. Kod fragmentu wygląda następująco:


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;

// fragment to widok wyświetlany przez kontener fragmentów
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

...

  @Override
  public void onDestroyView() {
    // log
    Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // nadrzędny
    super.onDestroyView();
  }

}

Uruchommy aplikację. Pierwsze wpisy w dzienniku mają następującą postać:

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
  • wiersz 1: utworzenie pojedynczej aktywności;
  • wiersz 2: metoda [afterViews] aktywności: jej pola oznaczone przez [@ViewById] są inicjowane;
  • wiersze 3–5: tworzenie trzech fragmentów;
  • wiersze 6–7: kontener fragmentów [ViewPager] żąda dwóch pierwszych fragmentów;
  • wiersze 8–9: tworzony jest widok fragmentu 2 (niekoniecznie widoczny);
  • wiersze 10–11: tworzony jest widok fragmentu 1 (niekoniecznie jest on widoczny);
  • wiersze 12–13: metoda [onResume] fragmentu 1;
  • wiersze 14–15: metoda [onResume] fragmentu 2;
  • wiersz 16: utworzenie menu aktywności;

Przejdźmy z zakładki 1 do zakładki 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
  • wiersz 1: kontener fragmentów żąda trzeciego fragmentu;
  • wiersze 2–3: tworzony jest widok fragmentu 3 (niekoniecznie wyświetlany);
  • wiersze 4–5: wykonywana jest metoda [onResume] fragmentu 3;
  • wiersz 6: wykonywana jest metoda [onDestroyView] fragmentu 1. Oznacza to, że gdy użytkownik powróci do fragmentu 1 lub do fragmentu sąsiedniego, cykl życia tego fragmentu zostanie ponownie uruchomiony;

Powrót z zakładki 3 do zakładki 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
  • wiersze 1–4: cykl życia fragmentu 1 jest ponownie wykonywany, ponieważ został poddany operacji [onDestroyView];
  • wiersz 5: teraz wykonywana jest metoda [onDestroyView] fragmentu 3. Również w tym przypadku, gdy użytkownik powróci do fragmentu 3 lub do fragmentu sąsiedniego, cykl życia tego fragmentu zostanie ponownie uruchomiony;

1.8.7. setUserVisibleHint

Metoda [onCreateView] cyklu życia instancjonuje widok powiązany z fragmentem, ale niekoniecznie czyni go widocznym. Temu właśnie przyjrzymy się teraz. Metoda [Fragment.setUserVisibleHint] jest wykonywana za każdym razem, gdy zmienia się widoczność fragmentu. Dodajemy tę metodę do kodu fragmentu:


package exemples.android;

....

// fragment to widok wyświetlany przez kontener fragmentów
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // komponent interfejsu wizualnego
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;

  ...

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
  }
}

Po uruchomieniu logi wyglądają następująco:


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
  • Wpisy w wierszach 7, 9–10 pokazują, że widoczny staje się tylko fragment 1. Widać również, że staje się on widoczny przed wykonaniem swojej metody [onCreateView];

Przejdźmy z zakładki 1 do zakładki 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
  • fragment 1 jest ukryty (wiersz 3), fragment 2 jest wyświetlany (wiersz 4);

Przejdźmy z zakładki 2 do zakładki 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
  • fragment 2 jest ukryty (wiersz 1), fragment 3 jest widoczny (wiersz 2);

Wróćmy do zakładki 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
  • fragment 3 jest ukryty (wiersz 2), fragment 1 jest widoczny (wiersz 3);

Czego się dowiedzieliśmy?

  • metoda [setUserVisibleHint] jest wykonywana jednokrotnie z właściwością [isVisibleToUser] do true dla fragmentu, który ma zostać wyświetlony;
  • nie da się określić, w którym momencie cyklu życia fragmentu ta metoda zostanie wykonana. Tak więc w przypadku fragmentu 1 metoda [setUserVisibleHint, true] została wykonana przed metodą [onCreateView] na początku cyklu życia tego fragmentu, podczas gdy w przypadku fragmentów 2 i 3 miało miejsce odwrotne zjawisko;

1.8.8. setOffscreenPageLimit

Z poprzednich logów wynika, że gdy kontener fragmentów [ViewPager] przygotowuje się do wyświetlenia fragmentu nr i, uruchamia – o ile jeszcze tego nie zrobił – cykl życia sąsiednich fragmentów i-1 i i+1. Działanie to można kontrolować za pomocą metody [ViewPager].setOffscreenPageLimit:

// przesunięcie fragmentów
    [ViewPager].setOffscreenPageLimit(n);

Zgodnie z powyższą instrukcją,

  1. gdy kontener fragmentów [ViewPager] przygotowuje się do wyświetlenia fragmentu nr i, wykonuje on, o ile nie zostało to jeszcze zrobione, cykl życia sąsiednich fragmentów z przedziału [i-n, i+n];
  2. jeśli następnie wyświetlany jest fragment j:
    • to samo zjawisko powtarza się w przypadku sąsiednich fragmentów z przedziału [j-n, j+n];
    • fragmenty zainicjowane w etapie 1, które nie znajdują się już w sąsiedztwie [j-n, j+n] nowego fragmentu, mogą wówczas zostać poddane operacji [onDestroyView]. Niemniej jednak zauważyłem w innych aplikacjach, zwłaszcza w tej z rozdziału 3, że nie zawsze tak się dzieje;

Modyfikujemy metodę [MainActivity.afterViews] w następujący sposób:


  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    // pasek narzędzi
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // menedżer fragmentów
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // kontener fragmentów jest powiązany z menedżerem fragmentów
    // czyli fragment nr i z kontenera fragmentów jest fragmentem nr i dostarczonym przez menedżera fragmentów
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // blokuje się przesuwanie między fragmentami
    mViewPager.setSwipeEnabled(false);

    // przesunięcie fragmentów
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);

    // pasek zakładek jest również powiązany z kontenerem fragmentów
    // czyli zakładka nr i wyświetla fragment nr i z kontenera
    tabLayout.setupWithViewPager(mViewPager);

    // pływający przycisk
    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();
      }
    });
}
  • wiersz 20: ustawiamy liczbę sąsiednich fragmentów do zainicjowania na całkowitą liczbę fragmentów minus 1. W ten sposób podczas uruchamiania, gdy kontener fragmentów wyświetli fragment nr 1, zainicjuje jednocześnie fragmenty 2, 3, ..., n, gdzie n = 1 + mSectionsPagerAdapter.getCount() – 1 = mSectionsPagerAdapter.getCount(). Zostaną więc zainicjowane wszystkie fragmenty. Gdy okno wyświetlania przesunie się na inny fragment, kontener fragmentów:
    • stwierdzi, że wszystkie fragmenty sąsiadujące z nowym fragmentem są już zainicjowane i w związku z tym nie przeprowadzi ich inicjalizacji;
    • ponieważ sąsiedztwo nowego fragmentu również obejmuje wszystkie fragmenty, żaden z nich nie zostanie „deinstancjonowany” przez kontener fragmentów;

W sumie powinniśmy zobaczyć wszystkie fragmenty instancjonowane i zainicjowane przy uruchomieniu aplikacji, a potem już nigdy więcej. Właśnie to teraz sprawdzamy, analizując logi.

Podczas uruchamiania mamy następujące logi:

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
  • wiersze 4–6: tworzenie trzech fragmentów;
  • wiersze 7, 9, 11: kontener fragmentów pobiera trzy fragmenty. W poprzedniej wersji pobierał dwa;
  • wiersze 14–25: realizowany jest cykl życia trzech fragmentów;

Przejdźmy teraz z zakładki 1 do zakładki 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

Przejdźmy z zakładki 2 do zakładki 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

Następnie z zakładki 3 do zakładki 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

Logi potwierdzają tę teorię. Wszystkie fragmenty zostały zainicjowane i zainicjowane podczas uruchamiania. Następnie metody ich cyklu życia nie są już wykonywane. Mamy tu do czynienia z bardzo przewidywalnym działaniem fragmentów, co znacznie ułatwia ich wykorzystanie.

Chcemy znaleźć sposób na aktualizację fragmentu, który ma zostać wyświetlony, niezależnie od sąsiedztwa fragmentów wybranego przez programistę. Logi pokazały nam dwie rzeczy:

  • metoda [setUserVisibleHint, true] jest zawsze wykonywana dla fragmentu, który ma zostać wyświetlony, a nie dla pozostałych;
  • zdarzenie to może wystąpić przed lub po cyklu życia fragmentu. Zależy to od sąsiedztwa fragmentów wybranego przez programistę. Stanowi to problem, ponieważ jeśli cykl życia jeszcze nie nastąpił, oznacza to, że fragment nie może zostać zaktualizowany za pomocą metody [setUserVisibleHint, true];

Logi przy uruchomieniu aplikacji, gdy sąsiedztwo fragmentów wynosiło 1, wyglądały następująco:


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
  • widać, że gdy fragment 1 staje się widoczny, jego widok nie został jeszcze utworzony. Nie można więc go modyfikować. Będzie to możliwe w trakcie cyklu życia fragmentu, na przykład w metodach [onCreateView] (wiersz 11) lub [onResume] (wiersze 13–14). Ponieważ korzystamy z adnotacji AA, zazwyczaj nie musimy pisać metody [onCreateView]. Wydaje się więc, że metoda [onResume] jest tutaj najbardziej odpowiednia do aktualizacji fragmentu 1;

Kiedy przeszliśmy z zakładki 1 do zakładki 2, logi wyglądały następująco:


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

Tym razem do aktualizacji fragmentu 2 służy wyłącznie metoda [setUserVisibleHint, true] z wiersza 4;

Po przejściu z zakładki 2 do zakładki 3 logi wyglądały następująco:


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

W tym przypadku mamy tylko metodę [setUserVisibleHint, true] z wiersza 2 do aktualizacji fragmentu 3;

Po przejściu z zakładki 3 do zakładki 1 logi wyglądały następująco:


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

W tym przypadku należy użyć metody [onResume] z fragmentu 1 (wiersze 6–7), aby zaktualizować fragment 1.

W tym przykładzie widać więc, że do aktualizacji fragmentu, który ma zostać wyświetlony, dostępne są dwie metody: [setUserVisibleHint] i [onResume].

Zastosujemy to rozwiązanie w nowym projekcie, w którym każdy fragment będzie musiał wyświetlać liczbę wyświetleń, którą nazwiemy „odwiedziną”. Będzie więc konieczne aktualizowanie jego zawartości za każdym razem, gdy zostanie wyświetlony. Jest to właśnie problem, który staramy się rozwiązać.

Zanim to zrobimy, przyjrzyjmy się ostatniemu etapowi cyklu życia aktywności lub fragmentu, czyli momentowi, w którym zostaje on zniszczony. System może podjąć inicjatywę usunięcia aktywności, jeśli inne aktywności o wyższym priorytecie wymagają zasobów, które są niedostępne. Aby je zwolnić, system podejmie inicjatywę usunięcia niektórych aktywności. Wówczas zostanie wywołana metoda [onDestroy] aktywności i fragmentów.

1.8.9. OnDestroy

Umożliwimy użytkownikowi usunięcie aktywności za pomocą opcji menu [5]. W tym celu dodajemy nową opcję menu w pliku [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>

Wystarczy skopiować i wkleić pierwszą opcję menu, a następnie dostosować wynik (wiersze 9 i 10). Nazwa tej nowej opcji jest dodawana do pliku [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>

Na koniec w klasie [MainActivity] obsługujemy kliknięcie opcji [Terminate]:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Obsługuj tutaj kliknięcia elementów paska akcji. Pasek akcji będzie
    // automatycznie obsługiwać kliknięcia przycisku Strona główna/W górę, o ile
    // jak długo określisz aktywność nadrzędną w 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");
      //zakończy się aktywność
      finish();
      return true;
    }
    // nadrzędną
    return super.onOptionsItemSelected(item);
}
  • wiersze 14–19: kopiujemy i wklejamy wiersze 10–13, a następnie dostosowujemy kod do nowej opcji;
  • wiersz 17: działanie jest kończone przez akcję programową;

Teraz uruchommy tę nową wersję, a gdy tylko wyświetli się pierwszy widok, kliknijmy opcję menu [Terminate]. Logi będą wówczas następujące:

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
  • wiersze 1–2: kliknięcie opcji [Terminate];
  • wiersz 4: wywołano metodę [onDestroy] tej aktywności;
  • wiersze 4–5: wywoływana jest metoda [onDestroyView] fragmentu 1, a następnie jej metoda [onDestroy];
  • wiersze 6–9: operacja ta powtarza się dla pozostałych dwóch fragmentów;

Należy zatem pamiętać, że metoda [onDestroy] aktywności i fragmentów jest wywoływana, gdy aktywność ma zostać usunięta przez system, programistę lub użytkownika. Metodę tę można wykorzystać do zapisania informacji, na przykład lokalnie na tablecie, aby można było je odzyskać, gdy użytkownik ponownie uruchomi aplikację.

1.9. Przykład 08: aktualizacja fragmentu przy zmiennej sąsiedztwie fragmentów

1.9.1. Tworzenie projektu

Projekt [Exemple-07] powielamy jako [Exemple-08]. W tym celu postępujemy zgodnie z procedurą opisaną w punkcie 1.4, dotyczącą powielania projektu [Exemple-02] jako [Exemple-03].

1.9.2. Przepisanie fragmentu [PlaceholderFragment]

Nowy kod fragmentu [PlaceholderFragment] jest następujący. Działa on niezależnie od przypisanej fragmentom sąsiedztwa (1, częściowe, całkowite):


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;

// fragment to widok wyświetlany przez kontener fragmentów
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // komponent interfejsu wizualnego
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // dane
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;

  // numer fragmentu
  private static final String ARG_SECTION_NUMBER = "section_number";

  // konstruktor
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }


  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // dziennik
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // tekst początkowy
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // inicjalizacja zakończona
      initDone = true;
    }
    // wyświetlanie bieżącego tekstu
    textViewInfo.setText(text);
  }


  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }

  @Override
  public void onDestroyView() {
...
  }

  @Override
  public void onResume() {
...
  }

  // aktualizacja fragmentu
  public void update() {
    // zadanie do wykonania zależy od numeru wizyty
    if (numVisit > 1) {
      // dziennik
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // tekst zmieniony
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // informacje lokalne dla logów
  private String getInfos() {
    return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
  }
}
  • wiersze 34–48: metoda [@AfterViews] może zostać wykonana wielokrotnie. Wykorzystywaliśmy ją do inicjalizacji tekstu fragmentu (wiersz 42). Nadal to robimy, ale aby wykonać tę operację tylko raz, zarządzamy zmienną logiczną [initDone] (wiersz 44), która wskazuje, że inicjalizacja została przeprowadzona i nie należy jej powtarzać;
  • wiersze 56–59: wprowadzamy metodę [onDestroyView], aby odnotować fakt, że przy następnym wyświetleniu fragmentu jego cykl życia zostanie ponownie wykonany;
  • logi wykazały, że po metodzie [@AfterViews] mogą zostać wykonane dwie metody: [setUserVisibleHint] i [onResume]. Metoda [onResume] jest wykonywana wyłącznie podczas realizacji cyklu życia fragmentu. Natomiast metoda [setUserVisibleHint] nie zawsze jest wykonywana po metodzie [@AfterViews]. Z logów wynika, że przynajmniej jedna z tych dwóch metod jest wykonywana po metodzie [@AfterViews]. Logi nigdy nie wykazały, że obie metody mogłyby zostać wykonane jednocześnie po metodzie [@AfterViews]. Jest to albo jedna, albo druga. Na wszelki wypadek ustawimy zmienną logiczną [updateDone] po dokonaniu aktualizacji;

Metody [setUserVisibleHint] i [onResume] są następujące:


  // dane
  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) {
    // element nadrzędny
    super.setUserVisibleHint(isVisibleToUser);
    // pamięć
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // liczba odwiedzin
    if (isVisibleToUser) {
      // przyrost
      numVisit++;
      // aktualizacja fragmentu
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // fragment zostanie ukryty
      updateDone = false;
    }
  }

  @Override
  public void onResume() {
    // element nadrzędny
    super.onResume();
    // log
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // aktualizacja
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • wiersz 14: zapisuje się, czy fragment jest widoczny, czy nie;
  • wiersze 22–25: jeśli fragment jest widoczny i została wykonana metoda [@AfterViews], wykonywana jest metoda [update], a wartość logiczna [updateDone] jest przekazywana do true;
  • wiersze 26–28: jeśli fragment ma zostać ukryty, wartość logiczna [updateDone] jest resetowana do false. Potrzebujemy bowiem zdarzenia, które zresetuje zmienną logiczną [updateDone] – ustawioną na true – do wartości false, gdy tylko wywołana zostanie metoda [update], aby umożliwić przeprowadzenie nowych aktualizacji. Wykorzystujemy do tego fakt, że fragment nie jest już widoczny. Gdy znów stanie się widoczny, aktualizacja fragmentu będzie musiała zostać przeprowadzona ponownie;
  • wiersze 32–42: logi pokazują, że w zależności od wybranej sąsiedztwa fragmentów metoda [onResume] może zostać wykonana, mimo że fragment nie jest widoczny. Jeśli fragment nie jest widoczny, nie przeprowadza się aktualizacji (wiersz 39) i podobnie jak w przypadku metody [setMenuVisibility], zarządza się zmienną logiczną [updateDone].

Wreszcie metoda [onDestroyView] wygląda następująco:


  @Override
  public void onDestroyView() {
    // element nadrzędny
    super.onDestroyView();
    // aktualizacja wskaźnika
    afterViewsDone = false;
    // log
    Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}

Metoda [onDestroyView] jest wykonywana, gdy cykl życia fragmentu dobiega końca. Kolejny cykl może zostać wznowiony w późniejszym terminie.

  • wiersz 6: metoda [onDestroyView] usuwa wszelkie powiązania z widokiem przypisanym do fragmentu. Zostaną one odtworzone w następnym cyklu życia fragmentu. Na razie musimy ustawić wartość logicznego [afterViews] na false, aby wskazać, że powiązanie z widokiem już nie istnieje;

Uruchomimy aplikację z 5 fragmentami o sąsiedztwie równym 2. Zmiany wprowadzono w [MainActivity]:


    // liczba fragmentów
  private final int FRAGMENTS_COUNT = 5;
  // sąsiedztwo fragmentów
  private final int OFF_SCREEN_PAGE_LIMIT=2;


  // menedżer fragmentów
  private SectionsPagerAdapter mSectionsPagerAdapter;

   @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    ....

    // przesunięcie fragmentów
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

...
}

Logi przy uruchomieniu są następujące:


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
  • wiersze 8, 10, 12: kontener fragmentów żąda wszystkich fragmentów sąsiadujących z fragmentem 1;
  • wiersze 9, 11, 13: metoda [setUserVisibleHint] tych fragmentów jest wykonywana wraz z metodami od [visibleToUser] do false;
  • wiersz 14: metoda [setUserVisibleHint] fragmentu 1 jest wykonywana z metodami od [visibleToUser] do true;
  • wiersze 15–17: wywoływana jest metoda [afterViews] dla trzech sąsiednich segmentów. Mamy tu zatem do czynienia z przypadkiem, w którym metoda ta jest wywoływana po tym, jak fragment stał się widoczny (fragment 1, wiersz 14);
  • wiersze 18–20: wywoływana jest metoda [onResume] dla trzech sąsiednich segmentów;

Przechodzimy z zakładki 1 do zakładki 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
  • ponieważ sąsiedztwo fragmentów jest przesunięte o jedną pozycję w prawo, fragment 4 jest pobierany przez kontener fragmentów;
  • wiersz 2: wywoływana jest metoda [setUserVisibleHint] fragmentu 4 wraz z metodami od [visibleToUser] do false;
  • wiersz 3: wywoływana jest metoda [setUserVisibleHint] fragmentu 1 z [visibleToUser] do false. Fragment 1 jest bowiem teraz ukryty;
  • wiersz 4: wywoływana jest metoda [setUserVisibleHint] fragmentu 2 z [visibleToUser] do true. Fragment 2 jest teraz widoczny;
  • wiersze 5–6: cykl życia fragmentu 4 jest kontynuowany;

Przechodzimy z zakładki 2 do zakładki 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
  • ponieważ sąsiedztwo fragmentów jest przesunięte o jedną pozycję w prawo, fragment 5 jest pobierany przez kontener fragmentów;
  • wiersz 2: wywoływana jest metoda [setUserVisibleHint] fragmentu 5 wraz z metodami od [visibleToUser] do false;
  • wiersz 3: wywoływana jest metoda [setUserVisibleHint] fragmentu 2, od [visibleToUser] do false. Wynika to z faktu, że fragment 2 jest teraz ukryty;
  • wiersz 4: wywoływana jest metoda [setUserVisibleHint] z fragmentu 3, przechodząc od [visibleToUser] do true. Fragment 3 jest teraz widoczny;
  • wiersze 5–6: cykl życia fragmentu 5 jest kontynuowany;

Przechodzimy z zakładki 3 do zakładki 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
  • wiersz 1: fragment 3 jest teraz ukryty;
  • wiersz 2: fragment 4 jest teraz widoczny. Należy zauważyć, że cykl życia fragmentu 4 nie jest wykonywany. Został on już zrealizowany dwa etapy wcześniej;
  • wiersz 3: fragment 1 wychodzi z sąsiedztwa wyświetlanego fragmentu 4. Wykonana zostaje jego metoda [onDestroyView]. Przy następnym wyświetleniu fragmentu jego cykl widoku [onCreateView, afterViews, onResume] zostanie ponownie wykonany;

Przechodzimy z zakładki 4 do zakładki 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
  • wiersz 1: fragment 4 jest teraz ukryty;
  • wiersz 2: fragment 5 jest teraz widoczny. Należy zauważyć, że cykl życia fragmentu 5 nie jest wykonywany. Został on już wykonany dwa etapy wcześniej;
  • wiersz 3: fragment 2 wychodzi z sąsiedztwa wyświetlanego fragmentu 5. Wykonana zostaje jego metoda [onDestroyView];

Przechodzimy z zakładki 5 do zakładki 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
  • wiersze 1, 4, 5, 6: cykl życia fragmentu 1 jest ponownie wykonywany. Stracił on bowiem połączenie ze swoim widokiem;
  • wiersze 2, 5, 8, 9: z tego samego powodu cykl życia fragmentu 2 jest ponownie wykonywany;
  • wiersze 10–11: fragmenty 4 i 5 wychodzą z sąsiedztwa wyświetlanego fragmentu;
  • wiersz 7: fragment 1 zostaje zaktualizowany;
 

W logach nigdy nie odnotowano, aby metody [setUserVisibleHint] i [onResume] próbowały jednocześnie zaktualizować fragment. Działa albo jedna, albo druga. Zachęcamy czytelnika do przeprowadzenia dalszych testów i śledzenia logów, aby dobrze zrozumieć pojęcie sąsiedztwa i cyklu życia fragmentów.

Teraz załóżmy całkowitą sąsiedztwo i przeprowadźmy te same testy.

W pliku [MainActivity]:


  // liczba fragmentów
  private final int FRAGMENTS_COUNT = 5;
  // sąsiedztwo fragmentów
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;

Logi przy uruchomieniu są następujące:


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
  • logi wskazują, że cykl życia 5 fragmentów został wykonany;
  • fragment 1 jest wyświetlany w wierszu 18;

Przechodzimy z zakładki 1 do zakładki 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
  • wiersz 1: fragment 1 jest ukryty;
  • wiersz 2: wyświetlany jest fragment 2;

Przechodzimy z zakładki 2 do zakładki 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
  • wiersz 1: fragment 2 jest ukryty;
  • wiersz 2: fragment 3 jest widoczny;

Przechodzimy z zakładki 3 do zakładki 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
  • wiersz 1: fragment 3 jest ukryty;
  • wiersz 2: fragment 4 jest widoczny;

Przechodzimy z zakładki 4 do zakładki 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
  • wiersz 1: fragment 4 jest ukryty;
  • wiersz 2: fragment 5 jest widoczny;

Przechodzimy z zakładki 5 do zakładki 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
  • wiersz 1: fragment 5 jest ukryty;
  • wiersz 2: fragment 1 jest widoczny;
  • wiersz 3: fragment 1 jest aktualizowany;

Przechodzimy z zakładki 1 do zakładki 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
  • wiersz 1: fragment 1 jest ukryty;
  • wiersz 2: wyświetlany jest fragment 4;
  • wiersz 3: fragment 4 zostaje zaktualizowany;

Zauważamy, że przy pełnej sąsiedztwie zachowanie fragmentów jest znacznie bardziej przewidywalne.

Teraz ustawmy sąsiedztwo zerowe i zobaczmy, co się stanie. Klasa [MainActivity] zmienia się w następujący sposób:


  // liczba fragmentów
  private final int FRAGMENTS_COUNT = 5;
  // sąsiedztwo fragmentów
private final int OFF_SCREEN_PAGE_LIMIT = 0;

Logi przy uruchomieniu są następujące:


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
  • W wierszach 8 i 10 widać, że kontener fragmentów zażądał 2 fragmentów, nr 1 i 2. Wszystko przebiega więc tak, jakby sąsiedztwo wynosiło 1. Sąsiedztwo równe 0 zostało zatem zignorowane.

1.9.3. Komunikacja między fragmentami

W poprzedniej architekturze mamy aktywność i n fragmentów. Użytkownik wchodzi w interakcję z różnymi fragmentami. Interakcje te zmieniają stan aplikacji. Stan aplikacji to tutaj zbiór wszystkich informacji, które aplikacja przechowuje przez cały czas swojego działania. Pojawia się wówczas następujący problem:

  • gdy użytkownik wchodzi w interakcję z fragmentem i, aplikacja przechodzi ze stanu E1 do stanu E2;
  • działanie użytkownika na fragmencie i powoduje wyświetlenie fragmentu j;
  • jak zaktualizować fragment j zgodnie z aktualnym stanem aplikacji E2;

Z poprzednich przykładów wiemy, jak zaktualizować fragment j. Ale skąd wziąć stan aplikacji E2, aby go zaktualizować?

Istnieją różne rozwiązania tego problemu. Jedno z nich już omówiliśmy: fragment i może przekazać stan aplikacji E2 do fragmentu j za pomocą argumentów. Metodę tę poznaliśmy w klasie [MainActivity] podczas tworzenia fragmentów:


      for (int i = 0; i < fragments.length; i++) {
        // tworzy się fragment
        fragments[i] = new PlaceholderFragment_();
        // można przekazać argumenty do fragmentu
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
}

To rozwiązanie nie nadaje się tutaj do natychmiastowego wykorzystania. Dzieje się tak, ponieważ gdy użytkownik kliknie zakładkę j, co spowoduje wyświetlenie fragmentu j, nasz kod nie jest wywoływany. Wykonany zostaje wyłącznie kod systemowy. W kolejnym projekcie zobaczymy, jak przechwycić kliknięcie na zakładce, ale na razie pójdziemy inną drogą.

Mówiliśmy o stanie aplikacji: zbiorze danych zarządzanych przez aplikację w miarę upływu czasu. W tym przypadku aplikacja składa się z jednej aktywności i n fragmentów, z których każdy jest instancjonowany tylko raz podczas uruchamiania aplikacji, a ich czas życia pokrywa się z czasem działania aplikacji. Zatem każdy z tych elementów lub kilka z nich razem może służyć do przechowywania stanu aplikacji. Każdy fragment ma dostęp, za pośrednictwem metody [Fragment.getActivity()], do aktywności, która go utworzyła. Ponieważ wszystkie fragmenty mają dostęp do tej aktywności, naturalnym rozwiązaniem wydaje się przechowywanie stanu aplikacji właśnie w niej.

Jednak wynik metody [Fragment.getActivity()] zależy od momentu jej wywołania w cyklu życia aplikacji. Ilustrujemy to, dodając kilka wpisów do dziennika w klasie [PlaceholderFragment]:


  // aktualizacja fragmentu
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // zadanie do wykonania zależy od numeru wizyty
    if (numVisit > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // zmodyfikowany tekst
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // informacje lokalne dla logów
  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);
}
  • wiersze 14–16: metoda [getInfos] wyświetla część raportu o stanie aplikacji;

Uruchamiamy aplikację z liczbą sąsiednich fragmentów równą 2. Logi podczas uruchamiania aplikacji:


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
  • wiersze 9, 10, 13, 14: widać, że w metodach [setUserVisibleHint] wywoływana jest metoda [getActivity()==null], jeśli fragment nie jest jeszcze widoczny (isVisibleToUser == false);
  • wiersz 19: widać, że gdy strumień wykonania dociera do metody [update] fragmentu 1, metoda [getActivity] prawidłowo zwraca aktywność;

Gdy ustawimy sąsiedztwo fragmentów na 4 (pełne sąsiedztwo), logi wyglądają następująco:


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 -- akceptowanie i odrzucanie
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

Otrzymujemy te same wyniki. Wnioskujemy z tego, że gdy tylko fragment staje się widoczny, metoda [getActivity] zwraca aktywność fragmentu. Zauważamy również, że gdy wykonanie dochodzi do metody [update] fragmentu, który ma zostać wyświetlony, metoda [getActivity] prawidłowo zwraca wartość.

Aby zilustrować komunikację między fragmentami, tworzymy nowy projekt.

1.10. Przykład 09: komunikacja między fragmentami, przesuwanie i przewijanie

1.10.1. Tworzenie projektu

Duplikujemy projekt [Exemple-07] do [Exemple-08]. W tym celu postępujemy zgodnie z procedurą opisaną w punkcie 1.4 dotyczącą duplikowania projektu [Exemple-02] do [Exemple-03].

1.10.2. Sesja

W tym nowym projekcie chcemy, aby fragmenty wyświetlały łączną liczbę fragmentów wyświetlonych przez użytkownika. W tym celu należy prowadzić licznik, który będzie dostępny dla wszystkich fragmentów. Obiektem, w którym będą przechowywane dane współdzielone przez fragmenty, nazwiemy „sesją”. Terminologia ta wywodzi się z programowania stron internetowych, gdzie w sesji umieszcza się dane, które mają być współdzielone przez różne widoki wywoływane przez tego samego użytkownika. Zamknięcie informacji współdzielonych przez różne fragmenty w jednym obiekcie sprawia, że kod staje się bardziej czytelny.

Klasa [Session] będzie wyglądać następująco:

  

package exemples.android;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // liczba odwiedzonych fragmentów
  private int numVisit;

  // metody pobierające i ustawiające

  public int getNumVisit() {
    return numVisit;
  }

  public void setNumVisit(int numVisit) {
    this.numVisit = numVisit;
  }
}
  • wiersz 8: sesja będzie zawierać liczbę odwiedzonych fragmentów;
  • wiersz 5: adnotacja [EBean] jest adnotacją AA. Atrybut [scope] określa zakres (lub czas życia) klasy opatrzonej tą adnotacją. W tym przypadku atrybut [scope = EBean.Scope.Singleton] sprawia, że klasa [Session] jest singletonem: zostanie ona zainicjowana tylko raz, przy uruchomieniu aplikacji. Odwołanie do klasy opatrzonej adnotacją [EBean] może następnie zostać wstrzyknięte do innej klasy. Jest to pojęcie wstrzykiwania zależności;

1.10.3. Aktywność [MainActivity]

Aktywność [MainActivity] zmienia się w następujący sposób:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  ...

  // wstrzyknięcie sesji
  @Bean(Session.class)
  protected Session session;

  // liczba fragmentów
  private final int FRAGMENTS_COUNT = 5;
  // sąsiedztwo fragmentów
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

    @AfterInject
  protected void afterInject(){
    Log.d("MainActivity", "afterInject");

    // inicjalizacja sesji
    session.setNumVisit(0);
  }

...
  • wiersze 7–8: wstrzyknięcie odwołania do singletonu sesji za pomocą adnotacji [@Bean]. Parametrem adnotacji jest klasa bean, który ma zostać wstrzyknięty. Pole opatrzone tą adnotacją nie może mieć zakresu [private];
  • wiersz 15: adnotacja [@AfterInject] służy do wskazania metody, która ma zostać wywołana po zakończeniu wszystkich wstrzyknięć w klasie. Tak więc po wejściu do metody [afterInject] w wierszu 16 odwołanie z wiersza 8 zostało zainicjowane;
  • wiersz 20: licznik odwiedzin jest zerowany;

1.10.4. Fragment [PlaceholderFragment]

Fragment [PlaceholderFragment] przebiega w następujący sposób:


@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

....

  // sesja
  protected Session session;

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // element nadrzędny
    super.setUserVisibleHint(isVisibleToUser);
    // pamięć
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // liczba odwiedzin
    if (isVisibleToUser) {
      // aktualizacja fragmentu
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // fragment zostanie ukryty
      updateDone = false;
    }
  }

  // aktualizacja fragmentu
  public void update() {
    // log
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // sesja
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // zwiększenie liczby odwiedzin
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // zmodyfikowany tekst
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
  • wiersz 7: sesja;
  • wiersze 35–37: wiemy, że po wejściu do metody [update] metoda [getActivity] prawidłowo zwraca aktywność. Korzystamy z tego, aby pobrać sesję i zapisać ją lokalnie (wiersz 36);
  • wiersze 39–41: aby zwiększyć numer wizyty, pobieramy go z sesji. Moglibyśmy umieścić ten kod w metodzie [setUserVisibleHint], począwszy od wiersza 19, ponieważ wiemy, że w tym momencie metoda [getActivity] zwraca aktywność. Zdecydowaliśmy się tutaj nie przypisywać tej metodzie żadnej szczególnej roli i przenieść kod specyficzny dla danego fragmentu do metody [update], która jest do tego przeznaczona;
  • wiersz 43: wyświetla numer wizyty;

Po uruchomieniu tej aplikacji z 5 fragmentami i sąsiedztwem 2 fragmentów pierwsze logi wyglądają następująco:


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
...
  • wiersze 2–3: widać, że metoda [afterInject] tej aktywności jest wykonywana przed metodą [afterViews];

Zachęcamy czytelnika do przetestowania tej nowej aplikacji.

1.10.5. Wyłączenie funkcji przesuwania (Swipe)

W poprzedniej aplikacji, gdy przesuwa się myszką po emulatorze Androida w lewo lub w prawo, bieżący widok ustępuje miejsca widokowi po prawej lub lewej stronie, w zależności od sytuacji. To domyślne zachowanie nie zawsze jest pożądane. Nauczymy się, jak wyłączyć przesuwanie widoków (swipe).

Wróćmy do głównego widoku XML [activity_main]:

  

W kodzie widoku XML znajduje się kod kontenera fragmentów:


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

Wiersz 1 określa klasę zarządzającą stronami tej aktywności. Klasę tę można znaleźć w aktywności [MainActivity]:


import android.support.v4.view.ViewPager;
...

@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // menedżer fragmentów
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // kontener fragmentów
  @ViewById(R.id.container)
  protected ViewPager mViewPager;
...

W wierszu 12 kontener fragmentów jest typu [android.support.v4.view.ViewPager] (wiersz 1). Aby wyłączyć skanowanie, należy utworzyć pochodną tej klasy w następujący sposób:

  

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 {

  // kontrola przesuwania
  private boolean isSwipeEnabled;

  // konstruktory
  public MyPager(Context context) {
    super(context);
  }

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

  // metody do przedefiniowania w celu obsługi gestu przesuwania
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // czy gest przesuwania jest dozwolony?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // czy przesunięcie jest dozwolone?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // metoda setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

}
  • wiersz 8: klasa [MyPager] rozszerza klasę Androida [ViewPager] (wiersz 4);
  • po przesunięciu dłonią mogą zostać wywołane procedury obsługi zdarzeń z wierszy 24 i 34. Obie zwracają wartość logiczną. Wystarczy, że zwrócą wartość logiczną [false], aby zablokować przesunięcie;
  • wiersz 11: wartość logiczna służąca do wskazania, czy gest przesunięcia dłonią jest akceptowany, czy nie.

Po wykonaniu tych czynności należy teraz użyć naszego nowego menedżera stron. Odbywa się to w widoku XML [activity_main.xml] oraz w głównej aktywności [MainActivity]. W [activity_main.xml] wpisujemy:

  

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

W wierszu 1 stosuje się nową klasę. W [MainActivity] kod zmienia się w następujący sposób:


package exemples.android;

...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // menedżer fragmentów
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // kontener fragmentów
  @ViewById(R.id.container)
  protected MyPager mViewPager;

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // kontener fragmentów jest powiązany z menedżerem fragmentów
    // czyli fragment nr i z kontenera fragmentów jest fragmentem nr i dostarczonym przez menedżera fragmentów
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // blokuje się przesuwanie między fragmentami
    mViewPager.setSwipeEnabled(false);
    // pasek zakładek jest również powiązany z kontenerem fragmentów
...
  • wiersz 12: menedżer stron ma teraz typ [MyPager];
  • wiersz 23: można włączyć lub wyłączyć przesuwanie dłonią.

Przetestuj tę nową wersję. Wyłącz lub nie przewijanie i sprawdź różnicę w zachowaniu widoków podczas przeciągania ich myszką w prawo lub w lewo. We wszystkich przyszłych aplikacjach przewijanie będzie wyłączone. Nie będziemy o tym przypominać.

1.10.6. Wyłączanie przewijania między fragmentami

Przejdźmy do ulepszenia menedżera kart. Podczas przechodzenia z karty 1 do karty 4 widoczne jest przewijanie dwóch pośrednich kart 2 i 3. W żargonie Androida nazywa się to smoothScrolling. Takie zachowanie może być uciążliwe, jeśli jest dużo kart. Można je wyłączyć, dodając następujący kod do menedżera fragmentów [MyPager]:


// kontroluje przesuwanie
  private boolean isSwipeEnabled;
  // kontroluje przewijanie
  private boolean isScrollingEnabled;

 ...
  // przewijanie
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }

  // ustawienia
...

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

Ponieważ menedżer kart został powiązany z menedżerem fragmentów o nazwie [MyPager], po kliknięciu karty nr i fragment nr i jest wyświetlany przez kontener fragmentów za pomocą powyższej metody [setCurrentItem] (wiersz 9). [position] to numer fragmentu do wyświetlenia;

  • wiersz 10: wywoływana jest metoda [setCurrentItem] klasy nadrzędnej. Drugi argument metody [false] nakazuje natychmiastowe przejście między starym a nowym fragmentem (bez przewijania), natomiast w przypadku metody [true] nakazuje przejście za pośrednictwem metody scrolling. W tym przypadku drugim argumentem jest wartość pola z wiersza 4, którą programista może ustawić za pomocą metody z wierszy 16–18;

Jeśli chcemy wyłączyć przewijanie, klasa [MainActivity] będzie wyglądać następująco:


...
    // przesunięcie fragmentów
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // blokuje przesuwanie między fragmentami
    mViewPager.setSwipeEnabled(false);

    // brak przewijania
    mViewPager.setScrollingEnabled(false);
...

Uruchom ponownie projekt i sprawdź, czy na przykład między zakładkami 1 i 4 nie ma już klasy scrolling. W dalszej części zawsze będziemy wyłączać przewijanie. Nie będziemy do tego wracać.

1.10.7. Nowy fragment

W naszym przykładzie wszystkie fragmenty są tego samego typu – [PlaceHolderFragment]. Teraz nauczymy się tworzyć nowy fragment i wyświetlać go.

Najpierw skopiujmy widok [vue1.xml] z projektu [Exemple-04] do projektu [Exemple-09] [1]:

 
  • w [1] widok [vue1.xml];
  • w pliku [3] widok zawiera błędy wynikające z brakujących tekstów w pliku [res/values/strings.xml];

W pliku [2] dodaje się brakujące teksty, pobierając je z pliku [res/values/strings.xml] z projektu [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>
  <!-- widok 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>
  • powyżej dodano wiersze 6–9;

Teraz tworzymy klasę [Vue1Fragment], która będzie fragmentem odpowiedzialnym za wyświetlanie widoku [vue1.xml]:

  

Klasa [Vue1Fragment] będzie wyglądać następująco:


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 {

  // elementy interfejsu wizualnego
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // menedżer zdarzeń
  @Click(R.id.buttonValider)
  protected void doValider() {
    // wyświetla się wpisana nazwa
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
}

  • wiersz 10: adnotacja [@EFragment] powoduje, że fragment wykorzystywany przez aktywność będzie w rzeczywistości klasą [Vue1Fragment_]. Należy o tym pamiętać. Fragment jest powiązany z widokiem [vue1.xml];
  • wiersze 14–15: komponent oznaczony jako [R.id.editTextNom] jest wstrzykiwany do pola [editTextNom] w wierszu 15;
  • wiersze 18–20: metoda [doValider] obsługuje zdarzenie „click” na przycisku oznaczonym jako [R.id.buttonValider];
  • wiersz 21: pierwszy parametr metody [Toast.makeText] jest typu [Activity]. Metoda [Fragment.getActivity()] pozwala uzyskać aktywność, w której znajduje się fragment. Jest to [MainActivity], ponieważ w tej architekturze mamy tylko jedną aktywność, która wyświetla różne widoki lub fragmenty;

W klasie [MainActivity] menedżer fragmentów zmienia się w następujący sposób:


public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmenty
    private Fragment[] fragments;
    // numer fragmentu
    private static final String ARG_SECTION_NUMBER = "section_number";

    // konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      // element nadrzędny
      super(fm);
      // inicjalizacja tablicy fragmentów
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        // tworzy się fragment
        fragments[i] = new PlaceholderFragment_();
        // można przekazać argumenty do fragmentu
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
      // fragment +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

 ...
  }
  • wiersz 13: występuje [FRAGMENTS_COUNT] fragmenty: [FRAGMENTS_COUNT-1] fragmenty typu [PlaceholderFragment] (wiersze 14–21) oraz fragment typu [Vue1Fragment_], wiersz 23 (należy zwrócić uwagę na znak podkreślenia);

Skompiluj, a następnie uruchom projekt [Exemple-09]. Zakładka nr 5 powinna wyglądać inaczej:

1.10.8. Wszystkie fragmenty powinny pochodzić od tej samej klasy abstrakcyjnej

Nowy fragment [Vue1Fragment] również wymaga aktualizacji podczas wyświetlania. W tym celu należy stworzyć kod podobny do tego, który został stworzony dla fragmentu [PlaceholderFragment]. Aby uniknąć powtórzeń, wyodrębnimy to, co się da, do klasy abstrakcyjnej, z której będą dziedziczyć wszystkie fragmenty aplikacji.

W tym celu tworzymy nowy projekt.

1.11. Przykład 10: Sprawienie, by wszystkie fragmenty dziedziczyły po klasie abstrakcyjnej

1.11.1. Tworzenie projektu

Duplikujemy projekt [Exemple-09], tworząc projekt [Exemple-10]:

1.11.2. Zarządzanie trybem debugowania

Dodajemy do projektu opcję wyświetlania lub ukrywania logów trybu debugowania. W tym celu dodajemy stałą statyczną do klasy [MainActivity]:


  // tryb debugowania
public static final boolean IS_DEBUG_ENABLED = false;

1.11.3. Abstrakcyjna klasa nadrzędna wszystkich fragmentów

  

Klasa [AbstractFragment] ma następującą postać:


package exemples.android;

import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;

public abstract class AbstractFragment extends Fragment {

  // dane prywatne
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;

  // dane  dostępne dla klas potomnych
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;

  // aktywność
  protected MainActivity activity;

  // sesja
  protected Session session;

  // konstruktor
  public AbstractFragment() {
    // inicjalizacja
    isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // element nadrzędny
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }

  @Override
  public void onDestroyView() {
    // rodzic
    super.onDestroyView();
    ...
  }

  @Override
  public void onResume() {
    // nadrzędny
    super.onResume();
    ...
  }

  // informacje lokalne
  protected String getParentInfos() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }

  // aktualizacja fragmentu
  protected void update() {
    ...
    // klasa potomna otrzymuje polecenie aktualizacji
    updateFragment();
  }

  protected abstract void updateFragment();
}
  • wiersz 7: klasa [AbstractFragment] rozszerza klasę Android [Fragment];
  • każdy fragment musi mieć możliwość aktualizacji. Dlatego klasa nadrzędna [AbstractFragment] wymaga, aby jej klasy potomne posiadały metodę [updateFragment] (wiersz 68), którą wywołuje (wiersz 65);
  • wiersz 19: klasa będzie przechowywać odwołanie do aktywności aplikacji;
  • wiersz 22: klasa będzie przechowywać odwołanie do sesji, w której zgromadzone są dane współdzielone przez fragmenty i aktywność;
  • wiersze 25–33: konstruktor klasy abstrakcyjnej;
  • wiersz 27: utworzenie kopii stałej [MainActivity.IS_DEBUG_ENABLED] w polu z wiersza 16;
  • wiersz 28: zapisujemy nazwę instancjonowanej klasy, a więc nazwę klasy potomnej;
  • wiersze 15–22: pola te mają atrybut [protected], aby klasy potomne miały do nich dostęp. Należy zauważyć, że klasy potomne ignorują istnienie zmiennych logicznych [isVisibleToUser] i [updateDone] (wiersze 10–11);
  • wiersz 57: metoda [getParentInfos] posiada atrybut [protected], aby klasy potomne mogły ją wywoływać;

Metody [setUserVisibleHint, onDestroyView, onResume] pozostają analogiczne do tych, które występowały w klasie [PlaceholderFragment] z poprzedniego projektu:


@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // klasa nadrzędna
    super.setUserVisibleHint(isVisibleToUser);
    // pamięć
    this.isVisibleToUser = isVisibleToUser;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
    }
    // przypadek, w którym fragment stanie się widoczny
    if (isVisibleToUser) {
      // aktualizacja fragmentu
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // opuszczamy fragment
      updateDone = false;
    }
  }

  @Override
  public void onDestroyView() {
    // element nadrzędny
    super.onDestroyView();
    // aktualizacja wskaźnika
    afterViewsDone = false;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
    }
  }

  @Override
  public void onResume() {
    // element nadrzędny
    super.onResume();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
    }
    if (isVisibleToUser) {
      // aktualizacja
      if (!updateDone) {
        update();
        updateDone = true;
      }
    }
  }

Metoda [update] wygląda następująco:


  // aktualizacja fragmentu
  protected void update() {
    // pobieramy aktywność i sesję
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // wysyłamy żądanie do klasy potomnej o aktualizację
    updateFragment();
}

Zgodnie z powyższym kodem, gdy wykonywana jest metoda [update] danego fragmentu, fragment ten jest widoczny. Jest to istotne, ponieważ oznacza to, że metoda [Fragment.getActivity] zwraca wówczas odwołanie do aktywności aplikacji (patrz paragraf 1.10.8), co z kolei zapewnia dostęp do sesji.

  • wiersze 4–10: inicjuje się aktywność i sesję, jeśli nie zostało to jeszcze zrobione;
  • wiersz 12: wywołuje się metodę [updateFragment] klasy potomnej. Gdy ta metoda zostanie wykonana, pola [activity] i [session], do których ma ona dostęp, zostały już zainicjowane;

1.11.4. Klasa [PlaceholderFragment]

  

Klasa [PlaceholderFragment] zmienia się w następujący sposób:


package exemples.android;

import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;

// fragment to widok wyświetlany przez kontener fragmentów
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {

  // komponent interfejsu wizualnego
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;

  // dane
  private boolean initDone;

  // dane
  private String text;
  private int numVisit;

  // numer fragmentu
  private static final String ARG_SECTION_NUMBER = "section_number";

  // konstruktor
  public PlaceholderFragment() {
    super();
    // dziennik
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", "constructor");
    }
  }


  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
 ...
  }

  // aktualizacja fragmentu
  public void updateFragment() {
  ...
  }

}
  • wiersz 10: klasa [PlaceholderFragment] rozszerza klasę [AbstractFragment]. W tej architekturze zapisanie fragmentu polega na:
    • napisanie metody [@AfterViews], która służy do zainicjowania fragmentu podczas jego pierwszego cyklu życia lub do jego zresetowania, jeśli wcześniej wystąpiła metoda [onDestroyView]. Wiersz 39 jest obowiązkowy, aby poprawnie zarządzać cyklem życia fragmentu;
    • napisanie metody [updateFragment], która zaktualizuje fragment tuż przed jego wyświetleniem. Metoda ta może korzystać z sesji swojej klasy nadrzędnej;
    • napisać procedury obsługi zdarzeń fragmentu. Zajmiemy się tym w przyszłych projektach;

Metody [@AfterViews] i [updateFragment] pozostają analogiczne do tych z poprzedniego projektu:


@AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // dziennik
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // tekst początkowy
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // inicjalizacja zakończona
      initDone = true;
    }
    // wyświetlanie bieżącego tekstu
    textViewInfo.setText(text);
  }

  // aktualizacja fragmentu
  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // zwiększenie numeru wizyty
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // zmieniony tekst
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }

  // informacje lokalne dla logów
  protected String getLocalInfos() {
    return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
      numVisit, initDone, getActivity() == null);
  }
  • wiersze 7 i 23: w logach wyświetlamy informacje o klasie nadrzędnej za pomocą odziedziczonej metody [getParentInfos];

1.11.5. Klasa [Vue1Fragment]

  

Klasa [Vue1Fragment] ma taką samą strukturę jak klasa [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 {

  // elementy interfejsu graficznego
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // dane
  private int numVisit;

  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // dziennik
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
    }
  }

  // menedżer zdarzeń
  @Click(R.id.buttonValider)
  protected void doValider() {
    // wyświetla się wpisana nazwa
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // informacje lokalne dla logów
  protected String getLocalInfos() {
    return String.format("numVisit=%s", numVisit);
  }

  // aktualizacja fragmentu
  @Override
  protected void updateFragment() {
    // zwiększenie numeru wizyty
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // wyświetla numer wizyty
    Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}
  • wiersz 9: klasa [Vue1Fragment] rozszerza klasę [AbstractFragment];
  • wiersze 18–26: metoda [@AfterViews] nie wykonuje żadnych istotnych zadań. Należy ją jednak napisać, aby ustawić wartość logicznego [afterViewsDone] na true, ponieważ informacja ta jest wykorzystywana przez klasę nadrzędną;
  • wiersze 42–49: metoda [updateFragment] polega na wyświetleniu krótkiego komunikatu z numerem wizyty (wiersz 48) oraz zwiększeniu tego numeru w sesji (wiersze 44–46);

Zachęcamy czytelnika do przetestowania tego nowego projektu.

W wszystkich przyszłych projektach będziemy stosować tę architekturę:

  • jedna aktywność i n fragmentów;
  • wszystkie fragmenty dziedziczą po klasie [AbstractFragment];
  • dane, które mają być współdzielone między fragmentami oraz między fragmentami a aktywnością, umieszczone są w klasie [Session];

1.11.6. Powiązania między kartami a fragmentami

W klasie [MainActivity], która zarządza zakładkami, zapisano:


// pasek zakładek jest również powiązany z kontenerem fragmentów
// czyli zakładka nr i wyświetla fragment nr i z kontenera
tabLayout.setupWithViewPager(mViewPager);

Wiersz 3 powiązuje menedżera zakładek z kontenerem fragmentów. Widzieliśmy już jedną z konsekwencji tego powiązania: gdy użytkownik kliknie zakładkę nr i, kontener fragmentów wyświetla fragment nr i. Nie widzieliśmy jednak sytuacji odwrotnej: gdy poprosimy kontener fragmentów o wyświetlenie fragmentu nr i, wówczas zakładka nr i zostaje automatycznie zaznaczona.

Aby zilustrować to zachowanie, dodamy opcje [Fragment 1, Fragment 2, ...] do bieżącego menu. Gdy użytkownik kliknie opcję [Fragment i], kontener fragmentów otrzyma polecenie wyświetlenia fragmentu nr i. Wówczas sprawdzimy, czy zakładka nr i została zaznaczona, czy nie.

Ten etap rozpoczyna się od zmiany menu aplikacji:

 

Zawartość pliku [res / menu / menu_main.xml] zmienia się w następujący sposób:


<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>
  • wiersze 9–28: pięć nowych opcji menu;
  • nazwy opcji (wiersze 10, 14, 18, 22, 26) są zdefiniowane w pliku [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>
  <!-- widok 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>
  <!-- menu -->
  <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>

Efekt wizualny wygląda następująco:

  

Obsługa kliknięć na te opcje menu odbywa się w klasie [MainActivity]:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // dziennik
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // przetwarzanie opcji menu
    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;
      }
    }
    // przetworzona pozycja
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // zmiana wyświetlanego fragmentu
      mViewPager.setCurrentItem(i);
    }
  }
  • wiersz 2: metoda [onOptionsItemSelected] jest wywoływana po kliknięciu jednej z opcji menu;
  • wiersz 8: pobierany jest identyfikator klikniętej opcji;
  • wiersze 9–36: różne przypadki są obsługiwane przez metodę switch;
  • wiersze 16–36: kliknięcie opcji [Fragment i] przekierowuje do metody [showFragment(i-1)] z wierszy 41–45;
  • wiersz 43: wysyłane jest żądanie do kontenera fragmentów o wyświetlenie żądanego fragmentu;
  • wiersz 42: najpierw sprawdza się, czy jest to możliwe (warunek 1) i czy jest to konieczne (warunek 2);

Zachęcamy czytelnika do przetestowania tej nowej wersji. Można zauważyć, że po zażądaniu wyświetlenia fragmentu nr i jest on prawidłowo wyświetlany, a zakładka nr i zostaje zaznaczona.

Teraz, gdy już wiemy, jak działa powiązanie zakładek z fragmentami, przyjrzyjmy się innemu przypadkowi: sytuacji, w której zarządzanie zakładkami jest oddzielone od zarządzania fragmentami. Dzieje się tak na przykład wtedy, gdy jest mniej zakładek niż fragmentów. Aby zilustrować ten nowy przypadek użycia, stworzymy nowy projekt.

1.12. Przykład 11: Karty oddzielone od fragmentów

1.12.1. Tworzenie projektu

Duplikujemy projekt [Exemple-10], tworząc projekt [Exemple-11]:

1.12.2. Cele

Nowa aplikacja będzie miała dwie zakładki:

  • pierwsza zakładka będzie zawsze wyświetlać fragment [Vue1];
  • druga zakładka będzie wyświetlać fragment wybrany z menu;

Image

  • w [1] – fragment [Vue1];
  • w [2] – fragment typu [PlaceholderFragment] wybrany przez użytkownika;
  • w [3] kontynuuje się zliczanie odwiedzin;

1.12.3. Sesja

  

Nowa sesja będzie wyglądać następująco:


package exemples.android;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // liczba odwiedzonych fragmentów
  private int numVisit;
  // numer fragmentu typu [PlaceholderFragment] wyświetlonego w drugiej zakładce
  private int numFragment;

  // metody pobierające i ustawiające
...
}
  • wiersz 10: sami będziemy obsługiwać kliknięcia na zakładkach. Po kliknięciu na zakładkę należy przywrócić fragment, który był wyświetlany, gdy była ona ostatnio wybrana. Pole [numFragment] zapamięta numer tego fragmentu dla zakładki nr 2, czyli liczbę znajdującą się w polu [0, Fragments_COUNT-2]. Po kliknięciu zakładki nr 2 pobierzemy z sesji numer fragmentu, który ma zostać wyświetlony;

1.12.4. Menu

  

Menu [res / menu / menu_main.xml] zmienia się w następujący sposób:


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

Zakładka nr 2 wyświetli jeden z czterech fragmentów wierszy 9–24. Piątym fragmentem jest fragment [Vue1Fragment], który zawsze będzie wyświetlany w zakładce nr 1.

1.12.5. Klasa [MainActivity]

Klasa [MainActivity] musi teraz obsługiwać zakładki i nawigację między nimi, czego dotychczas nie robiła. Jej kod zmienia się w następujący sposób:


  // menedżer zakładek
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...

    // brak przewijania
    mViewPager.setScrollingEnabled(false);

    // wyświetlanie Widoku1
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

    // na początku mamy tylko jedną kartę
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);

    // menedżer zdarzeń
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // zaznaczono kartę – zmieniamy fragment wyświetlany przez kontener fragmentów
        ...
      }

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

      }

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

      }
    });

...

}
  • wiersz 17: pierwszym fragmentem wyświetlanym przez kontener fragmentów będzie fragment [Vue1Fragment]. Z założenia będzie to ostatni fragment w kontenerze;
  • wiersze 20–22: ponieważ nie utworzono powiązania między zakładkami a kontenerem fragmentów, musimy samodzielnie zarządzać zakładkami. Początkowo pasek zakładek [tabLayout] z wiersza 3 nie zawiera żadnej zakładki;
  • wiersz 20: tworzymy pierwszą zakładkę;
  • wiersz 21: nadajemy jej tytuł. W poprzednich przykładach tytułami zakładek były tytuły fragmentów. Teraz to się skończyło. W związku z tym usuwamy metodę [getPageTitle] z menedżera fragmentów. Nie jest już potrzebna:

    // opcjonalnie – nadanie tytułów zarządzanym fragmentom
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
}
  • wiersz 22: utworzona zakładka zostaje dodana do paska zakładek. Nasz pasek zakładek ma teraz jedną zakładkę. Co wyświetla ta zakładka? Należy zrozumieć, że zakładki i fragmenty to dwa niezależne pojęcia. Wyświetlany fragment to zawsze ten, który został wybrany przez kontener fragmentów. Jeśli zmienimy kartę i nie poprosimy kontenera o zmianę wyświetlanego fragmentu, nic się nie dzieje: nadal wyświetlany jest ten sam fragment, ale zmieniła się wybrana karta. Zatem w tym przypadku wyświetlany fragment to ten wybrany w wierszu 17: fragment [Vue1Fragment];
  • wiersze 26–30: metoda, którą należy napisać, aby obsłużyć zmianę zakładki przez użytkownika;

Metoda [onTabSelected] z wierszy 26–30 jest uruchamiana natychmiast po zmianie zakładki (jeśli użytkownik kliknie na już wybraną zakładkę, nic się nie dzieje). Jej kod wygląda następująco:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        if (IS_DEBUG_ENABLED) {
          Log.d("onglets", "onTabSelected");
        }
        // zaznaczono zakładkę – zmienia się fragment wyświetlany przez kontener fragmentów
        // pozycja zakładki
        int position = tab.getPosition();
        // numer fragmentu do wyświetlenia
        int numFragment;
        switch (position) {
          case 0:
            // nr fragmentu [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // numer fragmentu [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // wyświetlanie fragmentu
        mViewPager.setCurrentItem(numFragment);
}
  • wiersz 8: pobieramy pozycję zakładki, która została kliknięta. Otrzymamy tutaj liczbę 0 lub 1;
  • wiersze 12–15: jeśli kliknięto pierwszą kartę, przygotowujemy się do wyświetlenia fragmentu [Vue1Fragment];
  • wiersze 16–18: w pozostałych przypadkach (kliknięto zakładkę nr 2) przygotowujemy się do ponownego wyświetlenia fragmentu, który był wyświetlany ostatnim razem, gdy wybrano zakładkę nr 2. Jego numer został wówczas zapisany w sesji aplikacji;
  • wiersz 21: wysyłamy żądanie do kontenera fragmentów o wyświetlenie żądanego fragmentu;

Przyjrzyjmy się teraz obsłudze opcji menu (nadal w pliku [MainActivity]):


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // przetwarzanie opcji menu
    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;
      }
    }
    // przetworzona pozycja
    return true;
}
  • wiersze 16–31: obsługa 4 opcji menu. Każdy moduł obsługujący wywołuje metodę [showFragment], przekazując numer fragmentu do wyświetlenia;

Metoda [showFragment] wygląda następująco:


  // zakładka nr 2
  private TabLayout.Tab tab2 = null;

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // jeśli druga zakładka jeszcze nie istnieje, tworzymy ją
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // ustalamy tytuł drugiej zakładki
      tab2.setText(String.format("Fragment n° %s", (i + 1)));
      // zmienia się wyświetlany fragment
      mViewPager.setCurrentItem(i);
      // numer wyświetlanego fragmentu jest zapisywany w sesji
      session.setNumFragment(i);
      // wybieramy zakładkę 2 – nie wykonujemy żadnej czynności, jeśli jest ona już wybrana
      tab2.select();
    }
}
  • należy pamiętać, że w momencie uruchomienia aplikacji dostępna jest tylko jedna zakładka;
  • wiersz 2: odwołanie do zakładki nr 2, null na początku;
  • wiersz 5: warunki wyświetlania nie uległy zmianie w stosunku do poprzedniej wersji;
  • wiersze 7–10: jeśli zakładka nr 2 jeszcze nie istnieje, zostaje utworzona (wiersz 8) i dodana do paska zakładek (wiersz 9);
  • wiersz 12: w tytule drugiej zakładki umieszcza się numer fragmentu, który ma zostać wyświetlony, przy czym numeracja zaczyna się od 1;
  • wiersz 14: wyświetlany jest żądany fragment;
  • wiersz 16: jego numer jest zapisywany w sesji;
  • wiersz 18: wybierana jest zakładka nr 2. Jeśli była już wybrana, nic się nie stanie: metoda [onTabSelected] nie zostanie wykonana. Jeśli nie była jeszcze wybrana, uruchomi się metoda [onTabSelected]. Metoda ta nakazuje wówczas kontenerowi fragmentów wyświetlenie fragmentu już wyświetlonego w wierszu 14. Prosty test w metodzie [onTabSelected] pozwala uniknąć tej sytuacji:

        // wyświetlenie fragmentu tylko wtedy, gdy jest to konieczne
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

Zachęcamy czytelników do przetestowania tej nowej wersji.

1.12.6. Ulepszenia

Obecnie dobrze rozumiemy fragmenty, ich cykl życia, pojęcie sąsiedztwa fragmentów oraz ich związek z paskiem zakładek. Dysponujemy również solidną architekturą, która właśnie przeszła test z przykładu 11:

  • jedna aktywność i n fragmentów;
  • wszystkie fragmenty dziedziczą po klasie [AbstractFragment];
  • dane, które mają być współdzielone między fragmentami oraz między fragmentami a aktywnością, są umieszczone w klasie [Session];

W nowym projekcie sprecyzujemy relacje między aktywnością a fragmentami poprzez dodanie interfejsu.

1.13. Przykład 12: zdefiniowanie relacji między aktywnością a fragmentami

W tym przykładzie chcemy zdefiniować minimalne relacje między aktywnością a fragmentami. W tym celu wykorzystamy:

  • interfejs [IMainActivity], który określi, o co fragmenty mogą prosić aktywność;
  • klasę abstrakcyjną [AbstractFragment], która określi stan i metody, jakie powinien posiadać każdy fragment;

1.13.1. Tworzenie projektu

Duplikujemy projekt [Exemple-11] jako [Exemple-12], postępując zgodnie z procedurą opisaną w punkcie 1.4. Otrzymujemy następujący wynik:

1.13.2. Interfejs [IMainActivity]

Z powyższych przykładów wynika, że fragmenty muszą mieć dostęp do sesji zainicjowanej przez aktywność. Ponadto, choć nie widać tego w tych przykładach, można się spodziewać, że procedury obsługi zdarzeń fragmentów kończą się czasami zmianą widoku. Aktywność zostanie poproszona o dokonanie tej zmiany. Interfejs [IMainActivity] mógłby zatem wyglądać następująco:

  

package exemples.android;

public interface IMainActivity {

  // dostęp do sesji
  Session getSession();

  // zmiana widoku
  void navigateToView(int position);

  // tryb debugowania
  boolean IS_DEBUG_ENABLED = true;
}

W wierszu 12 należy zwrócić uwagę na obecność stałej, która wcześniej znajdowała się w klasie [MainActivity]. Chcemy zmniejszyć sprzężenie między fragmentami a aktywnością i ograniczyć je do sprzężenia między [AbstractFragment] a [IMainActivity]. Aktywność będzie mogła wówczas nosić inną nazwę niż [MainActivity]. Ponieważ stała [IS_DEBUG_ENABLED] jest wykorzystywana we fragmentach, zostaje przeniesiona do interfejsu [IMainActivity].

1.13.3. Klasa abstrakcyjna [AbstractFragment]

Klasa abstrakcyjna [AbstractFragment] ulega jedynie niewielkim zmianom:


  // dane  dostępne dla klas potomnych
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;

  // aktywność
  protected IMainActivity mainActivity;
  protected Activity activity;

...
  // aktualizacja fragmentu
  protected void update() {
    // pobieranie aktywności i sesji
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // wystosowuje się żądanie do klasy potomnej o aktualizację
    updateFragment();
}
  • wiersze 6 i 7: utrzymywane są dwa rodzaje odwołań do aktywności:
    • wiersz 6: odwołanie do aktywności implementującej interfejs [IMainActivity];
    • wiersz 7: odwołanie do aktywności dziedziczącej po klasie Android [Activity]. Tak jest w przypadku każdej aktywności;

Oba odwołania wskazują oczywiście na ten sam obiekt. Jednak obiekt ten jest postrzegany jako dwa różne typy. Pozwoli nam to uniknąć rzutowania typów podczas wykonywania;

  • wiersz 14: pobieramy odwołanie do aktywności za pomocą metody [getActivity];
  • wiersz 15: jeśli odwołanie to jest niezerowe, wówczas uzyskujemy dostęp do sesji;
  • wiersze 16–17: zapisujemy aktywność jako implementację interfejsu [IMainActivity] oraz sesję;

1.13.4. Modyfikacja menedżera fragmentów

Menedżer fragmentów [SectionsPagerAdapter] w klasie [MainActivity] został zmodyfikowany w jednym punkcie: zamiast obsługiwać fragmenty typu [Fragment], obsługuje teraz fragmenty typu [AbstractFragment]:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmenty
    private AbstractFragment[] fragments;
    // numer fragmentu
    private static final String ARG_SECTION_NUMBER = "section_number";

    // konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      // klasa nadrzędna
      super(fm);
      // inicjalizacja tablicy fragmentów
      fragments = new AbstractFragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        ...
      }
      // fragment +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

    // pozycja fragmentu nr
    @Override
    public AbstractFragment getItem(int position) {
      ...
    }

    // zwraca liczbę obsługiwanych fragmentów
    @Override
    public int getCount() {
      ...
    }
}

1.13.5. Modyfikacja klasy [MainActivity]

Klasa [MainActivity] musi implementować interfejs [IMainActivity]:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{

...
  // wstrzyknięcie sesji
  @Bean(Session.class)
  protected Session session;
...
  // pobieranie sesji
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // wyświetla widok pozycji
    if(mViewPager.getCurrentItem()!=position){
      // wyświetlanie fragmentu
      mViewPager.setCurrentItem(position);
    }
  }

  • wiersze 10–12: metoda [getSession] już istniała;
  • wiersze 15–22: metoda [navigateToView] powoduje wyświetlenie fragmentu nr [position];
  • wiersz 17: sprawdzane jest, czy jest coś do zrobienia;
  • wiersz 19: wyświetlany jest fragment nr [position];

Na tym etapie uruchom aplikację. Powinna działać.

1.13.6. Modyfikacja wyświetlania fragmentów w [MainActivity]

Obecnie klasa [MainActivity] wyświetla fragment za pomocą instrukcji:


    // wyświetlanie Widoku1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

Ponieważ metoda [navigateToView] działa w ten sam sposób, należy zastąpić ten typ instrukcji we wszystkich miejscach (w 2 miejscach) na:

navigateToView(...);

Następnie uruchom aplikację. Powinna nadal działać.

1.13.7. Wniosek

Od tej pory będziemy zawsze stosować poprzednią architekturę:

  • aktywność implementująca interfejs [IMainActivity];
  • fragmenty rozszerzające klasę [AbstractFragment], co wymaga od nich zaimplementowania metody [updateFragment]. Muszą one również posiadać metodę [@AfterViews], w której ustawiają wartość logicznego [afterViewsDone] na true;
  • sesję zawierającą dane, które mają być współdzielone między fragmentami a aktywnością;

1.14. Przykład 13: Przykład 05 z fragmentami

W projekcie [Exemple-05] wprowadziliśmy nawigację między widokami. Była to wówczas nawigacja między aktywnościami: 1 widok = 1 aktywność. Tutaj proponujemy, aby mieć jedną aktywność z wieloma widokami typu [AbstractFragment].

1.14.1. Tworzenie projektu

Duplikujemy poprzedni projekt [Exemple-12], tworząc projekt [Exemple-13], postępując zgodnie z procedurą opisaną w punkcie 1.4. Otrzymujemy następujący wynik:

1.14.2. Organizacja projektu

Zaczniemy korzystać z pakietów w celu uporządkowania kodu. Na razie możemy wyróżnić dwa odrębne obszary:

  • zarządzanie aktywnością;
  • zarządzanie fragmentami;

Tworzymy dla nich dwa pakiety: [exemples.android.activity] i [exemples.android.fragments]:

 

W ten sam sposób tworzymy pakiet [exemples.android.fragments]:

W pakiecie [8] tworzymy trzeci pakiet o nazwie [architecture], w którym umieścimy elementy [IMainActivity, AbstractFragment, Session, MyPager], stanowiące podstawę architektury naszej aplikacji. Ma to na celu przypomnienie nam, że dokonaliśmy konkretnego wyboru architektury. Następnie przenieś istniejące elementy projektu zgodnie z instrukcją zawartą w [9]. Każde przeniesienie należy zatwierdzić, klikając przycisk [Refactor].

Na tym etapie skompiluj aplikację. W pliku [MainActivity] występują następujące błędy:

 

Podczas przenoszenia klas do pakietów program Android Studio wprowadził niezbędne zmiany w kodzie aplikacji (na przykład w wierszach 18–21). Klasy, których dotyczą wiersze 15 i 17, nie zostały przeniesione. Są one generowane przez bibliotekę Android Annotations. W przypadku tych klas należy ręcznie zmienić imports. Wiersze te przyjmują zatem następującą postać:

 

Po dokonaniu tej zmiany nie ma już błędów kompilacji. Uruchom aplikację. Pojawi się wówczas następujący błąd:

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}: 

Ten błąd wynika z manifestu aplikacji:

  

<?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>

Wiersze 3 i 12 wskazują, że określoną aktywnością jest [exemples.android.MainActivity_]. Ponieważ jednak aktywność została przeniesiona do pakietu [activity], wiersz 12 powinien teraz wyglądać następująco:


      android:name=".activity.MainActivity_"

Należy zwrócić uwagę na znak „.” przed [activity]. Ponownie Android Studio nie mogło zaktualizować manifestu, ponieważ odwołuje się on do klasy Android Annotations, która nie została przeniesiona. Korzystanie z biblioteki AA wiąże się zatem z pewnymi niedogodnościami.

1.14.3. Porządkowanie projektu

W nowym projekcie:

  • nie ma już zakładek, pływających przycisków ani menu;
  • fragmenty [PlaceholderFragment] znikają. Aplikacja będzie obsługiwać dwa fragmenty: [Vue1Fragment], który już mamy, oraz [Vue2Fragment], który trzeba będzie utworzyć;
  • sesja nie jest już ta sama;

1.14.3.1. Czyszczenie fragmentów

Usuń klasę [PlaceHolderFragment] [1]:

 

Podobnie usuń widok [res / layout / fragment_main.xml] powiązany z tym fragmentem [2].

1.14.3.2. Czyszczenie sesji

Obecnie sesja wygląda następująco:


package exemples.android.architecture;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // liczba odwiedzonych fragmentów
  private int numVisit;
  // numer fragmentu typu [PlaceholderFragment] wyświetlonego w drugiej zakładce
  private int numFragment;

  // metody pobierające i ustawiające

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

Nie zachowujemy niczego z tej sesji.

Skompiluj projekt. Błędne wiersze to te, które wykorzystywały zawartość sesji. Usuń je. W klasie [Vue1Fragment] usuwamy również zmienną [numVisit] z kodu, który przybiera następującą postać:


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 {

  // elementy interfejsu wizualnego
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // menedżer zdarzeń
  @Click(R.id.buttonValider)
  protected void doValider() {
    // wyświetla się wpisana nazwa
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }


  // aktualizacja fragmentu
  @Override
  protected void updateFragment() {
  }
}

1.14.3.3. Usunięcie zakładek, pływającego przycisku i menu

Usunięcie zakładek i pływającego przycisku odbywa się w dwóch miejscach:

  • w widoku [res / layout / activity-main.xml], który definiuje te elementy i ich położenie w widoku;
  • w kodzie aktywności [MainActivity];

Usunięcie menu odbywa się również w dwóch miejscach:

  • w widoku [res / menu / menu-main.xml], który definiuje opcje menu;
  • w kodzie aktywności [MainActivity];

Kod widoku [res / layout / activity-main.xml] wygląda obecnie następująco:


<?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>
  • usuwa się wiersze [28-31, 41-47];
  • usuwa się również pasek narzędzi z wierszy 18–24;

Kod menu [res / menu / menu_main.xml] ma obecnie następującą postać:


<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>
  • usuwamy wiersze 9–24. Pozostawiamy w ten sposób opcję, której nie będziemy używać. Ma to na celu jedynie przedstawienie przykładu deklaracji opcji menu, którą można powielić poprzez skopiowanie i wklejenie;

W klasie [MainActivity] usuwamy wszystko, co odnosi się do zakładek, pływającego przycisku, paska narzędzi i menu. Najłatwiej znaleźć te odniesienia, usuwając ich deklaracje:


  // menedżer kart
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // pływający przycisk
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

i ponownie skompilować aplikację. Błędne wiersze to te, które odnoszą się do usuniętych elementów. Należy zatem usunąć wszystkie te wiersze. Ponadto należy zmodyfikować menedżer fragmentów, aby nie odnosił się już do fragmentu [PlaceholderFragment], który został usunięty:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmenty
    private AbstractFragment[] fragments;

    // konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      // element nadrzędny
      super(fm);
    }

    // pozycja fragmentu nr
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // zwraca liczbę obsługiwanych fragmentów
    @Override
    public int getCount() {
      return fragments.length;
    }
}
  • wiersze 7–10: usunięto całe generowanie fragmentów;

Na tym etapie nie powinno już występować żadnych błędów kompilacji. W klasie [MainActivity] uzyskaliśmy następujący kod pośredni:


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 {

  // kontener fragmentów
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// pasek narzędzi
@ViewById(R.id.toolbar)
protected Toolbar toolbar;

  // sesja wstrzykiwania
  @Bean(Session.class)
  protected Session session;

  // liczba fragmentów
  private final int FRAGMENTS_COUNT = 5;
  // sąsiedztwo fragmentów
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

  // tryb debugowania
  public static final boolean IS_DEBUG_ENABLED = true;

  // menedżer fragmentów
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // konstruktor
  public MainActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "constructor");
    }
  }

  @AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }

    // pasek narzędzi – tutaj wyświetlana jest nazwa aplikacji
    setSupportActionBar(toolbar);

    // menedżer fragmentów
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // kontener fragmentów jest powiązany z menedżerem fragmentów
    // czyli fragment nr i z kontenera fragmentów jest fragmentem nr i dostarczonym przez menedżera fragmentów
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // przesunięcie fragmentów
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // blokuje się przesuwanie między fragmentami
    mViewPager.setSwipeEnabled(false);

    // bez przewijania
    mViewPager.setScrollingEnabled(false);

    // wyświetlanie Widok1
    navigateToView(FRAGMENTS_COUNT - 1);

  }

  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
  }

  // pobieranie sesji
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // wyświetlanie widoku pozycji
    if (mViewPager.getCurrentItem() != position) {
      // wyświetlanie fragmentu
      mViewPager.setCurrentItem(position);
    }
  }

  // menedżer fragmentów
  // to do niego kierowane są żądania dotyczące fragmentów, które mają być wyświetlone w widoku głównym
  // musi zdefiniować metody [getItem] i [getCount] – pozostałe są opcjonalne
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmenty
    private AbstractFragment[] fragments;

    // konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      // element nadrzędny
      super(fm);
    }

    // numer fragmentu i pozycja
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // zwraca liczbę obsługiwanych fragmentów
    @Override
    public int getCount() {
      return fragments.length;
    }
  }
}

Pozostało jeszcze kilka zmian do wprowadzenia:

  • usuń wiersz 31, który nie jest już potrzebny;
  • w wierszu 33: ustaw 1 jako sąsiedztwo fragmentów;
  • w wierszu 76: przejdź do widoku 0. To właśnie on zostanie wyświetlony jako pierwszy;
  • wiersz 108: zainicjuj tablicę fragmentem [Vue1Fragment_]:

    // fragmenty
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Mamy więc tylko jeden fragment. Uruchom aplikację. Powinieneś uzyskać następujący wynik:

Image

Przycisk [Valider] powinien działać.

1.14.4. Tworzenie fragmentów i powiązanych widoków

Aplikacja będzie miała dwa widoki, pochodzące z projektu [Exemple-05]. W obecnym projekcie mamy już widok [vue1.xml]. Teraz skopiujemy widok [vue2.xml] z projektu [Exemple-05] do projektu [Exemple-12] (otwórz oba projekty i skopiuj oraz wklej widok między nimi).

 
  • w [1], nowy widok. Podczas próby edycji pojawiają się błędy w pliku [2]. Musimy zmodyfikować plik [strings.xml] oraz [3], aby dodać do nich ciągi znaków, do których odwołuje się ten nowy widok:

<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>
  <!-- widok 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- widok 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>

Duplikujemy klasę [Vue1Fragment] do pliku [Vue2Fragment]:

  

i modyfikujemy skopiowany kod w następujący sposób:


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() {
    // pamięć
    afterViewsDone = true;
    // dziennik
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // aktualizacja fragmentu
  @Override
  protected void updateFragment() {
  }
}
  • wiersz 9: fragment jest powiązany z widokiem [res / layout / vue2.xml];
  • wiersz 10: klasa ta rozszerza klasę abstrakcyjną [AbstractFragment];
  • wiersze 12–20: obowiązkowa metoda [@AfterViews];
  • wiersze 23–25: obowiązkowa metoda [updateFragment];

1.14.5. Wprowadzenie fragmentów i nawigacji między nimi

Aktywność będzie teraz obsługiwać dwa fragmenty. Jej klasa [SectionsPagerAdapter] zmienia się w następujący sposób:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmenty
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};

    ...
}

Interfejs [IMainActivity] zapewnia nawigację między widokami za pomocą metody [navigateToView]. Będziemy obsługiwać kliknięcie przycisku [Vue n° 2] w fragmencie [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 {

  // elementy interfejsu wizualnego
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // menedżery zdarzeń ----------------------------------
  @Click(R.id.buttonValider)
  protected void doValider() {
    // wyświetla się wpisana nazwa
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    mainActivity.navigateToView(1);
  }

  // aktualizacja fragmentu
  @Override
  protected void updateFragment() {
  }
}
  • wiersze 37–40: metoda [showVue2] obsługuje zdarzenie „kliknięcia” przycisku [Vue n° 2];
  • wiersz 39: nawigacja odbywa się za pomocą metody [navigateToView] tej aktywności. Przypominamy, że aktywność została zapisana w klasie nadrzędnej w postaci:

  // aktywność
protected IMainActivity mainActivity;

i że ta aktywność została już zainicjowana w momencie wejścia do dowolnego menedżera zdarzeń.

  • wiersz 34: instrukcja wykorzystuje zmienną [activity] z klasy nadrzędnej, która jest odwołaniem do aktywności jako instancji typu Android [Activity];

protected Activity activity;

Podobny kod występuje w przypadku fragmentu [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() {
    // pamięć
    afterViewsDone = true;
    // dziennik
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // menedżery zdarzeń ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // aktualizacja fragmentu
  @Override
  protected void updateFragment() {
  }
}
  • wiersze 24–27: metoda [showVue1] obsługuje zdarzenie „kliknięcia” przycisku [Vue n° 1];

Uruchom projekt i sprawdź, czy nawigacja między widokami działa.

1.14.6. Definicja sesji

Aplikacja działa w następujący sposób:

  • wprowadzenie nazwy w widoku nr 1;
  • wyświetlenie tej nazwy w widoku nr 2;

Aby widok nr 1 mógł przekazać wprowadzoną nazwę do widoku nr 2, użyjemy następującej sesji:


package exemples.android.architecture;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // nazwa
  private String nom;

  // metody pobierające i ustawiające
...
}
  • wiersz 8: wpisana nazwa;

Klasa [MainActivity] zainicjuje sesję w następujący sposób:


  // wstrzykiwanie sesji
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // inicjowanie sesji
    session.setNom("");
}

1.14.7. Ostateczne zapisanie fragmentów

W fragmencie [Vue1Fragment] modyfikujemy kod procedury obsługi kliknięcia przycisku [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 {

  // elementy interfejsu wizualnego
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

...
  // obsługa zdarzeń ----------------------------------

  @Click(R.id.buttonValider)
  protected void doValider() {
    // zapisywanie wprowadzonej nazwy
    String nom = editTextNom.getText().toString();
    // wyświetlanie nazwy
    Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
  }

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // wprowadzona nazwa jest zapisywana w sesji
    session.setNom(editTextNom.getText().toString());
    // przechodzimy do widoku nr 2
    mainActivity.navigateToView(1);
  }

  // aktualizacja fragmentu
  @Override
  protected void updateFragment() {

  }
}
  • wiersze: 31–37: obsługujemy kliknięcie przycisku [Vue n° 2];
  • wiersz 34: przed przejściem do widoku nr 2 zapisujemy wprowadzoną nazwę w sesji, aby nowy widok miał do niej dostęp;

Widok [Vue2Fragment] zmienia się w następujący sposób:


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 {

  // elementy interfejsu graficznego
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;

  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // obsługa zdarzeń ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // aktualizacja fragmentu
  @Override
  protected void updateFragment() {
    // pobieranie nazwy wprowadzonej podczas sesji
    String nom = session.getNom();
    // wyświetlanie nazwy
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
  }
}

Gdy wyświetla się widok nr 2, należy wyświetlić nazwę wprowadzoną w widoku nr 1. Wiemy, że zaraz po jego wyświetleniu zostanie wykonana jego metoda [updateFragment]. Dlatego właśnie w tej metodzie (wiersze 36–42) można umieścić kod wyświetlający nazwę.

  • wiersze 16–17: deklaracja jedynego komponentu wizualnego widoku;
  • wiersz 39: nazwa wprowadzona w widoku nr 1 jest pobierana z sesji;
  • wiersz 41: nazwa [textViewBonjour] zostaje zmodyfikowana;

Uruchom projekt i sprawdź, czy działa.

1.14.8. Zarządzanie cyklem życia fragmentów

W fragmencie [Vue1Fragment] metoda [@AfterViews] wygląda następująco:


  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
}

Ta metoda jest niekompletna. Należy bowiem zawsze uwzględnić przypadek, w którym fragment jest ponownie wykorzystywany po operacji [onDestroyView]. W takim przypadku widok fragmentu 1 jest odświeżany, a nazwa, która mogła zostać wcześniej wprowadzona, zniknie z widoku. Nie chcemy, aby tak się działo. Obecnie wprowadzona nazwa pozostaje widoczna, ponieważ sąsiedztwo fragmentów 1 powoduje, że cykl życia fragmentu [Vue1Fragment] jest wykonywany tylko raz. Lepiej jednak uwzględnić przypadek ponownego wykorzystania fragmentu.

Istnieje kilka sposobów rozwiązania tego problemu:

  • można wykorzystać fakt, że metoda [update] jest uruchamiana systematycznie przy każdym wyświetleniu fragmentu, aby zaktualizować wprowadzoną nazwę;
  • można dokonać tej aktualizacji wyłącznie wtedy, gdy metoda [@AfterViews] zostanie ponownie wykonana. Wybieramy właśnie tę ostatnią opcję;

Modyfikujemy kod metody [Vue1Fragment] w następujący sposób:


    // elementy interfejsu graficznego
    @ViewById(R.id.editTextNom)
    protected EditText editTextNom;

    // dane
    private String nom;

    @AfterViews
    protected void afterViews() {
        // pamięć
        afterViewsDone = true;
        // log
        if (isDebugEnabled) {
            Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // (ponowna) inicjalizacja wyświetlanego tekstu
        editTextNom.setText(nom);
    }

    // obsługa zdarzeń ----------------------------------

...

    @Click(R.id.buttonVue2)
    protected void showVue2() {
        // zapisuje się wprowadzoną nazwę, aby można było ją odzyskać w przypadku ponownego wykorzystania fragmentu
        nom = editTextNom.getText().toString();
        // wprowadzamy wprowadzoną nazwę do sesji
        session.setNom(nom);
        // przechodzi się do widoku nr 2
        activity.navigateToView(1);
}
  • wiersz 27: tuż przed przejściem z widoku 1 do widoku 2 zapisujemy wprowadzoną nazwę;
  • wiersz 17: przy każdym nowym uruchomieniu cyklu życia fragmentu ostatnia wprowadzona nazwa jest ponownie wyświetlana;

W przypadku fragmentu [Vue2Fragment] wystarczy istniejący kod:


  // komponenty interfejsu graficznego
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;

  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // aktualizacja fragmentu
  @Override
  protected void updateFragment() {
    // pobieramy nazwę wprowadzoną podczas sesji
    String nom = session.getNom();
    // wyświetlanie nazwy
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
  • jedyny element wizualny widoku (wiersz 3) jest aktualizowany za każdym razem, gdy widok jest wyświetlany (wiersz 21). Metoda [@AfterViews] nie ma zatem nic do dodania;

1.14.9. Wniosek

W tym momencie ponownie wykazaliśmy trafność naszej architektury:

  • aktywność implementująca interfejs [IMainActivity];
  • fragmenty rozszerzające klasę [AbstractFragment], co nakłada na nie obowiązek zaimplementowania metody [updateFragment]. Muszą one również posiadać metodę [@AfterViews], w której ustawiają wartość logicznego [afterViewsDone] na true;
  • sesję zawierającą dane, które mają być współdzielone między fragmentami a aktywnością;

1.15. Przykład 14: architektura dwuwarstwowa

Zbudujemy aplikację z jednym widokiem o następującej architekturze:

1.15.1. Tworzenie projektu

Duplikujemy poprzedni projekt [Exemple-12], tworząc projekt [Exemple-13], postępując zgodnie z procedurą opisaną w punkcie 1.4. Otrzymujemy następujący wynik:

1.15.2. Widok [vue1]

Aplikacja będzie miała tylko jeden widok [vue1.xml]. Usuwamy więc drugi widok [vue2.xml] wraz z powiązanym z nim fragmentem:

 

Skompiluj aplikację. W widoku [MainActivity] pojawiają się błędy:

 

Popraw poniższy wiersz 4 w menedżerze fragmentów [SectionsPagerAdapter]


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmenty
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...

Wiersz 4 powyżej przyjmuje następującą postać:


    // fragmenty
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Usuń zbędne importy [Ctrl-Shift-O]. Nie powinno już występować żadnych błędów kompilacji. Uruchom projekt: powinien pojawić się widok nr 1. Teraz go zmodyfikujemy.

Utworzymy widok [vue1.xml], który pozwoli generować liczby losowe:

 

Jego komponenty są następujące:

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

Jego kod XML jest następujący:


<?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>

Poprzedni widok wykorzystuje etykiety zdefiniowane w pliku [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>
  <!-- widok 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>

Kolory użyte w pliku [vue1.xml] są zdefiniowane w pliku [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>
  <!-- kolory aplikacji -->
  <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. Sesja

  

Ponieważ w tym przypadku występuje tylko jeden fragment, nie ma potrzeby przewidywania komunikacji między fragmentami. Sesja będzie zatem pusta:


package exemples.android.architecture;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
}

Na tym etapie należy skompilować aplikację. Błędy pojawią się w wierszach, w których wykorzystywano elementy sesji, która jest teraz pusta. Należy usunąć te wiersze i sprawdzić, czy kompilacja nie generuje już błędów.

1.15.4. Fragment [Vue1Fragment]

  

Modyfikujemy istniejący fragment [Vue1Fragment] w następujący sposób:


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 {

  // elementy interfejsu wizualnego
  @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;

  // lista odpowiedzi na polecenie
  private List<String> reponses = new ArrayList<>();
  // adapter widoku listy
  private ArrayAdapter<String> adapterReponses;

  // wprowadzone dane
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
    // pamięć
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // ukrywa się  komunikaty o błędach
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
    // ukrywa ewentualne poprzednie komunikaty o błędach
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // sprawdza się poprawność wprowadzonych danych
    if (!isPageValid()) {
      return;
    }
  }

  // sprawdza się poprawność wprowadzonych danych
  private boolean isPageValid() {
...
  }

  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
  }
}
  • W tym przypadku mamy do czynienia z jednym fragmentem, którego cykl życia zostanie wykonany tylko raz, podczas uruchamiania aplikacji. Z tego powodu metody [@AfterViews] (wiersze 46–57) i [udateFragment] (wiersze 75–81) zostaną wykonane tylko raz, podczas uruchamiania aplikacji;
  • wiersze 55–56: ukrywa się dwa komunikaty o błędach z widoku (przedstawione poniżej) [1-2];
 
  • wiersze 59–60: metoda wykonywana po kliknięciu przycisku [Exécuter];
  • wiersze 71–73: sprawdzana jest poprawność wprowadzonych danych;

Metoda [isPageValid] wygląda następująco:


  // wprowadzone dane
  private int nbAleas;
  private int a;
  private int b;

...

// sprawdzana jest poprawność wprowadzonych danych
  private boolean isPageValid() {
    // wprowadzanie liczby liczb losowych
    nbAleas = 0;
    Boolean erreur;
    int nbErreurs = 0;
    try {
      nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
      erreur = (nbAleas < 1);
    } catch (Exception ex) {
      erreur = true;
    }
    // błąd?
    if (erreur) {
      nbErreurs++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // wprowadzenie wartości „a”
    a = 0;
    erreur = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      erreur = true;
    }
    // błąd?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // wprowadzenie litery „b”
    b = 0;
    erreur = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      erreur = b < a;
    } catch (Exception ex) {
      erreur = true;
    }
    // błąd?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // powrót
    return (nbErreurs == 0);
  }

  • wiersze 2–4: te trzy pola są inicjowane przez metodę [isPageValid]. Ponadto metoda ta zwraca wartość true, jeśli wszystkie wprowadzone dane są prawidłowe, a w przeciwnym razie – false. Jeśli wprowadzone dane są nieprawidłowe, wyświetlane są odpowiednie komunikaty o błędach;

Na tym etapie aplikacja jest gotowa do uruchomienia. Sprawdź działanie metody [isPageValid], wprowadzając nieprawidłowe dane.

1.15.5. Warstwa [métier]

  

Warstwa [métier] posiada następujący interfejs [IMetier]:


package exemples.android.metier;

import java.util.List;

public interface IMetier {

    List<Object> getAleas(int a, int b, int n);
}

Metoda [getAleas(a,b,n)] zwraca zazwyczaj n losowych liczb całkowitych z przedziału [a,b]. Przewidziano również, że co trzeci raz zwraca ona wyjątek, który jest również dołączany do odpowiedzi zwracanych przez tę metodę. Ostatecznie metoda ta zwraca listę obiektów typu [Exception] lub [Integer].

Implementacja tego interfejsu o nazwie [Metier] wygląda następująco:


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) {
        // lista obiektów
        List<Object> réponses = new ArrayList<Object>();
        // kilka sprawdzeń
        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"));
        }
        // błąd?
        if (réponses.size() != 0) {
            return réponses;
        }
        // generowanie liczb losowych
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // generowanie losowego wyjątku 1 raz na 3
            int nombre = random.nextInt(3);
            if (nombre == 0) {
                réponses.add(new AleaException("Exception aléatoire"));
            } else {
                // w przeciwnym razie zwracana jest liczba losowa z przedziału [a,b]
                réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // wynik
        return réponses;
    }
}
  • wiersz 9: stosuje się adnotację AA [@EBean] w klasie [Metier], aby umożliwić wstawienie odwołań do tej klasy w warstwie [Présentation]. Atrybut (scope = EBean.Scope.Singleton) sprawia, że klasa [Metier] zostanie zainicjowana tylko w jednym egzemplarzu. W związku z tym zawsze wstrzykiwany jest ten sam odwołanie, nawet jeśli wstrzykuje się je wielokrotnie do warstwy [Présentation];
  • pozostała część kodu jest standardowa;

Typ [AleaException] używany przez klasę [Metier] ma następującą postać:


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

}
  • wiersz 3: klasa [AleaException] rozszerza klasę systemową [RuntimeException], co czyni ją wyjątkiem niekontrolowanym: nie ma obowiązku obsługi jej w bloku try/catch ani umieszczania jej w sygnaturze metod;

1.15.6. Aktywność [MainActivity] w nowej odsłonie

  

Warstwa

[metier]

Aktywność

Widok

Użytkownik

Aktywność ta będzie implementować interfejs [IMetier] warstwy [métier]. W ten sposób fragment / widok będzie miał wyłącznie tę aktywność jako partnera komunikacyjnego.

Aktywność [MainActivity] już implementuje interfejs [IMainActivity]. Aby implementowała ona również interfejs [IMetier], można:

  • dodać interfejs [IMetier] do interfejsów implementowanych przez aktywność;
  • sprawić, by interfejs [IMainActivity] sam rozszerzał interfejs [IMetier]. Właśnie tę drogę wybieramy;

Interfejs [IMainActivity] przyjmuje następujący kształt:

  

package exemples.android.architecture;

import exemples.android.metier.IMetier;

public interface IMainActivity extends IMetier {

  // dostęp do sesji
  Session getSession();

  // zmiana widoku
  void navigateToView(int position);

  // tryb debugowania
  public static final boolean IS_DEBUG_ENABLED = true;

}
  • wiersz 5: interfejs [IMainActivity] rozszerza interfejs [IMetier]

Klasa [MainActivity] zmienia się w następujący sposób:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {

  ...

  // wstrzyknięcie sesji
  @Bean(Session.class)
  protected Session session;

  // wstrzyknięcie logiki biznesowej
  @Bean(Metier.class)
  protected IMetier metier;

...
  // implementacja IMetier --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return metier.getAleas(a, b, n);
}
  • wiersze 11–12: warstwa [métier] jest wstrzykiwana do aktywności. W tym celu wykorzystuje się adnotację AA [@Bean], której parametrem jest klasa opatrzona adnotacją AA [@EBean];
  • wiersz 2: aktywność implementuje interfejs [IMainActivity], a tym samym interfejs [IMetier] warstwy [métier];
  • wiersze 16–19: implementacja jedynej metody interfejsu [IMetier]. Ograniczamy się do przekazania wywołania do warstwy [métier];

1.15.7. Ponowne spojrzenie na fragment [Vue1Fragment]

  

Kod klasy [Vue1Fragment] zmienia się w następujący sposób:


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 {

  // elementy interfejsu graficznego
  @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;

  // lista odpowiedzi na polecenie
  private List<String> reponses = new ArrayList<>();
  // adapter widoku listy
  private ArrayAdapter<String> adapterReponses;

  // wprowadzone dane
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
   ...
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
  ...
  }

  // sprawdzanie poprawności wprowadzonych danych
  private boolean isPageValid() {
   ...
  }

  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // zostanie wykonany tylko raz podczas uruchamiania aplikacji
    // tworzy się adapter ListView – wymaga to zainicjowania zmiennej [activity]
    adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
  }
}
  • wiersze 69–70: ustalono adapter komponentu typu [ListView];

Komponent [ListView] służy do wyświetlania listy elementów. Odbywa się to za pomocą adaptera typu [ListAdapter], który z kolei jest połączony ze źródłem danych, które ma zasilać komponent [ListView]. Aby zdefiniować adapter dla komponentu [ListView], należy skorzystać z następującej metody [ListView.setAdapter]:


public void setAdapter (ListAdapter adapter)

[ListAdapter] jest interfejsem. Klasa [ArrayAdapter] jest klasą implementującą ten interfejs. Konstruktor użyty w wierszu 69 powyżej wygląda następująco:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] to aktywność wyświetlająca [ListView];
  • [resource] to liczba całkowita identyfikująca widok używany do wyświetlania elementu z [ListView]. Widok ten może mieć dowolną złożoność. Tworzy go programista zgodnie ze swoimi potrzebami;
  • [textViewResourceId] to liczba całkowita identyfikująca komponent [TextView] w widoku [resource]. Wyświetlany ciąg znaków zostanie wyświetlony przez ten komponent;
  • [objects]: lista obiektów wyświetlanych przez [ListView]. Metoda [toString] obiektów służy do wyświetlenia obiektu w widoku [TextView], identyfikowanego przez [textViewResourceId], w widoku identyfikowanym przez [resource].

Zadaniem programisty jest utworzenie widoku o identyfikatorze [resource], który wyświetli każdy element obiektu o identyfikatorze [ListView]. W prostym przypadku, gdy chcemy wyświetlić tylko zwykły ciąg znaków, jak tutaj, system Android udostępnia widok o identyfikatorze [android.R.layout.simple_list_item_1]. Zawiera on komponent o identyfikatorze [TextView], który z kolei jest identyfikowany przez [android.R.id.text1]. Jest to metoda użyta w wierszu 69 do utworzenia adaptera dla [ListView]. Adapter ten należy zdefiniować tylko raz. Aby umożliwić jego ponowne wykorzystanie, zdefiniowano go jako zmienną instancji klasy (wiersz 39). Przyjrzyjmy się ponownie wierszowi 69:


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

Pierwszym parametrem konstruktora [ArrayAdapter] jest aktywność uzyskana we fragmencie przez metodę [getActivity], która została tutaj zapisana w zmiennej [activity] klasy nadrzędnej. Pole to nie zawsze ma wartość. Z logów wynika zatem, że w momencie wejścia do metody [@AfterViews] nie zostało ono jeszcze zainicjowane, a zatem nie można umieścić wierszy 69–70 w tej metodzie. W metodzie [updateFragment] jest to możliwe, ponieważ wiadomo, że gdy ta metoda jest wykonywana, to z konieczności istnieje już metoda [activity!=null]. Adapter jest tutaj powiązany ze źródłem danych [reponses] zdefiniowanym w wierszu 37;

Metoda [doExecuter] obsługuje kliknięcie przycisku [Exécuter]. Jej kod wygląda następująco:


@Click(R.id.btn_Executer)
  void doExecuter() {
    // ukrywa się ewentualne poprzednie komunikaty o błędach
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // kasuje się poprzednie odpowiedzi
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    // sprawdzamy poprawność wprowadzonych danych
    if (!isPageValid()) {
      return;
    }
    // żądamy liczb losowych od aktywności
    List<Object> data = mainActivity.getAleas(a, b, nbAleas);
    // tworzy się listę ciągów znaków na podstawie tych danych
    for (Object o : data) {
      if (o instanceof Exception) {
        reponses.add(((Exception) o).getMessage());
      } else {
        reponses.add(o.toString());
      }
    }
    // odśwież listview
    adapterReponses.notifyDataSetChanged();
  }
  • wiersze 7–8: chcemy wyczyścić ListView. W tym celu czynimy to poprzez wyczyszczenie źródła danych [reponses] i żądamy od adaptera powiązanego z ListView odświeżenia danych;
  • wiersze 10–12: przed wykonaniem żądanej czynności sprawdzamy, czy wprowadzone wartości są poprawne;
  • wiersz 14: od aktywności żąda się listy liczb losowych. Otrzymuje się listę obiektów, z których każdy jest typu [Integer] lub [AleaException];
  • wiersze 16–22: na podstawie otrzymanej listy obiektów aktualizuje się źródło danych [reponses], które wyświetla obiekt ListView;
  • wiersz 24: wysyłamy żądanie odświeżenia do adaptera ListView;

1.15.8. Uruchomienie

Uruchom projekt i sprawdź, czy działa poprawnie.

1.16. Przykład 15: architektura klient-serwer

Omówimy popularną architekturę aplikacji na Androida, w której aplikacja komunikuje się ze zdalnymi usługami internetowymi. Otrzymamy następującą architekturę:

Do aplikacji na Androida dodano warstwę [DAO] w celu komunikacji ze zdalnym serwerem. Będzie ona komunikować się z serwerem, który generuje liczby losowe wyświetlane na tablecie z systemem Android. Serwer ten będzie miał następującą dwuwarstwową architekturę:

Klienci wysyłają zapytania o niektóre elementy URL z warstwy [web / jSON] i otrzymują odpowiedź tekstową w formacie jSON (JavaScript Object Notation). W tym przypadku nasza usługa internetowa przetworzy pojedynczy element URL typu [/a/b], który zwróci liczbę losową z przedziału [a,b]. Opiszemy działanie aplikacji w następującej kolejności:

Serwer

  • jego warstwa [métier];
  • jego usługa [web / jSON] zaimplementowana przy użyciu Springa MVC;

Klient

  • jego warstwa [DAO]. Nie będzie warstwy [métier];

1.16.1. Serwer [web / jSON]

Chcemy zbudować następującą architekturę:

1.16.1.1. Tworzenie projektu

Zbudujemy serwis internetowy w oparciu o ekosystem Spring [http://spring.io/]. Przechodzimy na stronę [http://start.spring.io/] (czerwiec 2016 r.), która pozwoli nam wygenerować projekt Gradle wraz z zależnościami niezbędnymi dla naszego projektu, który nie jest projektem Android i przy którego tworzeniu Android Studio nie oferuje wówczas żadnej pomocy:

  • w [1]: wybierz projekt Gradle;
  • w [2-3]: specyfikacja zależności jar wygenerowanej przez projekt (patrz poniżej);
  • w [4]: wybierz zależność internetową [5], aby zapewnić dostępność plików binarnych niezbędnych dla naszej usługi internetowej;
  • w [6]: wygeneruj projekt. Zostanie wówczas wygenerowany plik ZIP zawierający szkielet projektu Gradle, który będzie dostępny do pobrania;

Co wpisać w polu [2-3]? Korzystaliśmy już z zależności Gradle. Na przykład w poprzednim projekcie wyglądało to następująco:

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Od wersji 0.11 wtyczki Gradle dla Androida należy używać android-apt w wersji >= 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'
}
  • wiersz 22: zależność ma postać [groupId:artifactId:version]. W formularzu na stronie wymagane jest podanie [http://start.spring.io/]:
    • w [2] to [groupId];
    • w [3] jest to [artifactId];

Rozpakuj w folderze z innymi projektami plik zip o nazwie:

W programie Android Studio otwórz projekt Gradle o nazwie [server-01] [1-2]. Otwarty projekt to [3] (perspektywa Project).

1.16.1.2. Konfiguracja Gradle

  

Wygenerowany plik Gradle (czerwiec 2016 r.) wygląda następująco:


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'
 }
}
  • wiersze 14 oraz 34–38 dotyczą pliku IDE w środowisku Eclipse. Usuwamy je;
  • wiersze 1–11 oraz 15 służą do dodania wtyczki o nazwie [spring-boot] do naszego projektu Gradle. Spring Boot to projekt należący do ekosystemu Spring [http://projects.spring.io/spring-boot/]. Wtyczka ta określa wersje najczęściej używanych zależności w połączeniu ze Springiem. Dzięki temu nie trzeba podawać ich wersji (wiersze 30 i 31). Wersja jest wówczas taka, jaka została zdefiniowana przez używaną wersję Spring Boot (wiersz 3);
  • wiersze 22–23: wersja Javy, której należy użyć, w tym przypadku wersja 1.8;
  • wiersze 25–27: repozytoria plików binarnych, z których należy pobierać zależności;
  • wiersz 26: wskazuje centralny repozytorium Maven. Jest to obecnie największy dostępny repozytorium plików binarnych open source;
  • wiersze 29–32: zależności niezbędne dla projektu:
  • wiersz 30: ta zależność zawiera wszystkie pliki binarne niezbędne do zbudowania serwisu internetowego Spring;
  • wiersz 31: ta zależność zawiera wszystkie pliki binarne niezbędne do testów, w szczególności do testów JUnit;
  • zależność [compile] oznacza, że jest ona potrzebna do kompilacji projektu. Zależność [testCompile] oznacza, że jest ona potrzebna wyłącznie do przeprowadzenia testów. Nie jest ona zatem dołączana do pliku binarnego projektu;

Przeprowadzamy wstępne czyszczenie pliku Gradle:


// Spring Boot
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

// wtyczki
apply plugin: 'java'
apply plugin: 'spring-boot'

// plik binarny projektu
jar {
  baseName = 'server-01'
  version = '0.0.1-SNAPSHOT'
}

// wersje Javy
sourceCompatibility = 1.8
targetCompatibility = 1.8

// repozytoria Maven
repositories {
  mavenLocal()
  mavenCentral()
}

// zależności
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • wiersz 30: dodaliśmy lokalny repozytorium Maven na stacji roboczej. Jest ono tworzone podczas instalacji Mavena (patrz punkt 6.10). Jeśli wymagana zależność znajduje się już w lokalnym repozytorium Maven, nie będzie pobierana z centralnego repozytorium Maven;
  • wiersze 19–22: zadanie Gradle umożliwiające wygenerowanie pliku binarnego projektu. Wykorzystamy je, aby sprawdzić, co się dzieje;
  • w pliku [1-4] należy uruchomić zadanie [jar] zdefiniowane w pliku [build.gradle] ([1] znajduje się w prawym górnym rogu oraz obok pliku IDE);

Powyższa operacja tworzy archiwum jar projektu i umieszcza je w folderze [build / libs] [5]:

  

Nazwa archiwum pochodzi bezpośrednio z informacji podanych w zadaniu [jar] w pliku [build.gradle] (wiersze 19–22).

Wszystkie zależności projektu można przedstawić w następujący sposób:

 

W pliku [1] widać, że jedyna zależność projektu [compile('org.springframework.boot:spring-boot-starter-web')] pociągnęła za sobą dziesiątki plików binarnych. Spring Boot dla sieci Web zawiera zależności, których prawdopodobnie będzie potrzebować aplikacja internetowa Spring MVC. Oznacza to, że niektóre z nich mogą być zbędne. Spring Boot idealnie nadaje się do samouczka:

  • dostarcza zależności, których prawdopodobnie będziemy potrzebować;
  • zawiera wbudowany serwer Tomcat [1], co pozwala nam uniknąć wdrażania aplikacji na zewnętrznym serwerze internetowym;

Na stronie ekosystemu Spring można znaleźć wiele przykładów wykorzystujących Spring Boot [http://spring.io/guides].

Teraz uzupełniamy plik [build.gradle] w następujący sposób:


// Spring Boot
...
// zależności
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}

// wtyczka do tworzenia pliku binarnego zgodnego ze standardami Maven w lokalnym repozytorium 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 {
      // zmień, aby wskazywało na Twoje repozytorium, e.g. http://my.org/repo
      url 'file://D:\\maven'
    }
  }
}
  • wiersz 10: importujemy wtyczkę Gradle o nazwie [maven-publish], która umożliwia opublikowanie pliku binarnego projektu w repozytorium Maven zgodnie ze standardami Maven;
  • wiersz 11: zadanie Gradle o nazwie [publishing];
  • wiersze 14–15: specyfikacja pliku binarnego Maven, który zostanie utworzony;
  • wiersz 23: repozytorium Maven, w którym zostanie opublikowany, w tym przypadku lokalne repozytorium Maven;

Dodanie wtyczki [maven-publish] spowodowało utworzenie nowych zadań w projekcie Gradle:

Jeśli w [2] uruchomimy zadanie [publish], plik binarny projektu zostanie utworzony i zainstalowany w folderze wskazanym w wierszu 23 pliku [build.gradle]:

 

Zadanie [jar] pozwala wygenerować plik binarny projektu. Plik ten nie zawiera zależności, więc nie jest wykonywalny. Możliwe jest wygenerowanie pliku binarnego zawierającego wszystkie zależności i będącego wykonywalnym. W tym celu dodajemy do pliku [build.gradle] następujący kod:


// utwórz plik binarny wraz ze wszystkimi zależnościami
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
}
  • wiersz 6: należy wpisać pełną nazwę klasy wykonywalnej projektu:
  

Kod tej klasy będzie wyglądał następująco:


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

Odśwież projekt Gradle, a następnie uruchom zadanie [fatJar]:

 

Plik binarny zostanie wygenerowany w folderze [build / libs] i można go uruchomić jako [1-7]:

1.16.1.3. Konfiguracja projektu

Konfiguracja Gradle’a nie wystarczy. Musimy również skonfigurować projekt. Ponieważ nie jest to projekt Android wygenerowany przez IDE, należy tutaj wykonać tę konfigurację, której do tej pory nie przeprowadzaliśmy.

 
  • w [3-4]: należy użyć JDK 1.8;

Aby skompilować projekt, przycisk dostępny dla projektów Android nie jest już obecny. Użyjemy opcji z menu [1-2]:

W dalszej części czytelnik zostanie poproszony o utworzenie poniższego projektu. Omówimy ostateczną wersję kodu projektu [3].

1.16.1.4. Warstwa [métier]

  

Warstwa [métier] nawiązuje do warstwy [métier] z poprzedniego przykładu. Będzie miała następujący interfejs [IMetier]:


package exemples.android.server.metier;

public interface IMetier {
  // liczba losowa w [a,b]
    int getAlea(int a, int b);
}
  • wiersz 5: metoda generująca 1 liczbę losową w klasie [a,b]

Kod klasy [Metier] implementującej ten interfejs wygląda następująco:


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) {
    // kilka weryfikacji
    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);
    }
    // generowanie wyniku
    Random random=new Random();
    random.setSeed(new Date().getTime());
    return a + random.nextInt(b - a + 1);
  }
}

Nie komentujemy tej klasy: jest ona analogiczna do tej z poprzedniego przykładu, z tą różnicą, że nie generuje losowo wyjątków. Warto zwrócić uwagę na adnotację Springa [@Service] w linii 8, która sprawi, że Spring utworzy tylko jedną instancję tej klasy (singleton) i udostępni jej odwołanie innym komponentom Springa. W tym miejscu można było użyć innych adnotacji Springa, aby osiągnąć ten sam efekt. Komponenty Springa mają domyślne nazwy, które można określić jako atrybut używanej adnotacji. Bez tego atrybutu, tak jak w tym przypadku, komponent Springa nosi nazwę klasy, której pierwsza litera jest mała. Tak więc w tym przypadku komponent Springa nosi domyślnie nazwę [metier];

Klasa [Metier] generuje wyjątki typu [AleaException]:


package exemples.android.server.metier;

public class AleaException extends RuntimeException {

  // kod błędu
  private int code;

  // konstruktorów
  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;
  }

  // metody pobierające i ustawiające
....
}
  • wiersz 3: [AleaException] rozszerza klasę [RuntimeException]. Jest to zatem wyjątek niekontrolowany (nie ma obowiązku obsługi go za pomocą try / catch);
  • wiersz 6: do klasy [RuntimeException] dodano kod błędu;

1.16.1.5. Usługa internetowa / jSON

 
  

Usługa internetowa / jSON jest zaimplementowana przez Spring MVC. Spring MVC implementuje model architektury zwany MVC (Model – Widok – Kontroler) w następujący sposób:

Przetwarzanie żądania klienta przebiega w następujący sposób:

  1. żądanie – żądane URL mają postać http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] to klasa Springa, która przetwarza przychodzące URL. „Przekierowuje” ona URL do akcji, która ma ją przetworzyć. Akcje te są metodami określonych klas o nazwach [Contrôleurs]. Litera „C” w MVC oznacza tutaj ciąg znaków [Dispatcher Servlet, Contrôleur, Action]. Jeśli nie skonfigurowano żadnej akcji do przetworzenia przychodzącego URL, serwlet [Dispatcher Servlet] odpowie, że żądany URL nie został znaleziony (błąd 404 NOT FOUND);
  1. przetwarzanie
  • wybrana akcja może wykorzystać parametry parami, które przekazała jej serwlet [Dispatcher Servlet]. Mogą one pochodzić z różnych źródeł:
    • ze ścieżki [/param1/param2/...] serwletu URL,
    • z parametrów [p1=v1&p2=v2] serwletu URL,
    • z parametrów przesłanych przez przeglądarkę wraz z żądaniem;
  • Podczas przetwarzania żądania użytkownika akcja może wymagać wykorzystania warstwy [metier] [2b]. Po przetworzeniu żądania klienta akcja ta może wywołać różne odpowiedzi. Klasycznym przykładem jest:
    • strona błędu, jeśli żądania nie udało się poprawnie przetworzyć
    • w przeciwnym razie strona potwierdzenia
  • akcja żąda wyświetlenia określonego widoku [3]. Widok ten wyświetli dane, które nazywamy modelem widoku. To właśnie litera M w MVC. Akcja utworzy ten model M [2c] i zażąda wyświetlenia widoku V [3];
  1. odpowiedź – wybrany widok V wykorzystuje model M utworzony przez akcję do zainicjowania dynamicznych części odpowiedzi HTML, którą musi wysłać do klienta, a następnie wysyła tę odpowiedź.

W przypadku usługi internetowej / jSON powyższa architektura ulega niewielkiej modyfikacji:

  • w [4a] model, który jest klasą Java, jest przekształcany w ciąg znaków jSON przez bibliotekę jSON;
  • w [4b] ten ciąg znaków jSON jest wysyłany do przeglądarki;

Przykład serializacji obiektu Java do ciągu znaków jSON oraz deserializacji ciągu znaków jSON do obiektu Java przedstawiono w załącznikach do paragrafu 6.14.

Wróćmy do warstwy [web] naszej aplikacji:

W naszej aplikacji występuje tylko jeden kontroler:

  

Usługa internetowa / jSON wyśle swoim klientom odpowiedź typu [Response] o następującej treści:


package exemples.android.server.web;

import java.util.List;

public class Response<T> {

    // ----------------- właściwości
    // status operacji
    private int status;
    // ewentualne komunikaty o błędach
    private List<String> messages;
    // treść odpowiedzi
    private T body;

    // konstruktory
    public Response() {

    }

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

    // metody pobierające i ustawiające
...
}
  • wiersz 13: pole [T body] to odpowiedź oczekiwana przez klienta. Zdecydowaliśmy się tutaj na ogólną odpowiedź typu T, zamiast typu Integer z oczekiwaną liczbą losową. Chcemy mieć możliwość ponownego wykorzystania tej klasy w innych sytuacjach. Podczas przetwarzania żądania klienta serwer może napotkać problem, który jest następnie podsumowany w dwóch pozostałych polach;
    • wiersz 8: kod stanu (0, jeśli nie wystąpił błąd);
    • wiersz 9: jeśli status!=0, lista komunikatów o błędach, zazwyczaj pochodzących ze stosu wyjątków, jeśli wystąpił wyjątek, null, jeśli nie ma błędów;

Kontroler [WebController] wygląda następująco:


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 {

  // warstwa biznesowa
  @Autowired
  private IMetier metier;
  // mapper JSON
  @Autowired
  private ObjectMapper mapper;

  // liczby losowe
  @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 {

    // odpowiedź
    Response<Integer> response = new Response<>();
    // korzystamy z warstwy biznesowej
    try {
      response.setBody(metier.getAlea(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // zwracamy odpowiedź
    return mapper.writeValueAsString(response);
  }

  private List<String> getMessagesFromException(Throwable e) {
    // lista komunikatów
    List<String> messages = new ArrayList<String>();
    // przeglądamy stos wyjątków
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // zwracamy wynik
    return messages;
  }

}
  • wiersz 17: adnotacja [@Controller] wskazuje, że klasa jest kontrolerem MVC, którego metody obsługują żądania dotyczące niektórych URL aplikacji internetowej;
  • wiersze 21–22: adnotacja [@Autowired] nakazuje Springowi wstrzyknięcie do pola komponentu typu [IMetier]. Będzie to poprzednia klasa [Metier]. Dzieje się tak, ponieważ umieściliśmy na niej adnotację [@Service], dzięki czemu jest ona traktowana jako komponent Springa;
  • wiersze 24–25: postępujemy analogicznie z mapperem jSON, który zdefiniujemy w dalszej części. Nasz serwis internetowy wyśle swoją odpowiedź w postaci ciągu znaków jSON. To właśnie ten mapper przeprowadzi serializację odpowiedzi do formatu jSON;
  • wiersz 30: metoda generująca liczbę losową. Jej nazwa nie ma znaczenia. W momencie jej wykonania jej parametry zostały zainicjowane przez Spring MVC. Zobaczymy, w jaki sposób. Ponadto, jeśli metoda ta zostanie wykonana, to dlatego, że serwer WWW otrzymał żądanie HTTP GET dotyczące URL z wiersza 28;
  • wiersz 28: adnotacja [@RequestMapping] określa pewne właściwości metody opatrzonej adnotacją:
    • [value]: URL akceptowana przez metodę;
    • [method]: metoda HTTP akceptowana przez metodę. Są to głównie dwie: GET i POST. Metoda [POST] jest używana, gdy klient chce dołączyć dokument do swojego żądania HTTP;
    • [produces]: ustala jeden z nagłówków odpowiedzi HTTP, która zostanie wysłana do klienta. W tym przypadku wśród nagłówków HTTP wysyłanych wraz z odpowiedzią dla klienta znajdzie się jeden, który poinformuje go, że odpowiedź jest wysyłana w postaci ciągu znaków jSON. Ten nagłówek nie jest obowiązkowy. Jest podawany klientowi w celach informacyjnych, jeśli oczekuje on odpowiedzi, które mogą przybierać różne formy;
    • [consumes]: nie występuje w tym przypadku. Służy do wskazania nagłówków HTTP, które muszą towarzyszyć żądaniu klienta HTTP, aby zostało ono zaakceptowane;
  • wiersz 29: adnotacja [@ResponseBody] wskazuje, że wynik wygenerowany przez metodę musi zostać wysłany do klienta. Bez tej adnotacji odpowiedź metody jest traktowana jako klucz umożliwiający wybór strony HTML, która ma zostać wysłana do klienta. W usłudze internetowej /jSON nie ma stron o nazwie HTML;
  • wiersz 28: przetwarzana strona URL ma postać /{a}/{b}, gdzie {x} oznacza zmienną. Zmienne {a} i {b} są przypisywane do parametrów metody w wierszu 30. Odbywa się to za pomocą adnotacji @PathVariable("x"). Należy zauważyć, że {a} i {b} są składnikami obiektu typu URL, a zatem są typu String. Konwersja typu String na typ parametrów może się nie powieść. W takim przypadku Spring MVC zgłasza wyjątek. Podsumowując: jeśli za pomocą przeglądarki wywołam metodę URL /100/200, metoda getAlea z linii 30 zostanie wykonana z parametrami całkowitoliczbowymi a=100, b=200;
  • w linii 36: warstwa [métier] jest proszona o wygenerowanie liczby losowej z przedziału [a,b]. Należy pamiętać, że metoda [metier].getAlea może wygenerować wyjątek;
  • wiersz 37: brak błędu;
  • wiersz 39: kod błędu;
  • wiersz 40: lista komunikatów odpowiedzi odpowiada stosowi wyjątków (wiersze 46–57). W tym przypadku wiemy, że stos zawiera tylko jeden wyjątek, ale chcieliśmy pokazać bardziej ogólną metodę;
  • wiersz 43: odpowiedź typu [Response<Integer>] jest zwracana w postaci ciągu znaków jSON;

1.16.1.6. Konfiguracja projektu Spring

  

Istnieje wiele sposobów konfiguracji Springa:

  • z plikami XML;
  • z kodem Java;
  • z połączeniem obu;

Decydujemy się skonfigurować naszą aplikację internetową przy użyciu kodu Java. Konfigurację tę zapewnia następująca klasa [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 {
  // konfiguracja internetowa ------------------------------------
  @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);
  }

  // mapper jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }

}
  • wiersz 12: informujemy Spring, w jakich pakietach znajdzie dwa komponenty, którymi ma zarządzać:
    • komponent [Metier] opatrzony adnotacją [@Service] w pakiecie [exemples.android.server.metier];
    • komponent [WebController] oznaczony adnotacją [@Controller] w pakiecie [exemples.android.server.web];
  • wiersz 13: adnotacja [@EnableWebMvc] pozwala Spring Bootowi samodzielnie dokonać pewnej liczby standardowych konfiguracji dla aplikacji Spring MVC. Odciąża to tym samym programistę;
  • wiersze 16, 22, 27 i 33: adnotacja [@Bean] również definiuje komponenty (beany) Springa, podobnie jak dwie poprzednie adnotacje (@Service, @Controller). W tym przypadku adnotacja [@Bean] oznacza metodę, a nie klasę, i to wynik tej metody stanowi komponent Spring. Wobec braku atrybutu nazwy w adnotacji [@Bean] utworzony komponent Spring nosi nazwę oznaczonej metody;
  • wiersze 16–20: definiują bean [dispatcherServlet]. Jest to predefiniowana nazwa w Springu MVC, która określa kontroler frontowy aplikacji MVC, obiekt, przez który przechodzą wszystkie żądania klientów i który rozdziela je (stąd jego nazwa) do różnych [@Controller] w aplikacji Spring MVC;
  • wiersz 18: bean [dispatcherServlet] jest instancją klasy [DispatcherServlet] dostarczanej przez Spring MVC;
  • wiersze 22–25: bean [servletRegistrationBean] służy do określenia, które instancje URL są akceptowane przez aplikację. W wierszu 24 akceptowane są wszystkie instancje URL;
  • wiersze 27–30: bean [embeddedServletContainerFactory] służy do zdefiniowania serwera wbudowanego w zależnościach projektu, który ma hostować aplikację internetową. Wiersz 29 wskazuje, że jest to serwer Tomcat i że będzie on działał na porcie 8080. Domyślnie pliki binarne tego serwera internetowego są dostarczane przez zależność [org.springframework.boot:spring-boot-starter-web] z pliku Gradle;

1.16.1.7. Uruchomienie serwisu internetowego / jSON

  

Projekt uruchamia się z następującej klasy wykonywalnej [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) {
    // uruchamianie aplikacji
    SpringApplication.run(Config.class, args);
  }

}
  • klasa [Boot] jest klasą wykonywalną (wiersze 7–10);
  • wiersz 9: metoda statyczna [SpringApplication.run] jest metodą klasy [spring Boot] (wiersz 4), która uruchamia aplikację. Jej pierwszym parametrem jest klasa Java konfigurująca projekt. W tym przypadku jest to klasa [Config], którą właśnie opisaliśmy. Drugim parametrem jest tablica argumentów przekazywana do metody [main] (wiersz 7);

Aplikację internetową można uruchomić na różne sposoby, między innymi w następujący sposób:

 

W konsoli pojawia się wówczas pewna liczba wpisów dziennika:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 [/**] do modułu obsługi typu [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/**] na handler typu [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)
  • wiersze 12–14: uruchamiany jest wbudowany serwer Tomcat;
  • wiersze 15–19: serwlet [DispatcherServlet] biblioteki Spring MVC jest ładowany i konfigurowany;
  • wiersz 20: wykryto serwer WWW URL [/{a}/{b}];

Teraz otwórzmy przeglądarkę i przetestujmy serwis internetowy / jSON:

Za każdym razem otrzymujemy reprezentację jSON obiektu typu [Response<Integer>].

Zamiast standardowej przeglądarki, skorzystajmy teraz z rozszerzenia [Advanced Rest Client] przeglądarki Chrome (patrz załączniki, punkt 6.13):

Image

  • na [1], zamiast żądanego URL;
  • na [2], za pomocą GET;
  • na [3] wysyłane jest żądanie;

Image

  • w [4] nagłówki HTTP z odpowiedzi serwera. Należy zauważyć, że serwer wskazuje, iż wysłany dokument jest ciągiem znaków jSON;
  • w [5] – otrzymany ciąg znaków jSON;

1.16.1.8. Generowanie pliku jar z aplikacją

W punkcie 1.16.1.2 pokazaliśmy, jak skonfigurować plik Gradle w celu wygenerowania pliku wykonywalnego aplikacji wraz ze wszystkimi jej zależnościami. Dostosowana do niniejszej aplikacji konfiguracja wygląda następująco:


// utworzyć plik binarny wraz ze wszystkimi zależnościami
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
}

Aby wygenerować ten plik wykonywalny, można postępować w następujący sposób [1-5]:

Aby go uruchomić, należy zatrzymać usługę internetową, jeśli jest uruchomiona ([1]), a następnie uruchomić archiwum ([2-4]):

 

Otwórz przeglądarkę i wywołaj adresy URL oraz [localhost:8080/100/200]. Powinieneś uzyskać takie same wyniki jak poprzednio.

1.16.1.9. Zarządzanie logami

Po uruchomieniu archiwum wykonywalnego można zauważyć, że nie otrzymujemy tych samych logów, co w przypadku uruchomienia projektu z pliku IDE. Otrzymujemy logi w trybie [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)

Poziom logów można zarządzać, dodając plik [logback.xml] do folderu [resources] projektu:

  

Plik ten może mieć następującą zawartość:


<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!--mają domyślnie przypisany typ
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- kontrola poziomu logów -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Poziom logów jest kontrolowany w wierszu 12. Jeśli teraz wygenerujemy ponownie archiwum wykonywalne i uruchomimy je, otrzymamy wyłącznie logi poziomu [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. Klient Android serwera WWW / jSON

Aplikacja na Androida będzie miała następującą strukturę:

Klient będzie składał się z dwóch komponentów:

  1. warstwę [Présentation] (widok + aktywność) analogiczną do tej, którą omówiliśmy w przykładzie [Exemple-14];
  2. warstwa [DAO], która komunikuje się z usługą [web / jSON], którą omówiliśmy wcześniej.

1.16.2.1. Tworzenie projektu

Duplikujemy poprzedni projekt [Exemple-14], tworząc projekt [Exemple-15], postępując zgodnie z procedurą opisaną w punkcie 1.4. Otrzymujemy następujący wynik:

W dalszej części czytelnik jest proszony o utworzenie kolejnego projektu.

1.16.2.2. Konfiguracja Gradle

 

Plik [build.gradle] ma następującą treść:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Od wersji 0.11 wtyczki Gradle dla systemu Android należy używać android-apt w wersji >= 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'
    }
  }

  // opcje pakowania niezbędne do wygenerowania pliku 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'
  }
}

Omówimy tylko te elementy, które nie zostały jeszcze poruszone:

  • wiersze 46–47: dodanie wtyczki AA. Wtyczka [rest-spring-api] umożliwia przekazanie komunikacji klient–serwer bibliotece AA;
  • wiersz 50: biblioteka [spring-android-rest-template] jest biblioteką wykorzystywaną przez AA do zapewnienia komunikacji między klientem a serwerem. Wersja [2.0.0.M3] jest wersją tzw. „milestone”, której nie ma w standardowych repozytoriach Maven. Dlatego też w wierszach 56–59 należy określić repozytorium (wiersz 58), z którego należy pobrać bibliotekę;
  • wiersz 51: biblioteka jSON;
  • wiersze 33–39: bez tej właściwości pojawiają się błędy podczas generowania pliku binarnego APK projektu;

1.16.2.3. Manifest aplikacji na Androida

  

Plik [AndroidManifest.xml] wymaga modyfikacji. Domyślnie dostęp do Internetu jest wyłączony. Należy go włączyć za pomocą specjalnej dyrektywy:


<?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>
  • wiersz 5: dostęp do Internetu jest dozwolony;

1.16.2.4. Warstwa [DAO]

  

1.16.2.4.1. Interfejs [IDao] warstwy [DAO]

Interfejs warstwy [DAO] będzie wyglądał następująco:


package exemples.android.dao;

public interface IDao {

  // liczba losowa
  int getAlea(int a, int b);

  // URL usługi internetowej
  void setUrlServiceWebJson(String url);

  // maksymalny czas oczekiwania (ms) na odpowiedź serwera
  void setTimeout(int timeout);

  // czas oczekiwania klienta w milisekundach przed wysłaniem żądania
  void setDelay(int delay);

}
  • wiersz 6: metoda serwisu internetowego / jSON służąca do uzyskania liczby losowej z przedziału [a,b] tego serwisu internetowego;
  • wiersz 9: URL usługi internetowej / jSON służącej do generowania liczb losowych;
  • wiersz 12: ustalamy maksymalny czas oczekiwania na odpowiedź serwera;
  • wiersz 15: ustalamy czas oczekiwania przed wysłaniem żądania do serwera, aby dać użytkownikowi czas na anulowanie żądania;

1.16.2.4.2. Interfejs [WebClient]
  

Interfejs [WebClient] odpowiada za komunikację z serwisem internetowym. Jego kod wygląda następująco:


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 liczba losowa z przedziału [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
  • wiersz 12: [WebClient] to interfejs, który biblioteka AA sama zaimplementuje dzięki adnotacjom, które w nim umieścimy. Interfejs ten musi implementować wywołania funkcji URL udostępnianych przez serwis internetowy / jSON:

  // liczba losowa
  @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 {
  • wiersz 11: adnotacja [@Rest] jest adnotacją AA. Wartość atrybutu [converters] to tablica konwerterów. W tym przypadku konwerter [MappingJackson2HttpMessageConverter.class] powoduje, że gdy serwer wysyła ciąg znaków jSON, jest on automatycznie deserializowany. W ten sposób wiersz (d) pokazuje, że URL [/{a}/{b}] zwraca typ String, który w rzeczywistości jest ciągiem znaków jSON (wiersz b). Korzystając z tych informacji oraz danych dotyczących oczekiwanego typu z wiersza 16, instancja klienta o nazwie [WebClient] przeprowadzi deserializację otrzymanego ciągu znaków do typu [Response<Integer>];
  • wiersz 15: adnotacja AA wskazująca, że obiekt URL musi zostać wywołany za pomocą metody HTTP GET. Parametr adnotacji [@Get] określa formę URL oczekiwaną przez serwis internetowy. Wystarczy przejąć parametr [value] z adnotacji [@RequestMapping] (wiersz b) wywoływanej metody w kontrolerze [WebController] serwera. Dawki {} otaczają parametry metody URL, które należy przejąć do parametrów metody w wierszu 16. Składnia [@Path("a") int a] powoduje, że parametr metody [a] jest przypisywany do wartości {a} z URL. Gdy parametr URL i parametr metody mają tę samą nazwę, jak w tym przypadku, można zapisać to prościej jako [@Path int a];

W przypadku zapytania HTTP POST metoda wywołania miałaby następującą sygnaturę:


  @Post("/{a}/{b}")
  Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);

To adnotacja [@Body] określa wartość wysyłaną. Zostanie ona automatycznie zserializowana do postaci jSON. Po stronie serwera będziemy mieli następujący podpis:


  // liczby losowe
  @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) {
  • wiersz 2: określa się, że oczekiwane jest żądanie o treści HTTP POST oraz że treść tego żądania (przesyłany obiekt) musi zostać przekazana w postaci ciągu znaków jSON (atrybut consumes);
  • wiersz 4: wartość przesłana metodą POST zostanie pobrana z parametru [@RequestBody T body] tej metody;

Wróćmy do kodu klasy [WebClient]:


@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
  • musimy mieć możliwość wskazania parametru URL serwisu internetowego, z którym należy się skontaktować. Osiągamy to poprzez rozszerzenie interfejsu [RestClientRootUrl] dostarczanego przez AA. Interfejs ten udostępnia metodę [setRootUrl(urlServiceWeb], która pozwala ustalić identyfikator URL serwisu internetowego, z którym należy się skontaktować;
  • ponadto chcemy kontrolować wywołanie serwisu internetowego, ponieważ chcemy ograniczyć czas oczekiwania na odpowiedź. W tym celu rozszerzamy interfejs [RestClientSupport], który udostępnia metodę [setRestTemplate], która pozwoli nam:
    • samodzielnie utworzyć obiekt [RestTemplate], który służy do zarządzania komunikacją między klientem a serwerem;
    • skonfigurować ten obiekt w celu ustalenia maksymalnego czasu oczekiwania na odpowiedź;

1.16.2.4.3. Klasa [Response]

Metoda [getAlea] interfejsu [IDao] zwraca odpowiedź typu [Response] o następującej treści:


package exemples.android.dao;

import java.util.List;

public class Response<T> {

    // ----------------- właściwości
    // status operacji
    private int status;
    // ewentualne komunikaty o błędach
    private List<String> messages;
    // treść odpowiedzi
    private T body;

    // konstruktory
    public Response() {

    }

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

    // metody pobierające i ustawiające
...
}

Jest to klasa [Response], która została już wykorzystana po stronie serwera (punkt 1.16.1.5). W rzeczywistości, z punktu widzenia programowania, wszystko przebiega tak, jakby warstwa kliencka [DAO] komunikowała się bezpośrednio z kontrolerem usługi internetowej [WebController]:

Komunikacja sieciowa między klientem a serwerem, a także serializacja i deserializacja obiektów Java po stronie klienta są dla programisty przezroczyste.

1.16.2.4.4. Implementacja warstwy [DAO]
  

Interfejs [IDao] jest zaimplementowany za pomocą następującej klasy [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 {

  // klient usługi REST
  @RestService
  protected WebClient webClient;

  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
  // opóźnienie oczekiwania przed wykonaniem żądania
  private int delay;

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

}
  • wiersz 15: oznaczamy klasę [Dao] adnotacją [@EBean], aby przekształcić ją w bean AA, który będzie można wstrzyknąć w innym miejscu;
  • wiersze 19–20: wstrzykujemy implementację interfejsu [WebClient], który wcześniej opisaliśmy. Za to wstrzyknięcie odpowiada adnotacja [@RestService];
  • pozostałe metody implementują interfejs [IDao] (wiersze 27–46);

Metoda [setTimeout]

Metoda [setTimeout] wygląda następująco:


  @Override
  public void setTimeout(int timeout) {
    // ustawia się limit czasu dla żądań klienta REST
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // tworzy się restTemplate
    RestTemplate restTemplate = new RestTemplate(factory);
    // ustawiamy konwerter jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // ustawiamy restTemplate dla klienta internetowego
    webClient.setRestTemplate(restTemplate);
}
  • interfejs [WebClient] zostanie zaimplementowany przez klasę AA korzystającą z zależności Gradle [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] realizuje komunikację klienta z serwerem WWW / jSON za pomocą klasy typu [RestTemplate];
  • wiersz 4: klasa [SimpleClientHttpRequestFactory] jest dostarczana przez zależność [spring-android-rest-template]. Pozwoli nam ona ustawić maksymalny czas oczekiwania na odpowiedź serwera (wiersze 5–6);
  • wiersz 8: tworzymy obiekt typu [RestTemplate], który będzie służył do komunikacji z serwisem internetowym. Jako parametr przekazujemy mu obiekt [factory], który właśnie został utworzony;
  • wiersz 10: dialog między klientem a serwerem może przybierać różne formy. Wymiana danych odbywa się za pomocą wierszy tekstowych i musimy wskazać obiektowi typu [RestTemplate], co ma zrobić z danym wierszem tekstowym. W tym celu dostarczamy mu konwertery – klasy zdolne do przetwarzania wierszy tekstowych. Wybór konwertera odbywa się zazwyczaj za pomocą nagłówków HTTP, które towarzyszą wierszowi tekstowemu. W tym przypadku wiemy, że otrzymujemy wyłącznie wiersze tekstowe w formacie jSON. Ponadto w punkcie 1.16.1.7 widzieliśmy, że serwer wysyła nagłówek HTTP:

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

W wierszu 10 jedynym konwerterem dla [RestTemplate] będzie konwerter jSON zaimplementowany przy użyciu biblioteki [Jackson]. Istnieje pewna osobliwość dotycząca tych konwerterów: AA wymaga, abyśmy umieścili go również w adnotacji klienta internetowego [WebClient]:


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

W wierszu 1 musimy określić konwerter, mimo że robimy to programowo.

  • Wiersz 12: tak utworzony obiekt [RestTemplate] jest wstrzykiwany do implementacji interfejsu [WebClient] i to właśnie ten obiekt będzie obsługiwał komunikację klient–serwer;

Metoda [getAlea]

Metoda [getAlea] wygląda następująco:


  @Override
  public int getAlea(int a, int b) {
    // uruchomienie usługi
    Response<Integer> info;
    DaoException ex;
    try {
      // oczekiwanie
      waitSomeTime(delay);
      // uruchomienie usługi
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // zwracamy wynik
        return info.getBody();
      } else {
        // odnotowuje się wyjątek
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // odnotowuje się wyjątek
      ex = new DaoException(e, 100);
    }
    // rzucanie wyjątku
    throw ex;
  }
...
  // metody prywatne -------------------
  private void waitSomeTime(int delay) {
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}
  • wiersz 8: oczekuje się przez [delay] milisekund;
  • wiersz 10: wywołuje się metodę o tej samej sygnaturze w klasie implementującej interfejs [WebClient];
  • wiersz 11: analizujemy odpowiedź otrzymaną z serwera, sprawdzając jej [status];
  • wiersze 12–14: jeśli po stronie serwera nie wystąpił błąd (status = 0), zwracamy wynik metody;
  • wiersz 17: jeśli wystąpił błąd po stronie serwera (status!=0), przygotowujemy wyjątek, ale go nie wywołujemy. Serwer przesłał listę komunikatów o błędach. Tworzymy wyjątek, którego jedynym komunikatem jest ciąg znaków jSON z listy komunikatów serwera;
  • wiersze 19–22: inne przypadki wyjątków;
  • wiersz 24: gdy dotrzemy do tego miejsca, oznacza to, że na pewno wystąpił wyjątek. W związku z tym wywołujemy go;

Wyjątek [DaoException] używany w tym kodzie ma następującą postać:


package exemples.android.dao;

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

public class DaoException extends RuntimeException {

  // kod błędu
  private int code;

  // konstruktory
  public DaoException() {
  }

  public DaoException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public DaoException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  // metody pobierające i ustawiające
...
}
  • wiersz 6: wyjątek [DaoException] jest wyjątkiem niekontrolowanym;

Metoda [setUrlServiceWebJson]

Metoda [setUrlServiceWebJson] ma następującą postać:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // ustawiamy URL usługi REST
    webClient.setRootUrl(urlServiceWebJson);
}
  • wiersz 4: ustawia się URL usługi internetowej za pomocą metody [setRootUrl] interfejsu [WebClient]. Metoda ta istnieje, ponieważ interfejs ten rozszerza interfejs [RestClientRootUrl];

1.16.2.5. Pakiet [architecture]

Pakiet [architecture] zawiera elementy nadające strukturę aplikacji:

1.16.2.5.1. Interfejs [IMainActivity]

Interfejs [IMainActivity] zawiera listę metod, które musi zaimplementować aktywność aplikacji:


package exemples.android.architecture;

import exemples.android.dao.IDao;

public interface IMainActivity extends IDao {

  // dostęp do sesji
  Session getSession();

  // zmiana widoku
  void navigateToView(int position);

  // oczekiwanie
  void beginWaiting();

  void cancelWaiting();

  // tryb debugowania
  boolean IS_DEBUG_ENABLED = true;
  // czas oczekiwania na odpowiedź
  int TIMEOUT = 1000;
  // sąsiedztwo fragmentów
  int OFF_SCREEN_PAGE_LIMIT = 1;

}
  • wiersz 5: interfejs [IMainActivity] rozszerza interfejs [IDao];
  • wiersze 13–16: do metod już obecnych w poprzednich przykładach (wiersze 7–11) dodaliśmy dwie metody do obsługi obrazu oczekiwania aplikacji (wiersze 14, 16);
  • wiersz 21: ustalamy maksymalny czas oczekiwania na odpowiedź serwera na 1 sekundę;

1.16.2.5.2. Klasa [Utils]

W klasie [Utils] zgromadzono statyczne metody pomocnicze, które mogą być wywoływane z różnych miejsc w architekturze aplikacji:


package exemples.android.architecture;

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

public class Utils {

  // lista komunikatów wyjątku – wersja 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // tworzy się listę komunikatów o błędach ze stosu wyjątków
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // lista komunikatów wyjątku – wersja 2
  static public String getMessagesForAlert(Throwable th) {
    // tworzy się tekst do wyświetlenia
    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--;
    }
    // wynik
    return texte.toString();
  }

}
  • wiersze 9–18: tworzy listę komunikatów o błędach zawartych w pliku Throwable;
  • wiersze 21–32: na podstawie poprzedniej metody tworzy z uzyskanej listy komunikatów tekst, który ma zostać wyświetlony w komunikacie ostrzegawczym systemu Android;
  • wiersze 27–28: komunikaty są ponumerowane. Najmniejszy numer (1) odpowiada początkowemu wyjątkowi, a największy – najnowszemu wyjątkowi w stosie wyjątków;

1.16.2.5.3. Klasa abstrakcyjna [AbstractFragment]

Klasa [AbstractFragment] pełni dwie funkcje:

  1. zapewnienie, że metoda [updateFragments] klas potomnych jest zawsze wywoływana podczas wyświetlania fragmentu i to tylko raz;
  2. wyodrębnienie stanu i metod klas potomnych, które można wyodrębnić;

To właśnie zadanie nr 2 sprawia, że umieszczamy w tej klasie operacje związane z obsługą obrazu oczekiwania: wszystkie fragmenty asynchronicznej aplikacji na Androida muszą radzić sobie z tego typu problemami:


  // obsługa oczekiwania
  protected void beginWaiting() {
    // wyświetlanie ikony klepsydry
    mainActivity.beginWaiting();
  }

  protected void cancelWaiting() {
    // usuwa się symbol klepsydry
    mainActivity.cancelWaiting();
}

1.16.2.6. Widok

1.16.2.6.1. Widok [vue1.xml]
  

W porównaniu z poprzednim przykładem widok [vue1.xml] zmienia się w następujący sposób:

 
 
  • w [1] użytkownik musi podać identyfikator URL usługi internetowej oraz czas oczekiwania [2] przed każdym wywołaniem usługi internetowej;
  • w [3] odpowiedzi są zliczane;
  • w [4] użytkownik może anulować swoje żądanie;
  • w [5] wyświetla się wskaźnik oczekiwania, gdy żądane są liczby. Znika on, gdy wszystkie liczby zostaną odebrane lub gdy operacja zostanie anulowana;

Image

  • w [6] sprawdzana jest poprawność wprowadzonych danych;

Użytkownik jest proszony o załadowanie pliku [vue1.xml] z przykładów. W dalszej części podajemy identyfikatory nowych komponentów:

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

Przyciski [10-11] są fizycznie umieszczone jeden nad drugim. W danym momencie widoczny będzie tylko jeden z nich.

1.16.2.6.2. Fragment [Vue1Fragment]
  

Struktura fragmentu [Vue1Fragment] jest następująca:


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 {

  // elementy interfejsu graficznego
  @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;
...
  // dane lokalne
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;

  @AfterViews
  void afterViews() {
    // pamięć
    afterViewsDone=true;
    // na początku brak komunikatów o błędach
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    textViewErreurDelay.setVisibility(View.INVISIBLE);
    // ukryty przycisk [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
    // lista odpowiedzi
    reponses = new ArrayList<>();
  }
...
  • wiersze 24–49: odwołania do komponentów widoku [vue1.xml] (wiersz 20);
  • wiersze 55–69: metoda [@AfterViews] wykonywana po zainicjowaniu odwołań z wierszy 24–49;
  • wiersz 58: nie zapomnij – niezbędne dla cyklu życia fragmentu;
  • wiersze 60–63: komunikaty o błędach są ukrywane;
  • wiersze 65–66: ukrywa się przycisk [Annuler] (wiersz 65) i wyświetla przycisk [Exécuter] (wiersz 66). Przypominamy, że fizycznie znajdują się one jeden nad drugim;
  • wiersz 68: pole z wiersza 52 będzie zawierało listę ciągów znaków, które mają zostać wyświetlone przez ListView w odpowiedziach;

Zaraz po metodzie [@AfterViews] zostanie wykonana następna metoda [updateFragment]:


  @Override
  protected void updateFragment() {
    // tworzy się adapter listy odpowiedzi
    adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
}
  • wiersze 4–5: tworzony jest adapter odpowiedzi o nazwie ListView. Jest on zapisywany w zmiennej instancji, aby był dostępny dla innych metod tej klasy;

„Kliknięcie” przycisku [Exécuter] powoduje wykonanie następującej metody:


// wpisy
  private int nbAleas;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int delay;

  // dane lokalne
  private int nbInfos;
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
  private boolean hasBeenCanceled;

  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // kasowanie poprzednich odpowiedzi
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // zerowanie licznika odpowiedzi
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // sprawdzanie poprawności wprowadzonych danych
    if (!isPageValid()) {
      return;
    }
    // inicjowanie działania
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // żądanie liczb losowych
    for (int i = 0; i < nbAleas; i++) {
      getAlea(a, b);
    }
    // rozpoczyna się oczekiwanie
    beginWaiting();
  }

  @Background(id = "alea")
  void getAlea(int a, int b) {
    // należy wykonać tutaj jak najmniej czynności
    // w każdym razie żadnych wyświetleń – te muszą odbywać się w UiThead
    try {
      // wyświetlamy wynik w pliku UiThread
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // wyświetla się wyjątek w pliku UiThread
      showAlert(e);
    }
  }
  • wiersze 17–18: usuwamy poprzednią listę odpowiedzi z serwera. W tym celu w wierszu 17 opróżniamy źródło danych [reponses] powiązane z adapterem ListView;
  • wiersz 19: zmienna logiczna, która pozwoli nam ustalić, czy użytkownik anulował swoje żądanie, czy nie;
  • wiersze 21–22: wyświetla się licznik o wartości zerowej dla liczby odpowiedzi;
  • wiersze 24–26: pobieramy dane wprowadzone w wierszach [2-6] i sprawdzamy ich poprawność. Jeśli którykolwiek z nich jest nieprawidłowy, metoda zostaje przerwana (wiersz 25), a użytkownik wraca do interfejsu graficznego;
  • wiersze 28–29: jeśli wszystkie wprowadzone dane są prawidłowe, wówczas do aktywności przekazywany jest identyfikator URL usługi internetowej (wiersz 28) oraz czas oczekiwania przed każdym wywołaniem usługi (wiersz 29). Informacje te są niezbędne dla warstwy [DAO]; należy pamiętać, że to aktywność komunikuje się z tą warstwą;
  • wiersze 31–33: liczby losowe są pobierane pojedynczo z metody [getAlea] z wiersza 39;
  • wiersz 38: metoda [getAlea] jest opatrzona adnotacją AA [@Background], co powoduje, że zostanie ona wykonana w innym wątku (strumieniu wykonania, procesie) niż ten, w którym działa interfejs graficzny. Wymagane jest bowiem, aby każde wywołanie internetowe było wykonywane w wątku innym niż ten, w którym działa interfejs graficzny. W ten sposób w danym momencie może istnieć kilka wątków:
    • ten, który wyświetla interfejs graficzny UI (User Interface) i obsługuje jego zdarzenia,
    • wątki [nbAleas], z których każdy wysyła żądanie o losową liczbę do serwisu internetowego. Wątki te są uruchamiane asynchronicznie: wątek UI uruchamia wątek [getAlea] (wiersz 32), który żąda losowej liczby od serwisu internetowego i nie czeka na zakończenie tej operacji. O jej zakończeniu zostanie poinformowany poprzez zdarzenie. W ten sposób wątki o nazwach [nbAleas] będą uruchamiane równolegle. Możliwe jest skonfigurowanie aplikacji tak, aby uruchamiała tylko jeden wątek na raz. W takim przypadku powstaje kolejka wątków do wykonania;

W wierszu 38 parametr [id] nadaje nazwę wygenerowanemu wątku. W tym przypadku wszystkie wątki o nazwie [nbAleas] mają tę samą nazwę: [alea]. Umożliwi nam to anulowanie ich wszystkich jednocześnie. Parametr ten jest opcjonalny, jeśli nie obsługujemy funkcji anulowania wątku;

  • wiersz 44: wywoływana jest metoda [getAlea] aktywności. Zostanie ona zatem wywołana w wątku odrębnym od wątku UI. Ten ostatni wykona wywołanie do serwisu internetowego i nie będzie czekał na odpowiedź. Później zostanie powiadomiony poprzez zdarzenie, że odpowiedź jest dostępna. W tym momencie, w wierszu 44, zostanie wywołana metoda [showInfo] z otrzymaną odpowiedzią jako parametrem;
  • wiersze 45–47: wykonanie żądania internetowego może spowodować wyjątek. W takim przypadku wyświetlane są komunikaty o błędach wyjątku w komunikacie ostrzegawczym;
  • wiersz 35: następuje oczekiwanie na wyniki:
    • zostanie wyświetlony wskaźnik oczekiwania;
    • przycisk [Annuler] zastąpi przycisk [Exécuter]. Ponieważ uruchomione wątki działają asynchronicznie, wątek UI nie czeka na ich zakończenie, a wiersz 35 jest wykonywany przed ich zakończeniem. Po zakończeniu działania metody [beginWaiting] metoda UI może ponownie reagować na żądania użytkownika, takie jak kliknięcie przycisku [Annuler]. Gdyby uruchomione wątki były synchroniczne, do wiersza 35 dotarliśmy dopiero po zakończeniu wszystkich wątków. Anulowanie tych wątków nie miałoby wówczas sensu;

Metoda [showInfo] wygląda następująco:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // dodatkowa informacja
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // czy już skończyliśmy?
      if (nbInfos == nbAleas) {
        // kończymy oczekiwanie
        cancelWaiting();
      }
      // dodajemy informację do listy odpowiedzi
      reponses.add(0, String.valueOf(alea));
      // wyświetlamy odpowiedzi
      adapterReponses.notifyDataSetChanged();
    }
}
  • metoda [showInfo] jest wywoływana wewnątrz wątku [getAlea], oznaczonego adnotacją [@Background]. Metoda ta zaktualizuje interfejs wizualny UI. Może to zrobić wyłącznie wtedy, gdy jest wykonywana w wątku UI. Takie jest znaczenie adnotacji [@UiThread] w wierszu 1;
  • wiersz 2: metoda otrzymuje liczbę losową;
  • wiersz 3: treść metody jest wykonywana tylko wtedy, gdy użytkownik nie anulował swojego żądania;
  • wiersze 5–6: zwiększa się licznik odpowiedzi i wyświetla się jego wartość;
  • wiersze 8–11: jeśli otrzymano wszystkie oczekiwane odpowiedzi, wówczas kończy się oczekiwanie (koniec sygnału oczekiwania, przycisk [Exécuter] zastępuje przycisk [Annuler]);
  • wiersze 12–15: dodaje się otrzymaną liczbę losową do listy odpowiedzi wyświetlanej przez komponent [ListView listReponses] i odświeża się tę listę;

Metoda [showAlert] wygląda następująco:


  @UiThread
  protected void showAlert(Throwable th) {
    if (!hasBeenCanceled) {
      // anulujemy wszystko
      doAnnuler();
      // wyświetla się
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}

Logika jest analogiczna do tej stosowanej w metodzie [showInfo]:

  • wiersz 1: adnotacja [@UiThread] jest obowiązkowa;
  • wiersz 2: metoda odbiera wystąpiony wyjątek;
  • wiersz 3: metoda jest wykonywana tylko wtedy, gdy użytkownik nie anulował swojego żądania;
  • wiersz 5: anuluje się żądanie użytkownika tak, jakby sam kliknął przycisk [Annuler];
  • wiersz 7: wyświetla się alert przy użyciu klasy Android [AlertDialog]:
    • [activity]: jest aktywnością typu [Activity] zapisaną w klasie nadrzędnej [AbstractFragment];
    • [setTitle]: ustala tytuł okna alertu [1];
    • [setMessage]: określa komunikat wyświetlany w oknie ostrzegawczym [2];
    • [setNeutral]: określa przycisk, który zamknie okno ostrzegawcze [3];
    • [show]: powoduje wyświetlenie okna ostrzegawczego;
 

„Kliknięcie” przycisku [Annuler] jest obsługiwane za pomocą następującej metody:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // pamięć
    hasBeenCanceled=true;
    // anulowanie zadania asynchronicznego
    BackgroundExecutor.cancelAll("alea", true);
    // koniec oczekiwania
    cancelWaiting();
}
  • wiersz 4: odnotowuje się, że użytkownik anulował swoje żądanie;
  • wiersz 6: anuluje wszystkie zadania oznaczone ciągiem znaków [alea]. Drugi parametr [true] oznacza, że zadania te muszą zostać anulowane, nawet jeśli zostały już uruchomione. Identyfikator [alea] służy do określenia metody [getAlea] z fragmentu (wiersz 1 poniżej):

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

Uwaga: okazało się, że wiersz 6 kodu metody [doAnnuler] działał nieprawidłowo. Z tego powodu dodano zmienną logiczną [hasBeenCanceled]. W rzeczywistości w przypadku wystąpienia wyjątku (brak serwera) okno ostrzegawcze pojawiało się n razy, jeśli zażądano n liczb losowych.

1.16.2.7. Aktywność [MainActivity]

1.16.2.7.1. Widok [activity-main.xml]
  

W porównaniu z poprzednim przykładem dodaliśmy obrazek oczekiwania w widoku powiązanym z aktywnością [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">
      <!-- obraz oczekiwania -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>

    </android.support.v7.widget.Toolbar>
    <!-- obrazek tymczasowy -->
  </android.support.design.widget.AppBarLayout>
...
  • wiersze 17–21: obrazek oczekiwania;

1.16.2.7.2. Proces [MainActivity]

Działanie [MainActivity] niewiele się różni od tego, czym było w [Exemple-14]. Po pierwsze, dodaje się do niego warstwę [DAO]:


  // wstrzyknięcie dao
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // konfiguracja warstwy [DAO]
    setTimeout(TIMEOUT);
}
  • wiersze 2–3: wstrzyknięcie warstwy [DAO] poprzez adnotację AA;
  • wiersze 5–13: kod wykonywany po tym wstrzyknięciu;
  • wiersz 12: ustalanie wartości timeout warstwy [DAO]

Ponadto aktywność [MainActivity] musi implementować interfejs [IMainActivity], który z kolei rozszerza interfejs [IDao]:


  // implementacja IMainActivity --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // wyświetlany jest widok pozycji
    if (mViewPager.getCurrentItem() != position) {
      // wyświetlanie fragmentu
      mViewPager.setCurrentItem(position);
    }
  }

  // obsługa obrazu oczekiwania
  public void cancelWaiting() {
    loadingPanel.setVisibility(View.INVISIBLE);
  }

  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }

  // implementacja IDao --------------------------------------------------------------------

  @Override
  public int getAlea(int a, int b) {
    // wykonanie
    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. Uruchomienie projektu

Uruchom usługę internetową (punkt 1.16.1.7), a następnie uruchom klienta na Androida:

Image

Aby dowiedzieć się, co wpisać w [1], wykonaj następujące czynności. Otwórz okno poleceń i wpisz następujące polecenie:


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

Jeśli zainstalowano [GenyMotion], maszyna wirtualna VirtualBox dodała adresy IP do komputera (wiersze 10 i 18). Adresy te są szczególnie przydatne, ponieważ nie są blokowane przez zaporę systemu Windows. W wierszu 30 podano adres IP Twojego komputera w sieci lokalnej. Aby skorzystać z tego adresu, zazwyczaj konieczne jest wyłączenie zapory systemu Windows. Jeśli korzystasz z sieci Wi-Fi, użyj adresu Wi-Fi i również w tym przypadku wyłącz zaporę, jeśli ją posiadasz.

Przetestuj aplikację w następujących przypadkach:

  • 100 liczb losowych w przedziale [1000, 2000] bez czasu oczekiwania;
  • 2000 liczb losowych z przedziału [10000, 20000] bez opóźnienia oraz anuluj oczekiwanie przed zakończeniem generowania;
  • 5 liczb losowych z przedziału [100, 200] z opóźnieniem wynoszącym 5000 ms oraz anuluj opóźnienie przed zakończeniem generowania;

1.16.2.9. Obsługa anulowania

Aby śledzić, co się dzieje, gdy użytkownik żąda anulowania lub gdy anulowanie jest wymagane z powodu wystąpienia wyjątku, dodajemy następującą metodę do interfejsu [IDao] (patrz punkt 1.16.2.4.1):


package exemples.android.dao;

public interface IDao {

  ...

  // tryb debugowania
  void setDebugMode(boolean isDebugEnabled);
}

W klasie [Dao] dodajemy następujący kod:


  // tryb debugowania
  private boolean isDebugEnabled;
  // nazwa klasy
  private String className;
..
  // konstruktor
  public Dao() {
    // nazwa klasy
    className = getClass().getSimpleName();
  }
...
  // interfejs IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // uruchomienie usługi
    Response<Integer> info;
...
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
}
  • wiersz 9: wpisujemy nazwę klasy;
  • wiersze 16–18: zapisujemy komunikat logowania za każdym razem, gdy wywoływana jest metoda [getAlea];

Ponadto w fragmencie [Vue1Fragment] dodajemy następujące wpisy do dziennika:


  @UiThread
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    ....
  }

  @UiThread
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    ...
    }
}

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
   ...
}

Za każdym razem, gdy fragment [Vue1Fragment] otrzymuje informację z warstwy [DAO], generowany jest wpis w dzienniku. Ponadto, gdy wywoływana jest metoda [doAnnuler], rejestrujemy to zdarzenie w dzienniku.

Test 1

Żądamy 5 liczb, mimo że serwer nie został uruchomiony. Otrzymujemy następujące wpisy w dzienniku:

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
  • wiersze 1–5: metoda [getAlea] klasy [Dao] została wywołana pięć razy. Przypominamy, że są to wywołania asynchroniczne dokonywane przez fragment [VueFragment], który nie oczekuje na wynik swojego wywołania;
  • wiersz 7: pierwsze żądanie HTTP zostało wysłane, a fragment [VueFragment] otrzymał swój pierwszy wyjątek;
  • wiersz 8: następnie żąda on anulowania wszystkich żądań;
  • wiersze 9–12: widać jednak, że otrzymuje on cztery kolejne wyjątki. Oznacza to, że wszystkie oczekujące żądania asynchroniczne zostały wykonane;

Test 2

Teraz uruchommy serwer i wywołajmy 5 liczb z opóźnieniem 5 sekund, a następnie kliknijmy na [Annuler] przed upływem tego czasu. Logi są następujące:

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)
  • wiersze 1–5: metoda [getAlea] klasy [Dao] została wywołana pięć razy;
  • wiersz 7: użytkownik zażądał anulowania żądań;
  • wiersz 8: widać, że metoda [Vue1_Fragment] otrzymuje 5 wartości. Ponownie wszystkie oczekujące żądania asynchroniczne zostały wykonane;

To właśnie dlatego musieliśmy zastosować zmienną logiczną [hasBeenCanceled], aby uniknąć wyświetlania jakichkolwiek treści w sytuacji, gdy zgłoszono anulowanie. W kodzie anulowania:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // pamięć
    hasBeenCanceled = true;
    // anulowanie zadania asynchronicznego
    BackgroundExecutor.cancelAll("alea",true);
    // koniec oczekiwania
    cancelWaiting();
}

kod w linii 10 nie działa zgodnie z oczekiwaniami. Być może wynika to z faktu, że zadania asynchroniczne współdzielą tę samą metodę oznaczoną adnotacją [@Background]:


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

1.17. Przykład 16: obsługa asynchroniczności za pomocą RxAndroid

Teraz zamierzamy zarządzać asynchronicznością niezbędną w aplikacjach na Androida za pomocą biblioteki o nazwie RxJava [http://reactivex.io/] oraz jej wersji dostosowanej do środowiska Android – [RxAndroid]. W tym celu wykorzystamy kurs [Introduction à RxJava. Application aux environnements Swing et Android].

1.17.1. Tworzenie projektu

Duplikujemy projekt [Exemple-1] do [Exemple-16]:

1.17.2. Konfiguracja Gradle

  

W projekcie [build.gradle] dodajemy zależność od biblioteki [RxAndroid]:


dependencies {
  ...
  compile 'io.reactivex:rxandroid:1.2.0'
}

1.17.3. Warstwa [DAO]

  

1.17.4. Interfejs [IDao]

Interfejs [IDao] przyjmuje następujący kształt:


package exemples.android.dao;

import rx.Observable;

public interface IDao {

  // liczba losowa
  Observable<Integer> getAlea(int a, int b);

  // URL usługi internetowej
  void setUrlServiceWebJson(String url);

  // maksymalny czas oczekiwania (ms) na odpowiedź serwera
  void setTimeout(int timeout);

  // czas oczekiwania klienta w milisekundach przed wysłaniem żądania
  void setDelay(int delay);

  // tryb debugowania
  void setDebugMode(boolean isDebugEnabled);
}
  • wiersz 8: metoda [getAlea] zwraca teraz typ [Observable] z biblioteki RxJava (wiersz 3). Zasada działania jest następująca:

Strumień elementów typu Observable<T> jest obserwowany przez jednego lub więcej subskrybentów (obserwatorów, konsumentów) typu Subscriber<T>. Biblioteka RxJava umożliwia wykonywanie strumienia Observable<T> w wątku T1, a jego obserwatora Subscriber<T> w wątku T2, bez koniecznościmusiał martwić się o zarządzanie cyklem życia tych wątków oraz o naturalnie trudne problemy, takie jak współdzielenie danych między wątkami i ich synchronizacja w celu wykonania zadania globalnego. Ułatwia to zatem programowanie asynchroniczne.

1.17.5. Klasa [AbstractDao]

Utworzymy klasę [Dao] jako pochodną od następującej klasy [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 {

  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();

  // metody chronione ----------------------------------------------------------
  // interfejs generyczny
  protected interface IRequest<T> {
    Response<T> getResponse();
  }

  // ogólne żądanie
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // wykonanie usługi
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // wykonanie usługi
        try {
          // wysyłamy żądanie synchroniczne i przekazujemy odpowiedź abonentowi
          Response<T> response = request.getResponse();
          // błąd?
          int status = response.getStatus();
          if (status != 0) {
            // odnotowuje się wyjątek
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // wysyłamy odpowiedź
            subscriber.onNext(response.getBody());
            // zgłaszamy zakończenie obserwowalnego
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // odnotowuje się wyjątek
          ex = new DaoException(e, 100);
        }
        // wyjątek?
        if (ex != null) {
          // wysyłamy wyjątek
          subscriber.onError(ex);
        }
      }
    });
  }

}
  • klasa [AbstractDao] zawiera jako główny element metodę generyczną [getResponse], która służy do uzyskania z serwera typu [Response<T>], gdzie T jest typem wyniku pożądanego przez klienta HTTP (w tym przypadku Integer);
  • wiersz 20: jedynym parametrem metody generycznej [getResponse] jest instancja interfejsu generycznego [IRequest<T>] z wierszy 15–17. Interfejs ten posiada tylko jedną metodę [getResponse] i to właśnie ta metoda dostarcza pożądaną odpowiedź [Response<T>];
  • dzięki dwóm poprzednim elementom klasa [AbstractDao] może służyć jako klasa nadrzędna dla każdej warstwy klienckiej [Dao] serwera wysyłającego odpowiedzi typu [Response<T>];
  • wiersz 20: metoda generyczna [getResponse] zwraca typ [Observable<T>], który reprezentuje wynik faktycznie oczekiwany przez klienta HTTP (w tym przypadku typ Observable<Integer>);
  • wiersze 22–51: metoda statyczna [rx.Observable.create] tworzy typ [Observable];
  • wiersz 22: jedynym parametrem tej metody jest instancja typu [rx.Observable.OnSubscribe<T>], interfejsu posiadającego następujące metody:
    • [onNext(T element)]: umożliwia wysłanie do obserwatora elementu typu T;
    • [onError(Throwable th)]: umożliwia wysłanie wyjątku do obserwatora;
    • [onCompleted]: pozwala poinformować obserwatora o zakończeniu wysyłania;

Typ [Observable<T>] podlega pewnym ograniczeniom:

  • wysyła swoje elementy za pomocą metody [onNext(T element)];
  • metoda [onCompleted] musi zostać wywołana tylko raz, gdy nie ma już elementów do wysłania do obserwatora;
  • metoda [onCompleted] nie jest wywoływana, jeśli wywołano już metodę [onError(Throwable th)];

W naszym przykładzie:

  • obserwatorem będzie fragment [Vue1Fragment]. To właśnie on przetwarza elementy wysyłane przez [Observable<T>] (elementy lub wyjątki);
  • utworzony typ [Observable<T>] wygeneruje tylko jeden element (wiersz 37);
  • wiersz 29: wysyła synchroniczne zapytanie HTTP do serwera i otrzymuje typ [Response<T>]. To żądanie HTTP jest realizowane przez typ [IRequest] przekazany jako parametr do metody generycznej [getResponse];
  • wiersz 31: pobieramy status z odpowiedzi;
  • wiersze 32–34: jeśli ten identyfikator status wskazuje na błąd, przygotowuje się wyjątek;
  • wiersze 36–39: jeśli ten status nie wskazuje na błąd, wówczas wysyłamy odpowiedź, której faktycznie oczekuje klient (wiersz 37) i informujemy obserwatora, że nie będą już wysyłane żadne kolejne odpowiedzi (wiersz 39);
  • wiersze 41–44: jeśli żądanie HTTP kończy się wyjątkiem, odnotowujemy go;
  • wiersze 46–49: jeśli wyjątek [ex] różni się od null, wówczas wysyła się go do obserwatora. Nie ma tu potrzeby wywoływania metody [onCompleted] w celu poinformowania obserwatora, że nie będą już wysyłane żadne elementy. Jest to domyślne;

Z powyższych wyjaśnień wynika, że:

  • metoda generyczna [<T> Observable<T> getResponse(final IRequest<T> request)] zwraca typ [Observable<T>], który emituje albo jeden element typu T, albo wyjątek;
  • że metoda ta przyjmuje jako jedyny parametr typ [IRequest<T>], którego jedyna metoda [getResponse()] realizuje dostęp HTTP, zwracający typ [Response<T>];

1.17.6. Klasa [Dao]

Klasa [Dao] zmienia się w następujący sposób:


@EBean
public class Dao extends AbstractDao implements IDao {

  // klient usługi REST
  @RestService
  protected WebClient webClient;

  // czas oczekiwania przed wykonaniem żądania
  private int delay;
  // tryb debugowania
  private boolean isDebugEnabled;
  // nazwa klasy
  private String className;

  // konstruktor
  public Dao() {
    // nazwa klasy
    className = getClass().getSimpleName();
  }


  // interfejs IDao -------------------------------------------------------------------
  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // działanie klienta internetowego
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // oczekiwanie
        waitSomeTime(delay);
        // synchroniczne wywołanie HTTP
        return webClient.getAlea(a, b);
      }
    });
}
...
  • wiersz 2: klasa [Dao] rozszerza klasę [AbstractDao];
  • wiersz 24: metoda [getAlea] zwraca teraz typ [Observable<Integer>];
  • wiersz 30: wywołanie metody generycznej [getResponse] klasy nadrzędnej. Przekazano jej parametr typu [IRequest<Integer>];
  • wiersze 32–37: implementacja interfejsu [IRequest<Integer>];
  • wiersz 36: wysyłamy zapytanie HTTP za pośrednictwem interfejsu AA [webClient], tak jak to miało miejsce wcześniej. Wiadomo, że otrzymamy typ [Response<Integer>], który jest właśnie tym typem, jaki powinna zwracać metoda [IRequest<Integer>.getReponse()];
  • wiersz 36: wykorzystujemy tutaj właściwość o nazwie closure: możliwość enkapsulacji w instancji wartości zewnętrznych względem niej w momencie jej utworzenia, w tym przypadku wartości [a, b] z wiersza 24. To właśnie pozwala metodzie [IRequest<Integer>.getReponse()] na brak parametrów. Zostały one zapisane w treści metody. I tam, gdzie normalnie zmienialibyśmy parametry metody (a, b) -> (x, y), w tym przypadku tworzymy nową instancję klasy [IRequest<Integer>], enkapsulującą wartości x i y;

1.17.7. Klasa [MainActivity]

Klasa [MainActivity], która implementuje interfejs [IDao], zmienia się w następujący sposób:


  // implementacja IDao --------------------------------------------------------------------

  @Override
  public Observable<Integer> getAlea(int a, int b) {
    // wykonanie
    return dao.getAlea(a, b);
}

1.17.8. Klasa [Vue1Fragment]

Klasa [Vue1Fragment] zmienia się w następujący sposób:


  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // kasuje się poprzednie odpowiedzi
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // zerowanie licznika odpowiedzi
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // sprawdzanie poprawności wprowadzonych danych
    if (!isPageValid()) {
      return;
    }
    // inicjowanie działania
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // żądanie liczb losowych
    getAleasInBackground(a, b);
    // rozpoczyna się oczekiwanie
    beginWaiting();
}
  • wiersz 18: losowe liczby są pobierane z metody [getAleasInBackground], nazwanej tak, ponieważ liczby będą pobierane w wątku innym niż wątek interfejsu użytkownika;

  private int nbReponses = 0;
  // subskrypcje obserwowalnych
  private List<Subscription> abonnements;

// adnotacja [Background] zbędna
  void getAleasInBackground(int a, int b) {
    // na początku brak odpowiedzi i brak subskrypcji
    nbReponses = 0;
    abonnements.clear();
    // przygotowujemy obserwowalną
    Observable<Integer> response = Observable.empty();
    // scalamy wyniki różnych wywołań HTTP
    // są one wykonywane w wątku wejścia/wyjścia
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
    }
    // skumulowana obserwowalna będzie obserwowana na wątku UI
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // uruchamia się obserwowalną
      abonnements.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // informacja jest dodawana do listy odpowiedzi
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // komunikat o błędzie
          showAlert(th);
          // koniec oczekiwania
          doAnnuler();
        }
      }, new Action0() {
        @Override
        public void call() {
          // koniec oczekiwania
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // wyświetla się wyjątek w UiThread
      showAlert(e);
    }
}
  • wiersz 3: obserwowalny ma subskrybentów. Związek między subskrybentem a procesem, który obserwuje, nazywany jest subskrypcją (Subscription). W tym przypadku będziemy mieli tylko jeden obserwowany proces i jednego subskrybenta. Będziemy więc mieli tylko jedną subskrypcję. Dla zasady postępujemy tak, jakbyśmy mogli mieć kilka procesów obserwowanych przez różnych obserwatorów, co dałoby kilka subskrypcji;
  • wiersze 11–18: konfigurujemy obserwowany proces (obserwowalny). Należy pamiętać, że jest to jedynie konfiguracja: proces nie jest uruchamiany;
  • wiersz 11: zaczynamy od pustej obserwowalnej, która niczego nie emituje;
  • wiersze 14–16: do tej pustej obserwowalnej dodajemy obserwowalne [nbAleas], które będą żądaniami [nbAleas], które zwrócą liczby losowe [nbAleas];
  • wiersz 15: podobnie jak poprzednio, liczba losowa nr i jest żądana od klasy [MainActivity]. Należy naprawdę zrozumieć, że w tym miejscu żadne żądanie HTTP nie zostało jeszcze wykonane. Metoda [mainActivity.getAlea(a, b)] jest wykonywana i zwraca typ [Observable<Integer>]. Jest to proces, który będzie obserwowany po jego uruchomieniu;
  • wiersz 15: metoda [subscribeOn(Schedulers.io())] wymaga, aby proces ten był wykonywany (gdy zostanie uruchomiony) w wątku wejścia/wyjścia. Biblioteka RxJava oferuje różne typy wątków. Wątek wejścia/wyjścia jest dostosowany do wywołań HTTP;
  • wiersz 15: obserwowalny nr i jest scalany z obserwowalnym początkowym z wiersza 11: z obserwowalnych [nbAleas], z których każdy emituje jeden element, tworzy się obserwowalny, który wyemituje [nbAleas] elementów. To właśnie on będzie obserwowany. Ta obserwowalna wysyła powiadomienie [onCompleted], gdy wszystkie obserwowalne, z których się składa, wygenerują swoje własne powiadomienia [onCompleted]. Dzięki temu nie będziemy musieli liczyć odpowiedzi, tak jak to robiliśmy w poprzedniej wersji, aby sprawdzić, czy otrzymaliśmy wszystkie oczekiwane liczby;
  • wiersz 18: w tym miejscu skonfigurowaliśmy obserwowalną, która jest kompozycją obserwowalnych [nbAleas], z których każda działa w osobnym wątku wejścia/wyjścia;
  • wiersz 18: metoda [observeOn(AndroidSchedulers.mainThread())] służy do określenia, w którym wątku ma odbywać się obserwacja wartości generowanych przez obserwowalną. W tym przypadku wątek [AndroidSchedulers.mainThread())] należy do biblioteki RxAndroid, a nie do RxJava. Oznacza on wątek interfejsu użytkownika, zwany również pętlą zdarzeń. Jest to istotna kwestia: w aplikacji na Androida modyfikacja komponentu interfejsu użytkownika może odbywać się wyłącznie w wątku interfejsu użytkownika, w przeciwnym razie wystąpi wyjątek;
  • wiersze 19–45: po skonfigurowaniu procesu, który ma być obserwowany, uruchamia się go;
  • wiersz 21: to operacja [Observable.subscribe] uruchamia obserwowany proces. Operacja ta uruchomi wcześniej skonfigurowane procesy asynchroniczne [nbAleas]. Wyniki tych procesów zostaną automatycznie udostępnione obserwatorowi w wątku interfejsu użytkownika;
  • przypomnijmy, że obserwowalny obiekt generuje trzy rodzaje zdarzeń:
    • [onNext]: gdy wysyła element;
    • [onError]: gdy napotka wyjątek;
    • [onCompleted]: gdy sygnalizuje, że nie będzie już generować zdarzeń;

Metoda [Observable.subscribe] przyjmuje jako parametry trzy obiekty typu [Action1<Integer>, Action1<Throwable>, Action0], których metody [call] służą do obsługi każdego z tych trzech zdarzeń;

  • wiersze 21–27: pierwszy parametr typu [Action1<Integer>] służy do obsługi zdarzenia [onNext]. Jego metoda [call] odbiera element, który został wyemitowany przez obserwowalną (wiersz 23);
  • wiersz 25: ponownie wykorzystuje się metodę [showInfo] z poprzedniego przykładu;
  • wiersze 27–35: drugi parametr typu [Action1<Throwable>] służy do obsługi zdarzenia [onError]. Jego metoda [call] odbiera wyjątek wygenerowany przez obserwowalną (wiersz 29);
  • wiersz 31: ponownie wykorzystujemy metodę [showAlert] z poprzedniego przykładu;
  • wiersz 33: uruchamiana jest procedura anulowania żądania użytkownika. Polega to na anulowaniu wszystkich obserwowalnych obiektów, które są aktualnie wykonywane;
  • wiersze 35–41: trzeci parametr typu [Action0] służy do obsługi zdarzenia [onCompleted]. Jego metoda [call] nie przyjmuje żadnych parametrów;
  • wiersz 39: anuluje się oczekiwanie;

Metoda [showInfo] zmienia się w następujący sposób:


  // adnotacja [UiThread] zbędna
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // dodatkowa informacja
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // dodajemy informację do listy odpowiedzi
      reponses.add(0, String.valueOf(alea));
      // wyświetla się lista odpowiedzi
      adapterReponses.notifyDataSetChanged();
    }
}

W metodzie wprowadzono dwie zmiany:

  • wiersz 1: usunięto adnotację AA [@UiThread];
  • nie liczy się już odpowiedzi, aby ustalić, czy należy przerwać oczekiwanie, czy nie. Teraz informację tę dostarcza nam zdarzenie [onCompleted] z obserwowalnego obiektu;

Metoda [showAlert] zmienia się w następujący sposób:


  // adnotacja [UiThread] niepotrzebna
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    if (!hasBeenCanceled) {
      // wszystko odwołujemy
      doAnnuler();
      // wyświetla się
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}
  • jedyna zmiana dotyczy linii 1: usunięto adnotację AA [@UiThread];

Wreszcie metoda [doAnnuler] zmienia się w następujący sposób:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // pamięć
    hasBeenCanceled = true;
    // anulowanie zadań asynchronicznych
    if (abonnements != null) {
      for (Subscription abonnement : abonnements) {
        abonnement.unsubscribe();
      }
    }
    // koniec oczekiwania
    cancelWaiting();
}
  • wiersz 12: anuluje subskrypcję, a tym samym obserwację powiązanego procesu;

1.17.9. Wykonanie

Uruchom usługę internetową (punkt 1.16.1.7), uruchom klienta na Androida i powtórz testy, które przeprowadziłeś na poprzednim przykładzie (punkt 1.16.2.8).

1.17.10. Obsługa anulowania

Powtórz te same testy, co w poprzednim przykładzie (punkt 1.16.2.9).

Test 1

Żądamy 5 liczb, mimo że serwer nie został uruchomiony. Otrzymujemy następujące logi:

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

Po wierszu 7 nie ma już żadnych wpisów w logach, co wskazuje, że obserwator (Vue1Fragment) nie otrzymuje już powiadomień od obserwowanego procesu.

Test 2

Teraz uruchommy serwer i zażądajmy 5 liczb z 5-sekundowym opóźnieniem, a następnie kliknijmy na [Annuler] przed upływem tego czasu. Otrzymujemy następujące zapisy:

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

Po wierszu 6 nie ma już żadnych wpisów w logach, co wskazuje, że obserwator (Vue1Fragment) nie otrzymuje już powiadomień od obserwowanego procesu.

Jest to oczekiwane zachowanie w przypadku anulowania. Możemy zatem w kodzie [Vue1Fragment] usunąć zmienną logiczną [hasBeenCanceled], którą wprowadziliśmy w poprzednim przykładzie, ponieważ anulowanie nie działało zgodnie z oczekiwaniami.

Fakt, że obserwator nie otrzymuje już powiadomień po anulowaniu obserwowalnego, nie oznacza, że same żądania HTTP są anulowane. Aby to sprawdzić, modyfikujemy klasę [Dao] w następujący sposób:


  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // działanie klienta internetowego
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // oczekiwanie
        waitSomeTime(delay);
        // synchroniczne wywołanie 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;
      }
    });
}
  • wiersze 15–21: rejestrujemy wynik zapytania HTTP z wiersza 14;

Logi dla testu nr 2 wyglądają zatem następująco:

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}]
  • wiersze 1–5: wysłano 5 żądań;
  • wiersz 6: użytkownik anulował;
  • wiersze 7–11: otrzymujemy odpowiedzi na pięć zapytań HTTP. Jednak z powodu anulowania obserwowalnego elementy te nie są przekazywane do obserwatora;

1.17.11. Wniosek

W dalszej części niniejszego dokumentu aplikacje typu klient-serwer będą tworzone z wykorzystaniem biblioteki RxAndroid zamiast biblioteki AA z następujących powodów:

  1. RxAndroid może być używana w aplikacji na Androida, która nie korzysta z biblioteki AA;
  2. RxAndroid nie tylko ułatwia operacje asynchroniczne. Oferuje ona bardzo wiele metod służących do tworzenia nowego obiektu obserwowalnego na podstawie innego. Metody te nie mają odpowiedników w bibliotece AA;
  3. gdy tylko chcemy utworzyć klasę opatrzoną adnotacją AA, taką jak fragment, napotykamy poważne problemy. W takiej sytuacji jesteśmy zmuszeni zrezygnować z AA i skorzystać z rozwiązania 1 do programowania asynchronicznego;

Czytelnik zainteresowany pogłębieniem wiedzy na temat możliwości biblioteki RxAndroid może zapoznać się z dokumentem [Introduction à RxJava. Application aux environnements Swing et Android]. Wykorzystuje się w nim RxAndroid bez biblioteki AA.

1.18. Przykład 17: komponenty do wprowadzania danych

Napiszemy nowy projekt, aby zaprezentować kilka typowych komponentów stosowanych w formularzach do wprowadzania danych.

1.18.1. Tworzenie projektu

Duplikujemy projekt [Exemple-13], tworząc projekt [Exemple-17]:

Nowy projekt będzie zawierał tylko jeden widok [vue1.xml]. Usuwamy zatem widok [vue2.xml] oraz powiązane z nim fragmenty [Vue2Fragment] i [2]. Uwzględniamy tę zmianę w menedżerze fragmentów [Mainactivity]:


  // nasz menedżer fragmentów, który należy zdefiniować na nowo dla każdej aplikacji
  // musi zdefiniować następujące metody: getItem, getCount, getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // fragmenty
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

Uruchom ponownie projekt. Powinien wyświetlić widok nr 1, tak jak poprzednio. Będziemy pracować na podstawie tego projektu.

1.18.2. Widok XML formularza

  

Widok wygenerowany przez plik [vue1.xml] wygląda następująco:

Image

Tekst widoku XML wygląda następująco:


<?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>

Główne elementy formularza to:

  • wiersz 2: układ pionowy [ScrollView]. Pozwala on na
  • wyświetlenie formularza większego niż ekran
  • tabletu. Całość formularza można wyświetlić poprzez
  • przewijanie;
 
  • wiersze 125–132: pole wyboru
  • wiersze 134–159: grupa trzech przycisków opcji
  • wiersze 161–166: pasek wyszukiwania
  • wiersze 16–176: pole wprowadzania danych
  • wiersze 178–186: przełącznik „tak/nie”
  • wiersze 188–195: pole wprowadzania godziny
  • wiersze 197–207: pole wielowierszowe
  • wiersze 209–215: lista rozwijana
  • wiersze 217–225: pole wprowadzania daty
  • wszystkie pozostałe elementy to [TextView], które wyświetlają teksty.
 

1.18.3. Ciągi znaków w formularzu

Ciągi znaków formularza są zdefiniowane w następującym pliku [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>
  <!-- widok 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. Fragment formularza

  

Klasa [Vue1Fragment] ma następującą postać:


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;

// fragment to widok wyświetlany przez kontener fragmentów
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {

  // pola widoku wyświetlanego przez fragment
  @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;

  // lista rozwijana
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // zaznacza się pierwszy przycisk
    radioButton1.setChecked(true);
    // kalendarz
    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));
      }
    });
    // lista rozwijana
    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() {
    // inicjalizacja adaptera listy rozwijanej
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }
}
  • wiersze 22–49: pobierane są odniesienia do wszystkich elementów formularza XML [vue1] (wiersz 18);
  • wiersz 58: metoda [setChecked] umożliwia zaznaczenie przycisku opcji lub pola wyboru;
  • wiersz 60: domyślnie komponent [DatePicker] wyświetla zarówno pole wprowadzania daty, jak i kalendarz. Wiersz 60 usuwa kalendarz;
  • wiersz 62: [SeekBar].setMax() pozwala ustawić maksymalną wartość suwaka. Minimalna wartość to 0;
  • wiersze 63–74: obsługujemy zdarzenia suwaka. Chcemy, aby przy każdej zmianie dokonanej przez użytkownika wyświetlana była wartość suwaka w [TextView] z wiersza 49;
  • wiersz 71: parametr [progress] reprezentuje wartość suwaka;
  • wiersze 76–79: lista [String], którą powiążemy z listą rozwijaną;
  • wiersz 90: metoda [updateFragment] fragmentu. Po jej wykonaniu zainicjowano zmienną [activity] klasy nadrzędnej;
  • wiersz 92: źródło danych [list] jest powiązane z adapterem listy rozwijanej;
  • wiersze 93–94: adapter [dataAdapter] jest powiązany z listą rozwijaną [dropDownList];
  • wiersz 84: metoda [doValider] jest powiązana z kliknięciem przycisku [Valider];

Metoda [doValider] służy do wyświetlania wartości wprowadzonych przez użytkownika. Jej kod jest następujący:


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // lista komunikatów do wyświetlenia
    List<String> messages = new ArrayList<>();
    // pole wyboru
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // przyciski opcji
    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));
    // pole wprowadzania danych
    String texte = String.valueOf(saisie.getText());
    messages.add(String.format("Saisie simple [value=%s]", texte));
    // przełącznik
    boolean état = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", état));
    // data
    int an = datePicker1.getYear();
    int mois = datePicker1.getMonth() + 1;
    int jour = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
    // tekst wielowierszowy
    String lignes = String.valueOf(multiLignes.getText());
    messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
    // godzina
    int heure = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Heure [%d, %d]", heure, minutes));
    // lista rozwijana
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // wyświetlanie
    doAfficher(messages);
}
  • wiersz 4: wprowadzone wartości zostaną zebrane w liście komunikatów;
  • wiersz 6: metoda [CheckBox].isCkecked() pozwala sprawdzić, czy pole jest zaznaczone, czy nie;
  • wiersz 9: metoda [RadioGroup].getCheckedButtonId() pozwala uzyskać identyfikator przycisku opcji, który został zaznaczony, lub -1, jeśli żaden nie został zaznaczony;
  • wiersz 10: kod [activity.findViewById(id)] pozwala zidentyfikować zaznaczone pole wyboru i uzyskać w ten sposób jego etykietę;
  • wiersz 13: metoda [SeekBar].getProgress() pozwala uzyskać wartość suwaka;
  • wiersz 19: metoda [Switch].isChecked() pozwala sprawdzić, czy przełącznik ma wartość On (prawda) czy Off (fałsz);
  • wiersz 22: metoda [DatePicker].getYear() pozwala uzyskać wybrany rok za pomocą obiektu [DatePicker];
  • wiersz 23: metoda [DatePicker].getMonth() pozwala uzyskać wybrany miesiąc za pomocą obiektu [DatePicker] w przedziale [0,11];
  • wiersz 24: metoda [DatePicker].getDayOfMonh() pozwala uzyskać wybrany dzień miesiąca przy użyciu obiektu [DatePicker] w przedziale [1,31];
  • wiersz 30: metoda [TimePicker].getHour() pozwala uzyskać wybraną godzinę za pomocą obiektu [TimePicker];
  • wiersz 31: metoda [TimePicker].getMinute() pozwala uzyskać wybrane minuty za pomocą obiektu [TimePicker];
  • wiersz 34: metoda [Spinner].getSelectedItemPosition() pozwala uzyskać pozycję wybranego elementu na liście rozwijanej;
  • wiersz 35: metoda [Spinner].getSelectedItem() pozwala uzyskać obiekt wybrany z listy rozwijanej;

Metoda [doAfficher], która wyświetla listę wprowadzonych wartości, wygląda następująco:


    private void doAfficher(List<String> messages) {
        // tworzymy tekst do wyświetlenia
        StringBuilder texte = new StringBuilder();
        for (String message : messages) {
            texte.append(String.format("%s\n", message));
        }
        // wyświetlanie
        new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
  • wiersz 1: metoda otrzymuje listę komunikatów do wyświetlenia;
  • wiersze 3–6: na podstawie tych komunikatów tworzony jest obiekt typu [StringBuilder]. Do łączenia ciągów znaków typ [StringBuilder] jest bardziej wydajny niż typ [String];
  • wiersz 8: okno dialogowe wyświetla tekst z wiersza 3:

Image

1.18.5. Uruchomienie projektu

Uruchom projekt i przetestuj różne komponenty wprowadzania danych.

1.19. Przykład 18: wykorzystanie szablonu widoków

1.19.1. Tworzenie projektu

Tworzymy nowy projekt [Exemple-18] poprzez skopiowanie projektu [Exemple-13].

1.19.2. Szablon widoków

Chcemy przejąć dwa widoki z projektu i umieścić je w szablonie:

  

Image

Każdy z tych widoków będzie miał taką samą strukturę:

  • w [1] – nagłówek;
  • w [2] – lewa kolumna, która może zawierać linki;
  • w [3] – stopka;
  • w [4] – treść.

Osiąga się to poprzez modyfikację widoku podstawowego [activity_main.xml] danej aktywności;

Kod XML widoku [main] wygląda następująco:


<?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>
  • nagłówek [1] uzyskuje się z wierszy 38–54;
  • lewy pasek [2] uzyskuje się z wierszy 56–84;
  • stopka [3] pochodzi z wierszy 86–101;
  • treść [4] pochodzi z wierszy 78–84;

Widok XML [main] wykorzystuje informacje zawarte w plikach [res / values / colors.xml] i [res / values / strings.xml]:

  

Plik [colors.xml] ma następującą treść:


<?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>

a plik [strings.xml] ma następującą treść:


<?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>

Utwórz środowisko uruchomieniowe dla tego projektu i uruchom go.

1.20. Przykład 19: komponent [ListView]

Komponent [ListView] umożliwia powtórzenie określonego widoku dla każdego elementu listy. Powtarzany widok może mieć dowolną złożoność — od prostego ciągu znaków po widok umożliwiający wprowadzanie informacji dla każdego elementu listy. Utworzymy następujący komponent [ListView]:

Image

Każdy widok na liście składa się z trzech elementów:

  • [TextView] zawierający informacje;
  • [CheckBox];
  • klikalny element [TextView];

1.20.1. Tworzenie projektu

Tworzymy nowy projekt [Exemple-19] poprzez skopiowanie projektu [Exemple-18].

  

Będziemy rozwijać projekt zgodnie z opisem w [3].

1.20.2. Sesja

  

Sesja przechowuje dane współdzielone między aktywnością a fragmentami:


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 {
  // lista danych
  private List<Data> liste=new ArrayList<>();

  // metody pobierające i ustawiające
...
}
  • wiersz 11: lista danych wykorzystywana przez oba widoki;

Klasa [Data] ma następującą postać:


package exemples.android.architecture;

public class Data {

    // dane
    private String texte;
    private boolean isChecked;

    // konstruktor
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }

    // metody pobierające i ustawiające
    ...
}
  • wiersz 6: tekst, który zostanie umieszczony w pierwszym [TextView] każdego elementu listy;
  • wiersz 7: wartość logiczna, która służy do zaznaczenia lub odznaczenia elementu [checkBox] dla każdego elementu listy;

1.20.3. Aktywność [MainActivity]

Kod metody [@AfterInject] wygląda następująco:


  // wstrzykiwanie sesji
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // tworzymy listę danych
    List<Data> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new Data("Texte n° " + i, false));
    }
}
  • wiersze 12–15: inicjalizacja listy danych dostępnych w sesji;

1.20.4. Początkowy widok [Vue1]

Widok XML [vue1.xml] wyświetla powyższe pole [1]. Jego kod jest następujący:


<?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>
  • wiersze 7–16: komponent [TextView] [2];
  • wiersze 27–35: komponent [ListView] [4];
  • wiersze 18–25: komponent [Button] [3];

1.20.5. Widok powtórzony przez [ListView]

Widok powtórzony przez [ListView] to następujący widok [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>
  • wiersze 8–14: komponent [TextView] [1];
  • wiersze 16–23: komponent [CheckBox] [2];
  • wiersze 25–35: komponent [TextView] [3];

1.20.6. Fragment [Vue1Fragment]

  

Fragment [Vue1Fragment] obsługuje widok XML [vue1]. Jego kod wygląda następująco:


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 {

  // pola widoku wyświetlanego przez fragment
  @ViewById(R.id.listView1)
  protected ListView listView;
  // adapter listy
  private ListAdapter adapter;
  // inicjalizacja zakończona
  private boolean initDone = false;

  @AfterViews
  void afterViews() {
    // pamięć
    afterViewsDone = true;
  }

  @Click(R.id.button_vue2)
  void navigateToView2() {
    // przechodzimy do widoku 2
    mainActivity.navigateToView(1);
  }

  public void doRetirer(int position) {
   ...
  }

  @Override
  protected void updateFragment() {
    if (!initDone) {
      // przypisywanie danych do [ListView]
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // sytuacja, w której fragment został (ponownie) wygenerowany – w takim przypadku należy ponownie połączyć ListView z jego adapterem
    listView.setAdapter(adapter);
    // przypadek, w którym inne fragmenty zmieniły źródło danych – w tym przypadku należy odświeżyć ListView
    adapter.notifyDataSetChanged();
  }
}
  • wiersz 15: widok XML [vue1] jest powiązany z fragmentem;
  • wiersze 26–30: metoda [@AfterViews] nie wykonuje żadnej operacji. Jest ona jednak niezbędna do ustawienia zmiennej [afterViewsDone] na wartość true, ponieważ ta ostatnia jest wykorzystywana przez klasę nadrzędną [AbstractFragment];
  • wiersze 42–53: metoda [updateFragment], która jest wywoływana za każdym razem, gdy fragment ma stać się widoczny. Metoda została tutaj napisana tak, jakby fragment mógł wyjść z sąsiedztwa wyświetlanego fragmentu, a tym samym zresetować swój cykl życia. W tym przypadku tak nie jest, ale miałoby to miejsce, gdyby aplikacja miała 3 fragmenty o sąsiedztwie równym 1;
  • wiersz 44: adapter [ListView] musi zostać zainicjowany tylko raz;
  • wiersz 46: do tego [ListView] przypisujemy adapter typu [ListAdapter]. Zbudujemy tę klasę. Wywodzi się ona z klasy [ArrayAdapter], z której mieliśmy już okazję korzystać w celu powiązania danych z [ListView]. Przekazujemy różne informacje do konstruktora klasy [ListAdapter]:
    • odwołanie do bieżącej aktywności,
    • identyfikator widoku, który zostanie zainicjowany dla każdego elementu listy,
    • źródło danych do zasilania listy,
    • odwołanie do fragmentu. Zostanie ono wykorzystane do obsługi kliknięcia linku [Retirer] w [ListView] za pomocą metody [doRetirer] z wiersza 38;
  • wiersz 50: adapter jest powiązany z [ListView]. Jednocześnie źródło danych [listes] jest powiązane z [ListView]. Operacja ta będzie tutaj wykonywana za każdym razem, gdy wyświetlany jest widok nr 1. W rzeczywistości należałoby ją wykonać tylko wtedy, gdy została wykonana metoda [@AfterViews]. W tym przypadku instrukcja jest wykonywana zbyt często. Wydaje się, że potrzebna jest zmienna logiczna, która wskazywałaby, że metoda [@AfterViews] została właśnie wykonana, a zatem [ListView] musi zostać ponownie powiązana ze swoim adapterem;
  • wiersz 52: odświeżamy metodę [ListView]. W tym przykładzie nie ma to sensu, ponieważ tylko widok nr 1 może modyfikować źródło danych metody [ListView]. Rozważmy bardziej ogólny przypadek, w którym widok nr 2 również mógłby zmienić źródło danych elementu [ListView]. Takie przykłady pojawią się w dalszej części niniejszego dokumentu. W tym przypadku, podczas przechodzenia z widoku nr 2 do widoku nr 1, obiekt [ListView] w widoku nr 1 musi zostać odświeżony;

1.20.7. Adapter [ListAdapter] z pliku [ListView]

Klasa [ListAdapter]

  • konfiguruje źródło danych dla klasy [ListView];
  • zarządza wyświetlaniem różnych elementów klasy [ListView];
  • zarządza zdarzeniami tych elementów;

Jej kod jest następujący:


package exemples.android.fragments;

import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {

    // kontekst wykonania
    private Context context;
    // identyfikator układu wyświetlania wiersza na liście
    private int layoutResourceId;
    // dane z listy
    private List<Data> data;
    // fragment wyświetlający [ListView]
    private Vue1Fragment fragment;
    // adapter
    final ListAdapter adapter = this;

    // producent
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // zapisuje się informacje
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.data = data;
        this.fragment = fragment;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • wiersz 5: klasa [ListAdapter] rozszerza klasę [ArrayAdapter];
  • wiersz 19: konstruktor;
  • wiersz 20: nie zapomnij wywołać konstruktora klasy nadrzędnej [ArrayAdapter] z trzema pierwszymi parametrami;
  • wiersze 22–25: zapisujemy informacje z konstruktora;
  • wiersz 29: metoda [getView] będzie wielokrotnie wywoływana przez klasę [ListView] w celu wygenerowania widoku elementu o numerze [position]. Wynikiem jest obiekt [View], który stanowi odwołanie do utworzonego widoku.

Kod metody [getView] jest następujący:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // tworzy się bieżący wiersz z ListView
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // tekst
        TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
        textView.setText(data.get(position).getTexte());
        // pole wyboru
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // link [Retirer]
        TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
        txtRetirer.setOnClickListener(new OnClickListener() {

            public void onClick(View v) {
                fragment.doRetirer(position);
            }
        });
        // obsługa kliknięcia pola wyboru
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // wyświetlanie linii
        return row;
}
  • wiersz 2: metoda przyjmuje trzy parametry. Wykorzystamy tylko pierwszy;
  • wiersz 4: tworzymy widok elementu o numerze [position]. Jest to widok [list_data], którego identyfikator został przekazany jako drugi parametr do konstruktora. Następnie pobieramy odwołania do komponentów widoku, który właśnie zainicjowaliśmy;
  • wiersz 6: pobieramy odwołanie do elementu nr 1 o nazwie [TextView];
  • wiersz 7: przypisuje się mu tekst pochodzący ze źródła danych, które zostało przekazane jako trzeci parametr do konstruktora;
  • wiersz 9: pobieramy odwołanie do [CheckBox] nr 2;
  • wiersz 10: zaznacza się lub nie zaznacza pole wyboru, wykorzystując wartość pochodzącą ze źródła danych dla [ListView];
  • wiersz 12: pobierany jest numer referencyjny [TextView] nr 3;
  • wiersze 13–18: obsługuje się kliknięcie na linku [Retirer];
  • wiersz 16: to metoda [Vue1Fragment].doRetirer będzie obsługiwać to kliknięcie. Wydaje się bowiem bardziej logiczne, aby to zdarzenie obsługiwał fragment wyświetlający [ListView]. Ma on bowiem szerszy obraz sytuacji, którego nie ma klasa [ListAdapter]. Odwołanie do fragmentu [Vue1Fragment] zostało przekazane jako czwarty parametr do konstruktora tej klasy;
  • wiersze 20–25: obsługujemy kliknięcie pola wyboru. Działanie na nim ma wpływ na wyświetlane dane. Wynika to z następującego powodu. [ListView] to lista, która wyświetla tylko część tych elementów. W związku z tym element listy jest czasem ukryty, a czasem widoczny. Gdy element nr i ma zostać wyświetlony, wywoływana jest metoda [getView] z wiersza 2 powyżej dla pozycji nr i. Wiersz 10 ponownie oblicza stan pola wyboru na podstawie danych, z którymi jest ono powiązane. W związku z tym lista musi zapamiętywać stan pola wyboru na bieżąco;

1.20.8. Usunięcie elementu z listy

Kliknięcie linku [Retirer] jest obsługiwane w fragmencie [Vue1Fragment] przez następującą metodę [doRetirer]:


  public void doRetirer(int position) {
    // usuwamy element nr [position] z listy
    List<Data> liste = mainActivity.getListe();
    liste.remove(position);
    // zapisujemy pozycję przewijania, aby do niej powrócić
    // odczyt
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // pozycja pierwszego elementu – całkowicie widoczna lub nie
    int firstPosition = listView.getFirstVisiblePosition();
    // przesunięcie osi Y tego elementu względem górnej krawędzi ListView
    // mierzy wysokość ewentualnie ukrytej części
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // odświeża się element [ListView]
    adapter.notifyDataSetChanged();
    // ustawia się w odpowiednim miejscu w ListView
    listView.setSelectionFromTop(firstPosition, top);
}
  • wiersz 1: otrzymujemy pozycję w pliku [ListView] linku [Retirer], który został kliknięty;
  • wiersz 3: pobierana jest lista danych;
  • wiersz 4: usuwa się element o numerze [position];
  • wiersz 15: odświeżamy [ListView]. Bez tego wizualnie nic się nie zmienia.
  • wiersze 5–13, 17: dość skomplikowana operacja. Bez niej dzieje się następująca rzecz:
    • [ListView] wyświetla wiersze 15–18 listy danych,
    • usuwa się wiersz 16,
    • wiersz 15 powyżej całkowicie go resetuje, a [ListView] wyświetla wówczas wiersze 0–3 listy danych;

W przypadku powyższych wierszy usunięcie następuje, a [ListView] pozostaje ustawiony na wierszu następującym po usuniętym wierszu.

1.20.9. Widok XML [Vue2]

Kod widoku XML wygląda następująco:


<?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>
  • wiersze 6–15: komponent nr 1 o nazwie [TextView];
  • wiersze 26–33: komponent [TextView] nr 2;
  • wiersze 17–24: komponent [Button] nr 3;

1.20.10. Fragment [Vue2Fragment]

123

Fragment [Vue2Fragment] zarządza widokami XML i [vue2]. Jego kod jest następujący:


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 {

    // pola widoku
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

    @AfterViews
    void initFragment(){
        // pamięć
        afterViewsDone=true;
    }

  @Click(R.id.button_vue1)
    void navigateToView1() {
        // przechodzimy do widoku 1
        mainActivity.navigateToView(0);
    }

    @Override
    protected void updateFragment() {
        // wyświetla elementy listy, które zostały zaznaczone w widoku 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);
    }
}

Istotny fragment kodu znajduje się w metodzie [updateFragment] w wierszu 32:

  • wiersz 34: oblicza się tekst do wyświetlenia w [TextView] nr 2;
  • wiersze 35–39: przeglądamy listę danych wyświetlaną przez [ListView]. Jest ona przechowywana w aktywności;
  • wiersz 36: jeśli dane nr i zostały zaznaczone, dodaje się powiązany opis do typu [StringBuilder];
  • wiersz 41: [TextView] wyświetla obliczony tekst;

1.20.11. Wykonanie

Utwórz konfigurację wykonania dla tego projektu i uruchom ją.

1.20.12. Ulepszenie

W poprzednim przykładzie wykorzystaliśmy źródło danych List<Data>, w którym klasa [Data] miała następującą postać:


package exemples.android.fragments;

public class Data {

    // dane
    private String texte;
    private boolean isChecked;

    // producent
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
...

}

W wierszu 7 użyto wartości logicznej do obsługi pola wyboru elementów klasy [ListView]. Często klasa [ListView] musi wyświetlać dane, które można zaznaczyć poprzez zaznaczenie pola wyboru, nawet jeśli element źródła danych nie posiada pola logicznego odpowiadającego temu polu wyboru. Można wówczas postępować w następujący sposób:

Klasa [Data] przyjmuje następujący kształt:


package exemples.android.fragments;

public class Data {

    // dane
    private String texte;

    // konstruktor
    public Data(String texte) {
        this.texte = texte;
    }

    // metody pobierające i ustawiające
...
}

Tworzymy klasę [CheckedData] wywodzącą się z poprzedniej:


package exemples.android.fragments;

public class CheckedData extends Data {

    // element zaznaczony
    private boolean isChecked;

    // konstruktor
    public CheckedData(String text, boolean isChecked) {
        // element nadrzędny
        super(text);
        // lokalny
        this.isChecked = isChecked;
    }

    // metody pobierające i ustawiające
...
}

Następnie wystarczy zastąpić we wszystkich miejscach w kodzie (MainActivity, ListAdapter, Vue1Fragment, Vue2Fragment) typ [Data] na typ [CheckedData]. Na przykład w [MainActivity]:


  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // tworzymy listę danych
    List<CheckedData> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new CheckedData("Texte n° " + i, false));
    }
}

Projekt tej wersji jest udostępniony pod nazwą [Exemple-19B].

1.21. Przykład 20: korzystanie z menu

1.21.1. Tworzenie projektu

Duplikujemy projekt [Exemple-19B] do projektu [Exemple-20]:

3

Usuniemy przyciski z widoków 1 i 2, aby zastąpić je opcjami menu [1-2].

1.21.2. Definicja menu XML

  

Plik [res / menu / menu_vue1] definiuje menu widoku nr 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>

Elementy menu są definiowane przez następujące informacje:

  • android:id: identyfikator elementu;
  • android:title: nazwa elementu;
  • app:showsAsAction: wskazuje, czy element menu może zostać umieszczony na pasku akcji danej aktywności. [ifRoom] oznacza, że element powinien zostać umieszczony na pasku akcji, jeśli jest na nim wystarczająco dużo miejsca;
  • opcja menu może sama w sobie stanowić podmenu (tag <menu>, wiersze 25, 29);

Plik [res / menu / menu_vue2] definiuje menu widoku nr 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. Obsługa menu w klasie abstrakcyjnej [AbstractFragment]

Wyodrębnimy obsługę menu do klasy nadrzędnej [AbstractFragment] dla obu widoków:


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 {

  // dane  dostępne dla klas potomnych
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;

  // aktywność
  protected IMainActivity mainActivity;
  protected Activity activity;

  // sesja
  protected Session session;

  // menu
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;

  // konstruktor
  public AbstractFragment() {
    // inicjalizacja
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // pamięć
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    // pobieramy # opcji menu, jeśli nie zostało to jeszcze zrobione
    if (!initDone) {
      // pobieramy # opcji z menu
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // przenosimy listę opcji do tablicy
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // czynność
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // pamięć
      initDone = true;
    }

    // żądamy, aby fragment potomny się uruchomił
    updateFragment();
  }


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }

  // wyświetlenie opcji menu -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }

  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }

  // aktualizacja klasy potomnej
  protected abstract void updateFragment();
}
  • wiersz 42: logi pokazują, że metoda [onCreateOptionsMenu] jest wywoływana za każdym razem, gdy wyświetlany jest fragment. Jest ona wywoływana bardzo późno, a mianowicie po wywołaniu metody [updateFragment]. Sugeruje to, że mogłaby ona służyć do aktualizacji fragmentu. Właśnie to zrobimy w tym miejscu (wiersz 63);
  • wiersz 42: metoda ma dwa parametry:
    • [menu]: jest to puste menu;
    • [inflater]: narzędzie umożliwiające utworzenie menu na podstawie jego początkowego opisu. Nie skorzystamy tutaj z tej możliwości, ponieważ użyjemy adnotacji AA, która zrobi to za nas;
  • wiersz 44: zapisujemy menu. Będzie nam ono potrzebne później;
  • wiersze 52–53: zapisujemy w tablicy z wiersza 28 identyfikatory wszystkich elementów menu;
  • wiersze 55–57: z logów wynika, że po wywołaniu metody [onCreateOptionsMenu] metoda [Fragment.getActivity()] zwraca aktywność powiązaną z fragmentem;
  • wiersz 55: zapisujemy aktywność jako instancję klasy Android [Activity];
  • wiersz 56: zapisujemy aktywność jako instancję interfejsu [IMainActivity];
  • wiersz 57: zapisujemy sesję;
  • wiersz 59: odnotowujemy, że inicjalizacja klasy została już przeprowadzona, aby nie trzeba było jej powtarzać (wiersz 50);
  • wiersz 63: żądamy od fragmentu potomnego, aby się zaktualizował. Jest to możliwe, ponieważ fragment jest jednocześnie widoczny i powiązany ze swoim widokiem oraz menu;

Metoda [getMenuOptions], która pozwala uzyskać identyfikatory elementów menu, wygląda następująco:


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // przeglądanie wszystkich pozycji menu
    for (int i = 0; i < menu.size(); i++) {
      // pozycja nr i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // jeśli pozycja nr i jest podmenu, to zaczynamy od nowa
      if (menuItem.hasSubMenu()) {
        // rekurencja
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
}

Metoda [setAllMenuOptions] pozwala ukryć lub wyświetlić wszystkie opcje menu;


  protected void setAllMenuOptions(boolean isVisible) {
    // aktualizujemy wszystkie opcje menu
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

Metoda [setMenuOptions] pozwala ukryć lub wyświetlić niektóre opcje menu;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // aktualizujemy niektóre opcje menu
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

Klasa [MenuItemState] ma następującą postać:

  

package exemples.android.architecture;

public class MenuItemState {

  // identyfikator opcji menu
  private int menuItemId;
  // widoczność opcji
  private boolean isVisible;

  // konstruktorów
  public MenuItemState() {

  }

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

  // metody pobierające i ustawiające
...
}

1.21.4. Zarządzanie menu w fragmencie [Vue1Fragment]

Klasa [Vue1Fragment] przyjmuje następujący kształt:


@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {

...

  @OptionsItem(R.id.navigationVue2)
  void navigateToView2() {
    // przechodzimy do widoku 2
    mainActivity.navigateToView(1);
  }

  @OptionsItem(R.id.actionValider)
  void valider() {
    // wyświetla się komunikat
    Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
  }

  private boolean actionCacherMontrerTout = true;
  @OptionsItem(R.id.actionCacherMontrerTout)
  void cacherMontrerTout() {
    // zmiana stanu
    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() {
    // zmiana stanu
    actionCacherMontrerActions = !actionCacherMontrerActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
  }

  private boolean actionCacherMontrerActionsValider = true;
  @OptionsItem(R.id.actionCacherMontrerActionsValider)
  void actionCacherMontrerActionsValider() {
    // zmiana stanu
    actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
  }
...

  @Override
  protected void updateFragment() {
    ....
    // aktualizujemy menu
    //setMenuOptions(...)
  }
}
  • wiersz 2: menu [res / menu / menu_vue1.xml] jest powiązane z fragmentem;
  • wiersz 48: gdy uruchamia się metoda [updateFragment], menu może również zostać zaktualizowane, aby odzwierciedlało nowy stan fragmentu;
  • wiersz 7: adnotacja [@OptionsItem(R.id.navigationVue2)] określa metodę, która ma zostać wykonana po kliknięciu opcji menu [Navigation / Vue 2];
  • wiersze 19–25: aby ukryć gałąź menu, wystarczy ukryć jej opcję główną;
  • wiersz 24: wyświetla się / ukrywa opcję główną [menuNavigation, menuActions];
  • wiersz 40: aby wyświetlić opcję z gałęzi menu, należy wyświetlić nie tylko tę opcję, ale również wszystkie opcje, które napotykamy, przechodząc od opcji liścia w górę do korzenia menu;

1.21.5. Obsługa menu we fragmencie [Vue2Fragment]

Podobny kod znajduje się we fragmencie widoku nr 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 {

  // pola widoku
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // przechodzimy do widoku 1
    mainActivity.navigateToView(0);
  }

  @Override
  protected void updateFragment() {
    // wyświetla się elementy listy, które zostały zaznaczone w widoku 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);
    // aktualizujemy menu
    // setMenuOptions(...)
  }
}
  • wiersz 35: wyświetlana jest opcja [Navigation / Vue 1];
  • wiersze 17–20: po kliknięciu opcji [Navigation / Vue1] wywoływana jest metoda [navigateToView1];

1.21.6. Wykonanie

Utwórz kontekst wykonania dla tego projektu i uruchom go.

1.22. Przykład 21: refaktoryzacja klasy abstrakcyjnej [AbstractFragment]

Poprzedni przykład pokazał nam, że gdy fragment posiada menu, jego metoda [onCreateOptionsMenu] jest dobrym miejscem do wywołania aktualizacji fragmentu:

  • jest wywoływana dokładnie raz, gdy fragment ma zostać wyświetlony;
  • gdy jest wywoływana, ustanawiane są powiązania fragmentu z jego aktywnością, widokiem i menu;

Aby to zilustrować, powrócimy do przykładu 12, który charakteryzuje się dużą liczbą fragmentów, których sąsiedztwo można modyfikować. W tym przykładzie fragmenty nie miały menu. Przypiszemy im puste menu.

1.22.1. Tworzenie projektu

Duplikujemy projekt [Exemple-12] w projekcie [Exemple-21]:

1.22.2. Menu fragmentów

  

Menu dodane dla fragmentów będzie puste:


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

Należy tu pamiętać, że aktywność ma już swoje własne 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>

Gdy aktywność ma już swoje menu, menu powiązane z fragmentami zostaje dodane do menu aktywności: mamy więc do dyspozycji opcje z obu menu. W tym przypadku menu fragmentów będzie puste. Widoczne będzie więc tylko menu aktywności.

1.22.3. Fragmenty

  

Wykorzystujemy klasę abstrakcyjną [AbstractFragment] z poprzedniego przykładu (patrz punkt 1.21.3). Przypisujemy menu [menu_fragment] do obu fragmentów:


@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 {

W obu fragmentach [PlaceholderFragment] i [Vue1Fragment] usuwamy odniesienia do starej klasy abstrakcyjnej [AbstractFragment].

1.22.4. Uruchomienie

Uruchom aplikację i sprawdź, czy działa. Śledź logi, aby zobaczyć, kiedy wykonywana jest metoda [onCreateOptionsMenu] klasy [AbstractFragment]. To właśnie ona wywołuje teraz metodę [updateFragment] fragmentów potomnych.

1.23. Przykład 22: zapisywanie i przywracanie stanu aktywności oraz fragmentów

1.23.1. Problem

Zajmujemy się tutaj problemem obracania urządzenia z systemem Android (tryb pionowy <--> poziomy). Aby to zilustrować, powracamy do poprzedniego przykładu 21:

Image

Jeśli obrócimy urządzenie [1], otrzymamy następujący nowy widok:

Image

Widać, że:

  • w [1] zniknęła zakładka [Fragment n° 3];
  • w [2] wyświetlany tekst to rzeczywiście fragment nr 3, ale licznik odwiedzin jest nieprawidłowy;

Podczas tej rotacji logi wyglądają następująco:

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
  • wiersz 1: widać, że aktywność została całkowicie odtworzona;
  • wiersze 3–7: to samo dotyczy pięciu fragmentów obsługiwanych przez tę aktywność;
  • wiersz 21: zostanie wyświetlony fragment nr 3. Widać, że przed inkrementacją numer wizyty wynosi 0;

Wynik uzyskany po rotacji można zatem wyjaśnić w następujący sposób:

  • klasa [MainActivity] tworzy na początku pasek zakładek z jedną zakładką o nazwie [Vue 1]. To właśnie ta zakładka jest widoczna;
  • po obróceniu urządzenia menedżer stron [mViewPager] ponownie wyświetla ten sam fragment, czyli w tym przypadku fragment nr 3. Należy pamiętać, że zakładki i fragmenty to różne pojęcia i mają one odmienny cykl życia. Zostanie wykonana metoda [updateFragment] fragmentu nr 3:

  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // zwiększenie numeru wizyty
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // zmieniony tekst
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
  • wiersz 7: ostatni numer wizyty jest odczytywany z sesji. Jednak sesja ta, podobnie jak cała reszta, została odbudowana, a numer wizyty zresetowano do zera. To wyjaśnia wynik wyświetlany we fragmencie nr 3;

1.23.2. Metody zapisywania i przywracania aktywności oraz fragmentów

1.23.2.1. Rozwiązanie 1: ręczne zapisywanie

Podczas obracania urządzenia wywoływane są dwie metody aktywności:


// zarządzanie tworzeniem kopii zapasowych / przywracaniem aktywności ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // element nadrzędny
    super.onSaveInstanceState(outState);
    // zapis stanu aktywności
    // ....
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // element nadrzędny
    super.onCreate(savedInstanceState);
     // przywracanie aktywności
    // ...
  }
  • wiersze 2–8: metoda [onSaveInstanceState] jest wywoływana przez system podczas rotacji. W tym miejscu można wykonać zapis stanu aktywności. Jeśli nie podejmie się żadnych działań, nic nie zostanie zapisane. Zapis stanu aktywności musi nastąpić w parametrze [Bundle outState] przekazanym do metody. Klasa [Bundle] przypomina słownik. Posiada metodę [putString, putInt, putLong, putBoolean, putChar, ...] z dwoma parametrami: void putT(String key, T value);
  • wiersze 10–16: metoda [onCreate] jest wywoływana podczas tworzenia aktywności. Jeśli jej stan został zapisany, zapis ten jest przekazywany do tej metody w parametrze [Bundle savedInstanceState]. Aby odzyskać zapisane wartości, dostępne są metody takie jak [getString, getInt, getLong, geBoolean, getChar, ...] z jednym parametrem: T getT(String key);

Fragmenty dysponują tymi samymi dwiema metodami do zapisywania swojego stanu.

Wykorzystamy te informacje do zapisania i przywrócenia stanu z przykładu 21. W tym celu skopiujemy projekt [Exemple-21] do [Exemple-22].

1.23.2.2. Rozwiązanie 2: automatyczne zapisywanie

Dokumentacja systemu Android wskazuje, że podczas obracania urządzenia można zapobiec zniszczeniu fragmentu, stosując instrukcję: [Fragment].setRetainInstance(true). Wiele artykułów dotyczących [StackOverflow] zaleca stosowanie tej instrukcji wyłącznie w przypadku fragmentów bez interfejsu wizualnego [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]. Przetestowałem tę instrukcję na dwóch przykładach: Przykład-17 (punkt 1.18 – aplikacja składająca się z jednego fragmentu wyświetlającego formularz) oraz Przykład-21 (punkt 1.22 – aplikacja składająca się z pięciu fragmentów). W obu przypadkach zastosowanie tej jednej instrukcji do wszystkich fragmentów aplikacji okazało się niewystarczające do prawidłowego przywrócenia widoku wyświetlanego podczas obracania urządzenia. Zamiast tworzyć dwa modele – jeden oparty na [setRetainInstance(true)], a drugi na [setRetainInstance(false)], który jest wartością domyślną – postanowiłem zastosować się do zaleceń zawartych w [StackOverflow] i zachować wartość domyślną false dla metody [setRetainInstance(boolean )]. Instrukcja: [Fragment].setRetainInstance(true) nie została nigdy wykorzystana w dalszej części niniejszego dokumentu.

1.23.3. Metoda tworzenia kopii zapasowej / przywracania projektu [Exemple-22]

Projekt [Exemple-22] rozwija się w następujący sposób:

  

Pojawiają się w nim dwie nowe klasy:

  • [PlaceHolderFragmentState], która będzie zapisywać stan fragmentu typu [PlaceHolderFragment];
  • [Vue1FragmentState], która będzie przechowywać stan fragmentu typu [Vue1Fragment];

Są to następujące klasy:


package exemples.android;

public class Vue1FragmentState {
  // stan Vue1Fragment
  private boolean hasBeenVisited=false;
  // metody pobierające i ustawiające
...
}
  • wiersz 5: wartość logiczna [hasBeenVisited] ma wartość true, jeśli fragment [Vue1Fragment] został wyświetlony co najmniej raz. Pole to zostało utworzone na potrzeby przykładu, ponieważ fragment [Vue1Fragment] nie ma żadnych danych do zapisania;

Klasa [PlaceHolderFragmentState] ma następującą postać:


package exemples.android;

public class PlaceHolderFragmentState {
  // stan: odwiedzony lub nieodwiedzony
  private boolean hasBeenVisited;
  // wyświetlany tekst
  private String text;

  // metody pobierające i ustawiające
...
}
  • wiersz 5: pojawia się wartość logiczna [hasBeenVisited];
  • wiersz 7: tekst wyświetlany przez fragment w momencie, gdy ma zostać zapisany. Widzieliśmy, że tekst ten został utracony podczas rotacji;

Stan fragmentów zostanie zapisany w sesji, a za zapisanie i przywrócenie tej sesji będzie odpowiedzialna aktywność. Sesja zmienia się w następujący sposób:


package exemples.android;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // liczba odwiedzonych fragmentów
  private int numVisit;
  // numer fragmentu typu [PlaceholderFragment] wyświetlany w drugiej zakładce
  private int numFragment = -1;
  // nr wybranej zakładki
  private int selectedTab = 0;
  // nr bieżącego widoku
  private int currentView;

  // zapisane fragmenty ---------------
  private Vue1FragmentState vue1FragmentState;
  private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];

  // konstruktor
  public Session() {
    for (int i = 0; i < placeHolderFragmentStates.length; i++) {
      placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
    }
    vue1FragmentState = new Vue1FragmentState();
  }
  // metody pobierające i ustawiające
...
}
  • wiersz 18: stan fragmentu [Vue1Fragment];
  • wiersz 19: stan fragmentów typu [PlaceHolderFragment];
  • wiersze 22–27: w konstruktorze sesji inicjowane są pola z wierszy 18 i 19;
  • wiersze 12–15: pojawiają się dwa nowe pola:
    • wiersz 13: numer ostatniej wybranej zakładki;
    • wiersz 15: numer ostatniego wyświetlonego fragmentu;

Aktywność zapisuje/przywraca sesję w następujący sposób:


  // zarządzanie zapisywaniem i przywracaniem aktywności ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // element nadrzędny
    super.onSaveInstanceState(outState);
    // zapis sesji
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // log
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // element nadrzędny
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
      // przywracanie sesji
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // log
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    }
}
  • wiersz 8: sesja jest zapisywana w postaci ciągu znaków jSON;
  • wiersz 29: sesja jest przywracana na podstawie jej ciągu znaków jSON;

Aby zarządzać zapisywaniem i przywracaniem fragmentów, klasa abstrakcyjna [AbstractFragment] zmienia się w następujący sposób:


// zarządzanie zapisywaniem/przywracaniem -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // nadrzędny
    super.setUserVisibleHint(isVisibleToUser);
    // kopia zapasowa?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // fragment zostanie ukryty – tworzymy jego kopię zapasową
      saveFragment();
      saveFragmentDone = true;
    }
    // pamięć
    this.isVisibleToUser = isVisibleToUser;
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // nadrzędny
    super.onActivityCreated(savedInstanceState);
    // dziennik
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // fragment należy przywrócić
    fragmentHasToBeInitialized = true;
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // element nadrzędny
    super.onSaveInstanceState(outState);
    // zapis fragmentu tylko wtedy, gdy jest widoczny
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }

  // klasy potomne
  protected abstract void updateFragment();

  protected abstract void saveFragment();
  • postanowiono zapisywać stan fragmentów w sesji w dwóch momentach:
    • wiersze 2–14: gdy fragment przechodzi ze stanu widocznego do ukrytego;
    • wiersze 29–42: gdy system sygnalizuje konieczność zapisania fragmentu, a fragment ten jest widoczny (wiersz 38);

Mechanizm ten pozwala uniknąć zapisywania stanu częściej niż to konieczne. Ponieważ bowiem zapisano stan fragmentu i w momencie, gdy przeszedł on ze stanu widocznego do ukrytego, to gdy fragment j jest wyświetlany i następuje obrót, nie ma potrzeby ponownego zapisywania fragmentu i. Jeśli nie został on ponownie wyświetlony od czasu ostatniego zapisu, to jego stan nie uległ zmianie. Należy zapisać jedynie stan fragmentu j. Mechanizm ten ma również inną zaletę: zapis stanu fragmentu jest potrzebny nie tylko podczas obracania urządzenia. Istnieje również przypadek czystej nawigacji między fragmentami, na przykład w systemie zakładek. Chcemy wtedy odzyskać fragment w stanie, w jakim go pozostawiliśmy, gdy był wyświetlany po raz ostatni. Stan ten mógł częściowo zniknąć, jeśli fragment ten w pewnym momencie znalazł się poza sąsiedztwem wyświetlanych fragmentów. Fragment nie jest wówczas odtwarzany w całości, ale odtwarzany jest powiązany z nim widok. Zapis wykonany w momencie, gdy fragment stał się ukryty, posłuży do odtworzenia ostatniego stanu tego widoku;

  • wiersze 10, 40: aby uniknąć wykonywania dwóch kolejnych zapisów, używa się zmiennej logicznej [saveFragmentDone] w celu wskazania, że zapis został wykonany;
  • wiersze 9, 39: żądamy od fragmentu potomnego, aby zapisał swój stan. Metoda [saveFragment] jest abstrakcyjna (wiersz 47). Zatem to klasy potomne muszą ją zaimplementować;
  • wiersze 16–26: metoda [onActivityCreated] służy do ustawienia wartości logicznej [fragmentHasToBeInitialized] na „prawda”. Fragment potomny musi bowiem wiedzieć, że ma całkowicie zresetować stan fragmentu na podstawie stanu, który znajdzie w sesji;

Również w klasie [AbstractFragment] metoda [onCreateOptionsMenu] zmienia się w następujący sposób:


// aktualizacja fragmentu
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // pamięć
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    ...
    // wysyłanie żądania aktualizacji do fragmentu potomnego
    updateFragment();
    // należy wykonać zapis
    saveFragmentDone = false;
  }
  • wiersz 14: zauważyliśmy, że wartość logiczna [saveFragmentDone] zmieniała się na vrai po wykonaniu zapisu. W pewnym momencie musi ona powrócić do wartości faux. Gdy zostanie wykonana metoda [updateFragment] (wiersz 12) fragmentu potomnego, fragment ten stanie się widoczny. Jednak fragment należy zapisać właśnie wtedy, gdy jest widoczny, w konkretnym momencie, w którym przejdzie ze stanu widocznego do stanu ukrytego. Następnie ustawiamy wartość zmiennej logicznej [saveFragmentDone] na false, aby umożliwić zapisanie fragmentu;

1.23.4. Zapisywanie fragmentu [Vue1Fragment]

Zapis fragmentów odbywa się w metodzie [saveFragment] wywoływanej przez klasę nadrzędną [AbstractFragment]:


// zapis stanu fragmentu
  @Override
  public void saveFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // zapis stanu fragmentu w sesji
    Vue1FragmentState state = new Vue1FragmentState();
    state.setHasBeenVisited(true);
    session.setVue1FragmentState(state);
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • wiersze 9–11: zapis stanu fragmentu w sesji. Gdy wywoływana jest metoda [saveFragment], fragment jest widoczny. Należy zatem ustawić wartość logicznego [hasBeenVisited] na vrai (wiersz 10);

1.23.5. Zapisywanie fragmentu [PlaceHolderFragment]

Zapisywanie fragmentów odbywa się w metodzie [saveFragment] wywoływanej przez klasę nadrzędną [AbstractFragment]:


  @Override
  public void saveFragment() {
    // zapisywanie stanu fragmentu w sesji
    PlaceHolderFragmentState state = new PlaceHolderFragmentState();
    state.setText(textViewInfo.getText().toString());
    state.setHasBeenVisited(true);
    session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • wiersze 4–7: zapis stanu fragmentu w sesji;
  • wiersz 5: zapisywany jest tekst aktualnie wyświetlany przez [TextView] textViewInfo;
  • wiersz 6: wartość logiczna [hasBeenVisited] fragmentu zostaje zmieniona na vrai;
  • wiersz 7: stan fragmentu jest zapisywany w tablicy [placeHolderFragmentStates]. Numer elementu do zainicjowania to numer sekcji fragmentu pomniejszony o jeden;

1.23.6. Przywracanie fragmentu [Vue1Fragment]

Przywracanie fragmentów odbywa się w metodzie [updateFragment]:


@Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // przywracanie?
    if (fragmentHasToBeInitialized) {
      // przywracanie stanu
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // nawigacja?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // zwiększenie numeru wizyty
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // wyświetlanie numeru wizyty
      Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
    }
    // zmiana numeru bieżącej strony
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • wiersze 8–12: przywrócenie stanu fragmentu. Zmienna logiczna [fragmentHasToBeInitialized] została zainicjowana przez klasę nadrzędną [AbstractFragment]. Gdy przyjmuje wartość vrai, oznacza to, że fragment został właśnie odtworzony i należy go zresetować. To właśnie tutaj ma to miejsce. W tym konkretnym przykładzie nie ma nic do zrobienia. Pokazaliśmy jedynie, że można odzyskać wartość zmiennej logicznej [hasBeenVisited] z zapisanego stanu fragmentu (wiersz 10);
  • wiersz 11: nie wolno zapomnieć o zmianie wartości [fragmentHasToBeInitialized] na faux, aby w przypadku powrotu do tego fragmentu w przyszłości, gdy urządzenie nie uległo obrotowi, nie doszło do niepotrzebnej ponownej inicjalizacji fragmentu;
  • wiersze 18–26: zwiększenie licznika odwiedzin. W tym miejscu pojawia się trudność: podczas przywracania fragmentu nie chcemy zwiększać tego licznika. Musimy tutaj rozróżnić między:
    • zwykłą nawigacją, która przenosi użytkownika z powrotem do zakładki [Vue 1];
    • przywróceniem fragmentu, gdy użytkownik obraca urządzenie, gdy wyświetlana jest zakładka [Vue 1];

Te dwa przypadki rozróżniamy dzięki numerowi widoku zapisanym w sesji. Numer ten odpowiada ostatniemu wyświetlonymu widokowi (wiersz 28).

  • wiersz 18: następuje nawigacja, a nie przywrócenie, jeśli numer ostatniego widoku różni się od numeru bieżącego widoku;
  • wiersze 21–25: zwiększenie licznika odwiedzin i jego wyświetlenie;

1.23.7. Przywrócenie fragmentu [PlaceHolderFragment]

Przywracanie fragmentów odbywa się w metodzie [updateFragment]:


  // dane
  private String text;
  private int numVisit;
  private String newText;
  private boolean hasBeenVisited = false;
  private ObjectMapper jsonMapper = new ObjectMapper();
...

public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // o jaki fragment chodzi?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // czy fragment powinien zostać zainicjowany?
    if (fragmentHasToBeInitialized) {
      // tekst początkowy
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // nawigacja?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // zwiększenie liczby odwiedzin
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // zmodyfikowany tekst
      newText = String.format("%s, visite %s", text, numVisit);
    } else {
      // mamy do czynienia z przywróceniem
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // wyświetlanie tekstu
    textViewInfo.setText(newText);
    // bieżący widok
    session.setCurrentView(numView);
}
  • wiersze 15–16: określa się numer widoku, który jest aktualnie aktualizowany;
  • wiersze 18–22: sytuacja, w której fragment znajduje się w cyklu zapisywania/przywracania po zmianie orientacji urządzenia. Należy go wówczas przywrócić. Zazwyczaj polega to na przywróceniu niektórych pól fragmentu;
  • wiersz 20: pole [text] w wierszu 2 musi zawierać początkowy tekst wyświetlany przez fragment: [Hello world from section i]. Należy je tutaj odtworzyć;
  • wiersz 21: zauważamy, że fragment został zainicjowany;
  • wiersze 24–36: podobnie jak wcześniej w przypadku fragmentu [Vue1Fragment], podczas przywracania nie należy zwiększać licznika odwiedzin. Tak jak poprzednio, należy rozróżnić nawigację od przywracania;
  • wiersze 32–36: przypadek przywracania;
  • wiersz 34: stan fragmentu przed obróceniem urządzenia jest pobierany z sesji;
  • wiersz 35: pobierany jest tekst, który był wówczas wyświetlany;
  • wiersz 38: tekst ten jest ponownie wyświetlany;
  • wiersz 40: w sesji zapisuje się numer nowo wyświetlonego widoku;

1.23.8. Zarządzanie kartami

W poprzednich akapitach nie poruszono kwestii zarządzania kartami. Jednak w przykładzie 21 zauważyliśmy problem podczas obracania urządzenia: zachowana została tylko pierwsza karta [Vue 1]. Druga karta została utracona.

Problem ten rozwiązujemy w klasie [MainActivity] w następujący sposób:


@AfterViews
  protected void afterViews() {
    // dziennik
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // pasek narzędzi
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

 ...

    // pierwsza zakładka
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // druga zakładka?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }

    // którą kartę wybrać?
    tabLayout.getTabAt(session.getSelectedTab()).select();

...

  }
  • wiersze 14–16: utworzenie pierwszej zakładki;
  • wiersze 18–23: utworzenie drugiej zakładki. Aby sprawdzić, czy należy ją utworzyć, sprawdzamy w sesji numer fragmentu wyświetlanego w zakładce 2. Jeśli numer ten różni się od -1, czyli swojej wartości początkowej, wówczas tworzona jest druga zakładka. Na tym etapie mamy dwie zakładki, z których domyślnie zaznaczona jest pierwsza;
  • wiersz 26: wyszukujemy w sesji numer zakładki, która była zaznaczona przed zapisaniem/przywróceniem, i ponownie ją zaznaczamy. Jeśli pole [selectedTab] nie zostało jeszcze zainicjowane przez kod, używana jest wówczas jego wartość początkowa 0;