2. Szkielet aplikacji klienckiej na Androida komunikującej się z serwisem internetowym / jSON
Przedstawiamy teraz szkielet aplikacji na Androida komunikującej się z jedną lub kilkoma usługami internetowymi / jSON. Jest to projekt [client-android-skel], który można znaleźć w folderze [architecture] zawierającym przykłady:
![]() |
Analiza tej aplikacji szkieletowej będzie okazją do powtórzenia niektórych zagadnień, z którymi zetknęliśmy się w poprzednich przykładach. Aplikacja ta posłuży jako szkielet dla wszystkich przyszłych aplikacji. Została stworzona po wielu iteracjach. Jej celem jest wyodrębnienie do klas abstrakcyjnych jak największej liczby elementów aplikacji, które wkrótce będziemy tworzyć, aby uniknąć konieczności ciągłego pisania tego samego rodzaju kodu, różniącego się jedynie szczegółami. Jej cechy są następujące:
- komunikacja asynchroniczna z serwerem WWW / jSON odbywa się za pomocą biblioteki RxJava;
- cykl życia fragmentu (aktualizacja, zapis, przywrócenie) jest zarządzany przez jego klasę nadrzędną [AbstractFragment], która w określonych momentach wywołuje określone metody swoich klas podrzędnych. Klasa podrzędna nie musi zatem zajmować się etapami cyklu życia, a jedynie zaimplementować określone metody narzucone przez klasę nadrzędną;
- cykl życia aktywności (zapis / przywrócenie) jest zarządzany przez klasę abstrakcyjną [AbstractActivity], która również nakłada na aktywność potomną obowiązek zaimplementowania określonych metod;
- klasa [AbstractActivity] może obsługiwać aplikację z zakładkami lub bez, z obrazem oczekiwania lub bez, z podstawowym uwierzytelnianiem na serwerze WWW / jSON lub bez. Obecność lub brak tych elementów ustala się poprzez konfigurację;
Ten szkielet został wykorzystany we wszystkich kolejnych przykładach. Ze względu na ich różnorodność to, co sprawdzało się w jednym przykładzie, mogło nie działać w następnym. Ponieważ szkielet ten został wykorzystany łącznie w siedmiu przykładach, miało miejsce wiele iteracji. Gdybyśmy wykorzystali go w ósmym przykładzie, możliwe, że ponownie okazałoby się, iż specyfika tego nowego przykładu generuje nowe błędy. Niemniej jednak użycie tego szkieletu znacznie uprości pisanie kolejnych przykładów. W rzeczywistości zarządzanie cyklem życia fragmentu (aktualizacja, zapis, przywrócenie) w połączeniu z pojęciem sąsiedztwa fragmentów jest szczególnie złożone. W tym przypadku jest ono całkowicie ukryte w klasie [AbstractFragment].
2.1. Architektura klienta na Androida
Proponowany klient na Androida opiera się na następującej architekturze:
![]() |
- warstwa [DAO] implementuje interfejs [IDao]. To właśnie ona komunikuje się z serwerem WWW / jSON;
- istnieje tylko jedna aktywność, która również implementuje interfejs [IDao]. Widoki zwracają się do niej, aby uzyskać dostęp do serwera;
- widoki są realizowane przez fragmenty;
Projekt na Androida odzwierciedla tę architekturę:
![]() |
Przedstawimy po kolei poszczególne elementy tego projektu.
2.2. Konfiguracja Gradle
![]() |
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
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 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 'io.reactivex:rxandroid:1.2.0'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
- wszystkie numery wersji mogą ulec zmianie. Można jednak wyjść od aktualnych numerów, jeśli skonfiguruje się Android Studio tak, aby te wersje narzędzi Androida (wiersze 15–16, 47–48) były rzeczywiście zainstalowane (patrz punkt 6.11);
2.3. Manifest aplikacji
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="client.android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity_"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- wiersz 3: zmienimy pakiet aplikacji;
- wiersze 10, 15: ustalamy wartość elementu [app_name] w pliku [res / values / strings.xml]. Na razie wygląda to następująco:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- nazwa aplikacji -->
<string name="app_name">[Donnez un nom à votre application]</string>
</resources>
2.4. Struktura kodu Java
![]() |
- [architecture] zawiera główne elementy struktury kodu;
- [activity] zawiera pojedynczą aktywność aplikacji;
- [fragments] obejmuje fragmenty lub widoki aplikacji;
- [dao] grupuje elementy komunikacji z serwerem WWW / jSON;
2.5. Elementy aktywności
![]() | ![]() |

2.5.1. Widok powiązany z działaniem
Widok [activity_main.xml] powiązany z działaniem 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>
<!-- kontener fragmentów -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
- wiersz 29: używany jest specjalny kontener fragmentów;
Czynność posiada również menu [res / menu / menu_main.xml] dla swojego widoku:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
</menu>
Na razie jest ono puste. Programista uzupełni je w razie potrzeby.
2.5.2. Kontener fragmentów [MyPager]
![]() |
package client.android.architecture;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// kontroluje przesuwanie
private boolean isSwipeEnabled;
// steruje przewijaniem
private boolean isScrollingEnabled;
// konstruktory
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// metody do przedefiniowania w celu obsługi przesuwania
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// czy przesunięcie jest dozwolone?
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;
}
}
// kontrola przewijania
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// metody ustawiające
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
}
Ta klasa rozszerza standardową klasę Androida [ViewPager] wyłącznie w celu obsługi przesuwania (wiersz 11) i przewijania (wiersz 13) między widokami.
- wiersze 26–43: metody, które blokują przesuwanie, jeśli zostało ono wyłączone;
- wiersze 46–49: redefinicja metody [setCurrentItem] służącej do zmiany wyświetlanego widoku. Jeśli przewijanie zostało zablokowane, zmiana widoku nastąpi bez przewijania. Należy zauważyć, że programista może ominąć ten sposób działania, korzystając z metody [setCurrentItem(int position, boolean smoothScrolling)], która pozwala mu określić pożądany sposób przewijania;
2.5.3. Klasa [CoreState]
![]() |
Klasa [CoreState] jest klasą nadrzędną stanów poszczególnych fragmentów:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// zadanie: dodać tutaj podklasy klasy [CoreState]
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragment odwiedzony lub nie
protected boolean hasBeenVisited = false;
// stan ewentualnego menu fragmentu
protected MenuItemState[] menuOptionsState;
// metody pobierające i ustawiające
...
}
- wiersz 16: każdy fragment posiada w swoim stanie zmienną logiczną [hasBeenVisited], która określa, czy został już odwiedzony, czy nie. Jest to konieczne, ponieważ czasami podczas pierwszego wyświetlenia fragmentu należy wykonać pewne specjalne czynności;
- wiersz 18: projekt [client-android-skel] automatycznie zapisuje i przywraca menu fragmentów, jeśli takie posiadają. W tablicy MenuItemState[] menuOptionsState przechowywany jest stan widoczności wszystkich opcji menu;
- wiersze 10–13: podobnie jak w przypadku [Exemple-22], stan aktywności i jej fragmentów zostanie zapisany w sesji, która z kolei zostanie zapisana w postaci ciągu znaków jSON. Zobaczymy, że sesja przechowuje tablicę elementów typu [CoreState]. Jeśli nie podejmiemy żadnych działań, zapisany zostanie ciąg jSON typu [CoreState]. My jednak chcemy zapisać stany fragmentów, czyli stany pochodne od typu [CoreState]. Aby wygenerowano ciąg jSON typu pochodnego, a nie typu nadrzędnego, należy zadeklarować typy pochodne zgodnie z instrukcjami w wierszach 10–13. Klasa [CoreState] jest jedną z klas architektury, którą programista musi modyfikować dla każdej nowej aplikacji (wiersze 10–13);
2.5.4. Interfejs [IMainActivity]
![]() |
Interfejs [IMainActivity] określa, o co fragmenty mogą prosić aktywność w następującej architekturze:

