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:
- Wprowadzenie do programowania tabletów z systemem Android na przykładach;
- 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:
Charakter | |
Importowanie istniejącego projektu Android | |
Podstawowy projekt na Androida | |
Podstawowy projekt [Android Annotations] | |
Widoki i zdarzenia | |
Nawigacja między widokami | |
Nawigacja za pomocą zakładek | |
Korzystanie z biblioteki [Android Annotations] w Gradle | |
Zarządzanie fragmentami aplikacji na Androida | |
Nowa nawigacja między widokami | |
Architektura dwuwarstwowa | |
Architektura klient-serwer | |
Zarządzanie asynchronicznością za pomocą RxJava | |
Komponenty do wprowadzania danych | |
Wykorzystanie szablonu widoków | |
Komponent ListView | |
Korzystanie z menu | |
Korzystanie z klasy nadrzędnej dla fragmentów | |
Zapisywanie i przywracanie stanu aktywności i fragmentów | |
Aplikacja pogodowa | |
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. | |
Zarządzanie wizytami w gabinecie lekarskim | |
Ćwiczenie praktyczne – Zarządzanie podstawową listą płac | |
Ć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]:
![]() |

![]() | ![]() |
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]:
![]() | ![]() | ![]() |
![]() |

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

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

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

1.5.3. Obsługa zdarzeń
Zajmijmy się teraz obsługą kliknięcia przycisku [Valider] w widoku [Vue1]:

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

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

- 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:
- 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ć;
- 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];
- 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];
- 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:
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:
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:
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:
Klasa [MainActivity] została zatem ponownie zainicjowana. Po kliknięciu przycisku [Vue n° 2] pojawiają się następujące nowe wpisy w dzienniku:
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]:

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:

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:

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:
- 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:
- 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:
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:
- 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:
- 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:
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:
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:
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:
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:
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:
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ć:
- 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:
Zgodnie z powyższą instrukcją,
- 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];
- 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:
- 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:
Przejdźmy z zakładki 2 do zakładki 3:
Następnie z zakładki 3 do zakładki 1:
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:
- 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;

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

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:
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:
- [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:
- żą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);
- 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];
- 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:
- 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):

- na [1], zamiast żądanego URL;
- na [2], za pomocą GET;
- na [3] wysyłane jest żądanie;

- 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:
- warstwę [Présentation] (widok + aktywność) analogiczną do tej, którą omówiliśmy w przykładzie [Exemple-14];
- 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:
- zapewnienie, że metoda [updateFragments] klas potomnych jest zawsze wywoływana podczas wyświetlania fragmentu i to tylko raz;
- 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;

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

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:

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:
- 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:
- 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:
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:
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:
- 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:
- RxAndroid może być używana w aplikacji na Androida, która nie korzysta z biblioteki AA;
- 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;
- 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:

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:
| |
| |
| |
| |
| |
| |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
|
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:

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:
![]() |

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

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:

Jeśli obrócimy urządzenie [1], otrzymamy następujący nowy widok:

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











































































































































































































































































































