package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// dostęp do sesji
ISession getSession();
// zmiana widoku
void navigateToView(int position, ISession.Action action);
// obsługa oczekiwania
void beginWaiting();
void cancelWaiting();
// stałe aplikacji (do zmodyfikowania) -------------------------------------
// tryb debugowania
boolean IS_DEBUG_ENABLED = true;
// maksymalny czas oczekiwania na odpowiedź serwera
int TIMEOUT = 1000;
// czas oczekiwania przed wykonaniem żądania klienta
int DELAY = 0;
// uwierzytelnianie podstawowe
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// sąsiedztwo fragmentów
int OFF_SCREEN_PAGE_LIMIT = 1;
// pasek kart
boolean ARE_TABS_NEEDED = false;
// obraz oczekiwania
boolean IS_WAITING_ICON_NEEDED = false;
// liczba fragmentów aplikacji
int FRAGMENTS_COUNT = 0;
// zadania – dodaj tutaj swoje stałe i inne metody
}
- wiersz 6: interfejs [IMainActivity] rozszerza interfejs [IDao] warstwy [DAO];
- wiersz 9: jest to aktywność, która zapewnia dostęp do sesji w postaci instancji interfejsu [ISession];
- wiersz 12: za pomocą tej aktywności zmienia się widok. Drugim parametrem jest akcja powodująca tę zmianę widoku, jedna z wartości: SUBMIT, NAVIGATION, RESTORE;
- wiersze 15–17: to aktywność, która zarządza obrazem oczekiwania;
- wiersz 22: służy do debugowania aplikacji;
- wiersz 25: aby nie czekać zbyt długo, jeśli serwer przestanie odpowiadać;
- wiersz 28: podczas debugowania należy ustawić wartość wynoszącą kilka sekund, aby mieć czas na anulowanie operacji z serwerem i sprawdzenie, co się dzieje;
- wiersz 31: na true, jeśli usługa jSON wymaga uwierzytelnienia podstawowego;
- wiersz 34: sąsiedztwo fragmentów;
- wiersz 37: na vrai, jeśli aplikacja posiada karty;
- wiersz 39: do vrai, jeśli aplikacja komunikuje się z serwerem WWW / jSON i chcemy wyświetlić obrazek oczekiwania podczas wymiany danych;
- wiersz 43: liczba fragmentów obsługiwanych przez aplikację;
Interfejs [IMainActivity] jest drugim elementem architektury, który programista musi uzupełnić (wiersz 45).
2.5.5. Interfejs [IDao]
Interfejs [IMainActivity] rozszerza następujący interfejs [IDao]:
![]() |
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// adres URL serwisu internetowego
void setUrlServiceWebJson(String url);
// użytkownik
void setUser(String user, String mdp);
// limit czasu klienta
void setTimeout(int timeout);
// uwierzytelnianie podstawowe
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// tryb debugowania
void setDebugMode(boolean isDebugEnabled);
// czas oczekiwania klienta przed wysłaniem żądania w milisekundach
void setDelay(int delay);
// zadanie: zadeklaruj tutaj swój interfejs
}
- wiersz 24: programista uzupełni tutaj interfejs;
2.5.6. Sesja
![]() |
Klasa [Session] zawiera elementy wspólne dla aktywności i fragmentów. Implementuje ona następujący interfejs [ISession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
public interface ISession {
// numer ostatnio wyświetlonego widoku
int getPreviousView();
void setPreviousView(int numView);
// ostatni stan widoku
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// bieżąca akcja
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// stany wszystkich widoków –
// nie jest używany przez kod, ale jest niezbędny do serializacji / deserializacji jSON
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// numer ostatniej wybranej karty
int getPreviousTab();
void setPreviousTab(int position);
// nawigacja po wybranej zakładce
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
Wprowadzamy interfejs [ISession], aby wymusić obecność określonych metod w sesji:
- wiersze 7–10: numer ostatniego wyświetlonego widoku (fragmentu);
- wiersze 12–15: stan konkretnego widoku;
- wiersze 17–24: wprowadzamy pojęcie trwającej akcji. Są cztery (wiersz 17):
- RESTORE: trwa zapisywanie/przywracanie. Nie następuje zmiana widoku;
- NAVIGATION: trwa nawigacja. Terminem „nawigacja” określimy tutaj zmianę widoku, w której nowy widok można przywrócić na podstawie jego ostatniego stanu zapisanego w sesji;
- SUBMIT: typ [SUBMIT] przypisuje się do trwającej akcji, gdy następuje zmiana widoku, a nowy widok zależy od ogólnego stanu aktywności, a nie wyłącznie od swojego własnego stanu. Czasami trudno jest odróżnić typy NAVIGATION od SUBMIT. W takim przypadku przyjmuje się bardziej ogólny przypadek typu SUBMIT;
- NONE: wartość akcji, gdy nie otrzymała ona jeszcze swojej pierwszej wartości;
- wiersze 26–30: stany aktywności i fragmentów będą zapisywane w tablicy typu CoreState[]. Aby tablica ta była poprawnie obsługiwana podczas serializacji i deserializacji jSON, musi posiadać metody getter i setter;
- wiersze 32–35: numer ostatniej wybranej karty. Jest wykorzystywany podczas cyklu zapisywania/przywracania w celu ponownego wybrania karty, która była zaznaczona przed obróceniem urządzenia;
- wiersze 37–40: obsługa wartości logicznej wskazującej, czy wyborowi zakładki powinna towarzyszyć zmiana fragmentu;
Interfejs [ISession] jest zaimplementowany przez następującą klasę abstrakcyjną [AbstractSession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class AbstractSession implements ISession {
// numer poprzedniego widoku
private int preViousView;
// stan widoków
private CoreState[] coreStates = new CoreState[0];
// bieżąca czynność
private Action action = Action.NONE;
// poprzednio wybrana karta
private int previousTab;
// nawigacja po wybranych kartach
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// konstruktor
public AbstractSession() {
// inicjowanie tablicy stanów fragmentów
coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
for (int i = 0; i < coreStates.length; i++) {
coreStates[i] = new CoreState();
}
}
// interfejs ISession ---------------------------------------------------------
@Override
public int getPreviousView() {
return preViousView;
}
@Override
public void setPreviousView(int numView) {
this.preViousView = numView;
}
@Override
public CoreState getCoreState(int numView) {
return coreStates[numView];
}
@Override
public void setCoreState(int numView, CoreState coreState) {
coreStates[numView] = coreState;
}
@Override
public Action getAction() {
return action;
}
@Override
public void setAction(Action action) {
this.action = action;
}
@Override
public CoreState[] getCoreStates() {
return coreStates;
}
@Override
public void setCoreStates(CoreState[] coreStates) {
this.coreStates = coreStates;
}
@Override
public int getPreviousTab() {
return previousTab;
}
@Override
public void setPreviousTab(int position) {
this.previousTab = position;
}
@Override
public boolean isNavigationOnTabSelectionNeeded() {
return navigationOnTabSelectionNeeded;
}
@Override
public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
}
}
- wiersz 9: numer widoku, który był wyświetlany przed aktualnie wyświetlanym. Informacja ta jest przydatna, gdy do danego widoku można przejść z kilku miejsc. Typowym przykładem jest nawigacja za pomocą zakładek. Wyświetlany widok może wówczas rozpoznać, jaki był poprzedni widok;
- wiersz 12: tabela stanów wszystkich fragmentów wyświetlanych przez aktywność;
- wiersz 18: numer wcześniej zaznaczonej zakładki. Pełni rolę analogiczną do numeru poprzedniego widoku z wiersza 9. Informacja ta jest przydatna w przypadku obrotu urządzenia, gdy konieczne jest powrót do zakładki, która była zaznaczona przed obrotem;
- wiersz 22: wartość logiczna wskazująca, czy zaznaczenie zakładki powinno wiązać się ze zmianą wyświetlanego fragmentu. Należy pamiętać, że projekt [client-android-skel] oddzielnie zarządza zakładkami i fragmentami, aby można go było wykorzystać w sytuacjach, gdy liczba zakładek jest mniejsza niż liczba fragmentów. Istnieją dwa rodzaje zaznaczeń:
- wybór dokonywany przez użytkownika poprzez kliknięcie zakładki. W tym przypadku zazwyczaj wyświetlany fragment powinien ulec zmianie;
- wybór programowy za pomocą metody [Tablayout.Tab.select()]. W tym przypadku zmiana wyświetlanego fragmentu nie zawsze jest pożądana. Oto dwa przykłady:
- podczas obracania urządzenia aktywność jest odtwarzana, podobnie jak zakładki. Jednak gdy tworzona jest pierwsza zakładka, automatycznie poddawana jest operacji programowej [select]. W takiej sytuacji zmiana wyświetlanego fragmentu nie jest pożądana, ponieważ trwa proces ponownego tworzenia aktywności, w którym ostatecznie wyświetlany fragment niekoniecznie będzie tym powiązanym z pierwszą zakładką;
- ponieważ zarządzanie zakładkami jest oddzielone od zarządzania fragmentami, może zaistnieć potrzeba aktualizacji zakładek (usuwanie, dodawanie) bez ingerowania w powiązane z nimi fragmenty. Jednak niektóre z tych operacji mogą ponownie wywołać domyślną operację programową [select] na jednej z zakładek. Wybór ten nie musi zatem oznaczać przejścia do powiązanego fragmentu;
- wiersz 21: pole [navigationOnTabSelectionNeeded] nie jest przeznaczone do zapisywania podczas operacji zapisywania aktywności i jej fragmentów. Adnotacja [@JsonIgnore] powoduje, że pole to jest pomijane podczas serializacji/deserializacji jSON;
- wiersze 25–31: konstruktor inicjuje tablicę stanów fragmentów aplikacji [FRAGMENTS_COUNT]. Elementy tej tablicy są inicjowane za pomocą pola [hasBeeenVisited=false]. Informacja ta służy do ustalenia, czy mamy do czynienia z pierwszą wizytą w danym fragmencie;
Klasa [Session] ma następującą postać:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// dane do współdzielenia między samymi fragmentami oraz między fragmentami a aktywnością
// elementy, których nie można zserializować w jSON, muszą posiadać adnotację @JsonIgnore
// nie zapomnij o metodach pobierających i ustawiających niezbędnych do serializacji / deserializacji w formacie jSON
}
- wiersz 5: klasa [Session] rozszerza klasę [AbstractSession], którą właśnie omówiliśmy. Programista umieści w niej elementy, które mają być współdzielone między samymi fragmentami oraz między fragmentami a aktywnością. Należy zauważyć, że klasa [Session] nie jest już opatrzona adnotacją AA [@EBean]. Stała się ona zwykłą klasą;
2.5.7. Klasa abstrakcyjna [AbstractActivity]
![]() |
2.5.7.1. Squelette
Klasa [AbstractActivity] liczy ponad 300 wierszy. Przeanalizujemy ją krok po kroku. Jej szkielet wygląda następująco:
package client.android.architecture;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// warstwa [DAO]
private IDao dao;
// sesja
protected Session session;
// kontener fragmentów
protected MyPager mViewPager;
// pasek narzędzi
private Toolbar toolbar;
// obraz oczekiwania
private ProgressBar loadingPanel;
// pasek kart
protected TabLayout tabLayout;
// menedżer fragmentów lub sekcji
private FragmentPagerAdapter mSectionsPagerAdapter;
// nazwa klasy
protected String className;
// mapper jSON
private ObjectMapper jsonMapper;
// konstruktor
public AbstractActivity() {
// nazwa klasy
className = getClass().getSimpleName();
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructeur");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// implementacja IMainActivity --------------------------------------------------------------------
...
// cykl życia – tworzenie kopii zapasowej / przywracanie aktywności ------------------------------------
...
// zarządzanie obrazem oczekiwania ---------------------------------
...
// interfejs IDao -----------------------------------------------------
...
// menedżer fragmentów --------------------------------
...
// klasy potomne
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
}
Klasa [AbstractActivity]:
- implementuje interfejs [IMainActivity] (wiersze 21, 55);
- zarządza zapisywaniem i przywracaniem aktywności oraz jej fragmentów podczas obracania urządzenia (wiersz 58);
- zarządza obrazem oczekiwania podczas wymiany danych z serwerem WWW / jSON (wiersz 61);
- implementuje interfejs IDao warstwy [DAO] (wiersz 64);
- implementuje menedżera fragmentów (wiersz 67);
- wymaga od swoich klas potomnych obecności sześciu metod (wiersze 71–81);
2.5.7.2. Implementacja interfejsu [IMainActivity]
Implementacja interfejsu [IMainActivity] (patrz punkt 2.5.4) wygląda następująco:
// implementacja IMainActivity --------------------------------------------------------------------
@Override
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position, ISession.Action action) {
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
}
// wyświetlanie nowego fragmentu
mViewPager.setCurrentItem(position);
// odnotowuje się bieżącą akcję podczas tej zmiany widoku
session.setAction(action);
}
2.5.7.3. Zapis stanu aktywności i jej fragmentów
Stan aktywności i jej fragmentów znajduje się w całości w sesji. Należy zatem zapisać tę sesję. Powtarzamy tutaj to, co zostało zrobione w projekcie [Exemple-22] (patrz punkt 1.23):
// zarządzanie zapisywaniem i przywracaniem aktywności ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// element nadrzędny
super.onSaveInstanceState(outState);
// zapis sesji w postaci ciągu znaków jSON
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();
}
}
}
2.5.7.4. Przywracanie stanu aktywności i jej fragmentów
Chodzi o przywrócenie sesji. Postępujemy tak, jak pokazano w projekcie [Exemple-22]:
@Override
protected void onCreate(Bundle savedInstanceState) {
// nadrzędny
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
// czy jest coś do przywrócenia?
if (savedInstanceState != null) {
// przywrócenie 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();
}
}
} else {
// sesja
session = new Session();
}
...
- wiersze 10–26: jeśli parametr [Bundle savedInstanceState] w wierszu 2 nie jest równy null, wówczas sesja zostaje przywrócona (wiersze 12–17);
- wiersze 26–29: sytuacja, w której parametr [Bundle savedInstanceState] z wiersza 2 ma wartość null, odpowiada pierwszemu uruchomieniu aktywności. Wówczas tworzona jest pusta sesja;
2.5.7.5. Inicjalizacja warstwy [DAO]
@Override
protected void onCreate(Bundle savedInstanceState) {
// element nadrzędny
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// warstwa [DAO]
dao = getDao();
if (dao != null) {
// konfiguracja warstwy [DAO]
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// klasy potomne
protected abstract IDao getDao();
....
}
- wiersz 11: żądane jest odwołanie do warstwy [DAO] w aktywności potomnej (wiersz 21);
- wiersze 14–17: jeśli warstwa [DAO] istnieje, konfiguruje się ją na podstawie informacji zawartych w interfejsie [IMainActivity];
2.5.7.6. Inicjalizacja widoku powiązanego z aktywnością
Widok powiązany z aktywnością został przedstawiony w punkcie 2.5.1:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- kontener fragmentów -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
Widok ten jest inicjowany za pomocą następującego kodu:
@Override
protected void onCreate(Bundle savedInstanceState) {
// klasa nadrzędna
super.onCreate(savedInstanceState);
// dziennik
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// powiązany widok
setContentView(R.layout.activity_main);
// komponenty widoku ---------------------
// pasek narzędzi
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// obrazek ładowania?
if (IS_WAITING_ICON_NEEDED) {
// dodajemy obrazek oczekiwania
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// tworzenie ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// dodanie ProgressBar do paska narzędzi
toolbar.addView(loadingPanel);
}
...
- wiersz 11: widok XML [activity_main] jest powiązany z aktywnością;
- wiersze 14–15: pasek narzędzi jest zintegrowany i obsługiwany;
- wiersze 17–27: ewentualne dodanie obrazu oczekiwania: jeśli wartość logiczna [IS_WAITING_ICON_NEEDED] ma wartość „prawda” w interfejsie [IMainActivity];
- wiersz 23: utworzenie obrazu oczekiwania typu [ProgressBar], do którego odwołuje się pole [loadingPanel];
- wiersz 24: początkowo obraz ten jest ukryty;
- wiersz 26: obraz ten zostaje dodany do paska narzędzi;
2.5.7.7. Zarządzanie kartami
Interfejs [IMainActivity] może wymagać paska kart. Jest on dodawany i zarządzany w następujący sposób:
// pasek kart
protected TabLayout tabLayout;
...
// pasek kart?
if (ARE_TABS_NEEDED) {
// dodaje się pasek kart
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// brak nawigacji po zaznaczeniu do momentu wyświetlenia fragmentu
session.setNavigationOnTabSelectionNeeded(false);
// utworzenie paska zakładek
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// dodanie paska zakładek do paska aplikacji
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// menedżer zdarzeń paska zakładek
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// zaznaczono kartę
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
}
if (session.isNavigationOnTabSelectionNeeded()) {
// pozycja karty
int position = tab.getPosition();
// pamięć
session.setPreviousTab(position);
// czy wyświetlany jest powiązany fragment?
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// klasy potomne
protected abstract void navigateOnTabSelected(int position);
...
- wiersze 12–48: dodawanie i zarządzanie paskiem kart;
- wiersz 6: pasek kart jest dodawany, jeśli stała [ARE_TABS_NEEDED] ma wartość vrai w interfejsie [IMainActivity];
- wiersz 12: podczas tworzenia paska zakładek mogą wystąpić operacje [Tablayout.Tab.select], które są domyślne (nie są one wywoływane przez użytkownika). Ustawiamy wartość logiczną [session.navigationOnTabSelectionNeeded] na faux, aby zapobiec nawigacji podczas tych fałszywych wyborów. To programista będzie odpowiedzialny za wybranie fragmentu do wyświetlenia za pomocą metody [navigateToView]. Wartość logiczna [session.navigationOnTabSelectionNeeded] zostanie przywrócona do vrai po wyświetleniu tego fragmentu (patrz klasa AbstractFragment);
- wiersz 14: utworzenie paska zakładek, do którego odwołuje się pole [tabLayout]. Korzystamy z niestandardowego paska zakładek [CustomTabLayout], do którego jeszcze wrócimy;
- wiersz 15: ustalamy kolory tytułów zakładek. Znajdują się one w następującym pliku [res / color / tab_txt.xml]:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="#FFFF00" />
<item android:state_selected="false" android:color="#FFFFFF" />
</selector>
- wiersz (c): kolor tytułu zakładki, gdy jest ona zaznaczona;
- wiersz (d): kolor tytułu zakładki, gdy nie jest ona zaznaczona;
Plik ten można oczywiście edytować. Kody szesnastkowe kolorów można znaleźć na przykład tutaj.
- wiersze 17–18: dodanie tego paska zakładek do paska aplikacji w widoku XML [activity_main];
- wiersze 20–47: menedżer zdarzeń paska zakładek;
- wiersze 22–36: obsługiwane jest wyłącznie zdarzenie [onTabSelected]. Odpowiada ono kliknięciu na zakładkę [Tab tab] przekazaną jako parametr do metody lub operacji programowej [TabLayout.Tab.select];
- wiersz 30: pozycja zaznaczonej zakładki;
- wiersz 32: pozycja ta jest zapisywana w sesji;
- wiersz 34: teraz należy wyświetlić fragment powiązany z tą zakładką. Tylko klasa potomna (wiersz 52) może dokonać tego powiązania. Należy zauważyć, że nie powiązujemy paska zakładek z kontenerem fragmentów [mViewPager], jak to miało miejsce w niektórych analizowanych przykładach. W tym przypadku całkowicie oddzielamy zarządzanie paskiem zakładek od zarządzania fragmentami. Dlatego po kliknięciu zakładki konieczne jest wskazanie, który widok ma zostać wyświetlony;
- wiersz 28: rozróżniamy wybór zakładki z nawigacją lub bez. Zazwyczaj, gdy użytkownik klika kartę, oczekuje nawigacji, natomiast w przypadku wyboru programowego nie jest ona pożądana. To programista rozróżnia te dwa przypadki za pomocą elementu [session.navigationOnTabSelectionNeeded]. Gdy nawigacja nie ma miejsca, numer ostatnio wybranej karty nie jest zapisywany w sesji. Zadaniem programisty będzie to zrobić;
2.5.7.8. Menedżer zakładek [CustomTabLayout]
![]() |
Korzystamy z niestandardowego menedżera kart, aby móc wyświetlać tytuły kart przy użyciu różnych czcionek. Klasa [CustomTabLayout] wygląda następująco:
package client.android.architecture.custom;
import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class CustomTabLayout extends TabLayout {
private Typeface mTypeface;
public CustomTabLayout(Context context) {
super(context);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
}
@Override
public void addTab(Tab tab) {
super.addTab(tab);
ViewGroup mainView = (ViewGroup) getChildAt(0);
ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());
int tabChildCount = tabView.getChildCount();
for (int i = 0; i < tabChildCount; i++) {
View tabViewChild = tabView.getChildAt(i);
if (tabViewChild instanceof TextView) {
((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
}
}
}
}
- dostosowanie czcionki tytułów zakładek odbywa się w wierszach 30 i 44;
Plik [fonts] wygląda następująco:
![]() |
Źródła:
- kod klasy [CustomTabLayout] został znaleziony w plikach URL i [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
- czcionki zostały znalezione w klasach URL i [https://www.fontsquirrel.com/fonts/roboto];
2.5.7.9. Ostatnie inicjalizacje
@Override
protected void onCreate(Bundle savedInstanceState) {
// klasa nadrzędna
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// instancjonowanie menedżera 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 = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// blokuje się przesuwanie między fragmentami
mViewPager.setSwipeEnabled(false);
// sąsiedztwo fragmentów
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// wyświetla się pierwszy widok
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// przekazujemy kontrolę do aktywności potomnej
onCreateActivity();
}
...
// klasy potomne
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- wiersze 10–19: jest to kod często spotykany w analizowanych przykładach;
- wiersze 21–23: wyświetlenie pierwszego widoku. Z pewnością istnieje wiele sposobów rozróżnienia tego przypadku. W tym przypadku wykorzystaliśmy fakt, że dla pierwszego widoku wartość akcji powodującej zmianę widoku to NONE;
- wiersz 22: nie przyjmujemy żadnych założeń dotyczących pierwszego fragmentu do wyświetlenia. W naszych przykładach często był to fragment nr 0, ale nie zawsze (por. Przykład-22). Poprosimy więc aktywność potomną (wiersz 30) o podanie, jaki jest ten pierwszy widok;
- wiersz 25: wyodrębniliśmy tutaj wszystko, co było możliwe. Teraz klasa potomna musi przeprowadzić własne inicjalizacje (wiersz 29);
2.5.7.10. Obsługa obrazu oczekiwania
W klasie [AbstractActivity] obraz oczekiwania jest zarządzany za pomocą dwóch następujących metod:
// zarządzanie obrazem oczekiwania ---------------------------------
public void cancelWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.INVISIBLE);
}
}
public void beginWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.VISIBLE);
}
}
2.5.7.11. Implementacja interfejsu [IDao]
W klasie [AbstractActivity] interfejs [IDao] (patrz punkt 2.5.5) jest zaimplementowany w następujący sposób:
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// warstwa [DAO]
private IDao dao;
...
// interfejs IDao -----------------------------------------------------
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setUser(String user, String mdp) {
dao.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
dao.setBasicAuthentification(isBasicAuthentificationNeeded);
}
@Override
public void setDebugMode(boolean isDebugEnabled) {
dao.setDebugMode(isDebugEnabled);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
- wiersz 3: przypominamy, że wartość tego pola została dostarczona przez aktywność potomną w metodzie [onCreate];
2.5.7.12. Implementacja menedżera fragmentów
W klasie [AbstractActivity] menedżer fragmentów jest zaimplementowany w następujący sposób:
...
// menedżer fragmentów --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// konstruktor
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// fragmenty klasy potomnej
fragments = getFragments();
}
// musi zwrócić fragment o numerze pozycji
@Override
public AbstractFragment getItem(int position) {
// wyświetla się fragment
return fragments[position];
}
// zwraca liczbę fragmentów do obsługi
@Override
public int getCount() {
return fragments.length;
}
// zwraca tytuł fragmentu o pozycji
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// klasy potomne
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
...
}
- wiersz 5: tablica fragmentów powiązanych z aktywnością. Wszystkie fragmenty będą pochodzić z klasy [AbstractFragment];
- wiersze 8–12: konstruktor inicjujący tablicę fragmentów. Pobiera je z klasy potomnej aktywności (wiersz 35);
- wiersze 28–31: tytuły fragmentów mogą być wykorzystywane w aplikacji, w której liczba zakładek odpowiada liczbie fragmentów. W takim przypadku zakładce można nadać tytuł fragmentu. Tutaj tytuły te są pobierane z klasy potomnej (wiersz 37);
2.5.7.13. Metoda [onResume]
Metoda [onResume] jest uruchamiana tuż przed tym, jak widok powiązany z daną czynnością stanie się widoczny. Wykorzystuje się ją tutaj do wybrania zakładki po zapisaniu/przywróceniu:
@Override
public void onResume() {
// klasa nadrzędna
super.onResume();
if (IS_DEBUG_ENABLED) {
Log.d(className, "onResume");
}
// jeśli ma nastąpić przywrócenie, należy przywrócić ostatnią wybraną kartę
if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
tabLayout.getTabAt(session.getPreviousTab()).select();
}
}
- wiersz 10: wybór zakładki, która była zaznaczona przed procesem zapisywania/przywracania. Należy pamiętać, że w metodzie [onCreate], która w cyklu życia aktywności jest wykonywana przed metodą [onResume], nawigacja po wybraniu zakładki została zablokowana. W tym przypadku następuje więc wybór zakładki, ale nie dochodzi do zmiany fragmentu;
2.5.7.14. Résumé
Klasa abstrakcyjna [AbstractActivity] będzie klasą nadrzędną dla jedynej aktywności w aplikacji.
Aktywność potomna będzie musiała zaimplementować następujące sześć metod:
// klasy potomne
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
Aktywność potomna ma ponadto dostęp do następujących elementów chronionych swojej klasy nadrzędnej:
// sesja
protected ISession session;
// kontener fragmentów
protected MyPager mViewPager;
// pasek kart
protected CustomTabLayout tabLayout;
// nazwa klasy
protected String className;
2.5.8. Aktywność [MainActivity]
![]() |
Klasa [MainActivity] może nosić inną nazwę. Jej jedynym ograniczeniem jest implementacja interfejsu [IMainActivity]. Podstawowa klasa dostarczona w pakiecie wygląda następująco:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// warstwa [DAO]
@Bean(Dao.class)
protected IDao dao;
// sesja
private Session session;
// metody klasy nadrzędnej -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sesja
this.session = (Session) super.session;
// zadania do wykonania: kontynuujemy inicjalizacje rozpoczęte przez klasę nadrzędną
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// do zrobienia: zdefiniować tutaj fragmenty
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// do zrobienia: zdefiniować tutaj tytuły fragmentów
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// zadanie: nawigacja za pomocą kart – zdefiniować widok do wyświetlenia
}
@Override
protected int getFirstView() {
// do zrobienia: nawigacja za pomocą zakładek – zdefiniować pierwszy widok do wyświetlenia
return 0;
}
}
- wiersz 14: aby notacja AA [@Bean] z wiersza 19 była zrozumiała, aktywność musi mieć notację AA [@EActivity];
- wiersz 15: aktywność jest powiązana z menu o oznaczeniu XML [menu_main]. Obecnie menu to jest puste. Programista będzie musiał je uzupełnić, jeśli zajdzie taka potrzeba;
- wiersz 16: klasa ta dziedziczy po klasie [AbstractActivity];
- wiersze 19–20: odwołanie do warstwy [DAO]. Zostanie ona zainicjowana przez bibliotekę AA przed zainicjowaniem tego pola. Oznacza to, że musi istnieć bean AA [Dao]. Tak jest zawsze w przypadku dostarczanego przez nas szkieletu aplikacji. Nawet w aplikacji bez warstwy [DAO] można pozostawić pakiet [dao]. Nie powoduje to żadnych komplikacji;
- wiersz 22: sesja jako instancja typu [Session]. Sesja istnieje w klasie nadrzędnej [AbstractActivity], ale jako instancja interfejsu [ISession] (wiersz 32);
- wiersze 24–63: sześć metod narzuconych przez klasę nadrzędną [AbstractActivity];
- wiersze 36–39: metoda [getDao] zwraca odwołanie do warstwy [DAO]. W tym przypadku odwołanie to nigdy nie jest null. Natomiast w klasie nadrzędnej [AbstractActivity] przewidziano przypadek, w którym klasa podrzędna zwraca odwołanie null, aby wskazać, że nie istnieje warstwa [DAO]. Jeśli chcemy skorzystać z tej możliwości (moim zdaniem niezbyt przydatnej), to właśnie tutaj należy zwrócić wskaźnik null;
2.6. Warstwa [DAO]

![]() |
2.6.1. Interfejs IDao
Został on przedstawiony w punkcie 2.5.5:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Adres URL serwisu internetowego
void setUrlServiceWebJson(String url);
// użytkownik
void setUser(String user, String mdp);
// limit czasu klienta
void setTimeout(int timeout);
// uwierzytelnianie podstawowe
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// tryb debugowania
void setDebugMode(boolean isDebugEnabled);
// czas oczekiwania klienta przed wysłaniem żądania w milisekundach
void setDelay(int delay);
// zadanie: zadeklaruj tutaj swój interfejs
}
Programista doda metody swojej warstwy [DAO], zaczynając od linii 24.
2.6.2. Interfejs [WebClient]
![]() |
Interfejs [WebClient] wygląda następująco:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// do zrobienia: zadeklaruj tutaj wartości URL, które mają zostać osiągnięte
}
Programista doda metody komunikujące się z interfejsem URL udostępnionym przez serwer jSON, począwszy od linii 17.
2.6.3. Interceptor uwierzytelniania [MyAuthInterceptor]
![]() |
Klasa [MyAuthInterceptor] ma następujący wygląd:
package client.android.dao.service;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
// użytkownik
private String user;
// hasło
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// nagłówki HTTP przechwyconego żądania HTTP
HttpHeaders headers = request.getHeaders();
// nagłówek HTTP uwierzytelniania podstawowego
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
// dodanie do nagłówków HTTP
headers.setAuthorization(auth);
// kontynuacja cyklu życia żądania HTTP
return execution.execute(request, body);
}
// elementy uwierzytelniania
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
Klasa ta generuje następujący nagłówek uwierzytelniający HTTP:
gdzie [code] to kod Base64 ciągu znaków „user:mp”. Klasa ta ma zastosowanie tylko wtedy, gdy serwer jSON oczekuje tej formy uwierzytelniania. Istnieją również inne klasy.
Uwaga: sposób użycia tej klasy został zilustrowany w paragrafie 3.6.3.1.
2.6.4. Klasa [AbstractDao]
![]() |
Klasa [AbstractDao] ma następujący wygląd:
package client.android.dao.service;
import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// tryb debugowania
protected boolean isDebugEnabled;
// nazwa klasy
protected String className;
// opóźnienie oczekiwania przed wykonaniem żądania
private int delay;
// konstruktor
public AbstractDao() {
// nazwa klasy
className = getClass().getName();
Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
}
// metody chronione ----------------------------------------------------------
// interfejs generyczny
protected interface IRequest<T> {
T getResponse();
}
// generyczne żądanie do serwisu internetowego / jSON
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("delay=%s", delay));
}
// wykonanie usługi – oczekuje się pojedynczej odpowiedzi
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// wykonanie usługi
try {
// oczekiwanie?
if (delay > 0) {
Thread.sleep(delay);
}
// wykonywanie żądania synchronicznego
T response = request.getResponse();
// log
if (isDebugEnabled) {
String log;
if (response instanceof String) {
log = (String) response;
} else {
log = mapper.writeValueAsString(response);
}
Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
}
// wysyłamy odpowiedź do obserwatora
subscriber.onNext(response);
// zgłaszane jest zakończenie obserwowalnego
subscriber.onCompleted();
} catch (InterruptedException | JsonProcessingException | RuntimeException e) {
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
} catch (JsonProcessingException e1) {
Log.d(className, String.format("Erreur jSON imprévue"));
}
}
// wygenerowano wyjątek
subscriber.onError(new DaoException(e, 100));
}
}
});
}
// tryb debugowania
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
public void setDelay(int delay) {
this.delay = delay;
}
}
- wiersze 35–81: metoda [getResponse] wykorzystuje bibliotekę RxAndroid do renderowania typu [Observable<T>]. W przeciwieństwie do niektórych przykładów omówionych wcześniej, nie renderowany jest typ [Response<T>], który jest typem zastrzeżonym, lecz dowolny typ T;
- wiersz 35: metoda [getResponse] otrzymuje jako parametr instancję typu [IRequest<T>] z wierszy 30–32, z której metoda [IRequest.getReponse()] uzyskuje typ T poprzez synchroniczną operację HTTP;
- wiersze 48–50: sztucznie oczekuje się [delay] milisekund. W środowisku produkcyjnym należy ustawić [delay=0]. W fazie debugowania należy ustawić [delay=qqs secondes], aby dać użytkownikowi możliwość anulowania operacji asynchronicznej i tym samym sprawdzenia, jak zachowuje się wówczas kod;
- wiersz 52: oczekiwana odpowiedź jest żądana za pomocą zapytania synchronicznego;
- wiersz 64: po otrzymaniu odpowiedzi jest ona przekazywana do obserwatora;
- wiersz 66: wskazujemy, że nie będzie już żadnych emisji. Mamy tu do czynienia ze szczególnym przypadkiem akcji asynchronicznej, która zwraca tylko jeden element;
- wiersze 67–78: w przypadku wystąpienia wyjątku, wyjątek jest przekazywany do obserwatora (wiersz 77);
2.6.5. Klasa [Dao]
![]() |
Klasa [Dao] wygląda następująco:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// klient serwisu internetowego
@RestService
protected WebClient webClient;
// bezpieczeństwo
@Bean
protected MyAuthInterceptor authInterceptor;
// RestTemplate
private RestTemplate restTemplate;
// fabryka RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// dziennik
Log.d(className, "afterInject");
// buduje się restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// montuje się konwerter jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// ustala się restTemplate klienta internetowego
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// ustawiamy URL serwisu internetowego
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// rejestruje się użytkownika w intercepterze
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// konfiguracja fabryczna
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// przechwytywacz uwierzytelniania?
if (isBasicAuthentificationNeeded) {
// dodajemy interceptor uwierzytelniający
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// metody prywatne -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// do zrobienia: implementacja IDao
}
- wiersze 21–22: wstrzyknięcie beanów AA i [WebClient], które będą obsługiwać komunikację z serwerem WWW / jSON;
- wiersze 24–25: wstrzyknięcie interceptora uwierzytelniania;
- wiersze 31–42: metoda wykonywana po wstawieniu pól z wierszy 21–25;
- wiersz 37: obiekt [RestTemplate], który zapewnia komunikację między klientem a serwerem, jest tworzony na podstawie obiektu factory. Nie jest to konieczne, ale to właśnie za pomocą obiektu factory można skonfigurować limity czasu wymiany danych. Dlatego nie używamy konstruktora bez parametrów [RestTemplate()];
- wiersz 39: dodajemy konwerter jSON do konwerterów [RestTemplate]. Będzie to jedyny konwerter. Ponadto, gdy metoda klienta [WebClient] otrzyma ciąg znaków jSON z serwera, zostanie on automatycznie deserializowany do obiektu, który metoda ma zwrócić;
- wiersz 41: tak skonfigurowany obiekt [RestTemplate] jest przekazywany do klienta internetowego, który za jego pomocą zapewni komunikację między klientem a serwerem;
- wiersze 44–48: ustala się obiekt URL jako katalog główny serwera WWW / jSON. Wszystkie obiekty URL zadeklarowane w klasie [WebClient] są obiektami URL powiązanymi z tym obiektem głównym URL;
- wiersze 50–54: metoda ta pozwala określić właściciela połączenia, gdy jest ono kontrolowane przez autoryzację typu podstawowego (patrz paragraf 2.6.3);
- wiersze 56–64: określają timeouts wymiany danych między klientem a serwerem. Odbywa się to za pośrednictwem factory obiektu [RestTemplate], który reguluje wymianę danych;
- wiersze 66–78: ta metoda pozwala wskazać, że serwer jest chroniony uwierzytelnianiem typu podstawowego;
- wiersze 72–77: jeśli wymagane jest uwierzytelnianie typu podstawowego, intercepter uwierzytelniający wstawiony w wierszu 25 jest dodawany do intercepterów obiektu [RestTemplate]. Ten interceptor automatycznie doda do wszystkich żądań klienta internetowego wiersz HTTP zawierający podstawowe uwierzytelnienie oczekiwane przez serwer;
- programista zaimplementuje interfejs [IDao], począwszy od linii 87;
2.7. Fragmenty
![]() |
2.7.1. Klasa [MenuItemState]
Klasa [MenuItemState] zawiera stan opcji menu:
package client.android.architecture;
public class MenuItemState {
// identyfikator opcji menu
private int menuItemId;
// widoczność opcji
private boolean isVisible;
// konstruktory
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// metody pobierające i ustawiające
...
}
2.7.2. Klasa [Utils]
Klasa [Utils] zawiera statyczne metody pomocnicze:
package client.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// lista komunikatów dotyczących wyjątku – wersja 1
static public List<String> getMessagesFromException(Throwable ex) {
// tworzy się listę zawierającą komunikaty 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 dotyczących wyjątku – wersja 2
static public String getMessageForAlert(Throwable th) {
// tworzenie tekstu 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();
}
// lista komunikatów wyjątku – wersja 3
static public String getMessageForAlert(List<String> messages) {
// tworzenie tekstu do wyświetlenia
StringBuilder texte = new StringBuilder();
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// wynik
return texte.toString();
}
}
2.7.3. Klasa nadrzędna [AbstractFragment]
Klasa [AbstractFragment] zawiera elementy wspólne dla wszystkich fragmentów aplikacji. Podobnie jak w przypadku klasy [AbstractActivity], jej kod jest złożony. Również w tym przypadku przeanalizujemy go krok po kroku.
2.7.3.1. Szkielet
package client.android.architecture.core;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// dane prywatne ------------------------------------------------------------
// subskrypcje obserwowalnych
private List<Subscription> abonnements = new ArrayList<>();
// menu fragmentu
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// cykl życia fragmentu
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// stan fragmentu
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// cykl życia fragmentu
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// zadania asynchroniczne
private boolean runningTasksHaveBeenCanceled;
// dane dostępne dla klas potomnych ---------------------------------------
// tryb debugowania
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// nazwa klasy
protected String className;
// zadania asynchroniczne
protected int numberOfRunningTasks;
// aktywność
protected IMainActivity mainActivity;
protected Activity activity;
// sesja
protected Session session;
// aktualizacja fragmentu ----------------------------------------------------------------------------------
...
// zarządzanie menu ------------------------------------------
...
// zarządzanie kolejką -------------------------------------------------------------
...
// zarządzanie operacjami asynchronicznymi --------------------------------------------------------------------
...
// obsługa wyjątków -------------------------------------------------------------------
....
// zarządzanie cyklem życia fragmentu --------------------------------------------------------
...
// klasy potomne -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
}
- wiersze 28–45: dane prywatne klasy;
- wiersze 47–58: dane chronione, dostępne dla klas potomnych;
- wiersze 61–62: kod aktualizujący fragment, który ma zostać wyświetlony;
- wiersze 64–65: kod pomocniczy do obsługi ewentualnego menu;
- wiersze 67–68: kod pomocniczy do zarządzania oczekiwaniem podczas operacji asynchronicznej;
- wiersze 70–71: kod ułatwiający komunikację fragmentu z warstwą [DAO];
- wiersze 73–74: kod pomocniczy do standardowego obsługi wszystkich wyjątków;
- wiersze 76–77: kod zarządzający cyklem życia fragmentu;
- wiersze 80–94: klasa nadrzędna narzuca 8 metod swoim klasom potomnym;
2.7.3.2. Konstruktor
Konstruktor klasy ma następującą postać:
// nazwa klasy
protected String className;
// cykl życia fragmentu
private boolean fragmentHasToBeInitialized = false;
...
// konstruktor ----------------------
public AbstractFragment() {
// inicjalizacja
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// log
if (isDebugEnabled) {
Log.d(className, "constructeur");
}
}
- wiersz 9: odnotowuje się nazwę klasy potomnej, która jest tutaj instancjonowana. Nazwa ta jest używana we wszystkich logach klasy nadrzędnej;
- wiersz 10: odnotowuje się, że fragment jest tworzony. Informacja ta zostanie wykorzystana, gdy fragment potomny zostanie poproszony o aktualizację;
2.7.3.3. Zarządzanie menu
W naszej architekturze każdy fragment musi posiadać menu, nawet jeśli jest puste. Logi wykazały bowiem, że gdy uruchamia się metoda [onCreateOptionsMenu] – co ma miejsce, gdy fragment posiada menu – fragment ten został już powiązany ze swoją aktywnością, widokiem i menu i wkrótce stanie się widoczny. Jest to zatem moment, w którym można dokonać aktualizacji interfejsu wizualnego i menu. To właśnie w tej metodzie [onCreateOptionsMenu] żądamy od fragmentu potomnego, aby się zaktualizował.
Zarządzanie menu obejmuje metody pomocnicze, które umożliwiają fragmentowi potomnemu wyświetlanie lub ukrywanie elementów menu:
// menu fragmentu
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// zarządzanie menu ------------------------------------------
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);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// wynik
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// pobieramy identyfikatory opcji menu
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// przenosimy opcje menu do tablicy
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// identyfikator opcji
int id = menuOptionsIds.get(i);
// stan opcji
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// wynik
if (isDebugEnabled) {
Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
}
}
// stany opcji menu
private MenuItemState[] getMenuOptionsStates() {
MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
for (int i = 0; i < menuOptionsStates.length; i++) {
// stan
MenuItemState state = this.menuOptionsStates[i];
// identyfikator menu
int id = state.getMenuItemId();
// inicjalizacja stanu
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// wynik
return menuOptionsStates;
}
// wyświetlanie opcji menu -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// aktualizujemy wszystkie opcje menu
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// aktualizujemy niektóre opcje menu
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
- wiersze 6–18: ta metoda pozwala uzyskać identyfikatory numeryczne wszystkich opcji menu;
- wiersz 6: metoda [getMenuOptions] przyjmuje dwa parametry:
- [Menu menu]: menu fragmentu;
- [List<Integer> menuOptionsIds]: lista identyfikatorów Android opcji menu. Początkowo lista ta jest pusta. Następnie jest wypełniana poprzez rekurencyjne przeszukiwanie (wiersz 15) drzewa menu;
- wiersze 20–40: na podstawie menu tworzy tablicę stanów (identyfikator, widoczność) opcji menu. Tablica ta jest zapisywana w wierszu 3. Klasa [MenuItemState] została opisana w paragrafie 2.7.1;
- wiersze 43–55: wariant poprzedniej metody. Wykonuje to samo zadanie, ale zamiast ponownie obliczać identyfikatory wszystkich opcji menu – co zostało już zrobione – wykorzystuje identyfikatory z tablicy stanów z wiersza 3;
- wiersze 58–63: metoda [setAllMenuOptionsStates] pozwala ukryć lub wyświetlić wszystkie opcje menu fragmentu;
- wiersze 65–69: metoda [setMenuOptionsStates] umożliwia selektywne wyświetlanie lub ukrywanie niektórych opcji menu;
- metody [getMenuOptions, getMenuOptionsStates] są zadeklarowane jako prywatne, ponieważ są używane wyłącznie w metodzie [AbstractFragment]. Metody [setAllMenuOptionsStates] (wiersz 58) i [setMenuOptionsStates] (wiersz 65) są zadeklarowane jako chronione, aby były dostępne dla klas potomnych;
2.7.3.4. Obsługa oczekiwania na zakończenie zadania asynchronicznego
// subskrypcje obserwowalnych
private List<Subscription> abonnements = new ArrayList<>();
// zadania asynchroniczne
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// zarządzanie oczekiwaniem na zakończenie operacji asynchronicznej -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// odnotowuje się liczbę zadań, które zostaną wykonane
this.numberOfRunningTasks = numberOfRunningTasks;
// ustawiamy obraz oczekiwania
mainActivity.beginWaiting();
// czyści się listę subskrypcji
abonnements.clear();
// jeszcze nie ma anulowania
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// ukrywa się obrazek oczekiwania
mainActivity.cancelWaiting();
}
- wiersze 9–18: aby uruchomić jedną lub więcej operacji asynchronicznych, fragment potomny wywoła metodę nadrzędną [beginRunningTasks]. Parametrem tej metody jest liczba zadań asynchronicznych, które fragment potomny zamierza uruchomić;
- wiersz 11: zapisywany jest parametr metody;
- wiersz 13: obraz oczekiwania zostaje wyświetlony;
- wiersz 15: czyści się listę subskrypcji operacji asynchronicznych. Nie zostały one jeszcze utworzone przez fragment potomny;
- wiersz 17: utrzymywana jest zmienna logiczna sygnalizująca, że zadania asynchroniczne żądane przez fragment potomny zostały anulowane. Początkowo zmienna ta ma wartość false;
- wiersze 20–25: fragment potomny wywołuje metodę nadrzędną [cancelWaitingTasks], aby wskazać, że chce anulować zadania, które uruchomił;
- wiersz 22: obraz oczekiwania jest ukryty;
2.7.3.5. Obsługa wyjątków
// obsługa wyjątku -------------------------------------------------------------------
// wyświetlanie alertu o wyjątku
protected void showAlert(Throwable th) {
// wyświetlanie komunikatów ze stosu wyjątków klasy Throwable th
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
}
// wyświetlanie listy komunikatów
protected void showAlert(List<String> messages) {
// wyświetla listę komunikatów
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
- wiersze 4–7: metoda [showAlert(Throwable)] pozwala fragmentowi potomnemu wyświetlić w oknie komunikaty ze stosu wyjątków metody Throwable przekazanej jako parametr;
- wiersze 10–13: metoda [showAlert(List<String>] umożliwia fragmentowi potomnemu wyświetlenie w oknie listy komunikatów przekazanej jako parametr;
- klasa [Utils] użyta w wierszach 6 i 12 została opisana w paragrafie 2.7.2;
2.7.3.6. Obsługa operacji asynchronicznych
...
// subskrypcje obserwowalnych
private List<Subscription> abonnements = new ArrayList<>();
// zadania asynchroniczne
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// wykonanie zadania asynchronicznego za pomocą RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// proces: obserwowalna do wykonania / obserwacji
// consumeResult: metoda przetwarzająca otrzymaną odpowiedź
//
// nowe subskrypcje tworzy się tylko wtedy, gdy nie doszło do anulowania
if (!runningTasksHaveBeenCanceled) {
// wykonanie w wątku wejścia/wyjścia i obserwacja w wątku interfejsu użytkownika
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// uruchamia się obserwowalną
try {
abonnements.add(process.subscribe(
// odczyt wyniku
consumeResult,
// obsługa wyjątku
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
consumeThrowable(th);
}
},
// zakończenie zadania
new Action0() {
@Override
public void call() {
endOfTask();
}
}));
} catch (Throwable th) {
consumeThrowable(th);
}
}
}
private void endOfTask() {
...
}
// operacja asynchroniczna wygenerowała wyjątek
// lub wystąpił wyjątek podczas wykonywania operacji asynchronicznej
private void consumeThrowable(Throwable th) {
...
}
- wiersze 9–41: wykonują zadanie asynchroniczne;
- wiersz 9: metoda [executeInBackground] oczekuje dwóch parametrów:
- [Observable<T> process]: proces asynchroniczny do wykonania;
- [Action1<T> consumeResult]: metoda fragmentu potomnego, którą należy wywołać w celu przekazania jej elementów wygenerowanych przez proces. W naszych poprzednich przykładach procesy zawsze generowały tylko jeden element. Typ T w [Action1<T>] to typ T wyniku zwracanego przez obserwowany proces;
- wiersz 14: zadanie asynchroniczne uruchamia się tylko wtedy, gdy nie nastąpiło jeszcze anulowanie przez użytkownika lub przez program (z powodu wyjątku);
- wiersz 16: proces jest skonfigurowany do działania w wątku wejścia/wyjścia i obserwowany w wątku interfejsu użytkownika;
- wiersz 16: instrukcja [process.subscribe] uruchamia proces w wątku wejścia/wyjścia. W tym wątku operacje przebiegają synchronicznie, ponieważ korzystamy z biblioteki HTTP, która jest synchroniczna;
- wiersz 19: metoda [process.subscribe] ma trzy parametry:
- wiersz 21: [consumeResult]: metoda fragmentu potomnego, która będzie przetwarzać elementy wysyłane przez proces;
- wiersze 22–28: metoda wykonywana w przypadku wystąpienia wyjątku podczas przetwarzania zadania asynchronicznego. Przetwarzanie jest przekazywane do metody [consumeThrowable] z wiersza 49;
- wiersze 29–36: metoda wykonywana, gdy zadanie wysyła powiadomienie o zakończeniu wysyłania. Obsługa jest przekazywana do metody [endOfTask] z wiersza 43;
- wiersz 19: właśnie uruchomione zadanie asynchroniczne jest rejestrowane w polu [abonnements], które przechowuje wszystkie uruchomione zadania asynchroniczne. Umożliwi to ich anulowanie w razie potrzeby;
- wiersze 37–39: metoda wykonywana w przypadku wystąpienia wyjątku podczas przetwarzania zadania asynchronicznego. Przetwarzanie jest przekazywane do metody [consumeThrowable] w wierszu 49;
Metoda [endOfTask] ma następującą postać:
// zadania asynchroniczne
protected int numberOfRunningTasks;
...
private void endOfTask() {
// jedno zadanie mniej do oczekiwania
numberOfRunningTasks--;
// skończone?
if (numberOfRunningTasks == 0) {
// koniec oczekiwania
cancelWaitingTasks();
// zgłaszamy zakończenie zadań do klasy potomnej
notifyEndOfTasks(false);
}
}
...
// klasy potomne -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- wiersz 6: zadanie asynchroniczne właśnie się zakończyło. Zmniejsza się licznik aktywnych zadań;
- wiersz 8: jeśli nie ma już żadnych aktywnych zadań, oznacza to, że fragment potomny uzyskał wszystkie odpowiedzi;
- wiersz 10: anuluje się oczekiwanie;
- wiersz 12: informuje się fragment potomny, że wszystkie zadania, które uruchomił, zostały zakończone, wywołując jego metodę [notifyEndOfTasks]. Parametr tej metody wskazuje, w jaki sposób zadania zostały zakończone: normalnie, w wyniku anulowania przez użytkownika lub przez kod z powodu wystąpienia wyjątku. W wierszu 12 sygnalizuje się normalne zakończenie. Należy zauważyć, że fragment potomny nie musi martwić się o śledzenie liczby nadal aktywnych zadań. Robi to za niego jego klasa nadrzędna;
Metoda [consumeThrowable] wygląda następująco:
// zadania asynchroniczne
protected int numberOfRunningTasks;
private boolean runningTasksHaveBeenCanceled;
...
// operacja asynchroniczna wygenerowała wyjątek
// lub wystąpił wyjątek podczas wykonywania operacji asynchronicznej
private void consumeThrowable(Throwable th) {
// th: wyjątek do przetworzenia
//
// log
if (isDebugEnabled) {
Log.d(className, "Exception reçue");
}
// anulowane zostają już uruchomione zadania
cancelRunningTasks();
// wyświetla się komunikaty o błędach
showAlert(th);
}
// anulowanie zadań
protected void cancelRunningTasks() {
// log
if (isDebugEnabled) {
Log.d(className, "Annulation des tâches lancées");
}
// anulowanie wszystkich zarejestrowanych zadań asynchronicznych
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
// odnotowuje się anulowanie
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// koniec oczekiwania
cancelWaitingTasks();
// zgłaszamy anulowanie zadań do fragmentu potomnego
notifyEndOfTasks(true);
}
...
// klasy potomne -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- wiersz 3: metoda [consumeThrowable] odbiera wystąpiony wyjątek;
- wiersz 15: wszystkie nadal aktywne zadania są anulowane;
- wiersz 17: wyświetlany jest tekst wyjątku;
- wiersze 21–37: anulowanie wszystkich zadań;
- wiersze 27–29: wszystkie subskrypcje są anulowane;
- wiersz 31: odnotowuje się, że nastąpiło anulowanie;
- wiersz 32: licznik zadań jest zerowany;
- wiersz 34: oczekiwanie zostaje anulowane;
- wiersz 36: poinformowanie fragmentu potomnego o zakończeniu zadań w wyniku anulowania;
2.7.3.7. Zarządzanie cyklem życia fragmentu
// cykl życia --------------------------------------------------------
@Override
public void onDestroyView() {
// klasa nadrzędna
super.onDestroyView();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// element nadrzędny
super.onDestroy();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroy");
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
private void saveState() {
...
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
...
}
- wiersze 2–20: metody [onDestroyView, onDestroy] służą wyłącznie do logowania. Pozwalają one programiście lepiej zrozumieć cykl życia fragmentów;
Zapis fragmentu podczas obracania urządzenia odbywa się za pomocą następujących metod [setUserVisibleHint, onSaveInstanceState, saveState]:
// cykl życia fragmentu
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// element nadrzędny
super.setUserVisibleHint(isVisibleToUser);
// zapisanie?
if (this.isVisibleToUser && !isVisibleToUser) {
// fragment zostanie ukryty – zapisujemy go
if (!saveFragmentDone) {
saveState();
}
}
// pamięć
this.isVisibleToUser = isVisibleToUser;
}
private void saveState() {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// dziennik
if (isDebugEnabled) {
Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
}
// nadrzędny
super.onSaveInstanceState(outState);
// zapis fragmentu tylko wtedy, gdy jest widoczny
if (isVisibleToUser) {
// być może fragment został już zapisany
if (!saveFragmentDone) {
saveState();
}
// przywrócenie należy wykonać w każdym przypadku
session.setAction(ISession.Action.RESTORE);
}
}
- wiersze 6–19: zapis fragmentu następuje, gdy przechodzi on ze stanu wyświetlonego do stanu ukrytego (wiersz 11). Informację tę dostarcza metoda [setUserVisibleHint];
- wiersz 14: zapis odbywa się za pomocą metody prywatnej z wierszy 21–23;
- wiersze 25–41: podczas obrotu urządzenia zostanie wywołana metoda [onSaveInstanceState]. Fragment jest zapisywany pod dwoma warunkami:
- jest widoczny (wiersz 34);
- nie został jeszcze zapisany (wiersz 36). Możliwe, że metody [setUserVisibleHint, onSaveInstanceState] nie będą mogły zostać wykonane obie jednocześnie, gdy fragment jest widoczny, a zatem zarządzanie zmienną logiczną [saveFragmentDone] jest zbędne. W razie wątpliwości zdecydowałem się jednak z niej skorzystać;
- wiersz 40: po zapisaniu nastąpi przywrócenie. Należy zauważyć, że następnym razem, gdy fragment będzie musiał się zaktualizować, będzie to musiało nastąpić w ramach operacji [RESTORE];
Należy zwrócić uwagę na dwa momenty, w których wymagane jest zapisanie fragmentu:
- gdy fragment przechodzi ze stanu widocznego do stanu ukrytego;
- gdy następuje obrót urządzenia;
Prywatna metoda [saveState] wygląda następująco:
...
private void saveState() {
// czy należy anulować zadania?
if (numberOfRunningTasks != 0) {
// anulujemy zadania
cancelRunningTasks();
}
// zapisujemy stan fragmentu
CoreState currentState = saveFragment();
// fragment został wyświetlony
currentState.setHasBeenVisited(true);
// zapisano stan menu
currentState.setMenuOptionsState(getMenuOptionsStates());
// zalogowano się
session.setCoreState(getNumView(), currentState);
// zapis zakończony
saveFragmentDone = true;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// klasy potomne -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
- wiersze 4–7: obrót urządzenia może nastąpić w trakcie wykonywania operacji asynchronicznych. W tym momencie podejmowana jest decyzja o anulowaniu ich wszystkich. Nie jest to dobra decyzja dla użytkownika, który będzie musiał ponownie wysłać nowe, potencjalnie długotrwałe żądanie, podczas gdy po prostu przesunął swój telefon lub tablet albo odebrał połączenie telefoniczne. Możliwe jest zachowanie połączeń sieciowych podczas cyklu tworzenia kopii zapasowej i przywracania. Rozwiązania nie są jednak oczywiste i postanowiłem nie poruszać ich w tym kursie dla początkujących. Najlepszym rozwiązaniem jest nawiązywanie tych połączeń sieciowych za pośrednictwem fragmentu bez przypisanej interfejsu graficznego, który nie jest niszczony podczas cyklu tworzenia kopii zapasowej i przywracania. W tym celu wystarczy użyć instrukcji [Fragment.setRetainInstance(true)];
- wiersz 9: żądamy od fragmentu potomnego zapisania swojego stanu w typie pochodnym od [CoreState] (wiersz 31);
- wiersz 11: odnotowuje się, że fragment został wywołany. Informacja ta jest przydatna. Gdy fragment jest wywoływany po raz pierwszy, jego aktualizacja może różnić się od kolejnych, ponieważ nie ma on wówczas poprzedniego stanu w sesji;
- wiersz 13: zapisujemy stan menu, co pozwoli nam na jego automatyczne przywrócenie;
- wiersz 15: bieżący stan jest zapisywany w sesji. W sesji stanach są pogrupowane według widoku / fragmentu, przy czym każdy z nich ma swój stan. Numer widoku jest podawany przez fragment potomny (wiersz 33);
- wiersz 17: odnotowujemy, że fragment został zapisany. Wynika to z faktu, że dwie metody mogą wywołać metodę [saveState] i nie ma sensu wykonywać dwóch zapisów;
Regeneracja widoku powiązanego z fragmentem jest realizowana przez następującą metodę:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// klasa nadrzędna
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// widok musi zostać przywrócony
viewHasToBeInitialized = true;
}
W cyklu życia metoda [onActivityCreated] jest wykonywana bezpośrednio po metodzie [onCreateView]. Wywołanie tej ostatniej metody oznacza, że widok powiązany z fragmentem musi zostać odbudowany. Wystarczy to odnotować w wierszu 10.
2.7.3.8. Aktualizacja fragmentu
Aktualizacja fragmentu jest ostatnią operacją wykonywaną na fragmencie, zanim stanie się on widoczny i zacznie oczekiwać na działania użytkownika. Jest ona realizowana przez następujący kod:
// menu fragmentu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// cykl życia fragmentu
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// stany fragmentu
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// cykl życia fragmentu
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// aktualizacja fragmentu ----------------------------------------------------------------------------------
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// dziennik
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// pamięć
this.menu = menu;
// pobieramy # opcji z menu, jeśli nie zostało to jeszcze zrobione
if (fragmentHasToBeInitialized) {
// pobieramy # opcji z menu
getMenuOptionsStates(menu);
// czynność
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = (Session) this.mainActivity.getSession();
}
// pobierany jest poprzedni stan fragmentu (za pierwszym razem tylko wartość logiczna hasBeenVisited ma znaczenie)
previousState = session.getCoreState(getNumView());
// aktualizacja fragmentu potomnego w kilku etapach
// etap 1 – czy to pierwsza wizyta?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// nie jest to pierwsza wizyta
// etap 2: czy fragment powinien zostać zainicjowany?
...
// etap 3: czy widok powinien zostać zainicjowany?
...
}
// krok 4: przesłanie danych, nawigacja, przywrócenie?
...
// krok 5: aktualizacje końcowe ----------------------
...
}
...
// klasy potomne -----------------------------------------------------
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
- wiersz 19: do aktualizacji fragmentu wykorzystywana jest metoda [onCreateOptionsMenu]. Z tego powodu fragment musi posiadać menu, w razie potrzeby puste. W momencie wykonania tej metody fragment został już powiązany ze swoim widokiem i aktywnością, a ponadto jest widoczny;
- wiersz 25: zapisujemy menu, które zostało przekazane jako parametr (wiersz 22) do metody;
- wiersze 27–34: jeśli fragment ma zostać zainicjowany:
- wiersz 29: stany opcji menu są umieszczane w tablicy [menuOptionsStates] z wiersza 3;
- wiersz 31: aktywność jest zapisywana jako instancja typu Android [Activity];
- wiersz 32: aktywność jest zapisywana jako instancja interfejsu [IMainActivity];
- wiersz 33: sesja zostaje zapisana. Zmiana typu jest konieczna, ponieważ metoda [mainActivity.getSession()] zwraca typ [ISession];
- wiersz 36: z sesji pobierany jest poprzedni stan fragmentu. Jeśli jest to pierwsza wizyta w fragmencie, znaczenie ma jedynie wartość logiczna [previousState.hasBeenVisited];
- wiersze 39–44: kod wykonywany, gdy jest to pierwsza wizyta w fragmencie. W tym przypadku jego poprzedni stan nie ma znaczenia;
- wiersze 44–50: kod wykonywany, gdy nie jest to pierwsza wizyta w fragmencie;
- wiersze 46–47: kod wykonywany, jeśli wywołano konstruktor fragmentu (fragmentHasToBeInitialized==true);
- wiersze 48–49: kod wykonywany, jeśli widok powiązany z fragmentem został odbudowany (viewHasToBeInitialized==true);
- wiersze 51–52: kod wykonywany w zależności od bieżącej akcji (SUBMIT, NAVIGATION, RESTORE);
- wiersze 54–55: kod jest nadal wykonywany;
Pięć etapów aktualizacji jest następujących:
etap 1
// menu fragmentu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// cykl życia fragmentu
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// stany fragmentu
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// cykl życia fragmentu
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// odzyskuje się poprzedni stan fragmentu (za pierwszym razem tylko wartość logiczna hasBeenVisited ma znaczenie)
previousState = session.getCoreState(getNumView());
// aktualizacja fragmentu potomnego w kilku etapach
// etap 1 – czy to pierwsza wizyta?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// inicjalizacja fragmentu i widoku
initFragment(null);
initView(null);
// wyzerowanie previousState na dalsze działania
previousState = null;
} else {
// to nie jest pierwsza wizyta
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- wiersz 19: poprzedni stan fragmentu jest pobierany z sesji;
- wiersze 22–31: kod wykonywany, jeśli fragment nigdy nie był odwiedzany;
- wiersz 27: klasa potomna otrzymuje polecenie zainicjowania fragmentu. Parametrem metody [initFragment] w wierszu 35 jest poprzedni stan fragmentu. W tym przypadku przekazujemy null, aby poinformować fragment potomny, że jest to pierwsza wizyta;
- wiersz 28: klasa potomna otrzymuje polecenie zainicjowania widoku powiązanego z fragmentem. Parametrem metody [initView] w wierszu 37 jest poprzedni stan fragmentu. W tym przypadku przekazujemy wartość null, aby wskazać fragmentowi potomnemu, że jest to pierwsza wizyta;
- wiersz 30: ustawia się poprzedni stan na null dla kolejnych etapów;
kroki 2 i 3
// menu fragmentu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// cykl życia fragmentu
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// stany fragmentu
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// cykl życia fragmentu
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// odzyskuje się poprzedni stan fragmentu (za pierwszym razem tylko wartość logiczna hasBeenVisited ma znaczenie)
previousState = session.getCoreState(getNumView());
// aktualizacja fragmentu potomnego w kilku etapach
// etap 1 – czy to pierwsza wizyta?
if (!previousState.getHasBeenVisited()) {
...
} else {
// nie jest to pierwsza wizyta
// etap 2: czy fragment należy zainicjować?
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation fragment");
}
// fragment potomny
initFragment(previousState);
}
// krok 3: czy widok powinien zostać zainicjowany?
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation vue");
}
// fragment potomny
initView(previousState);
}
}
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- wiersze 24–42: wykonywane, gdy nie jest to pierwsza wizyta fragmentu;
- wiersze 27–33: jeśli fragment został właśnie odbudowany, resetujemy go, wywołując metodę [initFragment] klasy potomnej (wiersze 32, 46). Przekazujemy jej poprzedni stan fragmentu;
- wiersze 35–51: jeśli widok powiązany z fragmentem ma zostać zainicjowany lub zresetowany, zlecamy to fragmentowi potomnemu (wiersze 40, 48). Również w tym przypadku przekazujemy mu ostatni znany stan fragmentu;
krok 4
// menu fragmentu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// cykl życia fragmentu
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// stany fragmentu
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// cykl życia fragmentu
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// odzyskuje się poprzedni stan fragmentu (za pierwszym razem tylko wartość logiczna hasBeenVisited ma znaczenie)
previousState = session.getCoreState(getNumView());
// aktualizacja fragmentu potomnego w kilku etapach
...
// etap 4: przesłanie danych, nawigacja, przywrócenie?
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// trwająca akcja
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// fragment podrzędny
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// przywrócenie menu
setMenuOptionsStates(previousState.getMenuOptionsState());
// fragment potomny
updateOnRestore(previousState);
} else {
// to pierwsza wizyta – nic do zrobienia
}
break;
case RESTORE:
// przywrócenie
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// przywracanie menu (previousState nie może być null)
setMenuOptionsStates(previousState.getMenuOptionsState());
// fragment „córka”
updateOnRestore(previousState);
break;
}
....
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
- wiersze 34–66: przetwarzana jest bieżąca akcja, która może być jedną z trzech następujących:
- RESTORE: trwa przywracanie fragmentu po obróceniu urządzenia;
- NAVIGATION: powracamy do fragmentu, chcąc odzyskać go w stanie, w jakim został pozostawiony podczas ostatniego użycia;
- SUBMIT: wszystkie pozostałe przypadki;
- wiersz 34: pobieramy bieżącą akcję;
- wiersze 36–42: w przypadku akcji typu SUBMIT wywołuje się metodę [updateOnSubmit] fragmentu potomnego (wiersze 41, 68), przekazując jej ostatni znany stan fragmentu;
- wiersze 43–55: dla akcji typu NAVIGATION;
- wiersze 47–54: chcemy przywrócić fragment do jego ostatniego znanego stanu. Operacja NAVIGATION może wiązać się z pierwszą wizytą. Tak byłoby na przykład w aplikacji z zakładkami: jeśli przechodzę z zakładki 1 do zakładki 4:
- muszę zainicjować fragment zakładki 4, jeśli jest to pierwsza wizyta;
- przywrócić fragment zakładki 4 do poprzedniego stanu, jeśli nie jest to pierwsza wizyta;
- wiersze 52–54: nie wykonuje się żadnych czynności, jeśli jest to pierwsza wizyta. Inicjalizacją zajmie się metoda potomna [initView(CoreState previousState)]. Pierwsza wizyta jest charakteryzowana przez warunek [previousState==null];
- wiersz 49: jeśli nie jest to pierwsza wizyta w danym fragmencie, przywraca się mu menu;
- wiersz 51: klasa potomna otrzymuje polecenie aktualizacji poprzez wywołanie metody z wiersza 70. Przekazuje się jej poprzedni stan fragmentu, aby mogła wykonać swoje zadanie;
- wiersze 56–66: w przypadku operacji przywracania fragmentu postępujemy tak samo, jak w przypadku nawigacji poza pierwszą wizytą;
krok 5
// menu fragmentu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// cykl życia fragmentu
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// stany fragmentu
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// cykl życia fragmentu
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// etap 5: aktualizacje końcowe ----------------------
// zmieniono widok
session.setPreviousView(getNumView());
// brak bieżących działań
session.setAction(ISession.Action.NONE);
// po opuszczeniu tego fragmentu należy go zapisać
saveFragmentDone = false;
// dopóki fragment nie zostanie odbudowany, nie ma potrzeby jego inicjalizacji
fragmentHasToBeInitialized = false;
// dopóki widok nie zostanie odtworzony, nie ma potrzeby jego inicjalizacji
viewHasToBeInitialized = false;
// powraca się do normalnego działania wyboru zakładek
session.setNavigationOnTabSelectionNeeded(true);
// informuje się fragment, że widok jest gotowy
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- wiersze 18–30: gdy dochodzimy do tego miejsca, fragment został zainicjowany i jest gotowy do wyświetlenia. Następnie przywracamy wszystkie wskaźniki wykorzystywane w zarządzaniu cyklem życia fragmentu do stanu początkowego;
- wiersz 20: nastąpiła zmiana widoku: odnotowujemy to w sesji;
- wiersz 22: nie ma już żadnej trwającej akcji;
- wiersz 24: gdy zamierzamy opuścić obecnie wyświetlany fragment, należy go zapisać przed opuszczeniem;
- wiersz 26: fragment nie wymaga już odtworzenia. Wskaźnik ten zostanie zresetowany do wartości vrai, gdy konstruktor fragmentu zostanie ponownie uruchomiony;
- wiersz 28: widok powiązany z fragmentem nie wymaga już inicjalizacji. Ten wskaźnik zostanie zresetowany do wartości vrai, gdy metoda [onActivityCreated] zostanie ponownie wykonana;
- wiersz 30: fragment może być wyświetlany w aplikacji z zakładkami. W takim przypadku, gdy użytkownik kliknie jedną z nich, musi nastąpić zmiana fragmentu;
- wiersz 36: informujemy klasę potomną, że fragment jest gotowy. Klasa ta może w metodzie [notifyEndOfUpdates] wprowadzić aktualizacje, które należy wykonać w każdym przypadku, uruchomić operację asynchroniczną w celu uzyskania nowych danych itp.
2.7.4. Przykład fragmentu
![]() |
W projekcie [client-android-skel] umieszczono przykładowy fragment, aby pokazać czytelnikowi typową strukturę fragmentu aplikacji opartej na tym projekcie.
Klasa [DummyFragment] wygląda następująco:
package client.android.fragments.behavior;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
public class DummyFragment extends AbstractFragment {
// pola odziedziczone po klasie nadrzędnej -------------------------------------------------------
// tryb debugowania
//-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// nazwa klasy
//-- chroniona zmienna typu String className;
// zadania asynchroniczne
//-- chroniona zmienna typu int numberOfRunningTasks;
// aktywność
//-- chroniona IMainActivity mainActivity;
//-- chroniona aktywność;
// sesja
//-- chroniona sesja session;
// metody odziedziczone po klasie nadrzędnej -------------------------------------------------------
// wyświetlanie opcji menu
//-- protected void setAllMenuOptionsStates(boolean isVisible) {
//-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// obsługa oczekiwania na zakończenie serii zadań asynchronicznych
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// wykonanie zadania asynchronicznego za pomocą RxAndroid
//-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// anulowanie zadań
//-- protected void cancelRunningTasks() {
// wyświetlanie alertu o wyjątku
//-- protected void showAlert(Throwable th) {
// wyświetlanie listy komunikatów
//-- protected void showAlert(List<String> messages) {
// metody narzucone przez klasę nadrzędną -------------------------------------------------------
@Override
public CoreState saveFragment() {
// należy zapisać fragment
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// jeślinie ma nic do zapisania, należy wykonać [return new CoreState();] i usunąć klasę [DummyFragmentState]
}
@Override
protected int getNumView() {
// należy zwrócić numer fragmentu do tabeli fragmentów zarządzanych przez aktywność (patrz MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// fragment staje się widoczny i został skonstruowany na tym etapie lub na poprzednim
// dzieje się to przy uruchomieniu aplikacji oraz przy każdym obróceniu urządzenia z systemem Android
// po którym koniecznie następuje wykonanie [initView]
// należy zainicjować pola fragmentu, który został odtworzony
// previousState to ostatnia kopia zapasowa fragmentu – przyjmuje wartość null, jeśli jest to pierwsza wizyta w tym fragmencie
}
@Override
protected void initView(CoreState previousState) {
// fragment staje się widoczny, a powiązany widok został odtworzony na tym etapie lub na poprzednim
// dzieje się to za każdym razem, gdy wykonywana jest funkcja [initFragment] oraz za każdym razem, gdy fragment opuszcza obszar sąsiedztwa wyświetlanego fragmentu
// należy zainicjować komponenty odtworzonego widoku
// previousState to ostatnia kopia zapasowa fragmentu – ma wartość null, jeśli jest to pierwsza wizyta w tym fragmencie
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// jest wykonywane po [initFragment, initView], jeśli te metody zostaną wykonane
// widok zostanie wyświetlony po operacji typu SUBMIT
// zazwyczaj należy zainicjować fragment i powiązany widok na podstawie sesji
// previousState to ostatni zapis fragmentu – ma wartość null, jeśli jest to pierwsza wizyta w tym fragmencie
// nie ma nic do zrobienia, jeśli nie można przejść do fragmentu za pomocą operacji SUBMIT
// jeśli do fragmentu można dotrzeć za pomocą operacji SUBMIT z różnych fragmentów, poprzedni widok można ustalić za pomocą [session.getPreviousView]
// jeśli do fragmentu można dotrzeć za pomocą kilku operacji SUBMIT z tego samego fragmentu, należy ustawić w sesji wskaźnik, aby odróżnić różne typy operacji SUBMIT z tego fragmentu
}
@Override
protected void updateOnRestore(CoreState previousState) {
// jest wykonywane po [initFragment, initView], jeśli te metody zostaną wykonane
// widok zostanie wyświetlony po operacji typu RESTORE lub NAVIGATION
// previousState to ostatnia kopia zapasowa fragmentu – nigdy nie przyjmuje wartości null
// należy przywrócić widok do poprzedniego stanu
}
@Override
protected void notifyEndOfUpdates() {
// wykonuje się po metodach [updateOnSubmit, updateOnRestore]
// w tym momencie widok został już utworzony i zainicjowany
// często nie ma tu nic do zrobienia, ale można tu również wyodrębnić działania, które należy wykonać niezależnie od tego, w jaki sposób trafiono na ten widok
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// wywoływana, gdy zadania asynchroniczne uruchomione przez fragment zostały zakończone lub anulowane
// te dwa przypadki można rozróżnić dzięki parametrowi runningTasksHaveBeenCanceled
// zazwyczaj konieczne jest przywrócenie widoku do stanu innego niż ten, w którym znajdował się podczas oczekiwania na odpowiedzi z zadań asynchronicznych
}
}
Klasa [DummyFragment] może nie posiadać stanu. W tym przypadku dodano go, aby przypomnieć, czego się w niej oczekuje:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class DummyFragmentState extends CoreState {
// stan fragmentu [DummyFragment]
// należy umieszczać wyłącznie pola, które można serializować w jSON
// dodać adnotację @JsonIgnore do pozostałych, ale trudno zrozumieć, do czego mogłyby służyć
// nie zapomnij o metodach getter i setter – służą one do serializacji i deserializacji
}
Aby zilustrować wykorzystanie projektu [client-android-skel], najpierw posłużymy się prostymi przykładami, a następnie przejdziemy do bardziej kompleksowego studium przypadku.
2.8. Ćwiczenia ilustracyjne
Zaczniemy od refaktoryzacji już napisanych przykładów.
2.8.1. Przykład 17B
Wracamy do przykładu 17 omówionego w paragrafie 1.18. Jest to aplikacja zawierająca jeden fragment, bez zadań asynchronicznych i bez kart. Przyjrzymy się jej, aby sprawdzić, jak zachowuje się podczas obracania urządzenia. Wprowadzamy następujące dane:

Następnie, w [1], obracamy urządzenie dwukrotnie. Nowy widok wygląda wówczas następująco:

Jeśli porównamy te widoki, wszystko zostało zachowane z wyjątkiem listy [2], która jest teraz pusta.
Ponadto po kliknięciu przycisku [Valider] pojawia się okno dialogowe pokazujące dane wprowadzone w formularzu. Jeśli w tym momencie obrócimy urządzenie, okno dialogowe zniknie.
W związku z tym podczas obrotu konieczne będzie odświeżenie:
- listę rozwijaną i jej zaznaczony element;
- okno dialogowe, jeśli było wyświetlone w momencie obrotu;
2.8.1.1. Projekt [Exemple-17B]
Duplikujemy projekt [client-android-skel] w katalogu exemples/Exemple-17B. Następnie wczytujemy nowy projekt [1]:
![]() | ![]() | ![]() |
- do pliku [2-3], znajdującego się w folderze [behavior], wklejamy fragment [Vue1Fragment] z projektu [Exemple-17];
![]() | ![]() | ![]() |
- do pliku [4-5], znajdującego się w folderze [layout] projektu [Exemple-17B], wklejamy widok [vue1.xml] z projektu [Exemple-17]. Jest to widok powiązany z fragmentem;
- w pliku [6] folder [values] z pliku [Exemple-17B] zostaje zastąpiony folderem [values] z pliku [Exemple-17];
Górny margines widoku [vue1.xml] zostanie zmieniony na 80 dp:
<TextView
android:id="@+id/textViewFormulaireTitre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="80dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
Na tym etapie można spróbować przeprowadzić pierwszą kompilację, aby sprawdzić, jakie błędy się pojawią. Pierwsze zgłoszone błędy dotyczą pliku imports i wynikają ze zmiany lokalizacji pakietów. Należy je poprawić (Ctrl-Shift-O). Inne błędy wynikają z faktu, że widok [Vue1Fragment] nie implementuje wszystkich metod wymaganych przez swoją klasę nadrzędną [AbstractParent]:

Generujemy brakujące metody (Alt-Enter).
Kolejny zgłoszony błąd kompilacji to:

Poprawiamy to w pliku [build.gradle] modułu (wiersz 20 poniżej):
![]() |
Na tym etapie można ponownie skompilować kod, aby sprawdzić pozostałe błędy. Jedyny zgłoszony błąd dotyczy metody [Vue1Fragment.updateFragment]:
![]() |
Należy usunąć adnotację [@Override] z wiersza 135. Nie ma już żadnych błędów. Na tej podstawie przystąpimy do modyfikacji projektu.
2.8.1.2. Stan fragmentu [Vue1Fragment]
Fragment [Vue1Fragment] musi zapisywać informacje podczas obracania urządzenia, aby można go było w pełni przywrócić. W tym celu tworzymy klasę [Vue1FragmentState]:
![]() |
Na razie ta klasa jest pusta:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.1.3. Dostosowywanie projektu
![]() |
W folderze [custom] znajdują się elementy architektury, które programista może dostosować do własnych potrzeb.
Stałe interfejsu [IMainActivity] będą następujące:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// dostęp do sesji
ISession getSession();
// zmiana widoku
void navigateToView(int position, ISession.Action action);
// obsługa oczekiwania
void beginWaiting();
void cancelWaiting();
// stałe aplikacji -------------------------------------
// tryb debugowania
boolean IS_DEBUG_ENABLED = true;
// maksymalny czas oczekiwania na odpowiedź serwera
int TIMEOUT = 1000;
// czas oczekiwania przed wykonaniem żądania klienta
int DELAY = 0;
// uwierzytelnianie podstawowe
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// sąsiedztwo fragmentów
int OFF_SCREEN_PAGE_LIMIT = 1;
// pasek kart
boolean ARE_TABS_NEEDED = false;
// obraz oczekiwania
boolean IS_WAITING_ICON_NEEDED = false;
// liczba fragmentów aplikacji
int FRAGMENTS_COUNT = 1;
}
- wiersze 24–31: aplikacja nie korzysta tutaj ze swojej warstwy [DAO]. Te stałe nie będą wykorzystywane;
- wiersz 34: sąsiedztwo fragmentów równe 1, co jest wartością domyślną. Ponieważ aplikacja ma tylko jeden fragment (wiersz 43), wartość ta nie ma znaczenia;
- wiersze 39–40: ponieważ nie ma żadnych operacji z warstwą [DAO], nie ma potrzeby stosowania obrazu oczekiwania;
- wiersz 37: nie jest to aplikacja z zakładkami;
- wiersz 43: występuje tylko jeden fragment;
Klasa [Session] ma następujący wygląd:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elementy, których nie można zserializować w formacie jSON, muszą posiadać adnotację @JsonIgnore
}
Jest pusta. Ponieważ występuje tylko jeden fragment, nie ma potrzeby przewidywania komunikacji między fragmentami z wykorzystaniem sesji.
Wreszcie klasa [CoreState] ma następujący wygląd:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// fragment odwiedzony lub nieodwiedzony
protected boolean hasBeenVisited = false;
// stan ewentualnego menu fragmentu
protected MenuItemState[] menuOptionsState;
// metody pobierające i ustawiające
...
}
- wiersze 11–13: należy umieścić wszystkie klasy pochodne od klasy [CoreState], które przechowują stan poszczególnych fragmentów. W tym przypadku jest tylko jedna (wiersz 12);
2.8.1.4. Aktywność [MainActivity]
Aktywność [MainActivity] wygląda obecnie następująco:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// warstwa [DAO]
@Bean(Dao.class)
protected IDao dao;
// sesja
private Session session;
// metody klasy nadrzędnej -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sesja
this.session = (Session) super.session;
// zadania do wykonania: kontynuujemy inicjalizacje rozpoczęte przez klasę nadrzędną
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// do zrobienia: zdefiniować tutaj fragmenty
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// do zrobienia: zdefiniować tutaj tytuły fragmentów
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// zadanie: nawigacja za pomocą zakładek – zdefiniować widok do wyświetlenia po wybraniu zakładki nr [position]
}
@Override
protected int getFirstView() {
// do zrobienia: zdefiniować numer pierwszego widoku (fragmentu) do wyświetlenia
return 0;
}
}
Komentarze [//todo] wskazują, co powinien zrobić programista. Klasa [MainActivity] zmienia się w następujący sposób:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// warstwa [DAO]
@Bean(Dao.class)
protected IDao dao;
// sesja
private Session session;
// metody klasy nadrzędnej -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sesja
this.session = (Session) super.session;
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
}
Należy zmodyfikować jedynie metodę w wierszach 41–44. Musi ona zwracać tablicę fragmentów aplikacji. W wierszu 43 nie należy zapomnieć o umieszczeniu znaku podkreślenia po nazwie fragmentu.
2.8.1.5. Stan fragmentu [FragmentState]
W wyniku testów rotacji przeprowadzonych na projekcie [Exemple-17] postanowiono zapisać następujące elementy fragmentu:
- listę wartości z listy rozwijanej;
- pozycję wybranego elementu na tej liście;
- komunikat wyświetlany w oknie dialogowym, jeśli jest ono aktywne w momencie rotacji;
Klasa [Vue1FragmentState] będzie wyglądać następująco:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// wartości z listy rozwijanej
private List<String> list;
// element wybrany z listy rozwijanej
private int listSelectedPosition;
// komunikat wyświetlany w oknie dialogowym
private String message;
// metody pobierające i ustawiające
...
}
2.8.1.6. Fragment [AbstractFragment]
Obecnie cykl życia fragmentu jest zarządzany przez dwie metody (wiersze 6 i 32):
// 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");
}
...
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);
}
Kod tych dwóch metod zostanie przeniesiony do metod narzuconych przez klasę [AbstractFragment] w następujący sposób:
// zarządzanie cyklem życia fragmentu ---------------------------------------------------------------------
@Override
public CoreState saveFragment() {
Vue1FragmentState state = new Vue1FragmentState();
state.setList(list);
state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
state.setMessage(message);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// Pierwsza wizyta?
if (previousState == null) {
// tworzenie wartości listy rozwijanej
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// wyświetlanie wartości z listy rozwijanej
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// oraz komunikat w oknie dialogowym
message = state.getMessage();
}
// inicjalizacja adaptera listy rozwijanej
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override
protected void initView(CoreState previousState) {
// 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));
}
});
// inicjalizacja adaptera listy rozwijanej
dropDownList.setAdapter(dataAdapter);
// Pierwsza wizyta?
if (previousState == null) {
// zaznaczamy pierwszy przycisk
radioButton1.setChecked(true);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// wartość suwaka
seekBarValue.setText(String.valueOf(seekBar.getProgress()));
// element wybrany z listy rozwijanej
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// czy okno dialogowe jest widoczne?
if (message != null) {
// wyświetla się
showMessage();
}
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
- wiersze 2–9: metoda [saveFragment] musi umieścić elementy fragmentu, które mają zostać zapisane, w klasie pochodnej od [CoreState] i zwrócić jej instancję;
- wiersze 11–14: metoda [getNumView] musi zwrócić numer fragmentu. W tym przypadku istnieje tylko jeden fragment o numerze 0;
- wiersze 16–34: metoda [initFragment] musi zainicjować pola fragmentu. Otrzymuje ona poprzedni stan fragmentu. Jeśli [previousState] ma wartość null, oznacza to, że jest to pierwsza wizyta;
- wiersze 19–25: podczas pierwszej wizyty tworzone są wartości listy rozwijanej;
- wiersze 26–30: jeśli nie jest to pierwsza wizyta, pola [list, message] fragmentu są przywracane na podstawie poprzedniego stanu;
- wiersze 33–34: inicjalizacja pola [dataAdapter] fragmentu. Jest to źródło danych listy rozwijanej;
- wiersze 37–62: metoda [initView] służy do inicjalizacji elementów interfejsu wizualnego. Jako parametr otrzymuje poprzedni stan [previousState]. Jeśli [previousState==null], oznacza to, że jest to pierwsza wizyta;
- znajduje się tu to, co wcześniej znajdowało się w metodzie [@AfterViews];
- wiersze 57–61: podczas pierwszej wizyty upewniamy się, że zaznaczony jest pierwszy przycisk opcji;
- wiersze 64–67: metoda [updateOnSubmit] jest wykonywana, gdy bieżącą akcją jest [SUBMIT]. W tym przypadku nie ma nawigacji między fragmentami, a zatem nie ma bieżącej akcji;
- wiersze 69–81: metoda [updateOnRestore] jest wykonywana, gdy bieżącą akcją jest [NAVIGATION] lub [RESTORE]. W tym przypadku nie ma nawigacji między fragmentami, a zatem nie jest możliwa akcja [NAVIGATION];
- wiersz 72: ponownie obliczamy (nie przywracamy) wartość TextView seekBarValue. W rzeczywistości podczas obrotów wartość ta była czasami tracona;
- wiersze 74–75: ustawia się listę na elemencie, który był zaznaczony przed obrotem. W przeciwnym razie lista ustawiała się na pierwszym elemencie;
- wiersze 76–80: ponownie wyświetla się okno dialogowe, jeśli komunikat z poprzedniego stanu nie jest null. Powrócimy do metody [showMessage] (wiersz 79);
- wiersze 83–86: metoda [notifyEndOfUpdates] jest ostatnią metodą wywoływaną przez klasę nadrzędną przed pozostawieniem fragmentu podrzędnego w spokoju. W tym miejscu nie ma nic do zrobienia;
- wiersze 88–91: metoda [notifyEndOfTasks] sygnalizuje zakończenie zadań asynchronicznych uruchomionych przez fragment. W tym przypadku nie ma żadnych takich zadań;
Przywrócenie okna dialogowego odbywa się w następujący sposób:
// treść komunikatu w oknie dialogowym
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// lista komunikatów do wyświetlenia
List<String> messages = new ArrayList<>();
...
// wyświetlanie
doAfficher(messages);
}
private void doAfficher(final List<String> messages) {
// tworzy się tekst do wyświetlenia
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// zapisywanie komunikatu
message = texte.toString();
// wyświetlanie
showMessage();
}
private void showMessage() {
// wyświetlanie
new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// resetowanie komunikatu
message = null;
}
}).show();
}
Gdy użytkownik zatwierdzi formularz, metoda [doValider] (wiersz 5) tworzy listę komunikatów, którą następnie wyświetla (wiersz 10) w oknie dialogowym.
- wiersze 14–20: lista komunikatów jest łączona w jeden komunikat, który jest zapisywany w wierszu 2;
- wiersze 25–33: to właśnie ten komunikat wyświetla okno dialogowe i ten sam komunikat wyświetla metoda [updateOnRestore];
- wiersz 27: drugim parametrem metody [setNeutralButton] jest metoda wykonywana po kliknięciu przez użytkownika przycisku [Fermer] w oknie dialogowym;
- wiersz 31: po zamknięciu okna dialogowego przekazujemy komunikat do metody null, aby wskazać, że okno dialogowe już nie istnieje;
2.8.1.7. Tests
Zachęcamy czytelnika do przetestowania tego projektu i sprawdzenia, czy fragment ten pozostaje nienaruszony po jednej lub kilku kolejnych rotacjach.
2.8.2. Przykład 23: klient pogodowy
Niektóre strony internetowe udostępniają informacje pogodowe w postaci ciągów znaków o formacie jSON. Oto przykład:

Kod URL ma następującą postać: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID}, gdzie:
- city: miasto, dla którego chcemy uzyskać prognozę pogody, w tym przypadku Angers;
- country: kraj, w którym znajduje się miasto, w tym przypadku Francja (fr);
- APPID: klucz uzyskany po zarejestrowaniu się na stronie [https://home.openweathermap.org/users/sign_up];
2.8.2.1. Projekt
![]() |
Projekt powstał na podstawie projektu [client-android-skel]. Charakteryzuje się następującymi cechami:
- składa się tylko z jednego fragmentu, którego stanu nie trzeba zachowywać;
- wykonywuje zapytania asynchroniczne;
2.8.2.2. Dostosowanie projektu
![]() |
Interfejs [IMainActivity] umożliwia określenie niektórych cech projektu:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// dostęp do sesji
ISession getSession();
// zmiana widoku
void navigateToView(int position, ISession.Action action);
// zarządzanie kolejką
void beginWaiting();
void cancelWaiting();
// stałe aplikacji -------------------------------------
// tryb debugowania
boolean IS_DEBUG_ENABLED = true;
// maksymalny czas oczekiwania na odpowiedź serwera
int TIMEOUT = 1000;
// czas oczekiwania przed wykonaniem żądania klienta
int DELAY = 5000;
// uwierzytelnianie podstawowe
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// sąsiedztwo fragmentów
int OFF_SCREEN_PAGE_LIMIT = 1;
// pasek kart
boolean ARE_TABS_NEEDED = false;
// obraz oczekiwania
boolean IS_WAITING_ICON_NEEDED = true;
// liczba fragmentów aplikacji
int FRAGMENTS_COUNT = 1;
}
- wiersze 25, 28, 31, 40: cechy warstwy [DAO]. W wierszu 31 nie ma potrzeby stosowania uwierzytelniania podstawowego;
- wiersz 34: sąsiedztwo fragmentów. W tym przypadku ta stała nie ma znaczenia, ponieważ występuje tylko jeden fragment;
- wiersz 37: nie jest to aplikacja z zakładkami;
- wiersz 43: występuje tylko jeden fragment;
Klasa [CoreState], która przechowuje stan fragmentów, będzie wyglądać następująco:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// zadanie: dodać tutaj podklasy [CoreState]
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragment odwiedzony lub nie
protected boolean hasBeenVisited = false;
// stan ewentualnego menu fragmentu
protected MenuItemState[] menuOptionsState;
// metody pobierające i ustawiające
...
}
- wiersze 10–13: nie ma nic do zadeklarowania, ponieważ w tej aplikacji występuje tylko jeden fragment, którego stanu nie zapisujemy;
Klasa [Session] ma następujący wygląd:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elementy, których nie można zserializować w jSON, muszą posiadać adnotację @JsonIgnore
}
Jest ona pusta, ponieważ w tej aplikacji nie ma komunikacji między fragmentami.
2.8.2.3. Warstwa [DAO]
![]() |
W warstwie [DAO] należy dostosować trzy klasy:
- interfejs IDao;
- jego implementacja Dao;
- interfejs WebClient do komunikacji z serwerem WWW / jSON;
Interfejs [WebClient] będzie wyglądał następująco:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// serwis pogodowy
@Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
- wiersze 18–19: URL usługi pogodowej. Przypominamy, że odnosi się on do korzeniowego URL (RestClientRootUrl, wiersz 12) klienta. W tym przypadku tym korzeniowym URL będzie [http://api.openweathermap.org/];
Interfejs [IDao] będzie wyglądał następująco:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// adres URL serwisu internetowego
void setUrlServiceWebJson(String url);
// użytkownik
void setUser(String user, String mdp);
// limit czasu klienta
void setTimeout(int timeout);
// uwierzytelnianie podstawowe
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// tryb debugowania
void setDebugMode(boolean isDebugEnabled);
// czas oczekiwania klienta przed wysłaniem żądania w milisekundach
void setDelay(int delay);
// serwis pogodowy
Observable<String> getWeatherForecast(String city, String country, String APPID);
}
- przypominamy, że metody z wierszy 6–22 są domyślnie obecne w interfejsie IDao projektu [client-android-skel];
- wiersz 25: metoda [getWeatherForecast] pozwala uzyskać ciąg znaków jSON dotyczący prognozy pogody dla miasta [city] w kraju [country]. Trzecim parametrem jest klucz uzyskany ze strony [https://home.openweathermap.org/users/sign_up];
Interfejs [IDao] jest zaimplementowany przez następującą klasę [Dao]:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// klient serwisu internetowego
@RestService
protected WebClient webClient;
// bezpieczeństwo
@Bean
protected MyAuthInterceptor authInterceptor;
// RestTemplate
private RestTemplate restTemplate;
// fabryka RestTemplate
private SimpleClientHttpRequestFactory factory;
// limit czasu
private int timeout;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// tworzymy restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// ustawiamy konwerter jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// ustawiamy restTemplate dla klienta internetowego
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// ustawiamy URL serwisu internetowego
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// rejestruje się użytkownika w intercepterze
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// pamięć
this.timeout = timeout;
// fabryka konfiguracji
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// przechwytywacz uwierzytelniania?
if (isBasicAuthentificationNeeded) {
// dodaje się interceptor uwierzytelniający
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// metody prywatne -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// serwis pogodowy ---------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
}
// wynik
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- przypominamy, że wiersze 17–90 są domyślnie obecne w klasie [Dao] projektu [client-android-skel]. Wystarczy tylko dodać metody implementujące interfejs [IDao], specyficzne dla aplikacji (wiersz 92);
- wiersze 93–105: implementacja metody [getWeatherForecast]. Jest ona bardzo prosta i zajmuje 6 wierszy, wiersze 100–105;
- wiersz 100: metoda [getResponse] jest metodą klasy nadrzędnej [AbstractDao]. Oczekuje ona parametru typu [IRequest<T>], gdzie T jest typem oczekiwanej odpowiedzi z serwera, w tym przypadku String, ponieważ oczekujemy ciągu znaków jSON. Typ T klasy [IRequest<T>] musi być typem T metody [Observable<T> getWeatherForecast];
- interfejs [IRequest<T>] posiada tylko jedną metodę: getResponse. Jej rolą jest dostarczenie odpowiedzi typu T, którą powinna zwrócić metoda [Observable<T> getWeatherForecast];
- wiersz 103: to interfejs [WebClient] dostarcza tę odpowiedź. Przekazuje się mu trzy parametry otrzymane w wierszu 94. Z tego powodu muszą one posiadać atrybut final;
2.8.2.4. Aktywność [MainActivity]
![]() |
Aktywność [MainActivity] wygląda następująco:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// warstwa [DAO]
@Bean(Dao.class)
protected IDao dao;
// metody klasy nadrzędnej -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new MeteoFragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
// interfejs IDao ---------------------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(String city, String country, String APPID) {
return dao.getWeatherForecast(city, country, APPID);
}
}
- przypominamy, że wiersze 15–55 są domyślnie obecne w projekcie [client-android-skel]. Wystarczy je dostosować;
- wiersze 37–40: tabela fragmentów. W tym przypadku występuje tylko jeden;
- wiersze 43–46: nie są wymagane tytuły fragmentów;
- wiersze 48–50: nie ma tu zakładek;
- wiersze 52–55: pierwszym widokiem do wyświetlenia jest widok nr 0, czyli ten z projektu [MeteoFragment];
- wiersze 58–61: implementacja interfejsu [IDao]. Tutaj nie pozostaje nic innego, jak tylko przekazać zadanie warstwie [DAO] z wiersza 21;
2.8.2.5. Fragment [MeteoFragment]
![]() |
Fragment [MeteoFragment] wysyła zapytanie do serwisu internetowego / jSON z informacjami o pogodzie. Jego szkielet wygląda następująco:
package client.android.fragments;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
- wiersz 14: widok [res / layout / meteo_fragment.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="Construisez votre interface visuelle"
android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
android:layout_marginTop="120dp"/>
</RelativeLayout>
Widok wyświetla jedynie tekst z wiersza 10;
- wiersz 15: menu [res / menu / menu_meteo.xml] wygląda następująco:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionMeteo"
android:title="@string/actionMeteo"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
<item
android:id="@+id/actionTerminer"
android:title="@string/actionTerminer"/>
</menu>
</item>
</menu>
- wiersze 10–12: ta opcja menu służy do sprawdzenia prognozy pogody dla danego miasta;
- wiersze 14–15: ta opcja menu służy do anulowania tego zapytania, jeśli jest ono w toku;
- wiersze 16–18: ta opcja menu zamyka aplikację;
Pełny kod fragmentu wygląda następująco:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {
// dane lokalne
private int nbReponsesRecues;
// zarządzanie zdarzeniami ---------------------------------------------------------------------------------------
// miasta, dla których potrzebne są prognozy pogody
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
@OptionsItem(R.id.actionMeteo)
protected void doMeteo() {
// jego kraj
String country = "fr";
// uzyskaj identyfikator API, zakładając konto [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// URL z serwisu internetowego / jSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// początek oczekiwania na zadania asynchroniczne [paysDeLoire.length]
beginWaiting(paysDeLoire.length);
// liczba otrzymanych odpowiedzi
nbReponsesRecues = 0;
// wywołania asynchroniczne są wykonywane równolegle
for (String city : paysDeLoire) {
// prognoza pogody
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// przetwarzanie odpowiedzi
consumeResponse(response);
// odpowiedź „+”
nbReponsesRecues++;
}
});
}
}
// przetwarzanie odpowiedzi serwera
private void consumeResponse(String response) {
// log
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// początek oczekiwania
protected void beginWaiting(int numberOfRunningTasks) {
// log
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// element nadrzędny
beginRunningTasks(numberOfRunningTasks);
// wyświetla się opcja [Annuler]
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{
new MenuItemState(R.id.menuActions, true),
new MenuItemState(R.id.actionAnnuler, true)});
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// wyświetlanie wyników
String message;
switch (nbReponsesRecues) {
case 0:
message = "Aucune réponse n'a été reçue";
break;
case 1:
message = "Une réponse a été reçue. Consultez vos logs...";
break;
default:
message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
break;
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
}
// metody prywatne -----------------------------------
private void initMenu() {
if (isDebugEnabled) {
Log.d(className, "initMenu");
}
// menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
// zarządzanie cyklem życia ---------------------------------------------------------------------------------------
...
}
- wiersze 25–50: obsługa kliknięcia opcji menu [Météo];
- wiersz 32: tworzenie URL usługi internetowej / jSON usługi pogodowej. Następnie jest ona przekazywana do warstwy [DAO] za pośrednictwem aktywności;
- wiersz 34: rozpoczynamy oczekiwanie. Przekazujemy liczbę zadań, które zostaną uruchomione, aby klasa nadrzędna mogła poinformować nas o ich zakończeniu. W tym przypadku jest pięć zadań, ponieważ zamierzamy uzyskać prognozę pogody dla pięciu miast z wiersza 23;
- wiersz 16: zliczamy liczbę otrzymanych odpowiedzi, aby móc ją wyświetlić;
- wiersze 38–50: przechodzimy do pętli obejmującej miasta, dla których chcemy uzyskać prognozę pogody;
- wiersz 40: wysyłamy równolegle 5 zapytań HTTP;
- wiersz 40: prosimy klasę nadrzędną [AbstractParent] o wysłanie zapytania do serwisu internetowego / jSON;
- wiersze 40–48: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersz 40: proces, który ma być obserwowany i uruchomiony, jest dostarczany przez metodę [mainActivity.getWeatherForecast];
- wiersze 40–48: instancja [Action1], która ma zostać uruchomiona po otrzymaniu odpowiedzi z serwisu asynchronicznego. Typ T metody [Action1<T>] musi być zgodny z typem T wyniku metody [getWeatherForecast];
- wiersz 44: otrzymano odpowiedź. Przekazujemy ją do metody [consumeResponse] z wiersza 53;
- wiersz 46: zwiększamy licznik otrzymanych odpowiedzi;
- wiersze 53–56: przetwarzanie odpowiedzi jSON z serwisu pogodowego;
- wiersz 55: ograniczamy się do zarejestrowania ciągu znaków jSON;
- wiersze 59–72: kod wykonywany przed uruchomieniem zadań asynchronicznych;
- wiersz 65: przekazujemy liczbę zadań do wykonania do klasy nadrzędnej [AbstractParent]. Dzięki temu klasa ta powiadomi nas, gdy wszystkie zadania zostaną zakończone;
- wiersze 67–70: przygotowanie menu do oczekiwania. Zachowujemy jedynie opcję [Actions/Annuler], która pozwoli użytkownikowi anulować uruchomione zadania;
- wiersze 74–92: kod wykonywany, gdy klasa nadrzędna powiadamia nas, że wszystkie uruchomione zadania zostały zakończone;
- wiersz 77: przywracamy menu do stanu początkowego. Metoda [initMenu] (wiersze 95–102) wyświetla menu ze wszystkimi opcjami z wyjątkiem opcji [Actions/Annuler], która jest ukryta;
- wiersze 80–91: wyświetlana jest liczba otrzymanych odpowiedzi;
Kliknięcie opcji menu [Annuler] jest obsługiwane przez następujący kod:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// anulowanie zadań asynchronicznych
cancelRunningTasks();
}
- wiersz 7: wysyłane jest żądanie do klasy nadrzędnej o anulowanie nadal aktywnych zadań;
Kliknięcie opcji menu [Terminer] jest obsługiwane przez następujący kod:
@OptionsItem(R.id.actionTerminer)
protected void doTerminer() {
// zatrzymuje się wszystko
System.exit(0);
}
Zarządzanie cyklem życia fragmentu zapewniają następujące metody:
// zarządzanie cyklem życia ---------------------------------------------------------------------------------------
@Override
public CoreState saveFragment() {
return new CoreState();
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
// Pierwsza wizyta?
if (previousState == null) {
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
- wiersze 3–6: służą do zapisania stanu fragmentu w klasie pochodnej od [CoreState]. Jeśli fragment nie ma stanu do zapisania, jak w tym przypadku, wystarczy zwrócić instancję klasy [CoreState]. Nie należy zwracać klasy null, ponieważ doprowadziłoby to później do awarii;
- wiersze 8–11: muszą zwracać numer widoku. W tym przypadku fragment [MeteoFragment] ma numer 0;
- wiersze 13–16: służą do zainicjowania fragmentu po jego utworzeniu (previousState==null) lub odtworzeniu (previousState!=null). W tym przypadku nie ma nic do zrobienia. Jedynym polem, które może wymagać zainicjowania, jest następujące:
// miasta, dla których potrzebna jest prognoza pogody
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
ale inicjuje się ono samoczynnie;
- wiersze 18–24: służą do zainicjowania widoku powiązanego z fragmentem po jego utworzeniu (previousState==null) lub odtworzeniu (previousState!=null);
- wiersze 21–23: jeśli jest to pierwsza wizyta w fragmencie, inicjuje się jego menu, aby ukryć opcję [Annuler];
- wiersze 27–30: wywoływane, jeśli przejście do fragmentu nastąpiło w wyniku nawigacji za pomocą akcji typu [SUBMIT]. W tym przypadku nie ma nawigacji między fragmentami, ponieważ istnieje tylko jeden fragment;
- wiersze 32–35: wywoływane podczas cyklu tworzenia kopii zapasowej / przywracania spowodowanego obróceniem urządzenia lub z innego powodu. W tym przypadku, ponieważ nie zapisano stanu, nie ma nic do zrobienia;
- wiersze 37–40: wywoływane po wykonaniu wszystkich poprzednich aktualizacji. W tym przypadku nie ma nic do zrobienia;
2.8.2.6. Tests
Teraz uruchamiamy przykład:


Wynikowe logi wyglądają następująco:
07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructeur
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructeur, thread=main
07-23 13:24:32.861 2642-2642/client.android D/client.android.dao.service.Dao_: afterInject
07-23 13:24:32.950 2642-2642/client.android D/MainActivity_: onCreate
07-23 13:24:32.951 2642-2642/client.android D/client.android.dao.service.Dao_: setTimeout thread=main, timeout=1000
07-23 13:24:32.952 2642-2642/client.android D/client.android.dao.service.Dao_: setBasicAuthentification thread=main, isBasicAuthentificationNeeded=false
07-23 13:24:33.041 2642-2642/client.android D/MainActivity_: adding loadingPanel
07-23 13:24:33.043 2642-2642/client.android D/MeteoFragment_: constructeur
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigation vers vue 0 sur action NONE
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: onCreateActivity
07-23 13:24:33.080 2642-2642/client.android D/MainActivity_: onResume
07-23 13:24:33.325 2642-2642/client.android D/MeteoFragment_: onActivityCreated
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: onCreateOptionsMenu
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: getMenuOptionsStates(Menu)
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: Nombre d'options de menu=4
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initFragment initView updateForFirstVisit
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initMenu
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: session={"action":"NONE","coreStates":[{"@type":"CoreState","hasBeenVisited":false,"menuOptionsState":null}],"previousTab":0,"previousView":0}
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: état précédent=null
07-23 13:24:33.558 2642-2642/client.android D/MeteoFragment_: notifyEndOfUpdates
07-23 13:24:39.766 2642-2642/client.android D/MeteoFragment_: beginWaiting
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=angers, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=le mans, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=nantes, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=laval, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=la roche sur yon, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:45.035 2642-2961/client.android D/client.android.dao.service.Dao_: response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200} sur thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-3]
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: initMenu
- wiersze 32–36: odpowiedzi jSON są uzyskiwane z wątków wejścia/wyjścia
- wiersze 37–41: fragment pobiera 5 odpowiedzi z wątku interfejsu użytkownika;
Teraz wysyłamy żądanie z nieprawidłowym identyfikatorem API:
String APIID = "";

Wynikowe logi wyglądają następująco:
07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
- wiersze 3–6, 10: 5 wywołań HTTP wygenerowało 5 wyjątków;
- wiersz 7: fragment [MeteoFragment] odbiera pierwszy wyjątek. Następnie anuluje wszystkie zadania;
Teraz ustawmy czas oczekiwania na 5 sekund [IMainActivity.DELAY] i anulujmy operację. Logi będą wówczas wyglądały następująco:
07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
- wiersz 3: żądanie anulowania;
- wiersz 4: oczekiwanie zostało anulowane, ponieważ nastąpiło anulowanie;
- wiersze 6–10: anulowanie zadań powoduje wygenerowanie wyjątku w każdym z wątków wszystkich pięciu zadań. Rodzaj wyjątku zależy od aplikacji. W tym przypadku wyjątkiem jest [java.lang.InterruptedException], ponieważ zadania zostały przerwane podczas wykonywania instrukcji [Thread.sleep(delay)], która sztucznie wstrzymuje je na [delay] milisekund;
2.8.3. Przykład 16B
Przeprojektowujemy tutaj przykład 16 z paragrafu 1.17. Przedstawia on fragment kodu, który wykonuje asynchroniczne wywołania do serwera liczb losowych. Zobaczmy, jak zachowuje się on podczas obrotu urządzenia:

- w [1] urządzenie obraca się dwukrotnie;

Widać, że utracono wszystkie komunikaty o błędach. Spróbujemy to poprawić.
2.8.3.1. Projekt Przykład-16B
Kopiujemy projekt [client-android-skel] do projektu [exemples/Exemple-16B], a następnie ładujemy nowy projekt:
![]() |
Z pierwotnego projektu [Exemple-16] kopiujemy do projektu [Exemple-16B] następujące elementy:
- plik [res/layout/vue1.xml], folder [res/values]:
![]() |
Zmienimy górny margines widoku [vue1.xml] na 80 dp:
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
- fragment [Vue1Fragment]:
![]() |
- klasę [dao / service / Response]:
![]() |
Na tym etapie można podjąć próbę pierwszej kompilacji:
- pierwszym rodzajem błędów są te dotyczące klasy imports. Podczas migracji do klasy [Exemple-16B] niektóre klasy zostały przeniesione do innych pakietów. Najpierw należy skorygować ten rodzaj błędów;
- drugi rodzaj błędu dotyczy klasy [Vue1Fragment], ponieważ nie implementuje ona metod wymaganych przez klasę nadrzędną [AbstractParent]. Należy je wygenerować automatycznie;
Próbujemy przeprowadzić drugą kompilację:
- wszystkie pozostałe błędy dotyczą teraz klasy [Vue1Fragment], która zostanie poddana największej liczbie zmian;
2.8.3.2. Tworzenie raportu dla fragmentu [Vue1Fragment]
Zauważyliśmy, że niektóre informacje z fragmentu będą musiały zostać zapisane podczas rotacji, aby przywrócić fragment do stanu sprzed rotacji. Tworzymy zatem stan [Vue1FragmentState], który na razie jest pusty:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.3.3. Dostosowanie projektu
![]() |
Interfejs [IMainActivity] pozwala określić niektóre cechy projektu:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// logowanie
ISession getSession();
// zmiana widoku
void navigateToView(int position, ISession.Action action);
// zarządzanie kolejką
void beginWaiting();
void cancelWaiting();
// stałe aplikacji -------------------------------------
// tryb debugowania
boolean IS_DEBUG_ENABLED = true;
// maksymalny czas oczekiwania na odpowiedź serwera
int TIMEOUT = 1000;
// czas oczekiwania przed wykonaniem żądania klienta
int DELAY = 5000;
// uwierzytelnianie podstawowe
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// sąsiedztwo fragmentów
int OFF_SCREEN_PAGE_LIMIT = 1;
// pasek kart
boolean ARE_TABS_NEEDED = false;
// obraz oczekiwania
boolean IS_WAITING_ICON_NEEDED = true;
// liczba fragmentów aplikacji
int FRAGMENTS_COUNT = 1;
}
- wiersze 25, 28, 31, 40: cechy warstwy [DAO]. Nie jest wymagane uwierzytelnianie podstawowe;
- wiersz 34: sąsiedztwo fragmentów. W tym przypadku ta stała nie ma znaczenia, ponieważ występuje tylko jeden fragment;
- wiersz 37: nie jest to aplikacja z zakładkami;
- wiersz 43: występuje tylko jeden fragment;
Klasa [CoreState], która przechowuje stan fragmentów, będzie wyglądać następująco:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// odwiedzony lub nieodwiedzony fragment
protected boolean hasBeenVisited = false;
// stan ewentualnego menu fragmentu
protected MenuItemState[] menuOptionsState;
// metody pobierające i ustawiające
...
}
- wiersz 12: deklarujemy klasę stanu fragmentu [Vue1Fragment];
Klasa [Session] ma następujący wygląd:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elementy, których nie można zserializować w jSON, muszą posiadać adnotację @JsonIgnore
}
Jest ona pusta, ponieważ w tej aplikacji nie ma komunikacji między fragmentami.
2.8.3.4. Warstwa [DAO]
![]() |
W warstwie [DAO] należy dostosować trzy klasy:
- interfejs IDao;
- jego implementacja Dao;
- interfejs WebClient do komunikacji z serwerem WWW / jSON;
Klasa [Response] pochodzi z projektu [Exemple-16], który z niej korzysta:
package client.android.dao.service;
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
...
}
Interfejs [WebClient] będzie wyglądał następująco:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// 1 liczba losowa z przedziału [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- wiersze 18–19: URL z usługi liczb losowych. Przypominamy, że odnosi się ona do URL korzeniowego (RestClientRootUrl, wiersz 12) klienta. W tym przypadku tym URL korzeniowym będzie [http://localhost:8080];
Interfejs [IDao] będzie wyglądał następująco:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// adres URL serwisu internetowego
void setUrlServiceWebJson(String url);
// użytkownik
void setUser(String user, String mdp);
// limit czasu klienta
void setTimeout(int timeout);
// uwierzytelnianie podstawowe
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// tryb debugowania
void setDebugMode(boolean isDebugEnabled);
// czas oczekiwania klienta przed wysłaniem żądania w milisekundach
void setDelay(int delay);
// usługa generowania liczb losowych
Observable<Response<Integer>> getAlea(int a, int b);
}
- przypominamy, że metody z wierszy 6–22 są domyślnie obecne w interfejsie IDao projektu [client-android-skel];
- wiersz 25: metoda [getAlea] pozwala uzyskać liczbę losową z przedziału [a,b]. Liczba ta jest uzyskiwana w odpowiedzi typu [Response<Integer>], gdzie liczba losowa znajduje się w polu [body] tego typu;
Interfejs [IDao] jest zaimplementowany przez następującą klasę [Dao]:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// klient serwisu internetowego
@RestService
protected WebClient webClient;
// bezpieczeństwo
@Bean
protected MyAuthInterceptor authInterceptor;
// RestTemplate
private RestTemplate restTemplate;
// fabryka RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// dziennik
Log.d(className, "afterInject");
// budujemy restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// montuje się konwerter jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// ustala się restTemplate klienta internetowego
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// ustawiamy URL serwisu internetowego
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// rejestruje się użytkownika w intercepterze
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// konfiguracja fabryczna
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// przechwytywacz uwierzytelniania?
if (isBasicAuthentificationNeeded) {
// dodajemy interceptor uwierzytelniający
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// metody prywatne -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// usługa generowania liczb losowych
@Override
public Observable<Response<Integer>> getAlea(final int a, final int b) {
// działanie klienta internetowego
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getAlea(a, b);
}
});
}
}
- przypominamy, że wiersze 17–85 są domyślnie obecne w klasie [Dao] projektu [client-android-skel]. Wystarczy tylko dodać metody implementujące interfejs [IDao];
- wiersze 88–97: implementacja metody [getAlea]. Jest ona bardzo prosta i zajmuje 6 wierszy (wiersze 91–96);
- wiersz 91: metoda [getResponse] jest metodą klasy nadrzędnej [AbstractDao]. Oczekuje ona parametru typu [IRequest<T>], gdzie T jest typem oczekiwanej odpowiedzi, w tym przypadku typu Response<Integer>. Typ T metody [IRequest<T>] (wiersz 91) musi być zgodny z typem T metody [Observable<T> getAlea] (wiersz 89);
- Interfejs [IRequest<T>] posiada tylko jedną metodę: getResponse. Jej zadaniem jest dostarczenie odpowiedzi typu T, którą powinna zwrócić metoda [Observable<T> getAlea];
- wiersz 94: to interfejs [WebClient] dostarcza tę odpowiedź. Przekazuje się mu dwa parametry otrzymane w wierszu 89. Z tego powodu muszą one posiadać atrybut final;
2.8.3.5. Aktywność [MainActivity]
![]() |
Aktywność [MainActivity] wygląda następująco:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// warstwa [DAO]
@Bean(Dao.class)
protected IDao dao;
// metody klasy nadrzędnej -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// kontynuujemy inicjalizacje rozpoczęte przez klasę nadrzędną
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// zdefiniuj fragmenty tutaj
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
// zdefiniuj tutaj tytuły fragmentów
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// nawigacja za pomocą zakładek – zdefiniuj widok do wyświetlenia
}
@Override
protected int getFirstView() {
return 0;
}
// interfejs IDao ------------------------------------------
@Override
public Observable<Response<Integer>> getAlea(int a, int b) {
return dao.getAlea(a, b);
}
}
- przypominamy, że wiersze 15–61 są domyślnie obecne w projekcie [client-android-skel]. Wystarczy je dostosować;
- wiersze 40–44: tabela fragmentów. W tym przypadku występuje tylko jeden;
- wiersze 47–51: nie są wymagane tytuły fragmentów;
- wiersze 53–56: nie ma tu zakładek;
- wiersze 58–61: pierwszym widokiem do wyświetlenia jest widok nr 0, czyli ten z [Vue1Fragment];
- wiersze 64–67: implementacja interfejsu [IDao]. Tutaj nie pozostaje nic innego, jak tylko przekazać zadanie warstwie [DAO] z wiersza 23;
2.8.3.6. Stan fragmentu [Vue1Fragment]
![]() |
Klasa [Vue1FragmentState] będzie wyglądać następująco:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.ArrayList;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// stan fragmentu ------------------------
// lista odpowiedzi
private List<String> reponses = new ArrayList<>();
// stan widoku ------------------------
// komunikat o błędzie dotyczący liczby żądanych liczb losowych
private boolean txtErrorAleasVisible = false;
// komunikat o błędzie dotyczący przedziału generowania [a,b]
private boolean txtErrorIntervalleVisible = false;
// komunikat o błędzie dotyczący URL usługi internetowej
private boolean txtMsgErreurUrlServiceWebVisible = false;
// komunikat o błędzie dotyczący czasu oczekiwania
private boolean textViewErreurDelayVisible = false;
// widoczność przycisku „Wykonaj”
private boolean btnExecuterVisible = true;
// metody pobierające i ustawiające
...
}
Aby ustalić, co należy zapisać w fragmencie, przeprowadzono obroty urządzenia w różnych sytuacjach i sprawdzono, co zniknęło podczas przywracania. Doszliśmy do wniosku, że należy zapisać informacje z wierszy 10–23.
2.8.3.7. Fragment [Vue1Fragment]
![]() |
Obecnie widok [Vue1Fragment] zawiera różne błędy wynikające z faktu, że zmieniła się klasa nadrzędna [AbstractFragment], od której pochodzi. Zamiast opisywać pojedynczo zmiany, które należy wprowadzić, omówimy bezpośrednio wersję końcową.
Szkielet fragmentu wygląda następująco:
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {
...
}
- w wierszu 26 przypominamy, że każdy fragment musi posiadać menu, nawet puste. Tak jest w tym przypadku.
2.8.3.7.1. Obsługa kliknięcia przycisku [Exécuter]
@Click(R.id.btn_Executer)
protected void doExecuter() {
// sprawdzanie wprowadzonych danych
if (!isPageValid()) {
return;
}
// kasowanie poprzednich odpowiedzi
reponses.clear();
dataAdapterReponses.notifyDataSetChanged();
// zerowanie licznika odpowiedzi
nbReponses = 0;
infoReponses.setText("Liste des réponses (0)");
// inicjowanie zadania
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// przygotowuje się zadanie losowe
beginWaiting(1);
// pobieranie liczb losowych
getAleasInBackground(nbAleas, a, b);
}
void getAleasInBackground(int nbAleas, int a, int b) {
// tworzy się proces, który ma być obserwowany
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getAlea(a, b));
}
// pobieranie liczb losowych
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// odbieramy odpowiedź
consumeAleaResponse(response);
}
});
}
protected void consumeAleaResponse(Response<Integer> response) {
// log
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// odpowiedź +
nbReponses++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
// analizujemy odpowiedź
// błąd?
if (response.getStatus() != 0) {
// wyświetlanie
showAlert(response.getMessages());
// anulowanie
doAnnuler();
// powrót do interfejsu użytkownika
return;
}
// dodajemy informację do listy odpowiedzi
reponses.add(0, String.valueOf(response.getBody()));
// odświeżanie odpowiedzi
dataAdapterReponses.notifyDataSetChanged();
}
// anulowanie ----------
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// anulowanie zadań asynchronicznych
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// wyświetla się symbol klepsydry
beginRunningTasks(nbRunningTasks);
// przycisk [Annuler] zastępuje przycisk [Exécuter]
btnExecuter.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
- wiersze 4–6: najpierw sprawdzamy, czy wprowadzone dane są prawidłowe. Mogą wtedy pojawić się komunikaty o błędach;
- wiersze 8–9: lista odpowiedzi jest czyszczona. Zmiana ta jest odzwierciedlana w ListView, który je wyświetla;
- wiersze 11–12: liczba otrzymanych odpowiedzi jest zerowana;
- wiersz 14: ustala się URL usługi liczb losowych. Informacja ta zostanie przekazana do warstwy [DAO];
- wiersz 15: ustala się czas oczekiwania przed wysłaniem żądania do serwisu liczb losowych. Informacja ta zostanie przekazana do warstwy [DAO];
- wiersz 17: przygotowujemy się do uruchomienia 1 zadania asynchronicznego (a nie N, zobaczymy dlaczego);
- wiersze 24–27: z N zadań asynchronicznych tworzymy jedną sekwencję operacji [merge];
- wiersze 29–36: zwracamy się do klasy nadrzędnej [AbstractParent] o wysłanie zapytania do serwisu internetowego / jSON o liczby losowe;
- wiersze 29–36: metoda [executeInBackground] oczekuje dwóch parametrów:
- wiersz 29: proces, który ma być obserwowany i uruchomiony, to ten, który został obliczony w poprzednich wierszach;
- wiersze 29–36: instancja [Action1], która ma zostać uruchomiona po otrzymaniu odpowiedzi z serwisu asynchronicznego. Typ T instancji [Action1<T>] musi być typem T wyniku metody [getAlea], tj. typem [Response<Integer>];
- wiersz 34: gdy nadejdzie odpowiedź (liczba losowa), jest ona przetwarzana w metodzie z wiersza 39;
- wiersze 49–50: odnotowuje się i sygnalizuje, że otrzymano nową odpowiedź;
- wiersze 53–60: typ [Response<T>] posiada pole [status], które jest kodem błędu. Jeśli kod ten jest różny od zera, oznacza to, że serwer napotkał problem;
- wiersz 55: wyświetlany jest komunikat o błędzie. Metoda [showAlert] należy do klasy nadrzędnej;
- wiersz 57: wywoływana jest metoda z wierszy 68–75. Spowoduje ona anulowanie nadal aktywnych zadań (wiersz 74);
- wiersz 62: odpowiedź zostaje dodana do listy odpowiedzi, która stanowi źródło danych dla ListView;
- wiersz 64: obiekt ListView jest odświeżany;
- wiersze 77–83: metoda [beginWaiting(int nbRunningTasks)] przygotowuje widok do oczekiwania (wiersze 81–82) i przekazuje klasie nadrzędnej informację, że zadania [nbRunningTasks] wkrótce zostaną wykonane (wiersz 79);
2.8.3.7.2. Cykl życia fragmentu
Cykl życia fragmentu jest obsługiwany przez następujące metody:
// dane lokalne
private List<String> reponses;
private ArrayAdapter<String> dataAdapterReponses;
private int nbReponses = 0;
...
// zarządzanie cyklem życia ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// aktualny stan widoku
Vue1FragmentState state = new Vue1FragmentState();
state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
state.setReponses(reponses);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// Pierwsza wizyta?
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
reponses = state.getReponses();
} else {
reponses = new ArrayList<>();
}
// źródło danych z listView
dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
// liczba odpowiedzi
nbReponses = reponses.size();
}
@Override
protected void initView(CoreState previousState) {
// powiązanie listview / adapter
listReponses.setAdapter(dataAdapterReponses);
// Pierwsza wizyta?
if (previousState == null) {
// ukrywanie komunikatów o błędach
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// przyciski
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// poprzedni stan widoku
Vue1FragmentState state = (Vue1FragmentState) previousState;
// wyświetlanie / ukrywanie komunikatów o błędach
txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
// przyciski
btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
// liczba odpowiedzi
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// przycisk [Exécuter] zastępuje przycisk [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
- wiersze 7–18: zapewniają zapisanie fragmentu, gdy klasa nadrzędna tego zażąda;
- wiersz 11: wyświetlenie komunikatu o błędzie dotyczącego czasu oczekiwania;
- wiersz 12: wyświetlanie komunikatu o błędzie dotyczącego liczby żądanych liczb losowych;
- wiersz 13: wyświetlanie komunikatu o błędzie dotyczącego URL serwisu internetowego / jSON;
- wiersz 14: widoczność komunikatu o błędzie dotyczącego przedziału [a,b] generowania liczb losowych;
- wiersz 15: widoczność przycisku [Exécuter];
- wiersz 16: lista otrzymanych odpowiedzi;
- wiersze 20–23: muszą zwracać numer widoku. Numer fragmentu wynosi tutaj 0, ponieważ jest tylko jeden;
- wiersze 25–38: inicjalizacja pól fragmentu, zarówno przy pierwszej wizycie (previousState==null), jak i przy kolejnej wizycie;
- wiersze 29–30: jeśli nie jest to pierwsza wizyta, pole [reponses] jest przywracane na podstawie poprzedniego stanu fragmentu;
- wiersze 31–33: jeśli jest to pierwsza wizyta, wówczas pole [reponses] jest inicjowane pustą listą;
- wiersze 34–37: na podstawie pola [reponses] można utworzyć źródło danych dla pola ListView fragmentu (wiersz 35), a także liczbę odpowiedzi (wiersz 37);
- wiersze 40–55: wykonywane w celu zainicjowania widoku powiązanego z fragmentem, zarówno podczas pierwszej wizyty (previousState==null), jak i podczas kolejnych wizyt;
- wiersz 43: przypisuje się ListView fragmentu do źródła danych, które zostało właśnie utworzone w metodzie [initFragment];
- wiersze 45–54: jeśli jest to pierwsza wizyta, przygotowuje się widok do pierwszego wyświetlenia;
- wiersze 57–60: wykonywane podczas nawigacji między fragmentami powiązanej z akcją typu [SUBMIT]. W tym przypadku występuje tylko jeden fragment, a zatem nie ma nawigacji między fragmentami;
- wiersze 63–76: wykonywane podczas nawigacji między fragmentami powiązanej z akcją typu [NAVIGATION] lub podczas cyklu zapisywania/przywracania spowodowanego obróceniem urządzenia lub z innego powodu. W tym przypadku może wystąpić wyłącznie ten ostatni scenariusz. Należy pamiętać, że w każdym przypadku [previousState] nigdy nie jest równoznaczne z null;
- wiersz 65: poprzedni stan jest rzutowany na typ stanu fragmentu;
- wiersze 66–75: wykorzystuje się zawartość poprzedniego stanu do przywrócenia widoku;
- wiersze 78–81: wywoływane po wykonaniu wszystkich poprzednich aktualizacji. W tym miejscu nie ma nic do zrobienia;
- wiersze 83–89: wykonywane po zakończeniu wszystkich zadań asynchronicznych. Tutaj ukrywamy przycisk [Annuler], aby zastąpić go przyciskiem [Exécuter];
2.8.3.8. Testy
Zachęcamy czytelnika do przeprowadzenia następujących testów:
- wywołać błędy i uruchomić urządzenie: komunikaty o błędach powinny pozostać widoczne;
- uzyskać liczby losowe i uruchomić urządzenie: uzyskane liczby losowe powinny pozostać wyświetlone;
- wprowadzić kilkusekundowe oczekiwanie i uruchomić urządzenie w trakcie oczekiwania: zadania powinny zostać anulowane (widać to w logach);
2.8.4. Przykład 22B
W tym miejscu powracamy do przykładu 22, aby go refaktoryzować zgodnie z modelem projektu [client-android-skel]. Przypominamy, że projekt [Exemple-22] poprawnie obsługuje cykl zapisywania i przywracania fragmentów podczas rotacji i to właśnie on posłużył za podstawę dla projektu [client-android-skel].
Duplikujemy projekt [client-android-skel] do [exemples/Exemple-22B] i ładujemy ten ostatni projekt:
![]() |
Następnie kopiujemy różne elementy z projektu [Exemple-22] do projektu [Exemple-22B].
Najpierw kopiujemy elementy z folderu [res]:
- [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, folder [values];
![]() |
Zmienimy górny margines obu widoków na 120 dp:
[vue1.xml]:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="120dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
[fragment_main]:
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>
Następnie skopiujemy elementy [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
Na tym etapie możemy spróbować przeprowadzić pierwszą kompilację. Pojawia się pierwszy rodzaj błędów: dotyczą one nieprawidłowych elementów imports, ponieważ klasy zostały przeniesione do innych pakietów. Poprawiamy te elementy imports. Drugi rodzaj błędów wynika z faktu, że fragmenty nie implementują wszystkich metod swojej klasy nadrzędnej [AbstractFragment]. Poprawiamy je za pomocą skrótu (Alt+Enter).
Pozostałe błędy wynikają z różnic między starą a nową klasą [AbstractFragment]. Na razie je pomijamy.
2.8.4.1. Dostosowywanie projektu
![]() |
W folderze [custom] znajdują się elementy architektury, które programista może dostosować do własnych potrzeb.
Interfejs [IMainActivity] umożliwia określenie niektórych cech projektu:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// dostęp do sesji
ISession getSession();
// zmiana widoku
void navigateToView(int position, ISession.Action action);
// zarządzanie kolejką
void beginWaiting();
void cancelWaiting();
// tryb debugowania
boolean IS_DEBUG_ENABLED = true;
// maksymalny czas oczekiwania na odpowiedź serwera
int TIMEOUT = 1000;
// czas oczekiwania przed wykonaniem żądania klienta
int DELAY = 0;
// uwierzytelnianie podstawowe
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// sąsiedztwo fragmentów
int OFF_SCREEN_PAGE_LIMIT = 1;
// pasek kart
boolean ARE_TABS_NEEDED = true;
// obraz oczekiwania
boolean IS_WAITING_ICON_NEEDED = false;
// liczba fragmentów
int FRAGMENTS_COUNT = 5;
}
- wiersze 23, 26, 29, 38: cechy warstwy [DAO]. Tutaj ich nie ma;
- wiersz 41: występuje tu pięć fragmentów;
- wiersz 32: sąsiedztwo fragmentów. Stała ta może tutaj przyjmować wartość z [1,4]. Zachęcamy czytelnika do zmiany tej wartości, aby sprawdzić, czy aplikacja nadal działa;
- wiersz 35: jest to aplikacja z zakładkami;
Klasa [CoreState], która przechowuje stan fragmentów, będzie wyglądać następująco:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
// odwiedzony lub nieodwiedzony fragment
protected boolean hasBeenVisited = false;
// stan ewentualnego menu fragmentu
protected MenuItemState[] menuOptionsState;
// metody pobierające i ustawiające
...
}
- wiersz 12: deklarujemy klasę stanu fragmentu [PlaceHolderFragment]. Fragment [Vue1Fragment] nie posiada natomiast stanu;
Klasa [Session] ma następujący wygląd:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// dane do współdzielenia między samymi fragmentami oraz między fragmentami a aktywnością
// elementy, których nie można zserializować w jSON, muszą posiadać adnotację @JsonIgnore
// nie zapomnij o metodach getter i setter niezbędnych do serializacji / deserializacji w formacie jSON
// liczba odwiedzonych fragmentów
private int numVisit;
// numer fragmentu typu [PlaceholderFragment] wyświetlany w drugiej zakładce
private int numFragment = -1;
// metody pobierające i ustawiające
...
}
Jest to sesja projektu [Exemple-22].
2.8.4.2. Czynność [MainActivity]
![]() |
Czynność [MainActivity] wygląda następująco:
package client.android.activity;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// warstwa [DAO]
@Bean(Dao.class)
protected IDao dao;
// sesja
private Session session;
// zarządzanie menu-----------------------
@Override
public boolean onOptionsItemSelected(MenuItem item) {
...
}
private void showFragment(int i) {
...
}
// implementacja metod klasy nadrzędnej ---------------------------------------------------
...
}
W tym przypadku klasa [MainActivity] jest bardziej rozbudowana niż w poprzednich przykładach z dwóch powodów:
- należy zarządzać zakładkami;
- należy obsłużyć menu;
2.8.4.2.1. Implementacja metod klasy nadrzędnej
// metody klasy nadrzędnej -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// kontynuujemy inicjalizacje rozpoczęte przez klasę nadrzędną
// sesja
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// numer fragmentu
final String ARG_SECTION_NUMBER = "section_number";
// inicjalizacja tablicy fragmentów
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// tworzymy 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[i] = new Vue1Fragment_();
// wynik
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
// brak nagłówków
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
...
}
@Override
protected int getFirstView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
- wiersze 2–12: metoda [onCreateActivity] jest wywoływana przez klasę nadrzędną [AbstractActivity] podczas pierwszego utworzenia aktywności lub jej ponownego utworzenia w cyklu zapisu/przywracania. W momencie wywołania tej metody klasa nadrzędna przywróciła już sesję;
- wiersz 10: pobierane jest lokalne odwołanie do sesji. Zmiana typu wynika z faktu, że sesja klasy nadrzędnej jest typu [AbstractSession];
- wiersze 19–38: metoda [getFragments] musi zwrócić klasie nadrzędnej tablicę fragmentów zarządzanych przez aplikację. W tym przypadku jest ich [FRAGMENTS_COUNT], liczba zdefiniowana w [IMainActivity]. Pierwsze fragmenty [FRAGMENTS_COUNT-1] są typu [PlaceHolderFragment], a ostatni – typu [Vue1Fragment];
- wiersze 41–45: metoda [getFragmentTitle] powinna zwracać tytuły fragmentów, gdy informacja ta może być przydatna. W tym przypadku tak nie jest;
- wiersze 47–50: ta metoda jest wywoływana przez klasę nadrzędną, gdy użytkownik kliknie kartę. Powrócimy do tego w następnym akapicie;
- wiersze 52–55: zwraca numer pierwszego widoku, który ma zostać wyświetlony po uruchomieniu aplikacji. W tym przypadku jako pierwszy powinien zostać wyświetlony fragment [Vue1Fragment]. Metodę [getFirstView] można by z korzyścią zastąpić stałą w [IMainActivity];
2.8.4.2.2. Zarządzanie zakładkami
Karty są zarządzane za pomocą następujących metod:
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// kontynuujemy inicjalizacje rozpoczęte przez klasę nadrzędną
// sesja
this.session = (Session) super.session;
// 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);
}
}
@Override
protected void navigateOnTabSelected(int position) {
// numer fragmentu do wyświetlenia
int numFragment;
switch (position) {
case 0:
// nr fragmentu [Vue1Fragment]
numFragment = getFirstView();
break;
default:
// numer fragmentu [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// wyświetlanie fragmentu
if (numFragment != mViewPager.getCurrentItem()) {
navigateToView(numFragment, ISession.Action.SUBMIT);
}
}
}
- wiersze 1–20: metoda [onCreateActivity] jest wywoływana przez klasę nadrzędną [AbstractActivity] podczas pierwszego utworzenia aktywności lub jej ponownego utworzenia w cyklu zapisu/przywracania. W momencie wywołania tej metody klasa nadrzędna przywróciła już sesję;
- wiersz 9: pobierane jest lokalne odwołanie do sesji. Zmiana typu wynika z faktu, że sesja klasy nadrzędnej jest typu [AbstractSession];
- wiersze 11–13: tworzy się pierwszą zakładkę;
- wiersze 15–20: tworzy się drugą zakładkę, jeśli w sesji zapisany jest numer fragmentu (wiersz 15). Numer ten ma początkowo wartość -1 podczas pierwszego utworzenia aktywności;
- wiersze 23–39: ta metoda jest wywoływana przez klasę nadrzędną, gdy użytkownik kliknie na zakładkę;
- wiersze 28–31: jeśli kliknięto zakładkę 0, należy wyświetlić [Vue1Fragment]. Wiemy, że jest to pierwszy widok wyświetlony po uruchomieniu aplikacji;
- wiersze 32–35: jeśli kliknięto zakładkę 1, należy wyświetlić fragment, którego numer jest zapisany w sesji;
- wiersze 37–39: przechodzimy do wybranego fragmentu. Powiązaną akcją jest [SUBMIT]. Czy mogło to być [NAVIGATION]? W niniejszym dokumencie kod [NAVIGATION] stosuje się wyłącznie wtedy, gdy wyświetlenie nowego fragmentu wymaga jedynie znajomości jego poprzedniego stanu. W tym przypadku tak nie jest, ponieważ wyświetlany fragment musi różnić się od swojego poprzedniego stanu, aby uwzględnić dodatkową wizytę;
2.8.4.2.3. Zarządzanie menu
Ta aktywność jest powiązana z następującym menu: [menu_main.xml]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
które wyświetla następujące elementy:
![]() |
Zarządzanie menu odbywa się za pomocą następujących metod:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onOptionsItemSelected");
}
// przetwarzanie opcji menu
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d(className, "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// przetworzona pozycja
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// brak nawigacji po wybraniu zakładki programowej
session.setNavigationOnTabSelectionNeeded(false);
// odtwarzamy obie zakładki ze względu na czcionkę tytułów
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
// numer fragmentu do wyświetlenia jest ustalany na bieżąco
session.setNumFragment(i);
// wybieramy zakładkę nr 2 z nawigacją
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- wiersze 16–31: obsługa kliknięcia opcji menu typu [Fragmenti];
- wiersze 37–50: wyświetlają fragment nr i (są to fragmenty typu PlaceHolderFragment) w zakładce nr 1 (druga zakładka);
- wiersze 42–44: podejmuje się decyzję o usunięciu istniejących zakładek w celu utworzenia dwóch nowych. Decyzja ta została podjęta w celu obejścia następującego problemu: gdy po prostu wyświetla się fragment w istniejącej zakładce nr 1 (nie usuwając jej), co ciekawe, jego tytuł wygląda (czcionka, rozmiar) inaczej niż tytuł zakładki nr 0;
- wiersze 43–44: obie zakładki są tworzone, ale nie są zaznaczane (ostatni parametr w funkcji false);
- wiersz 40: operacje z wierszy 42–44 mogą spowodować wykonanie operacji [select] na zakładkach, co wywoła menedżera [onTabSelected]. Jeśli nie podejmie się żadnych działań, nastąpi przejście do fragmentu. Można temu zapobiec, ustawiając wartość logicznego [navigationOnTabSelectionNeeded] na faux w sesji. Ta wartość logiczna jest automatycznie przywracana do vrai przez klasę [AbstractFragment], gdy fragment staje się widoczny;
- wiersz 46: zapisujemy numer fragmentu do wyświetlenia w sesji;
- wiersze 48–50: wybiera się zakładkę nr 2 z nawigacją (wiersz 48). Spowoduje to uruchomienie procedury [onTabSelected], która:
- wyświetli fragment, którego numer został zapisany w sesji;
- zapisze w sesji numer wybranej zakładki;
2.8.4.3. Fragment [Vue1Fragment]
Poniżej przedstawiamy ostateczną wersję fragmentu:
package client.android.fragments.behavior;
import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
// 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(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// cykl życia fragmentu -----------------------------------------------
private void initFragment() {
// nie ma nic do zrobienia
}
// zapis stanu fragmentu
@Override
public CoreState saveFragment() {
// stan widoku – nie ma czego zapisywać
return new CoreState();
}
@Override
protected int getNumView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// nie ma nic do zrobienia
}
@Override
protected void initView(CoreState previousState) {
// pierwsza wizyta?
if (previousState == null) {
// wyświetla się numer wizyty
showNumVisit();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// wyświetla się numer wizyty
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// metody prywatne -------------------------------------
// wyświetlanie numeru wizyty
private void showNumVisit() {
// zwiększ numer wizyty
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// wyświetla numer wizyty
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
Klasa jest prawie pusta.
- wiersze 35–39: wywoływane przez klasę nadrzędną, gdy fragment musi zapisać swój stan. Fragment [Vue1Fragment] nie ma stanu do zapisania. Zwracamy po prostu instancję klasy bazowej [CoreState] (przypomnienie: nie wolno zwracać null);
- wiersze 41–44: muszą zwracać numer fragmentu. Fragment [Vue1Fragment] ma z definicji numer [FRAGMENTS_COUNT-1];
- wiersze 51–59: wywoływane przez klasę nadrzędną, gdy fragment jest tworzony po raz pierwszy (previousState == null) lub przy kolejnych tworzeniach (previousState != null);
- wiersze 54–57: jeśli jest to pierwsza wizyta, zwiększa się liczbę wizyt i wyświetla się ją (wiersze 85–92);
- wiersze 61–65: wywoływane, gdy fragment ma zostać wyświetlony w powiązaniu z akcją [SUBMIT]. Zwiększa się numer wizyty i wyświetla go. W tym przypadku nie jest możliwe, aby numer wizyty został zwiększony dwukrotnie w cyklu życia. W rzeczywistości pierwsza wizyta w fragmencie [Vue1Fragment] ma miejsce przy uruchomieniu aplikacji, gdy akcja ma wartość [NONE] z założenia w sesji. Gwarantuje to, że metoda [updateOnSubmit] nie zostanie wywołana. Następnie nigdy więcej nie będzie to pierwsza wizyta, a metoda [initView] nie wykona żadnej operacji;
- wiersze 68–71: wywoływane w cyklu zapisywania/przywracania. Ponieważ fragment nie ma stanu, nie ma tu nic do przywrócenia;
- wiersze 73–76: wywoływane po wykonaniu wszystkich poprzednich aktualizacji. W tym momencie nie ma już nic więcej do zrobienia;
- wiersze 78–81: wywoływane, gdy wszystkie uruchomione zadania asynchroniczne zostaną zakończone. W tym przypadku nie ma żadnych zadań asynchronicznych;
2.8.4.4. Stan [PlaceHolderFragmentState]
Raport fragmentu [PlaceHolderFragment] będzie wyglądał następująco:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class PlaceHolderFragmentState extends CoreState {
// tekst
private String text;
// konstruktorzy
public PlaceHolderFragmentState() {
}
public PlaceHolderFragmentState(String text) {
super();
this.text = text;
}
// metody pobierające i ustawiające
...
}
- gdy zajdzie potrzeba zapisania stanu fragmentu, zostanie zapisany tekst, który był w nim wyświetlany (wiersz 7);
2.8.4.5. Fragment [PlaceHolderFragment]
Fragment [PlaceHolderFragment] będzie wyglądał następująco:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
// komponenty interfejsu wizualnego
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// dane
private String text;
// numer fragmentu
private static final String ARG_SECTION_NUMBER = "section_number";
// implementacja metod klasy nadrzędnej ----------------------------
@Override
public CoreState saveFragment() {
// zapisywanie stanu fragmentu
PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
placeHolderFragmentState.setText(textViewInfo.getText().toString());
return placeHolderFragmentState;
}
@Override
protected int getNumView() {
return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// tekst oryginalny
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// aktualizacja wyświetlanego tekstu
// zwiększanie liczby odwiedzin
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// zmodyfikowany tekst
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
}
}
@Override
protected void updateOnRestore(CoreState previousState) {
// przywracamy wyświetlany tekst
PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
textViewInfo.setText(state.getText());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
}
- wiersze 30–36: gdy klasa nadrzędna żąda od fragmentu zapisania jego stanu, zapisuje się tekst wyświetlany przez fragment (wiersz 34);
- wiersze 38–41: zwracają numer fragmentu. Zależy on od numeru sekcji, który został mu przekazany jako argument podczas tworzenia;
- wiersze 43–47: wywoływane podczas pierwszego utworzenia fragmentu (previousState==null) lub podczas kolejnych (previousState !=null);
- wiersz 46: w tym miejscu nie wykorzystuje się poprzedniego stanu. Pierwotny tekst [text] (wiersz 24), wyświetlany podczas pierwszej wizyty, jest za każdym razem przeliczany na nowo. Jest to dyskusyjne. Można było zdecydować się na umieszczenie tej informacji również w stanie fragmentu;
- wiersze 49–51: wywoływane podczas pierwszego tworzenia widoku powiązanego z fragmentem (previousState==null) lub podczas kolejnych (previousState!=null). Nie ma tu nic do zrobienia;
- wiersze 53–56: wywoływane, gdy fragment ma zostać wyświetlony w powiązaniu z akcją [SUBMIT]. Dzieje się tak zawsze, z wyjątkiem cyklu zapisywania/przywracania, w którym akcją jest [RESTORE]. Zwiększamy więc numer wizyty i wyświetlamy go;
- wiersze 68–74: wywoływane w cyklu zapisu/przywracania. Przywracamy tekst, który został zapisany w stanie fragmentu;
- wiersze 76–79: wywoływane po wykonaniu wszystkich poprzednich aktualizacji. W tym przypadku nie ma już nic więcej do zrobienia;
- wiersze 82–83: wywoływane, gdy wszystkie uruchomione zadania asynchroniczne zostaną zakończone. W tym przypadku nie ma żadnych zadań asynchronicznych;
2.8.4.6. Tests
Zachęcamy czytelnika do przetestowania aplikacji poprzez obrót urządzenia w celu sprawdzenia, czy wyświetlany fragment nie traci swojego stanu. Należy również przejrzeć logi.
2.9. Conclusion
Po zakończeniu tego rozdziału dysponujemy projektem wzorcowym [client-android-skel] przedstawiającym klienta na Androida komunikującego się z serwisem internetowym / jSON, o następujących cechach:
- asynchroniczna komunikacja z serwerem internetowym / jSON odbywa się za pomocą biblioteki RxJava;
- cykl życia fragmentu (aktualizacja, zapis, przywrócenie) jest zarządzany przez jego klasę nadrzędną [AbstractFragment], która w określonych momentach wywołuje określone metody swoich klas podrzędnych. Fragment podrzędny nie musi zatem zajmować się etapami cyklu życia, a jedynie zaimplementować określone metody narzucone przez klasę nadrzędną;
- cykl życia aktywności (zapis / przywrócenie) jest zarządzany przez klasę abstrakcyjną [AbstractActivity], która również nakłada na aktywność potomną obowiązek zaimplementowania określonych metod;
- klasa [AbstractActivity] może obsługiwać aplikację z zakładkami lub bez, z obrazem oczekiwania lub bez, z podstawowym uwierzytelnianiem na serwerze WWW / jSON lub bez. Obecność lub brak tych elementów ustala się poprzez konfigurację;
Przedstawimy teraz studium przypadku bardziej złożone niż poprzednie przykłady. Nowa aplikacja będzie oparta na projekcie wzorcowym [client-android-skel].



















































