1. Android-programmeren leren
De PDF van het document is beschikbaar |HIER|.
De voorbeelden uit het document zijn beschikbaar |HIER|.
1.1. Introduction
1.1.1. Inhoud
Dit document is een herschrijving van verschillende bestaande documenten:
- Inleiding tot het programmeren van Android-tablets aan de hand van voorbeelden;
- Een Arduino aansturen met een Android-tablet;
- Inleiding tot het programmeren van Android-tablets aan de hand van voorbeelden – versie 2
en introduceert de volgende nieuwigheden:
- document 1 presenteerde een architectuur genaamd AVAT (Activiteit-Weergaven-Acties-Taken) om asynchroon programmeren in een Android-app te vergemakkelijken. In dit document wordt de standaardbibliotheek RxJava gebruikt om asynchrone acties te beheren;
- in document 2 werd gebruikgemaakt van IDE Eclipse met een Android-plug-in. In dit document wordt Android Studio gebruikt;
- document 3 is ongewijzigd overgenomen;
- in document 4 werd de bibliotheek [Android Annotations] (AA) gebruikt in combinatie met de IntelliJ Community Edition IDE IDEA. Dit document bevat het volledige document 4 met de volgende verschillen:
- IDE is nu Android Studio;
- het buildsysteem is Gradle voor alle client- of serverprojecten (in document 4 werd soms Maven gebruikt)
- asynchrone programmering wordt uitgevoerd met de bibliotheek RxJava (in document 4 werd de bibliotheek AA gebruikt);
- dit document gaat in op onderwerpen die in de vorige documenten niet of nauwelijks aan bod zijn gekomen:
- het begrip 'adjacentie van fragmenten';
- het opslaan en herstellen van de activiteit en de bijbehorende fragmenten;
- de levenscyclus van fragmenten;
Ten slotte wordt het raamwerk gepresenteerd van een Android-client die communiceert met een webservice / jSON, waarin een groot aantal elementen is gefactoreerd die regelmatig in dit soort clients voorkomen. Dit raamwerk wordt in alle voorbeelden vanaf hoofdstuk 2 overgenomen. Dit is het echt innovatieve deel van het document.
De volgende voorbeelden worden gepresenteerd:
Aard | |
Een bestaand Android-project importeren | |
Een eenvoudig Android-project | |
Een eenvoudig [Android Annotations]-project | |
Weergaven en gebeurtenissen | |
Navigatie tussen weergaven | |
Navigatie via tabbladen | |
Gebruik van de bibliotheek [Android Annotations] met Gradle | |
Beheer van fragmenten in een Android-app | |
Navigatie tussen weergaven opnieuw bekeken | |
Tweelaagse architectuur | |
Client-serverarchitectuur | |
Asynchronisme beheren met RxJava | |
Componenten voor gegevensinvoer | |
Gebruik van een weergavesjabloon | |
De component ListView | |
Een menu gebruiken | |
Een bovenliggende klasse gebruiken voor fragmenten | |
De status van de activiteit en de fragmenten opslaan en herstellen | |
Weer-app | |
Basis van een Android-client die communiceert met een webservice / jSON. Hierin worden een groot aantal elementen onderverdeeld die regelmatig in dit soort Android-clients voorkomen. | |
Afspraakbeheer voor een artsenpraktijk | |
Toepassingsopdracht – Beheer van een eenvoudige salarisadministratie | |
Praktijkoefening – Besturing van Arduino-kaarten |
Dit document werd gebruikt in het laatste jaar van de ingenieursopleiding IstiA aan de universiteit van Angers [istia.univ-angers.fr]. Dit verklaart de soms wat eigenaardige toon van de tekst. De twee praktijkopdrachten zijn teksten van TP, waarvan alleen de hoofdlijnen van de oplossing worden gegeven. De lezer moet de oplossing zelf uitwerken.
De broncode van de voorbeelden is beschikbaar |ICI|. Om deze voorbeelden uit te voeren, moet u de procedure in paragraaf 6.12 volgen.
Dit document is bedoeld als inleiding tot het programmeren voor Android. Het is niet bedoeld als uitputtende handleiding. Het richt zich voornamelijk op beginners.
De referentiewebsite voor Android-programmeren is te vinden op URL [http://developer.android.com/guide/components/index.html]. Daar moet u terecht voor een algemeen overzicht van Android-programmeren.
1.1.2. Vereisten
Om optimaal gebruik te kunnen maken van dit document, is een goede beheersing van de programmeertaal Java vereist.
1.1.3. Gebruikte tools
De volgende voorbeelden zijn getest in de volgende omgeving:
- Windows 10 Pro 64-bits;
- JDK 1.8;
- Android SDK API 23;
- Android Studio, versie 2.1;
- Genymotion-emulator, versie 2.6.0;
Om dit document te kunnen volgen, moet u het volgende installeren:
- een JDK (zie paragraaf 6.8);
- de Genymotion Android-emulatormanager (zie paragraaf 6.9);
- de Maven-afhankelijkheidsmanager (zie paragraaf 6.10);
- de IDE [Android Studio] (zie paragraaf 6.11);
1.2. Voorbeeld-01: een Android-voorbeeld importeren
1.2.1. Het project aanmaken
Laten we met Android Studio een eerste Android-project aanmaken. Laten we eerst een lege map [exemples] aanmaken waarin al onze projecten zullen worden geplaatst:
![]() |
en vervolgens maken we een project aan met Android Studio. We gaan eerst een van de voorbeelden importeren die bij de IDE [1-5] worden geleverd:
![]() |

![]() | ![]() |
Het importeren van het project kan fouten opleveren doordat de omgeving die bij het aanmaken van het project is gebruikt, niet overeenkomt met de omgeving die hier voor de uitvoering ervan wordt gebruikt. Dit is een goede gelegenheid om te bekijken hoe dit soort fouten kunnen worden opgelost. Hier hebben we de volgende fout:
![]() | ![]() |
Het geïmporteerde project is geconfigureerd via het volgende bestand: [build.gradle] [2]:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
}
dependencies {
compile "com.android.support:support-v4:23.3.0"
compile "com.android.support:support-v13:23.3.0"
compile "com.android.support:cardview-v7:23.3.0"
}
// De voorbeeldbuild maakt gebruik van meerdere mappen om
// standaardcode en gemeenschappelijke code gescheiden te houden van
// de hoofdcode van het voorbeeld;
List<String> dirs = [
'main', // de hoofdcode van het voorbeeld; kijk hier voor de interessante onderdelen.
'common', // componenten die door meerdere voorbeelden worden hergebruikt
'template'] // standaardcode die wordt gegenereerd door het voorbeeldsjabloonproces
android {
compileSdkVersion 21
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 21
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
main {
dirs.each { dir ->
java.srcDirs "src/${dir}/java"
res.srcDirs "src/${dir}/res"
}
}
androidTest.setRoot('tests')
androidTest.java.srcDirs = ['tests/src']
}
aaptOptions {
noCompress "pdf"
}
}
- De gemelde fout is te wijten aan de regels 31, 34-35: we hebben geen SDK 21. We vervangen deze versie door versie 23, die we wel hebben.
In het bestand [build.gradle] doet Android Studio de volgende suggesties:
![]() |
Om de suggesties te accepteren, klik je op [alt-entrée] bij de suggestie:
![]() |
Er kan ook een foutmelding verschijnen met betrekking tot de Gradle-versie:
![]() |
Deze fout komt voort uit een discrepantie tussen de Gradle-versie die wordt gevraagd door het bestand [build.gradle] van het project (versie 2.10, regel 6 hieronder):
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
en de versie die is opgegeven in het bestand [<projet>/gradle/wrapper/gradle-wrapper.properties]:
#wo 10 apr 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
In regel 6, hierboven, moet 2,8 worden vervangen door 2,10.
Om toegang te krijgen tot het bestand [<projet>/gradle/wrapper/gradle-wrapper.properties], moet u de projectperspectief van het project gebruiken:
![]() | ![]() | ![]() |
Zodra dit is gecorrigeerd, kun je de applicatie [1] compileren, de Genymotion-emulator [2] starten en vervolgens het project [3] uitvoeren:
![]() | ![]() | ![]() |
![]() |

Laten we de applicatie stoppen:
![]() |
We kunnen het project nu sluiten. We gaan een nieuw project aanmaken.
![]() |
1.2.2. Enkele opmerkingen over de IDE
1.2.2.1. De perspectieven
IDE Android Studio (AS) biedt verschillende perspectieven om met een project te werken. We zullen er voornamelijk twee gebruiken:
- het perspectief [Android] [1]:
- de [Project] [4]-perspectief;
![]() | ![]() |
![]() |
Meestal werken we met het perspectief [Android]. Wanneer we een project naar een ander dupliceren, hebben we het perspectief [Project] nodig.
1.2.2.2. Beheer van de uitvoering
Er zijn verschillende manieren om een project AS uit te voeren, te stoppen of opnieuw uit te voeren. Allereerst zijn er de knoppen op de werkbalk:
![]() | ![]() | ![]() |
De knop [Rerun] [3] stopt de uitvoering van het project [2] en start het vervolgens opnieuw op [1].
1.2.2.3. Cachebeheer
Android Studio houdt een cache bij van de projecten die het beheert, om de IDE zo responsief mogelijk te maken. In versie Android 2.1 (mei 2016) hield deze cache vaak geen rekening met de codeaanpassingen die zojuist waren aangebracht. In dat geval moet deze cache worden geleegd:
![]() | ![]() |
Met Android 2.1 (mei 2016) moest de vorige handeling vaak worden herhaald en soms was dat niet voldoende om de gedetecteerde storing op te lossen. De oplossing was om de technologie [Instant Run] uit te schakelen:
![]() | ![]() |
- in [3-4], waarbij alles werd uitgeschakeld;
In het verdere verloop hebben we met deze cacheconfiguratie gewerkt en zijn we geen problemen tegengekomen.
1.2.2.4. Logboekbeheer
Tijdens de uitvoering van een project worden er logbestanden weergegeven in de Android-monitor:
![]() | ![]() |
In het tabblad [Android Monitor] [1] worden de logbestanden weergegeven in het tabblad [logcat] [2]. Met de knop [3] kunt u de logbestanden wissen. Deze knop is handig wanneer u de logbestanden van een bepaalde actie wilt bekijken:
- je wist de logbestanden;
- voert u op het Android-apparaat de actie uit waarvan u de logbestanden wilt bekijken;
- de logbestanden die vervolgens verschijnen, hebben betrekking op de uitgevoerde actie;
Er zijn verschillende logniveaus [4]. Standaard is de modus [Verbose] geselecteerd. Dit betekent dat de logs van alle niveaus worden weergegeven. Met [4] kunt u een specifiek niveau selecteren.
De logboeken zijn erg nuttig om te achterhalen op welke momenten tijdens de uitvoering van een project bepaalde methoden worden weergegeven. We zullen er vaak gebruik van maken. Laten we de code van de klasse [MainActivity] uit het project [Exemple-01] eens bekijken:
![]() |
package com.example.android.pdfrendererbasic;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_real);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.container, new PdfRendererBasicFragment(),
FRAGMENT_PDF_RENDERER_BASIC)
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_info:
new AlertDialog.Builder(this)
.setMessage(R.string.intro_message)
.setPositiveButton(android.R.string.ok, null)
.show();
return true;
}
return super.onOptionsItemSelected(item);
}
}
Hierboven zijn de methoden [onCreate, ligne 14] en [onCreateOptionsMenu, ligne 26] methoden van de bovenliggende klasse [Activity] (regel 9). Ze worden op verschillende momenten in de levenscyclus van de applicatie aangeroepen. Soms worden ze meerdere keren uitgevoerd. Zelfs als je de documentatie leest, is het soms moeilijk te zeggen of zo’n levenscyclusmethode vóór of na een methode wordt uitgevoerd die je zelf hebt geschreven. Deze informatie is echter vaak belangrijk om te weten. Je kunt dan logs toevoegen zoals hieronder:
public class MainActivity extends Activity {
public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
...
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("MainActivity","onCreateOptionsMenu");
getMenuInflater().inflate(R.menu.main, menu);
...
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("MainActivity","onOptionsItemSelected");
switch (item.getItemId()) {
...
}
}
- Op regels 7, 14 en 21 wordt de klasse [Log] gebruikt. Met deze klasse kun je logberichten naar de Android-console schrijven ([logcat]). De logberichten worden ingedeeld in verschillende niveaus (info, warning, debug, verbose, error). [Log.d] geeft logberichten van niveau [debug] weer. Het eerste argument is de bron van het logbericht. Er zijn namelijk verschillende bronnen die berichten naar de logconsole kunnen sturen. Om deze van elkaar te kunnen onderscheiden, wordt dit eerste argument gebruikt. Het tweede argument is het bericht dat naar de logconsole moet worden geschreven;
Als we het project [Exemple-01] opnieuw uitvoeren, krijgen we de volgende logberichten:
05-28 08:37:12.709 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreate
05-28 08:37:12.778 23881-23923/com.example.android.pdfrendererbasic D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
[ 05-28 08:37:12.781 23881:23881 D/ ]
HostConnection::get() New Host ...
05-28 08:37:12.967 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreateOptionsMenu
Zo zien we dat de methode [onCreate], die de Android-activiteit aanmaakt, wordt uitgevoerd vóór de methode [onCreateOptionsMenu], die het applicatiemenu aanmaakt.
Als we nu in de Android-emulator op de menuoptie [1] klikken:
![]() |
wordt het volgende logbericht toegevoegd aan de logconsole:
05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected
In het vervolg zullen we vaak loginstructies aan de Android-code toevoegen. Meestal zullen we deze niet van commentaar voorzien. Ze zijn er alleen om de lezer aan te moedigen naar de logconsole te kijken om zo geleidelijk de levenscyclus van een Android-app te begrijpen.
1.2.2.5. Beheer van de emulator [Genymotion]
Soms crasht de Genymotion-emulator en kun je hem niet meer opnieuw opstarten. Dit komt doordat er VirtualBox-processen actief zijn gebleven in Taakbeheer. Open Taakbeheer [Ctrl-Alt-Supp] en verwijder alle aanwezige VirtualBox-taken:
![]() | ![]() |
Start daarna de Genymotion-emulator opnieuw op vanuit Android Studio.
1.2.2.6. Beheer van het aangemaakte binaire bestand APK
Het compileren van het project levert een binair bestand met de extensie .apk op:
![]() | ![]() | ![]() |
Er zijn twee versies: de ene heet [debug] en de andere [debug-unaligned]. De eerste moet worden gebruikt; de andere is een tussenversie. Het .pak-bestand dat met [4] wordt gegenereerd, kan rechtstreeks naar een emulator of een Android-apparaat worden overgezet. Om het naar een emulator over te zetten, hoeft u het alleen maar met de muis naar de emulator te slepen en daar neer te zetten.
1.3. Voorbeeld-02: een eenvoudig Android-project
Laten we met Android Studio een nieuw Android-project [1-12] aanmaken:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() | ![]() | ![]() |
In [13] wordt de applicatie uitgevoerd. Vervolgens verschijnt de weergave [14] op de Genymotion-emulator.
1.3.1. Gradle-configuratie
Het aangemaakte project wordt geconfigureerd via het volgende bestand [build.gradle]:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
}
Dit bestand is gegenereerd door IDE met de configuratie-elementen ervan. Het is een minimaal bestand dat we geleidelijk zullen uitbreiden.
- regels 3-12: de kenmerken van de Android-app;
- regels 22-25: de afhankelijkheden. Dit is vooral waar we wijzigingen zullen aanbrengen op basis van de bestudeerde voorbeelden;
1.3.2. Het manifest van de app
![]() |
Het bestand [AndroidManifest.xml] [1] legt de kenmerken van het binaire bestand van de Android-app vast. De inhoud ervan is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- regel 3: het pakket van het Android-project;
- regel 10: de naam van de activiteit;
Deze twee gegevens zijn afkomstig van de gegevens die bij het aanmaken van het project zijn ingevoerd:
![]() |
- regel 3 van het manifest (package) is afkomstig van de hierboven vermelde invoer [4]. In dit pakket worden automatisch een aantal klassen gegenereerd;
![]() |
- regel 10 van het manifest (naam van de activiteit) is afkomstig van de hierboven vermelde invoer [1];
Laten we teruggaan naar het manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
![]() | ![]() | ![]() |
- regel 10: de hoofdactiviteit van de applicatie. Deze verwijst naar de hierboven genoemde klasse [1];
- regel 6: het pictogram [2] van de applicatie. Dit kan worden gewijzigd;
- regel 7: de naam van de applicatie. Deze staat in het bestand [strings.xml] [3]:
<resources>
<string name="app_name">Exemple-02</string>
</resources>
Het bestand [strings.xml] bevat de tekenreeksen die door de applicatie worden gebruikt. Regel 2: de naam van de applicatie is afkomstig van de invoer die is gedaan tijdens het opzetten van het project [4]:
![]() |
- regel 10: een activiteitstag. Een Android-applicatie kan meerdere activiteiten hebben;
- regel 12: de activiteit wordt aangeduid als de hoofdactiviteit;
- regel 13: en deze moet verschijnen in de lijst met applicaties die op het Android-apparaat kunnen worden gestart.
1.3.3. De hoofdactiviteit
![]() | ![]() |
Een Android-app is gebaseerd op een of meer activiteiten. Hier is een activiteit [1] gegenereerd: [MainActivity]. Een activiteit kan, afhankelijk van het type, een of meer weergaven tonen. De gegenereerde klasse [MainActivity] is als volgt:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- regel 6: de klasse [MyActivity] is een uitbreiding van de Android-klasse [AppCompatActivity]. Dit geldt voor alle toekomstige activiteiten;
- regel 9: de methode [onCreate] wordt uitgevoerd wanneer de activiteit wordt aangemaakt. Dit gebeurt voordat de bij de activiteit behorende weergave wordt getoond;
- regel 10: de methode [onCreate] van de bovenliggende klasse wordt aangeroepen. Dit moet altijd gebeuren;
- regel 11: het bestand [activity_main.xml] [2] is de weergave die aan de activiteit is gekoppeld. De definitie XML van deze weergave is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity">
<TextView
android:text="Hello World!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- regels b-k: de opmaakmanager. Standaard is het type [RelativeLayout] geselecteerd. In dit type container worden de componenten ten opzichte van elkaar geplaatst (rechts van, links van, onder, boven);
- regels m-p: een component van het type [TextView] die dient om tekst weer te geven;
- regel n: de weergegeven tekst. Het wordt afgeraden om tekst hard te coderen in de weergaven. Het is beter om deze teksten te verplaatsen naar het bestand [res/values/strings.xml] [3]:
De weergegeven tekst wordt dan [Hello World!]. Waar wordt deze weergegeven? De container [RelativeLayout] vult het scherm. De [TextView], het enige element daarin, wordt linksboven in deze container weergegeven, dus linksboven op het scherm;
Wat betekent [R.layout.activity_main] op regel 11? Aan elke Android-resource (weergaven, fragmenten, componenten, ...) wordt een identificatiecode toegekend. Zo wordt een view met de naam [V.xml] die zich in de map [res / layout] bevindt, geïdentificeerd als [R.layout.V]. R is een gegenereerde klasse in de map [app / build / generated] [1-3]:
![]() |
De klasse [R] is als volgt:
...............
public static final class string {
public static final int abc_action_bar_home_description=0x7f060000;
public static final int abc_action_bar_home_description_format=0x7f060001;
public static final int abc_action_bar_home_subtitle_description_format=0x7f060002;
...
public static final int app_name=0x7f060014;
}
public static final class layout {
public static final int abc_action_bar_title_item=0x7f040000;
public static final int abc_action_bar_up_container=0x7f040001;
...
public static final int activity_main=0x7f040019;
...
}
public static final class mipmap {
public static final int ic_launcher=0x7f030000;
}
- regel 14: het attribuut [R.layout.activity_main] is de identificatiecode van de weergave [res / layout / activity_main.xml];
- regel 7: het attribuut [R.string.app_name] is de identificatiecode van de tekenreeks [app_name] in het bestand [res / values / string.xml]:
- regel 19: het attribuut [R.mipmap.ic_launcher] is de identificatiecode van de afbeelding [res / mipmap / ic_launcher];
Houd er dus rekening mee dat wanneer we in de code naar [R.layout.activity_main] verwijzen, we verwijzen naar een attribuut van de klasse [R]. IDE helpt ons de verschillende elementen van deze klasse te identificeren:
![]() | ![]() |
1.3.4. De applicatie uitvoeren
Om een Android-applicatie uit te voeren, moeten we een uitvoerconfiguratie aanmaken:
![]() | ![]() | ![]() |
- in [1], kies [Edit Configurations];
- het project is aangemaakt met een configuratie [app], die we gaan verwijderen; [2] om deze opnieuw aan te maken;
- in [3], maak een nieuwe uitvoeringsconfiguratie aan;
![]() |
- in [4], kies [Android Application];

- in [5], kies in de vervolgkeuzelijst de module [app];
- in [6-8], de voorgestelde standaardwaarden behouden;
- in [7] is de standaardactiviteit die welke is gedefinieerd in het bestand [AndroidManifest.xml] (regel 1 hieronder):
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- in [8], selecteer [Show Chooser Dialog], waarmee u het apparaat kunt kiezen waarop de applicatie wordt uitgevoerd (emulator, tablet);
- in [9] geeft u aan dat deze keuze moet worden opgeslagen;
- bevestig de configuratie;
![]() |
- in [11] start u de emulatorbeheerder [Genymotion] (zie paragraaf 6.9);
![]() |
- in [12], selecteer een tabletemulator en start deze op [13];
![]() | ![]() |
- in [14], voer de uitvoerconfiguratie [app] uit;
- in [15] wordt het formulier voor de keuze van het uitvoeringsapparaat weergegeven. Er is hier slechts één beschikbaar: de eerder gestarte emulator [Genymotion];
De software-emulator toont na enige tijd het volgende scherm:

1.3.5. De levenscyclus van een activiteit
Laten we nog eens kijken naar de code van de activiteit [MainActivity]:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
De methode [onCreate] in de regels 8-12 behoort tot de methoden die tijdens de levenscyclus van een activiteit kunnen worden aangeroepen. De Android-documentatie geeft een overzicht van deze methoden:
![]() |
- [1]: de methode [onCreate] wordt aangeroepen bij het opstarten van de activiteit. In deze methode wordt de activiteit aan een weergave gekoppeld en worden de verwijzingen naar de componenten ervan opgehaald;
- [2-3]: vervolgens worden de methoden [onStart, onResume] aangeroepen. We zien dat de methode [onResume] de laatste methode is die wordt uitgevoerd voordat de status [4] van de actieve activiteit wordt bereikt;
1.4. Voorbeeld-03: herschrijven van het project [Exemple-02] met de bibliotheek [Android Annotations]
We gaan nu de bibliotheek [Android Annotations] introduceren, die het schrijven van Android-applicaties vergemakkelijkt. Hiervoor dupliceren we het voorbeeld [Exemple-02] naar [Exemple-03] volgens de procedure [1-16].
![]() | ![]() |
- naar [1]; gebruik het perspectief [Project] om het volledige Android-project te bekijken;
![]() | ![]() |
![]() | ![]() |
![]() | ![]() | ![]() | ![]() |
Opmerking: tussen [14] en [15] is er een overgang geweest van een perspectief [Android] naar een perspectief [Project] (zie paragraaf 1.2.2.1).
Vervolgens passen we het bestand [res / values / strings.xml] [17] aan:
![]() |
Het bestand [strings.xml] wordt als volgt gewijzigd:
<resources>
<string name="app_name">Exemple-03</string>
</resources>
Nu voeren we de nieuwe applicatie uit, die de volledige configuratie van [Exemple-02] heeft overgenomen:
![]() | ![]() |
In [19] krijgen we hetzelfde resultaat als met [Exemple-02], maar dan met een nieuwe naam.
We gaan nu de bibliotheek [Android Annotations] introduceren, die we voor het gemak AA zullen noemen. Deze bibliotheek introduceert nieuwe klassen voor het annoteren van Android-broncode. Deze annotaties worden gebruikt door een processor die nieuwe Java-klassen in de module aanmaakt; deze klassen dragen net als de door de ontwikkelaar geschreven klassen bij aan de compilatie ervan. Zo ontstaat de volgende compilatieketen:
![]() |
We gaan eerst in het bestand [build.gradle] de afhankelijkheden van de annotatiecompiler AA (de hierboven genoemde processor) opnemen:
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
}
- in de regels 4-5 worden de twee afhankelijkheden toegevoegd die samen de bibliotheek AA vormen;
Het bestand [build.gradle] wordt opnieuw aangepast om gebruik te maken van een plug-in met de naam [android-apt], die het compilatieproces in twee stappen wijzigt:
- verwerking van Android-annotaties, waardoor nieuwe klassen ontstaan;
- compilatie van alle klassen van het project;
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Vanaf Gradle-plugin 0.11 voor Android moet je android-apt >= 1.3 gebruiken
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
- regel 8: versie van de plug-in [android-apt] die in de centrale Maven-repository (regel 3) wordt opgezocht;
- regel 13: activering van deze plug-in;
Controleer in dit stadium of de uitvoerconfiguratie [app] nog steeds werkt.
We gaan nu een eerste annotatie van de bibliotheek AA toevoegen aan de klasse [MainActivity]:
![]() |
De klasse [MainActivity] ziet er op dit moment als volgt uit:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
We hebben deze code al uitgelegd in paragraaf 1.3.3. We passen deze als volgt aan:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- regel 7: de annotatie [@EActivity] is een annotatie van het type AA (regel 3). De parameter ervan is de weergave die aan de activiteit is gekoppeld;
Deze annotatie zal een klasse [MainActivity_] genereren die is afgeleid van de klasse [MainActivity], en deze klasse zal de daadwerkelijke activiteit vormen. We moeten daarom het manifest van het project [AndroidManifest.xml] als volgt aanpassen:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity_">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- regel 11: de nieuwe activiteit;
Zodra dit is gebeurd, kunnen we het project [1] compileren:
![]() | ![]() |
- in [2] zien we de gegenereerde klasse [MainActivity_] in de map [app / build / generated / source / apt / debug];
De gegenereerde klasse [MainActivity_] is als volgt:
//
// DO NOT EDIT THIS FILE.
// Gegenereerd met AndroidAnnotations 4.0.0.
//
// U kunt een groter werk maken dat dit bestand bevat en dat werk verspreiden onder voorwaarden naar keuze.
//
package exemples.android;
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
public final class MainActivity_
extends MainActivity
implements HasViews
{
private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
@Override
public void onCreate(Bundle savedInstanceState) {
OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
init_(savedInstanceState);
super.onCreate(savedInstanceState);
OnViewChangedNotifier.replaceNotifier(previousNotifier);
setContentView(R.layout.activity_main);
}
...
- regels 24-25: de klasse [MainActivity_] is een uitbreiding van de klasse [MainActivity];
We zullen niet proberen de code van de door AA gegenereerde klassen uit te leggen. Deze klassen beheren de complexiteit die de annotaties trachten te verbergen. Maar het kan soms nuttig zijn om deze te bekijken wanneer men wil begrijpen hoe de annotaties die men gebruikt, worden ‘vertaald’.
We kunnen nu de configuratie [app] opnieuw uitvoeren. We krijgen hetzelfde resultaat als eerder. We gaan nu verder met dit project, dat we zullen dupliceren om de belangrijke begrippen van Android-programmeren te presenteren.
1.5. Voorbeeld-04: weergaven en gebeurtenissen
1.5.1. Het project aanmaken
We volgen de procedure die in paragraaf 1.4 is beschreven om [Exemple-02] te dupliceren naar [Exemple-03]:
We:
- dupliceren het project [Exemple-03] naar [Exemple-04] (nadat we de map [app / build] uit [Exemple-03] hebben verwijderd);
- laden we het project [Exemple-04];
- wijzigen we de naam van het project in het bestand [app / res / values / strings.xml] (Android-perspectief);
- verwijderen we het bestand [Exemple-04 / Exemple-04.iml] (Project-perspectief);
- compileren en voeren we het project uit;
![]() | ![]() |
1.5.2. Een weergave samenstellen
We gaan nu met de grafische editor de weergave aanpassen die door het project [Exemple-04] wordt weergegeven:
![]() | ![]() |
- in [1-4], maak een nieuwe weergave aan met de naam XML;
- in [5], geef de weergave een naam;
- in [6], geef de root-tag van de weergave op. Hier kiezen we een container [RelativeLayout]. In deze componentcontainer worden de componenten ten opzichte van elkaar geplaatst: „rechts van“, „links van“, „onder“, „boven“;
![]() |
Het gegenereerde bestand [vue1.xml] is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</RelativeLayout>
- regel 2: een lege container [RelativeLayout] die de volledige breedte van de tablet (regel 3) en de volledige hoogte (regel 4) in beslag neemt;
![]() | ![]() |
- in [1], selecteer het tabblad [Design] in de weergegeven weergave [vue1.xml];
- in [2-4]: schakel over naar de tabletmodus;
![]() | ![]() | ![]() |
- in [5], stel de schaal van de tablet in op 1;
- in [6], kies de modus 'landschap' voor de tablet;
- de schermafbeelding [7] geeft een overzicht van de gemaakte keuzes.
![]() | ![]() | ![]() |
- in [1]: neem een [Large Text] en sleep deze naar het venster [2];
- in [3]: dubbelklik op de component;
- in [4], wijzig de weergegeven tekst. In plaats van deze 'vast' in de weergave XML op te nemen, gaan we deze extern opslaan in het bestand [res / values / string.xml]
![]() | ![]() | ![]() |
- in [5] voegen we een nieuwe waarde toe aan het bestand [strings.xml];
- in [8] wordt een identificatiecode aan de reeks toegekend;
- in [9] wordt de waarde van de tekenreeks opgegeven;
- in [10] wordt de nieuwe weergave getoond na validatie van de vorige stap;
![]() | ![]() | ![]() |
- na een dubbelklik op de component wijzigt men de ID ervan in [11];
- in [12]; in de eigenschappen van de component wordt de lettergrootte gewijzigd van [50sp];
- in [13], de nieuwe weergave;
Het bestand [vue1.xml] is als volgt gewijzigd:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
- de wijzigingen die in de grafische interface zijn aangebracht, staan op de regels 10, 11 en 14. De overige attributen van [TextView] zijn standaardwaarden of vloeien voort uit de positionering van de component in de weergave;
- regels 7-8: de afmetingen van de component komen overeen met die van de tekst die deze bevat (wrap_content), zowel in hoogte als in breedte;
- regel 13: de bovenkant van de component is uitgelijnd met de bovenkant van de weergave (regel 13), 50 pixels eronder (regel 13);
- regel 12: de linkerkant van de component is uitgelijnd met de linkerkant van de weergave (regel 13), 213 pixels naar rechts (regel 12);
Over het algemeen worden de exacte afmetingen van de linker-, rechter-, boven- en ondermarges rechtstreeks in het XML vastgelegd.
Maak op dezelfde manier de volgende weergave [1]:
![]() |
De componenten zijn als volgt:
Het positioneren van componenten ten opzichte van elkaar kan frustrerend zijn, omdat de grafische editor soms verrassend reageert. Het kan beter zijn om de eigenschappen van de componenten te gebruiken:
De component [textView1] moet 50 pixels onder de titel en 50 pixels vanaf de linkerrand van de container worden geplaatst:
![]() | ![]() | ![]() |
- in [1] wordt de bovenrand (top) van de component uitgelijnd ten opzichte van de onderrand (bottom) van de component [textViewTitreVue1] op een afstand van 50 pixels [3] (top);
- in [2] wordt de linkerrand (left) van de component uitgelijnd ten opzichte van de linkerrand van de container op een afstand van 50 pixels [3] (left);
De component [editTextNom] moet 60 pixels rechts van de component [textView1] worden geplaatst en aan de onderkant worden uitgelijnd met diezelfde component;
![]() | ![]() |
- bij [1] is de linkerrand (left) van de component uitgelijnd met de rechterrand (right) van de component [textView1] op een afstand van 60 pixels van [2] (left). Het is uitgelijnd met de onderrand (bottom:bottom) van de component [textView1] [1];
De component [buttonValider] moet 60 pixels rechts van de component [editTextNom] worden geplaatst en aan de onderkant worden uitgelijnd met diezelfde component;
![]() | ![]() |
- bij [1] is de linkerrand (left) van de component uitgelijnd met de rechterrand (right) van de component [editTextNom] op een afstand van 60 pixels van [2] (left). Het is uitgelijnd met de onderrand van de component (bottom:bottom) [editTextNom] [1];
De component [buttonVue2] moet 50 pixels onder de component [textView1] worden geplaatst en links uitgelijnd met diezelfde component;
![]() | ![]() |
- in [1] is de linkerrand (left) van de component uitgelijnd met de linkerrand (left) van de component [textView1] en wordt deze eronder (top:bottom) geplaatst op een afstand van 50 pixels ten opzichte van [2] (top);
Het gegenereerde bestand XML ziet er als volgt uit:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="49dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_nom"
android:id="@+id/textView1"
android:layout_below="@+id/textViewTitreVue1"
android:layout_alignParentLeft="true"
android:layout_marginLeft="50dp"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
</RelativeLayout>
Hierin staat alles wat grafisch is ingesteld. Een andere manier om een weergave te maken, is dus door dit bestand rechtstreeks te bewerken. Als je er eenmaal aan gewend bent, kan dit sneller gaan dan het gebruik van de grafische editor.
- Op regel 38 staat informatie die we niet hebben getoond. Deze wordt opgegeven via de eigenschappen van de component [editTextNom] [1]:
![]() | ![]() |
Alle teksten zijn afkomstig uit het volgende bestand [strings.xml] [2]:
<resources>
<string name="app_name">Exemple-04</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
Laten we nu de activiteit [MainActivity] aanpassen, zodat deze weergave wordt getoond bij het opstarten van de applicatie:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
- regel 7: de weergave [vue1.xml] wordt nu door de activiteit weergegeven;
Wijzig het bestand [AndroidManifest.xml] als volgt:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- regel 12: deze configuratieregel voorkomt dat het toetsenbord verschijnt zodra de weergave [vue1] wordt weergegeven. Deze weergave heeft namelijk een invoerveld dat de focus heeft wanneer de weergave wordt weergegeven. Door deze focus verschijnt standaard het virtuele toetsenbord;
Voer de applicatie uit en controleer of inderdaad de weergave [vue1.xml] wordt weergegeven:

1.5.3. Gebeurtenisbeheer
Laten we nu de klik op de knop [Valider] van de weergave [Vue1] afhandelen:

De code van [MainActivity] verandert als volgt:
package exemples.android;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
// de elementen van de visuele interface
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews(){
Log.d("MainActivity","afterViews");
}
// gebeurtenisbeheerder
@Click(R.id.buttonValider)
protected void doValider() {
// de ingevoerde naam wordt weergegeven
Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- regels 17-18: het veld [protected EditText editTextNom] wordt gekoppeld aan de identificatiecomponent [R.id.editTextNom] van de visuele interface. Het aan de component gekoppelde veld moet toegankelijk zijn in de afgeleide klasse [MainActivity_] en kan daarom geen bereik [private] hebben. Het veld met ID [R.id.editTextNom] is afkomstig uit de weergave [vue1.xml]:
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editTextNom"
android:minWidth="200dp"
android:layout_toRightOf="@+id/textView1"
android:layout_marginLeft="60dp"
android:layout_alignBottom="@+id/textView1"
android:inputType="textCapCharacters"/>
Opmerking: gebruik geen tekens met accenten in de identificatiecodes [id]. AA verwerkt deze niet correct.
- regel 32: de annotatie [@Click(R.id.buttonValider)] verwijst naar de methode die de 'Click'-gebeurtenis op de knop met identificatiecode [R.id.buttonValider] afhandelt. Deze identificatiecode is ook afkomstig uit de weergave [vue1.xml]:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_valider"
android:id="@+id/buttonValider"
android:layout_alignBottom="@+id/editTextNom"
android:layout_toRightOf="@+id/editTextNom"
android:textSize="30sp"
android:layout_marginLeft="60dp"/>
- regel 35: geeft de ingevoerde naam weer:
- Toast.makeText(...).show(): geeft een tekst weer op het scherm,
- de eerste parameter van makeText is de activiteit,
- de tweede parameter is de tekst die moet worden weergegeven in het venster dat door makeText wordt weergegeven,
- de derde parameter is de levensduur van het weergegeven venster: Toast.LENGTH_LONG of Toast.LENGTH_SHORT;
- regel 26: de annotatie [@AfterViews] geeft aan welke methode moet worden uitgevoerd wanneer alle velden die met [@ViewById] zijn geannoteerd, zijn geïnitialiseerd. Het is belangrijk om te weten wanneer deze velden worden geïnitialiseerd. Kunnen we bijvoorbeeld in de methode [onCreate] de verwijzing uit regel 18 gebruiken? Om deze vraag te beantwoorden, hebben we logbestanden aangemaakt;
Voer het project [Exemple-04] uit en controleer of er iets gebeurt wanneer u op de knop [Valider] klikt. We krijgen de volgende logberichten:
Hieruit kunnen we concluderen dat wanneer de methode [onCreate] wordt uitgevoerd, de velden die zijn gemarkeerd met [@ViewById] nog niet zijn geïnitialiseerd. Ook hier wordt de beginnende lezer aangemoedigd om dit soort logboeken op te nemen in de methoden die de levenscyclus van de applicatie beheren.
1.6. Voorbeeld-05: navigeren tussen weergaven
In het vorige project is de knop [Vue n° 2] niet gebruikt. We stellen voor om deze te gebruiken door een tweede weergave te maken en te laten zien hoe je van de ene weergave naar de andere kunt navigeren. Er zijn verschillende manieren om dit op te lossen. De hier voorgestelde manier is om elke weergave aan een activiteit te koppelen. Een andere methode is om één enkele activiteit van het type [AppCompatActivity] te hebben die weergaven van het type [Fragment] weergeeft. Dit zal de methode zijn die in toekomstige applicaties wordt gebruikt.
1.6.1. Het project aanmaken
We dupliceren het project [Exemple-04] naar [Exemple-05]. Hiervoor volgen we de procedure die is beschreven voor het dupliceren van [Exemple-02] naar [Exemple-03] in paragraaf 1.4 en die is overgenomen in paragraaf 1.5.
![]() | ![]() |
1.6.2. Een tweede activiteit toevoegen
Om een tweede weergave te beheren, gaan we een tweede activiteit aanmaken. Deze activiteit zal weergave nr. 2 beheren. We hanteren hier het model ‘één weergave = één activiteit’. Er zijn ook andere modellen mogelijk.
123

- in [1-4] maken we een nieuwe activiteit aan;

- in [5], de naam van de klasse die zal worden gegenereerd;
- in [6], de naam van de weergave (vue2.xml) die aan de nieuwe activiteit is gekoppeld;
![]() |
- in [7-8], de bestanden waarop de vorige configuratie van invloed is;
De activiteit [SecondActivity] is als volgt:
package exemples.android;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.vue2);
}
}
- regel 11: de activiteit is gekoppeld aan de weergave [vue2.xml];
De weergave [vue2.xml] is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
</RelativeLayout>
Dit is een voorlopig leeg scherm met een lay-outmanager van het type [RelativeLayout] (regel 2). Op regel 11 zien we dat deze aan de nieuwe activiteit is gekoppeld.
Het manifest van de Android-module [AndroidManifest.xml] is als volgt gewijzigd:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity">
</activity>
</application>
</manifest>
Op regel 20 is een tweede activiteit geregistreerd.
1.6.3. Navigatie van weergave nr. 1 naar weergave nr. 2
Laten we teruggaan naar de code van de klasse [MainActivity] die weergave nr. 1 weergeeft. De overgang naar weergave nr. 2 wordt momenteel niet ondersteund:
![]() |
We implementeren dit als volgt:
// naar weergave nr. 2 navigeren
@Click(R.id.buttonVue2)
protected void navigateToView2() {
// we navigeren naar weergave nr. 2 en geven daarbij de in weergave nr. 1 ingevoerde naam door
// er wordt een Intent aangemaakt
Intent intent = new Intent();
// we koppelen deze Intent aan een activiteit
intent.setClass(this, SecondActivity.class);
// we koppelen informatie aan deze Intent
intent.putExtra("NOM", editTextNom.getText().toString().trim());
// de activiteit van het type [SecondActivity] wordt gestart door de Intent door te geven
startActivity(intent);
}
- regels 2-3: de methode [navigateToView2] verwerkt de ‘klik’ op de knop die wordt aangeduid met [R.id.buttonVue2] en is gedefinieerd in weergave [vue1.xml]:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue2"
android:id="@+id/buttonVue2"
android:layout_below="@+id/textView1"
android:layout_alignLeft="@+id/textView1"
android:layout_marginTop="50dp"
android:textSize="30sp"/>
De opmerkingen beschrijven de stappen die moeten worden uitgevoerd om van weergave te wisselen:
- regel 6: maak een object van het type [Intent] aan. Met dit object kunnen zowel de te starten activiteit als de informatie die daaraan moet worden doorgegeven, worden gespecificeerd;
- regel 8: koppel de Intent aan een activiteit, in dit geval een activiteit van het type [SecondActivity] die verantwoordelijk is voor het weergeven van weergave nr. 2. Houd er rekening mee dat de activiteit [MainActivity] weergave nr. 1 weergeeft. We hebben dus één weergave per activiteit. We moeten het type [SecondActivity] definiëren;
- regel 10: optioneel kun je informatie invoeren in het object [Intent]. Deze informatie is bestemd voor de activiteit [SecondActivity] die zal worden gestart. De parameters van [Intent.putExtra] zijn (sleutelobject, waardeobject). Merk op dat de methode [EditText.getText()], die de in het invoerveld ingevoerde tekst weergeeft, niet het type [String] retourneert, maar het type [Editable]. Gebruik de methode [toString] om de ingevoerde tekst te verkrijgen;
- regel 12: start de activiteit die is gedefinieerd door het object [Intent].
Voer het project [Exemple-05] uit en controleer of u inderdaad weergave nr. 2 krijgt (die voorlopig leeg is):
![]() | ![]() |
1.6.4. Opbouw van weergave nr. 2
![]() | ![]() |
- in [1-2] verwijderen we de weergave [main.xml] die we niet meer nodig hebben, en vervolgens passen we de weergave [vue2.xml] als volgt aan:
![]() |
De componenten zijn als volgt:
Het bestand XML [vue2.xml] ziet er als volgt uit:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.SecondActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue2"
android:id="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textViewBonjour"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_below="@+id/textViewTitreVue2"
android:layout_marginTop="50dp"
android:layout_marginLeft="50dp"
android:textSize="30sp"
android:text="Bonjour !"
android:textColor="#ffffb91b"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_vue1"
android:id="@+id/buttonVue1"
android:layout_marginTop="50dp"
android:textSize="30sp"
android:layout_alignLeft="@+id/textViewBonjour"
android:layout_below="@+id/textViewBonjour"/>
</RelativeLayout>
Voer het project [Exemple-05] uit en controleer of u inderdaad de nieuwe weergave krijgt door op de knop [Vue n° 2] te klikken.
1.6.5. De activiteit [SecondActivity]
In [MainActivity] hebben we de volgende code geschreven:
// navigeer naar weergave nr. 2
protected void navigateToView2() {
// er wordt doorgestuurd naar weergave nr. 2, waarbij de naam die in weergave nr. 1 is ingevoerd, wordt doorgegeven
// er wordt een Intent aangemaakt
Intent intent = new Intent();
// we koppelen deze Intent aan een activiteit
intent.setClass(this, SecondActivity.class);
// we koppelen informatie aan deze Intent
intent.putExtra("NOM", edtNom.getText().toString().trim());
// de activiteit van het type [SecondActivity] wordt gestart door de Intent door te geven
startActivity(intent);
}
Op regel 9 hebben we voor [SecondActivity] informatie ingevoerd die nog niet is gebruikt. We gaan deze nu gebruiken en dat gebeurt in de code van [SecondActivity]:
![]() |
De code van [SecondActivity] verandert als volgt:
package exemples.android;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
// componenten van de visuele interface
@ViewById
protected TextView textViewBonjour;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@AfterViews
protected void afterViews() {
// de intent wordt opgehaald, indien deze bestaat
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// we halen de naam op
String nom = extras.getString("NOM");
if (nom != null) {
// we geven deze weer
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
}
}
}
- regel 11: we gebruiken de annotatie [@EActivity] om aan te geven dat de klasse [SecondActivity] een activiteit is die gekoppeld is aan de weergave [vue2.xml];
- regels 15-16: er wordt een verwijzing opgehaald naar de component [TextView], geïdentificeerd door [R.id.textViewBonjour]. Hier is [@ViewById(R.id.textViewBonjour)] niet vermeld. In dit geval gaat AA ervan uit dat de identificatiecode van de component identiek is aan het geannoteerde veld, in dit geval het veld [textViewBonjour];
- regel 23: de annotatie [@AfterViews] markeert een methode die moet worden uitgevoerd nadat de velden die zijn geannoteerd met [@ViewById] zijn geïnitialiseerd. In de methode [OnCreate] (regel 19) kunnen deze velden niet worden gebruikt, omdat ze nog niet zijn geïnitialiseerd. In het project [Exemple-05] wordt er van de ene activiteit naar de andere overgeschakeld en was het a priori niet duidelijk of de geannoteerde methode [@AfterViews] één keer bij de eerste instantiatie van de activiteit zou worden uitgevoerd of telkens wanneer de activiteit wordt gestart. Uit tests is gebleken dat de tweede veronderstelling klopte;
- regel 26: de klasse [AppCompatActivity] heeft een methode [getIntent] die het aan de activiteit gekoppelde object [Intent] retourneert;
- regel 28: de methode [Intent.getExtras] retourneert een type [Bundle], een soort woordenboek dat de informatie bevat die is gekoppeld aan het object [Intent] van de activiteit;
- regel 31: de naam die is opgeslagen in het object [Intent] van de activiteit wordt opgehaald;
- regel 34: deze wordt weergegeven.
Let op: de velden die zijn gemarkeerd met de annotatie [@ViewById] mogen geen tekens met accenten bevatten.
Laten we teruggaan naar de klasse [SecondActivity]. Omdat we het volgende hebben geschreven:
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
zal AA een klasse [SecondActivity_] genereren, afgeleid van [SecondActivity], en deze klasse zal de daadwerkelijke activiteit vormen. Dit brengt ons ertoe wijzigingen aan te brengen in:
[MainActivity]
// naar weergave nr. 2 navigeren
@Click(R.id.buttonVue2)
protected void navigateToView2() {
..
// we koppelen deze intent aan een activiteit
intent.setClass(this, SecondActivity_.class);
...
}
- op regel 6 moeten we [SecondActivity] vervangen door [SecondActivity_];
[AndroidManifest.xml]
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity_">
</activity>
</application>
</manifest>
- op regel 20 moet [SecondActivity] worden vervangen door [SecondActivity_];
Test deze nieuwe versie. Typ een naam in weergave nr. 1 en controleer of deze correct wordt weergegeven in weergave nr. 2.
![]() | ![]() |
1.6.6. Navigeren van weergave nr. 2 naar weergave nr. 1
Om van weergave nr. 2 naar weergave nr. 1 te navigeren, volgen we de eerder beschreven procedure:
- plaats de navigatiecode in de activiteit [SecondActivity] die weergave nr. 2 weergeeft;
- schrijf de methode [@AfterViews] in de activiteit [MainActivity] die weergave nr. 1 weergeeft;
De code van [SecondActivity] verandert als volgt:
@Click(R.id.buttonVue1)
protected void navigateToView1() {
// we maken een Intent aan voor de activiteit [MainActivity]
Intent intent1 = new Intent();
intent1.setClass(this, MainActivity_.class);
// we halen de Intent van de huidige activiteit op: [SecondActivity]
Intent intent2 = getIntent();
if (intent2 != null) {
Bundle extras2 = intent2.getExtras();
if (extras2 != null) {
// de naam wordt in de Intent van [MainActivity] geplaatst
intent1.putExtra("NOM", extras2.getString("NOM"));
}
// we starten [MainActivity]
startActivity(intent1);
}
}
- regels 1-2: de methode [navigateToView1] wordt gekoppeld aan het klikken op de knop [btn_vue1];
- regel 4: er wordt een nieuwe [Intent] aangemaakt;
- regel 5: gekoppeld aan de activiteit [MainActivity_];
- regel 7: de aan [SecondActivity] gekoppelde Intent wordt opgehaald;
- regel 9: de informatie van deze Intent wordt opgehaald;
- regel 12: de sleutel [NOM] wordt opgehaald uit [intent2] om in [intent1] te worden geplaatst met dezelfde bijbehorende waarde;
- regel 15: de activiteit [MainActivity_] wordt gestart.
In de code van [MainActivity] wordt de volgende methode [@AfterViews] toegevoegd:
@AfterViews
protected void afterViews() {
// we halen de intent op als deze bestaat
Intent intent = getIntent();
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
// de naam wordt opgehaald
String nom = extras.getString("NOM");
if (nom != null) {
// we geven deze weer
editTextNom.setText(nom);
}
}
}
}
Breng deze wijzigingen aan en test uw applicatie. Wanneer u nu terugkeert van weergave nr. 2 naar weergave nr. 1, moet de oorspronkelijk ingevoerde naam weer te zien zijn, wat tot nu toe niet het geval was.
![]() | ![]() |
1.6.7. Levenscyclus van activiteiten
In paragraaf 1.3.5 hebben we de levenscyclus van een activiteit besproken. We hebben hier twee activiteiten en tijdens de uitvoering schakelen we tussen beide heen en weer. Deze activiteiten bevatten twee methoden waarvan we niet precies weten wanneer ze ten opzichte van elkaar worden aangeroepen: [onCreate] en [afterViews]. Het is belangrijk om dit te weten. Daarom voegen we logs toe aan beide activiteiten:
Zo schrijven we in de klasse [MainActivity]:
// fabrikant
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
...
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
}
}
- regels 2-4: we willen weten of de klasse [MainActivity] één of meerdere keren wordt geïnstantieerd;
- regel 8: we willen weten of de methode [onCreate] één of meerdere keren wordt aangeroepen;
- regel 14: we willen weten of de methode [afterViews] één of meerdere keren wordt aangeroepen;
We doen precies hetzelfde in de klasse [SecondActivity].
Bij het opstarten van de applicatie krijgen we de volgende logberichten:
De methoden [onCreate, afterViews] van de eerste activiteit zijn in deze volgorde uitgevoerd. Wanneer we op de knop [Vue n° 2] klikken, zijn de nieuwe logberichten als volgt:
De methoden [onCreate, afterViews] van de tweede activiteit zijn in deze volgorde uitgevoerd. Wanneer je op de knop [Vue n° 1] klikt, zijn de nieuwe logbestanden als volgt:
De klasse [MainActivity] wordt dus opnieuw geïnstantieerd. Wanneer je op de knop [Vue n° 2] klikt, zijn de nieuwe logbestanden als volgt:
De klasse [SecondActivity] wordt dus opnieuw geïnstantieerd.
De twee activiteiten worden dus systematisch opnieuw aangemaakt wanneer van activiteit wordt gewisseld.
We gaan nu een architectuur bekijken met één enkele activiteit die meerdere weergaven, zogenaamde fragmenten, kan beheren. De activiteit en de weergaven worden slechts één keer geïnstantieerd, in tegenstelling tot de vorige methode waarbij een activiteit meerdere keren kon worden geïnstantieerd.
1.7. Voorbeeld-06: navigatie via tabbladen
We gaan hier de interfaces met tabbladen verkennen. Het voorbeeld is complex, maar introduceert alle elementen die we later zullen gebruiken: één enkele activiteit, fragmentbeheerder (weergaven), fragmentcontainer, navigatie tussen fragmenten. Het begrip ‘tabbladen’ verschilt van dat van ‘fragmenten’ en speelt slechts een ondergeschikte rol in wat we in dit voorbeeld willen laten zien.
1.7.1. Het project aanmaken
We maken een nieuw project aan:
![]() | ![]() |
![]() |
![]() |
- in [7] kiezen we een activiteit met tabbladen (Tabbed Activity);
![]() |
- in [10-14] behoudt men de voorgestelde standaardwaarden;
- in [15] kiest men tabbladen met een titelbalk;
Het aangemaakte project ziet er dan als volgt uit:
![]() | ![]() |
- in [1], de activiteit;
- in [2], de weergaven;
Er is automatisch een uitvoeringsconfiguratie [app] aangemaakt, met de naam van de module, [2b]:
![]() |
Deze kan worden uitgevoerd. Er verschijnt dan een venster met drie tabbladen [3-6]:

1.7.2. Gradle-configuratie
Het project [Exemple-06] is gegenereerd met het volgende bestand [build.gradle]:
![]() |
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
}
Er is één nieuw element ten opzichte van wat we eerder hebben gezien: regel 25. Deze bibliotheek is nodig voor de nieuwe componenten die door de gegenereerde applicatie worden gebruikt.
1.7.3. Weergave [activity_main]
![]() |
De weergave [activity_main] is de weergave die gekoppeld is aan de activiteit [MainActivity] van het project. In de modus [design] ziet de weergave er als volgt uit:

Deze bevat de volgende componenten:
![]() |
- [main_content] is de volledige weergave;
- [appbar] (rood kader, 1) is de applicatiebalk. Deze bevat twee componenten:
- [toolbar] (geel kader 4) is de werkbalk;
- [tabs] (oranje kader 5) is de titelbalk van de tabbladen;
- [container] (groen kader, 2) kan verschillende fragmenten bevatten. Een fragment is een weergave. Zo kan dezelfde activiteit meerdere weergaven (fragmenten) in deze container weergeven;
- [fab] (component 3) wordt een zwevende component genoemd;
In de modus [text] is de code als volgt:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="exemples.android.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
We zien hier de eerder beschreven elementen terug:
- regels 2-49: de definitie van de component [main_content] (regel 5), die de volledige weergave omvat. We zien dat het een layout (componentlay-outmanager) is van het type [CoordinatorLayout] (regel 2);
- regels 11-33: de container [appbar] (regel 12). Dit is een layout van het type [AppBarLayout] (regel 11);
- regels 18-24: de component [toolbar] (regel 19) van het type [Toolbar] (regel 18);
- regels 28-31: de container [tabs] (regel 29). Dit is een layout van het type [TabLayout] (regel 28). Deze geeft de titels van de tabbladen weer;
- regels 35-39: de component [container] (regel 36). Dit is de container die de verschillende weergaven van de activiteit toont;
- regels 41-47: de component [fab] (regel 42) van het type [FloatingActionButton] (regel 41). Dit is een knop waarop je kunt klikken. Deze wordt standaard rechtsonder in de totaalweergave geplaatst;
We zullen niet proberen de betekenis van alle attributen van deze componenten te begrijpen. We gaan ze gewoon gebruiken zoals ze zijn. Door ervaring op te doen, en vaak in de modus [design], ontdek je hun rol. In deze modus ontdek je dat de componenten tientallen attributen hebben. Over het algemeen worden slechts enkele geïnitialiseerd, terwijl de andere een standaardwaarde behouden.
Laten we echter enkele punten verduidelijken. De meeste waarden waarmee de verschillende weergaven worden geconfigureerd, zijn verzameld in de map [res / values]:
![]() |
Deze waarden worden vermeld op de regels 15-16, 23, 39 en 46 van het bestand [activity_main.xml]. Laten we een voorbeeld nemen:
- regel 15:
android:paddingTop="@dimen/appbar_padding_top"
De annotatie [@dimen] verwijst naar het bestand [res / values / dimens.xml]:
<resources>
<!-- Standaard schermmarges, volgens de Android-ontwerprichtlijnen. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen>
</resources>
Regel 15 van het bestand [activity_main.xml] verwijst naar regel (f) hierboven;
Op dezelfde manier verwijst de annotatie:
- [@string] verwijst naar het bronbestand [res / values / strings.xml];
- [@color] verwijst naar het bronbestand [res / values / colors.xml];
- [@style] verwijst naar het bronbestand [res / values / styles.xml];
1.7.4. De activiteit
![]() |
De code die voor de activiteit is gegenereerd, is net zo complex als de eerder beschreven weergave. We gaan deze in verschillende stappen analyseren.
1.7.4.1. Het beheer van fragmenten en tabbladen
De code van [MainActivity] met betrekking tot fragmenten en tabbladen is als volgt:
package exemples.android;
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// de fragmentmanager
private SectionsPagerAdapter mSectionsPagerAdapter;
// de fragmentcontainer
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// bovenliggend
super.onCreate(savedInstanceState);
// weergave
setContentView(R.layout.activity_main);
// werkbalk
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// de fragmentbeheerder
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// de fragmentcontainer is gekoppeld aan de fragmentbeheerder
// d.w.z. dat fragment nr. i van de fragmentcontainer het fragment nr. i is dat door de fragmentmanager wordt geleverd
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// de tabbalk is eveneens gekoppeld aan de fragmentcontainer
// dat wil zeggen dat tabblad nr. i fragment nr. i van de container weergeeft
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
// een fragment
public static class PlaceholderFragment extends Fragment {
...
}
// de fragmentbeheerder
// wordt gevraagd welke fragmenten in het hoofdvenster moeten worden weergegeven
// moet de methoden [getItem] en [getCount] definiëren – de overige zijn optioneel
public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
}
}
- regel 28: Android biedt een weergavecontainer van het type [android.support.v4.view.ViewPager] (regel 12). Deze container moet worden voorzien van een weergave- of fragmentbeheerder. Deze wordt door de ontwikkelaar geleverd;
- regel 25: de fragmenthandler die in dit voorbeeld wordt gebruikt. De implementatie ervan staat op de regels 61-63;
- regel 31: de methode die wordt uitgevoerd bij het aanmaken van de activiteit;
- regel 35: de weergave [activity_main.xml] wordt aan de activiteit gekoppeld;
- regel 37: de referentie van de component [toolbar] van de weergave wordt opgehaald via de ID;
- regel 38: deze werkbalk wordt de actiebalk (een Android-concept) van de activiteit;
- regel 40: de fragmentmanager wordt geïnstantieerd. De parameter van de constructor is de Android-klasse [android.support.v4.app.FragmentManager] (regel 10);
- regel 44: in de weergave [activity_main.xml] wordt de verwijzing naar de fragmentcontainer opgehaald via de ID;
- regel 45: de fragmentmanager wordt gekoppeld aan de fragmentcontainer. Dit betekent dat wanneer de fragmentcontainer wordt gevraagd om fragment nr. i weer te geven, dit aan de fragmentmanager wordt gevraagd;
- regel 48: er wordt een verwijzing naar de tabbalk opgehaald via de identificatiecode;
- regel 49: de tabbladbeheerder is gekoppeld aan de fragmentcontainer. Dit betekent dat wanneer er op tabblad nr. i wordt geklikt, de container fragment nr. i weergeeft. Door de koppeling tussen de tabbladbeheerder en de fragmentcontainer hoeven we ons niet bezig te houden met het beheer van de tabbladen. We hoeven dus geen gebeurtenisverwerker te definiëren voor het klikken op een tabblad. De koppeling met de fragmentcontainer zorgt hier standaard voor. We zullen een voorbeeld bekijken waarin er meer fragmenten zijn dan tabbladen. In dat geval maken we deze koppeling niet.
De fragmenthandler [SectionsPagerAdapter] ziet er als volgt uit:
// de fragmentbeheerder
// deze wordt gevraagd om de fragmenten die in de hoofdweergave moeten worden weergegeven
// moet de methoden [getItem] en [getCount] definiëren – de overige zijn optioneel
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment nr. positie
@Override
public Fragment getItem(int position) {
// een fragment [PlaceHolder] wordt geïnstantieerd en weergegeven
return PlaceholderFragment.newInstance(position + 1);
}
// geeft het aantal beheerde fragmenten weer
@Override
public int getCount() {
return 3;
}
// optioneel – geeft de beheerde fragmenten een titel
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "SECTION 1";
case 1:
return "SECTION 2";
case 2:
return "SECTION 3";
}
return null;
}
}
}
- de fragmenten die door een applicatie worden weergegeven, zijn afhankelijk van die applicatie. De fragmentmanager wordt door de ontwikkelaar gedefinieerd;
- regel 5: de fragmentmanager is een uitbreiding van de Android-klasse [android.support.v4.app.FragmentPagerAdapter]. De constructor wordt ons opgelegd. We moeten ten minste de volgende twee methoden definiëren:
- int getCount(): retourneert het aantal te beheren fragmenten;
- Fragment getItem(i): retourneert fragment nr. i;
De methode CharSequence getPageTitle(i), die de titel van fragment nr. i weergeeft, is optioneel. Omdat de tabbladbeheerder is gekoppeld aan de fragmentbeheerder, zal de titel van tabblad nr. i de titel van fragment nr. i zijn. De titels van de regels 27-33 worden dus de titels van de tabbladen;
- regels 18-21: getCount geeft het aantal beheerde fragmenten weer, in dit geval drie;
- regels 11-15: getItem(i) geeft fragment nr. i weer. Hier zullen alle fragmenten identiek zijn van het type [PlaceholderFragment];
- regels 24-35: getPageTitle(int i) geeft de titel van fragment nr. i weer;
1.7.4.2. De weergegeven fragmenten
![]() |
De fragmenten van de activiteit hebben hier allemaal hetzelfde type en zijn allemaal gekoppeld aan de volgende weergave XML [fragment_main]:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- regels 1-16: een layout van het type [RelativeLayout];
- regels 11-14: het enige onderdeel van de weergave (fragment): een [TextView] geïdentificeerd door [section_label];
In [MainActivity] zijn de beheerde fragmenten van het volgende type [PlaceholderFragment]:
// een fragment
public static class PlaceholderFragment extends Fragment {
// een tekst die in het fragment wordt weergegeven
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
// geeft een fragment weer met de informatie: het fragmentnummer dat als parameter is doorgegeven
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// ingebouwde informatie
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// resultaat
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// weergave [fragment_main] is geïnstantieerd
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// de [TextView] is gevonden
TextView textView = (TextView) rootView.findViewById(R.id.section_label);
// de inhoud ervan wordt gewijzigd
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
// de weergave wordt geretourneerd
return rootView;
}
}
- regel 2: de klasse [PlaceholderFragment] is een uitbreiding van de Android-klasse [Fragment]. Dit is over het algemeen altijd het geval;
- regel 2: de klasse [PlaceholderFragment] is statisch. Met de methode [newInstance] (regel 10) kunnen instanties van het type [PlaceholderFragment] worden verkregen;
- regels 10-19: de methode [newInstance] maakt een object van het type [PlaceholderFragment] aan en retourneert dit;
- regels 14-16: het fragment wordt aangemaakt met één argument;
Een fragment moet de methode [onCreateView] uit regel 22 definiëren. Deze methode moet de aan het fragment gekoppelde weergave retourneren.
- regel 25: de weergave [fragment_main.xml] is aan het fragment gekoppeld;
- regel 27: deze weergave bevat een component [TextView] waarvan de referentie via de identificatiecode wordt opgehaald;
- regel 29: er wordt tekst weergegeven in de [TextView];
- [getString] is een methode van de bovenliggende klasse [AppCompatActivity];
- het eerste argument is een componentnummer. [R.string.section_format] verwijst naar het nummer van de component die wordt geïdentificeerd door [section_format] in het bestand [res / values / strings.xml] (regel 4 hieronder):
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
- (vervolg)
- regel (d) hierboven %1$d geeft aan dat argument nr. 1 (%1) moet worden opgemaakt als een geheel getal ($d);
- het tweede argument van [getString] is de waarde die moet worden toegekend aan argument $1 van regel (d) hierboven;
- [getArguments] geeft de referentie van de bundel met de argumenten van het fragment. Hierbij moet worden opgemerkt dat elk argument is aangemaakt met de volgende bundel (regels f-h):
// geeft een fragment terug met de informatie: het fragmentnummer dat als parameter is doorgegeven
public static PlaceholderFragment newInstance(int sectionNumber) {
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
// ingebouwde informatie
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
// resultaat
return fragment;
}
- (vervolg)
- getArguments().getInt(ARG_SECTION_NUMBER) zal dus de waarde [sectionNumber] van de bovenstaande regels (g) en (b) opleveren;
- regel 31: de aldus gecreëerde weergave wordt weergegeven;
1.7.4.3. Menubeheer
In de gegenereerde applicatie is er een menu:
![]() |
De inhoud van het bestand [menu_main.xml] is als volgt:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- regels 1-9: het menu;
- regels 5-8: een menu-item met de identificatiecode [action_settings] (regel 5);
- regel 6: de tekst van de menuoptie. Deze is te vinden in het bestand [res / values / strings.xml] (regel (c) hieronder):
<resources>
<string name="app_name">Exemple-06</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
De bovenstaande code komt overeen met de volgende afbeelding (het menu bevindt zich rechtsboven in het Android-uitvoeringsvenster):
![]() | ![]() |
Dit menu wordt als volgt beheerd in de activiteit [MainActivity]:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Vul het menu aan; hierdoor worden items toegevoegd aan de actiebalk, indien deze aanwezig is.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Verwerk hier klikken op actiebalkitems. De actiebalk zal
// klikken op de Home/Omhoog-knop automatisch verwerken, zolang
// u een bovenliggende activiteit opgeeft in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
- regels 1-6: deze methode wordt aangeroepen wanneer het systeem klaar is om het applicatiemenu aan te maken. De invoerparameter [Menu menu] is een leeg menu dat nog geen opties bevat;
- regel 4: het bestand [res / menu / menu_main.xml] wordt verwerkt. Het als parameter doorgegeven object [Menu menu] krijgt de menuopties toegewezen die in dit bestand zijn gedefinieerd;
- regel 5: er wordt aangegeven dat het menu is aangemaakt;
- regels 8-21: de methode [onOptionsItemSelected] wordt uitgevoerd zodra er op een menuoptie wordt geklikt;
- regel 13: de referentie van de aangeklikte menuoptie;
- regels 16-18: als de aangeklikte optie de optie met ID [action_settings] is, gebeurt er niets en wordt aangegeven dat de gebeurtenis is verwerkt (regel 17);
- regel 20: de gebeurtenis wordt doorgegeven aan de bovenliggende klasse;
Om beter te zien wat er met dit menu gebeurt, voegen we logberichten toe aan de bovenstaande code:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.d("menu", "création menu en cours");
// Vul het menu in; hierdoor worden items aan de actiebalk toegevoegd, indien deze aanwezig is.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Verwerk hier klikken op items in de actiebalk. De actiebalk zal
// kliks op de Home/Omhoog-knop automatisch verwerken, zolang
// je een bovenliggende activiteit opgeeft in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
// bovenliggende
return super.onOptionsItemSelected(item);
}
1.7.4.4. De zwevende knop
De gegenereerde weergave heeft een zwevende knop:
![]() |
Deze component is gedefinieerd in de hoofdweergave [activity-main.xml]:
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
Regel 7 verwijst naar een afbeelding die door Android wordt geleverd, namelijk die van een envelop.
Dit onderdeel wordt in de klasse [MainActivity] als volgt beheerd:
// zwevende knop
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
- regel 2: de referentie van de zwevende knop wordt opgehaald in de weergave die aan de activiteit is gekoppeld (activity_main);
- regels 3-9: er wordt een handler aan gekoppeld om de klik erop te verwerken;
- regel 6: met de klasse [Snackbar] kunnen tijdelijke berichten in de weergave worden weergegeven via de methode [Snackbar.make]. Het eerste argument is een weergave waaruit [Snackbar] een bovenliggende weergave zoekt waarin het bericht moet worden weergegeven. Hier is [view] de weergave van de aangeklikte envelop (regel 5). De bovenliggende weergave die wordt gevonden, is de weergave [activity_main]. Het tweede argument is het weer te geven bericht. Het derde argument is de weergaveduur (SHORT of LONG);
- regel 7: men kan op het weergegeven bericht klikken en zo een actie activeren. Hier is geen actie gekoppeld aan het klikken op het bericht. Ten slotte geeft de methode [show] het bericht weer;
Een klik op de zwevende knop levert het volgende visuele resultaat op:
![]() |
1.7.5. Uitvoering van het project
Nu we de details van de gegenereerde code hebben uitgelegd, kunnen we de uitvoering ervan beter begrijpen:

Wanneer je op tabblad nr. i klikt, wordt fragment nr. i weergegeven in de weergavecontainer. Dit is te zien aan de tekst die wordt weergegeven in [4]. Je kunt ook merken dat je van het ene tabblad naar het andere kunt gaan door de weergave met de muis naar rechts of links te slepen (swipe). We zullen zien dat dit gedrag kan worden aangepast.
Wanneer je op de menuoptie in [6] klikt, krijg je de volgende logbestanden:
![]() |
1.7.6. Levenscyclus van fragmenten
![]() | ![]() |
- In [1] is te zien dat de methode [onCreateView] en de daaropvolgende methoden worden uitgevoerd bij de eerste weergave van het fragment en telkens wanneer de activiteit het opnieuw moet weergeven;
Om de levenscyclus van de activiteit en de fragmenten te volgen, voegen we de volgende logs toe aan de code van [MainActivity]:
// constructor
public MainActivity(){
Log.d("MainActivity","constructor");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity","onCreate");
// bovenliggend
super.onCreate(savedInstanceState);
...
}
// een fragment
public static class PlaceholderFragment extends Fragment {
// een tekst die in het fragment wordt weergegeven
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
// geeft een fragment terug met informatie: het fragmentnummer dat als parameter is doorgegeven
public static PlaceholderFragment newInstance(int sectionNumber) {
Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
// fragment
PlaceholderFragment fragment = new PlaceholderFragment();
...
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
...
}
}
}
We voeren het project opnieuw uit. De eerste logberichten zijn als volgt:
- regel 1: aanmaken van de activiteit;
- regel 2: uitvoering van de methode [onCreate];
- regels 3-4: instantiëren van fragment nr. 1;
- regels 5-6: instantiëren van fragment nr. 2;
- regel 7: initialisatie van fragment nr. 2;
- regel 8: initialisatie van fragment nr. 1;
- regel 9: aanmaken van het menu van de activiteit;
Hierbij moet de code worden onthouden die bepalend is voor het aanmaken van de fragmenten:
// de fragmentbeheerder
// hij wordt gevraagd om de fragmenten die in het hoofdscherm moeten worden weergegeven
// moet de methoden [getItem] en [getCount] definiëren – de overige zijn optioneel
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
// fragment nr. positie
@Override
public Fragment getItem(int position) {
// een fragment [PlaceHolder] wordt geïnstantieerd en weergegeven
return PlaceholderFragment.newInstance(position + 1);
}
...
- regels 11-15: een fragment wordt geïnstantieerd door [newInstance] telkens wanneer de fragmentcontainer erom vraagt;
Uit de bovenstaande logs blijkt dat de eerste twee fragmenten zijn geïnstantieerd en geïnitialiseerd.
Laten we nu op tabblad nr. 2 klikken. De nieuwe logbestanden zijn als volgt:
- regels 1-3: fragment nr. 3 wordt geïnstantieerd en geïnitialiseerd. Ter herinnering: fragment nr. 2 wordt weergegeven;
Laten we nu op tabblad nr. 3 klikken. Daar is geen logboek. Waarschijnlijk omdat fragment nr. 3, dat moet worden weergegeven, al was geïnstantieerd. Laten we nu teruggaan naar tabblad nr. 1. De logboeken zijn dan als volgt:
Fragment nr. 1 wordt niet opnieuw geïnstantieerd, maar de bijbehorende methode [onCreateView] wordt wel opnieuw uitgevoerd. Dit gedrag doet zich ook voor bij de twee andere fragmenten.
Uit deze logbestanden kunnen we het volgende opmaken:
- de activiteit één keer is geïnstantieerd en vervolgens geïnitialiseerd;
- dat elk fragment één keer is geïnstantieerd;
- dat de methode [onCreateView] van elk fragment meerdere keren is uitgevoerd;
Wat belangrijk is om te weten en wat de logbestanden bevestigen, is dat standaard, wanneer fragment nr. i wordt weergegeven, de fragmenten i-1 en i+1 worden geïnstantieerd, als dat nog niet het geval is. Dit verklaart bijvoorbeeld waarom bij het opstarten, terwijl fragment nr. 1 moet worden weergegeven, de fragmenten 1 en 2 zijn geïnstantieerd en geïnitialiseerd. Wat de logbestanden ook laten zien, is dat de methode [getItem(i)] slechts één keer wordt aangeroepen, zelfs als fragment nr. i meerdere keren wordt weergegeven. Het lijkt er dus op dat de fragmentcontainer [ViewPager], die fragment nr. i moet weergeven, dit fragment één keer opvraagt bij de fragmentmanager [SectionsPagerAdapter]. Daarna vraagt hij het niet meer op en blijft hij het fragment gebruiken dat hij heeft ontvangen.
Ten slotte geven de logbestanden aanwijzingen over de fragmentmethode [onCreateView]:
- bij het opstarten zijn de fragmenten 1 en 2 geïnstantieerd en is hun methode [onCreateView] uitgevoerd;
- wanneer men van fragment 1 naar fragment 2 overschakelt, wordt de methode [onCreateView] van fragment 2 niet opnieuw uitgevoerd. We kunnen deze dus niet gebruiken om fragment 2 bij te werken. De gebruiker kan echter met fragment 1 een bewerking hebben uitgevoerd waarvan het resultaat door fragment 2 zou moeten worden weergegeven. We zien dat de methode [onCreateView] niet kan worden gebruikt om fragment 2 bij te werken. Er moet een andere oplossing worden gevonden;
1.8. Voorbeeld-07: Voorbeeld-06 herschreven met de bibliotheek [AA]
1.8.1. Het project aanmaken
We gaan het project [Exemple-06] dupliceren naar [Exemple-07] om in dit laatste de Android-annotaties op te nemen. Volg hiervoor de procedure uit paragraaf 1.4. We krijgen het volgende resultaat:
![]() | ![]() |
1.8.2. Gradle-configuratie
![]() |
We passen het bestand [build.gradle] als volgt aan:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Sinds Android's Gradle-plugin 0.11 moet je android-apt >= 1.3 gebruiken
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
We hebben de configuratie toegevoegd die nodig is voor het gebruik van de bibliotheek [Android Annotations] (zie paragraaf 1.4).
1.8.3. Toevoeging van de eerste annotaties AA
We gaan AA-annotaties aanmaken in [MainActivity]:
![]() |
De klasse [MainActivity] verandert als volgt:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// de fragmentmanager
private SectionsPagerAdapter mSectionsPagerAdapter;
// de fragmentcontainer
@ViewById(R.id.container)
protected MyPager mViewPager;
// de tabbladbeheerder
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// de zwevende knop
@ViewById(R.id.fab)
protected FloatingActionButton fab;
// constructor
public MainActivity() {
Log.d("MainActivity", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// werkbalk
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// de fragmentbeheerder
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// de fragmentcontainer is gekoppeld aan de fragmentbeheerder
// d.w.z. dat fragment nr. i van de fragmentcontainer het fragment nr. i is dat door de fragmentbeheerder wordt geleverd
mViewPager.setAdapter(mSectionsPagerAdapter);
// de tabbalk is eveneens gekoppeld aan de fragmentcontainer
// dat wil zeggen dat tabblad nr. i fragment nr. i van de container weergeeft
tabLayout.setupWithViewPager(mViewPager);
// zwevende knop
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- regel 1: de annotatie [@EActivity] maakt van [MainActivity] een klasse die wordt beheerd door AA. De parameter [R.layout.activity_main] is de identificatiecode van de weergave [activity_main.xml] die aan de activiteit is gekoppeld;
- regels 11-12: de component met de identificatie [R.id.tabs] wordt in het veld [tabLayout] geïnjecteerd. Dit is de tabbladbeheerder;
- regels 14-15: de component met identificatiecode [R.id.fab] wordt in het veld [fab] geplaatst. Dit is de zwevende knop;
- regels 23-50: de code die voorheen in de methode [onCreate] stond, wordt verplaatst naar een methode met een willekeurige naam, maar die is geannoteerd met [@AfterViews] (regel 23). In de aldus geannoteerde methode is men er zeker van dat alle componenten van de visuele interface die zijn geannoteerd met [@ViewById], zijn geïnitialiseerd;
- daarnaast zijn er logboeken toegevoegd om de levenscyclus van de activiteit te kunnen volgen;
We herinneren ons dat de annotatie [@EActivity] een klasse [MainActivity_] zal genereren, die de daadwerkelijke activiteit van het project zal zijn. Het bestand [AndroidManifest.xml] moet daarom als volgt worden aangepast:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- regel 12: de nieuwe activiteit.
Voer het project nu opnieuw uit en controleer of u nog steeds de interface met tabbladen krijgt.
1.8.4. Herschrijven van fragmenten
We gaan het beheer van de fragmenten in het project herzien. Op dit moment is de klasse [PlaceholderFragment] een statische interne klasse van de activiteit [MainActivity]. We gaan terug naar een gebruikelijker gebruiksscenario, namelijk dat waarin fragmenten in externe klassen worden gedefinieerd. Daarnaast introduceren we de annotaties AA voor de fragmenten.
Het project [Exemple-07] ontwikkelt zich als volgt:
![]() |
Hierboven zien we de klasse [PlaceholderFragment] verschijnen, die buiten de klasse [MainActivity] is ondergebracht. Deze is als volgt herschreven:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// een fragment is een weergave die door een fragmentcontainer wordt weergegeven
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// onderdeel van de visuele interface
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// fragmentnummer
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// bovenliggend element
super.onResume();
// weergave
if (textViewInfo != null) {
Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
}
}
- regel 15: het fragment is voorzien van de annotatie [@EFragment], waarvan de parameter de ID is van de weergave XML die aan het fragment is gekoppeld, in dit geval de weergave [fragment_main.xml];
- regels 19-20: voegen in het veld [textViewInfo] de referentie toe van de component van [fragment_main.xml], geïdentificeerd door [R.id.section_label], die van het type [TextView] is (regel (l) hieronder):
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="exemples.android.MainActivity$PlaceholderFragment">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
- regels 42-52: de methode [onResume] wordt uitgevoerd voordat de weergave die bij het fragment hoort, wordt getoond. Deze kan worden gebruikt om de visuele interface die zal worden getoond bij te werken;
- regel 47: de gelijknamige methode van de bovenliggende klasse moet worden aangeroepen;
- regel 49: het is onduidelijk of de methode [onResume] al dan niet kan worden uitgevoerd vóór de initialisatie van het veld op regel 20. De logboeken die zijn ingesteld om de levenscyclus van het fragment te volgen, zullen hierover uitsluitsel geven. Voorlopig en uit voorzorg voeren we een null-test uit;
- regel 51: we werken de informatie van het veld [textViewInfo] bij met het gehele getal dat bij het aanmaken aan het fragment is doorgegeven;
De klasse [MainActivity] verliest haar interne klasse [PlaceholderFragment] en de fragmentmanager verandert als volgt:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// de fragmenten
private Fragment[] fragments;
// aantal fragmenten
private static final int FRAGMENTS_COUNT = 3;
// fragmentnummer
private static final String ARG_SECTION_NUMBER = "section_number";
// fabrikant
public SectionsPagerAdapter(FragmentManager fm) {
// bovenliggend
super(fm);
// initialisatie van de fragmententabel
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length; i++) {
// er wordt een fragment aangemaakt
fragments[i] = new PlaceholderFragment_();
// er kunnen argumenten aan het fragment worden doorgegeven
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
}
// fragment nr. positie
@Override
public Fragment getItem(int position) {
Log.d("MainActivity", String.format("getItem[%s]", position));
return fragments[position];
}
// geeft het aantal beheerde fragmenten weer
@Override
public int getCount() {
return fragments.length;
}
// optioneel – geeft de beheerde fragmenten een titel
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
}
- regel 4: de fragmenten worden in een array geplaatst;
- regels 16-23: de initialisatie van de array met fragmenten vindt plaats in de constructor. Ze zijn van het type [PlaceholderFragment_] (regel 18) en niet van het type [PlaceholderFragment]. De klasse [PlaceholderFragment] is namelijk geannoteerd met een annotatie AA en zal leiden tot een klasse [PlaceholderFragment_] die is afgeleid van [PlaceholderFragment], en het is deze klasse die de activiteit moet gebruiken. Elk aangemaakt fragment krijgt een geheel getal als argument doorgegeven, dat door het fragment wordt weergegeven;
- regels 42-45: we hebben de titels van de fragmenten gewijzigd. Aangezien dit ook de titels van de tabbladen zijn, zouden we een wijziging in de tabbalk moeten zien;
Laten we [Make] en [1] compileren:
![]() | ![]() |
- in [2], we zien dat de klassen die door de bibliotheek AA worden gegenereerd, in de map [app / build / generated / source / apt / debug] staan (je moet in het perspectief [Project] zijn om [2] te zien);
Voer het project [Exemple-07] uit en controleer of het nog steeds werkt.
1.8.5. Controle van de logbestanden
Wanneer de applicatie wordt gestart, zijn de logbestanden als volgt:
- regel 1: opbouw van de enige activiteit;
- regel 2: methode [afterViews] van de activiteit: de velden die zijn geannoteerd door [@ViewById] worden geïnitialiseerd;
- regels 3-5: opbouw van de drie fragmenten;
- regels 6-7: de fragmentcontainer [ViewPager] vraagt de eerste twee fragmenten op;
- regels 8-9: methoden van fragment 2;
- regels 10-11: methoden van fragment 1;
- regels 12-13: methode [onResume] van fragment 1;
- regels 14-15: methode [onResume] van fragment 2;
- regel 16: het menu van de activiteit aanmaken;
Merk op dat hiermee het antwoord op een eerder gestelde vraag wordt gegeven: de methode [onResume] uit fragment 1 (regel 12) wordt bijvoorbeeld uitgevoerd na de methode [afterViews] uit hetzelfde fragment (regel 11). Wanneer de methode [onResume] wordt uitgevoerd, kan deze dus gebruikmaken van de velden die door [@ViewById] zijn geannoteerd. We kunnen de methode [onResume] nu dus als volgt schrijven:
@Override
public void onResume() {
Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// bovenliggend
super.onResume();
// weergave
textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}
Laten we nu van tabblad 1 naar tabblad 2 gaan. De nieuwe logberichten zijn als volgt:
- regel 1: de fragmentcontainer [ViewPager] vraagt fragment nr. 3 op;
- regels 2-3: methoden van fragment nr. 3. Ter herinnering: dit fragment was al bij het opstarten van de applicatie geïnstantieerd;
- regels 4-5: de methode [onResume] van fragment nr. 3 wordt uitgevoerd. Ter herinnering: het is fragment nr. 2 dat wordt weergegeven;
Laten we nu van tabblad 2 naar tabblad 3 gaan. Er is geen logboek. Er wordt dus geen van de methoden [onCreateView, afterViews, onResume] van fragment nr. 3 uitgevoerd. De tekst [Hello World from section:3] wordt alleen correct weergegeven omdat deze tekst al in de vorige stap was aangemaakt bij het weergeven van fragment nr. 2. We herinneren ons namelijk dat in die stap de methode [onResume] van fragment nr. 3 was uitgevoerd. We zien hier dat de methode [onResume], net als de methode [onCreateView], niet kan worden gebruikt om fragment 3 bij te werken. Als de door het fragment weergegeven tekst had moeten worden gewijzigd, had geen van deze twee methoden dat kunnen doen.
Laten we nu teruggaan van tabblad nr. 3 naar tabblad nr. 1. De logbestanden zien er dan als volgt uit:
We zien dat alle methoden van fragment 1 zijn uitgevoerd. We zien dat de methode getItem niet is aangeroepen. Zoals gezegd wordt deze methode slechts één keer per fragment aangeroepen;
Laten we nu van tabblad 1 naar het aangrenzende tabblad 2 gaan. We krijgen de volgende logbestanden:
Verrassend, nietwaar? Alle methoden van fragment nr. 3 worden opnieuw uitgevoerd.
Om dit fenomeen te begrijpen, moeten we bedenken dat de fragmentcontainer, wanneer hij fragment i weergeeft, standaard de fragmenten i-1, i en i+1 initialiseert. Laten we de logbestanden nog eens bekijken in het licht van deze informatie.
Allereerst de logbestanden bij het opstarten van de applicatie:
Omdat de fragmentcontainer fragment 1 gaat weergeven, worden fragmenten 1 en 2 geïnitialiseerd (regels 8-15).
We gaan nu van tabblad 1 naar tabblad 2:
Omdat de fragmentcontainer fragment 2 gaat weergeven, moeten de fragmenten 1, 2 en 3 worden geïnitialiseerd. De fragmenten 1 en 2 zijn al geïnitialiseerd in de vorige stap. Fragment 3 wordt geïnitialiseerd in de regels 2-5.
We gaan van tabblad 2 naar tabblad 3. Er zijn geen logberichten. Omdat de fragmentcontainer fragment 3 gaat weergeven, moeten de fragmenten 2 en 3 worden geïnitialiseerd. Maar sinds de vorige stap zijn ze dat al. Wat we hier niet zien, is dat fragment 1, dat niet grenst aan fragment 3, zijn status verliest, die niet in het geheugen wordt bewaard.
We gaan van tabblad 3 naar tabblad 1. De logbestanden zijn als volgt:
Omdat de fragmentcontainer fragment 1 gaat weergeven, moet ook fragment 2 worden geïnitialiseerd. Dit is al gebeurd in de vorige stap. In dezezelfde stap was de status van fragment 1 verloren gegaan. Deze wordt daarom opnieuw geïnitialiseerd in de regels 1-4. Wat we hier niet zien, is dat fragment 3, dat niet grenst aan fragment 1, zijn status verliest, die vervolgens niet in het geheugen wordt bewaard.
Wanneer we van tabblad 1 naar het aangrenzende tabblad 2 gaan, krijgen we de volgende logberichten:
Omdat de fragmentcontainer fragment 2 gaat weergeven, moeten de fragmenten 1, 2 en 3 worden geïnitialiseerd. De fragmenten 1 en 2 zijn al geïnitialiseerd in de vorige stap. Fragment 3 wordt geïnitialiseerd op de regels 1-4.
Wat hebben we geleerd?
- dat het standaardbeheer van fragmenten heel bijzonder is en dat je dit moet kennen als je je haren niet wilt verliezen. We kunnen deze beheermodus wijzigen en dat zullen we iets verderop doen;
- dat met dit standaardbeheer geen van de methoden [onCreateView, onResume] kan worden gebruikt om het fragment dat wordt weergegeven bij te werken, omdat we niet zeker weten of ze zullen worden uitgevoerd;
1.8.6. onDestroyView
De methode [onDestroyView] maakt deel uit van de levenscyclus van fragmenten (zie paragraaf 1.7.6):
![]() | ![]() |
We zien dat in de levenscyclus van een fragment:
- de methode [onCreateView] meerdere keren kan worden uitgevoerd;
- voordat later wordt teruggekeerd naar de methode [onCreateView], moet er noodzakelijkerwijs eerst de methode [onDestroyView] [2] worden doorlopen;
We gaan deze methoden in de fragmenten opnemen om hun levenscyclus beter te kunnen volgen. De code van het fragment ziet er dan als volgt uit:
package exemples.android;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// een fragment is een weergave die door een fragmentcontainer wordt weergegeven
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
...
@Override
public void onDestroyView() {
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
// bovenliggend
super.onDestroyView();
}
}
Laten we de applicatie uitvoeren. De eerste logberichten zijn als volgt:
- regel 1: opbouw van de enige activiteit;
- regel 2: methode [afterViews] van de activiteit: de velden die zijn geannoteerd met [@ViewById] worden geïnitialiseerd;
- regels 3-5: aanmaken van de drie fragmenten;
- regels 6-7: de fragmentcontainer [ViewPager] vraagt de eerste twee fragmenten op;
- regels 8-9: de weergave van fragment 2 wordt aangemaakt (niet noodzakelijkerwijs zichtbaar gemaakt);
- regels 10-11: de weergave van fragment 1 wordt aangemaakt (wordt niet per se zichtbaar gemaakt);
- regels 12-13: methode [onResume] van fragment 1;
- regels 14-15: methode [onResume] van fragment 2;
- regel 16: het menu van de activiteit wordt aangemaakt;
Laten we van tabblad 1 naar tabblad 3 gaan:
06-03 02:50:02.685 2346-2346/exemples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 1
- regel 1: de fragmentcontainer vraagt om het derde fragment;
- regels 2-3: de weergave van fragment 3 wordt aangemaakt (niet noodzakelijkerwijs weergegeven);
- regels 4-5: de methode [onResume] van fragment 3 wordt uitgevoerd;
- regel 6: de methode [onDestroyView] van fragment 1 wordt uitgevoerd. Dit betekent dat wanneer de gebruiker terugkeert naar fragment 1 of naar een aangrenzend fragment, de levenscyclus van dit fragment opnieuw wordt uitgevoerd;
We keren terug van tabblad 3 naar tabblad 1:
06-03 02:53:46.255 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 3
- regels 1-4: de levenscyclus van fragment 1 wordt opnieuw uitgevoerd omdat er een [onDestroyView] op was toegepast;
- regel 5: nu wordt de methode [onDestroyView] van fragment 3 uitgevoerd. Ook hier geldt dat wanneer de gebruiker terugkeert naar fragment 3 of naar een aangrenzend fragment, de levenscyclus van dit fragment opnieuw wordt uitgevoerd;
1.8.7. setUserVisibleHint
De methode [onCreateView] van de levenscyclus instantiëert de weergave die aan het fragment is gekoppeld, maar maakt deze niet noodzakelijkerwijs zichtbaar. Dat gaan we nu bekijken. De methode [Fragment.setUserVisibleHint] wordt uitgevoerd telkens wanneer de zichtbaarheid van het fragment verandert. We voegen deze methode toe aan de code van het fragment:
package exemples.android;
....
// een fragment is een weergave die door een fragmentcontainer wordt weergegeven
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// onderdeel van de visuele interface
@ViewById(R.id.section_label)
protected TextView textViewInfo;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
}
}
Bij het opstarten zijn de logberichten als volgt:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- uit de logberichten op regel 7 en 9-10 blijkt dat alleen fragment 1 zichtbaar wordt. We zien ook dat het zichtbaar wordt voordat de bijbehorende methode [onCreateView] wordt uitgevoerd;
Laten we van tabblad 1 naar tabblad 2 gaan:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
- fragment 1 is verborgen (regel 3), fragment 2 wordt weergegeven (regel 4);
Laten we van tabblad 2 naar tabblad 3 gaan:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
- fragment 2 is verborgen (regel 1), fragment 3 wordt weergegeven (regel 2);
Laten we teruggaan naar tabblad 1:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
- fragment 3 is verborgen (regel 2), fragment 1 wordt weergegeven (regel 3);
Wat hebben we geleerd?
- de methode [setUserVisibleHint] wordt één keer uitgevoerd met de eigenschap [isVisibleToUser] tot true, voor het fragment dat zal worden weergegeven;
- we kunnen niet vaststellen wanneer deze methode wordt uitgevoerd ten opzichte van de levenscyclus van het fragment. Zo werd voor fragment 1 de methode [setUserVisibleHint, true] uitgevoerd vóór de methode [onCreateView] aan het begin van de levenscyclus van dit fragment, terwijl voor de fragmenten 2 en 3 het omgekeerde gebeurde;
1.8.8. setOffscreenPageLimit
Uit de voorgaande logbestanden blijkt dat wanneer de fragmentcontainer [ViewPager] op het punt staat fragment nr. i weer te geven, deze – indien dat nog niet is gebeurd – de levenscyclus van de aangrenzende fragmenten i-1 en i+1 uitvoert. Deze werking kan worden gecontroleerd via de methode [ViewPager].setOffscreenPageLimit:
Met de bovenstaande instructie,
- wanneer de fragmentcontainer [ViewPager] op het punt staat fragment nr. i weer te geven, voert deze, indien nog niet gebeurd, de levenscyclus van de aangrenzende fragmenten van het interval [i-n, i+n] uit;
- als vervolgens fragment j wordt weergegeven:
- doet zich hetzelfde fenomeen voor bij de aangrenzende fragmenten van het interval [j-n, j+n];
- de fragmenten die in stap 1 zijn geïnitialiseerd en die zich niet meer in de nabijheid [j-n, j+n] van het nieuwe fragment bevinden, kunnen dan een bewerking [onDestroyView] ondergaan. Ik heb echter bij andere toepassingen, met name die uit hoofdstuk 3, kunnen vaststellen dat dit niet altijd het geval was;
We passen de methode [MainActivity.afterViews] als volgt aan:
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
// werkbalk
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// de fragmentbeheerder
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// de fragmentcontainer is gekoppeld aan de fragmentbeheerder
// d.w.z. dat fragment nr. i van de fragmentcontainer het fragment nr. i is dat door de fragmentbeheerder wordt geleverd
mViewPager.setAdapter(mSectionsPagerAdapter);
// het swipen tussen fragmenten wordt geblokkeerd
mViewPager.setSwipeEnabled(false);
// offset van de fragmenten
mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
// de tabbalk is ook gekoppeld aan de fragmentcontainer
// dat wil zeggen dat tabblad nr. i fragment nr. i van de container weergeeft
tabLayout.setupWithViewPager(mViewPager);
// zwevende knop
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
- regel 20: we stellen het aantal aangrenzende fragmenten dat moet worden geïnitialiseerd gelijk aan het totale aantal fragmenten minus 1. Zo zal de fragmentcontainer bij het opstarten, wanneer fragment nr. 1 wordt weergegeven, tegelijkertijd de fragmenten 2, 3, ..., n initialiseren met n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount(). Dit zijn dus alle fragmenten die worden geïnitialiseerd. Wanneer het weergavevenster naar een ander fragment verschuift, zal de fragmentcontainer:
- ontdekken dat alle aangrenzende fragmenten van het nieuwe fragment al geïnitialiseerd zijn en zal deze daarom niet opnieuw initialiseren;
- aangezien de aangrenzende fragmenten van het nieuwe fragment ook alle andere fragmenten omvatten, zal geen enkel fragment door de fragmentcontainer worden ‘gede-initialiseerd’;
In totaal zouden we alle fragmenten geïnstantieerd en geïnitialiseerd moeten zien bij het opstarten van de applicatie en daarna nooit meer. Dit gaan we nu controleren door de logs te bekijken.
Bij het opstarten hebben we de volgende logbestanden:
- regels 4-6: aanmaken van de drie fragmenten;
- regels 7, 9, 11: de fragmentcontainer vraagt de drie fragmenten op. In de vorige versie vroeg hij er twee op;
- regels 14-25: de levenscyclus van de drie fragmenten wordt uitgevoerd;
Laten we nu van tabblad 1 naar tabblad 2 gaan:
Laten we van tabblad 2 naar tabblad 3 gaan:
Vervolgens van tabblad 3 naar tabblad 1:
De logs bevestigen de theorie. Alle fragmenten zijn bij het opstarten geïnstantieerd en geïnitialiseerd. Daarna worden de methoden van hun levenscyclus niet meer uitgevoerd. Dit zorgt voor een zeer voorspelbare werking van de fragmenten, wat het gebruik ervan enorm vergemakkelijkt.
Wat we willen vinden, is een manier om een fragment dat wordt weergegeven bij te werken, ongeacht de door de ontwikkelaar gekozen rangschikking van de fragmenten. De logbestanden hebben ons twee dingen laten zien:
- de methode [setUserVisibleHint, true] wordt altijd uitgevoerd voor het fragment dat wordt weergegeven, en niet voor de andere;
- deze gebeurtenis kan plaatsvinden vóór of na de levenscyclus van het fragment. Dit hangt af van de door de ontwikkelaar gekozen fragment-adjacency. Dit is een probleem, want als de levenscyclus nog niet heeft plaatsgevonden, betekent dit dat het fragment niet kan worden bijgewerkt via de methode [setUserVisibleHint, true];
De logboeken bij het opstarten van de applicatie, toen de fragmentadjacency 1 was, waren als volgt:
06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
- we zien dat wanneer fragment 1 zichtbaar wordt, de weergave ervan nog niet is aangemaakt. Er kan dus nog niets mee worden gedaan. Dit kan vervolgens tijdens de levenscyclus van het fragment gebeuren, bijvoorbeeld in de methoden [onCreateView] (regel 11) of [onResume] (regels 13-14). Aangezien we de annotaties AA gebruiken, hoeven we de methode [onCreateView] normaal gesproken niet te schrijven. De methode [onResume] lijkt hier dus het meest geschikt om fragment 1 bij te werken;
Toen we van tabblad 1 naar tabblad 2 gingen, waren de logboeken als volgt:
06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
Deze keer is er alleen de methode [setUserVisibleHint, true] op regel 4 om fragment 2 bij te werken;
Toen we van tabblad 2 naar tabblad 3 gingen, waren de logberichten als volgt:
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
Hier hebben we alleen de methode [setUserVisibleHint, true] op regel 2 om fragment 3 bij te werken;
Toen we van tabblad 3 naar tabblad 1 gingen, waren de logboekvermeldingen als volgt:
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
Hier moet de methode [onResume] van fragment 1 (regels 6-7) worden gebruikt om fragment 1 bij te werken.
In dit voorbeeld zien we dus dat er twee methoden beschikbaar zijn om een fragment bij te werken dat zal worden weergegeven: [setUserVisibleHint] en [onResume].
We gaan deze oplossing implementeren in een nieuw project waarin elk fragment moet weergeven hoe vaak het is weergegeven, wat we een ‘bezoek’ zullen noemen. De weergave moet dus telkens worden bijgewerkt wanneer het fragment wordt weergegeven. Dit is precies het probleem dat we willen oplossen.
Maar eerst bekijken we de laatste fase in de levenscyclus van een activiteit of een fragment, namelijk het moment waarop deze wordt verwijderd. Het systeem kan het initiatief nemen om een activiteit te verwijderen als andere activiteiten met een hogere prioriteit om onbeschikbare bronnen vragen. Om deze bronnen vrij te maken, zal het systeem het initiatief nemen om bepaalde activiteiten te verwijderen. De methode [onDestroy] van de activiteit en de fragmenten wordt dan aangeroepen.
1.8.9. OnDestroy
![]() | ![]() | ![]() |
We gaan de gebruiker de mogelijkheid bieden om de activiteit te verwijderen via een menuoptie [5]. Hiervoor voegen we een nieuwe menuoptie toe in het bestand [menu_main.xml] [1]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/action_terminate"
android:title="@string/action_terminate"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
We kopiëren en plakken de eerste menuoptie en passen het resultaat aan (regels 9 en 10). De tekst van deze nieuwe optie wordt toegevoegd aan het bestand [strings.xml] [2]:
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="action_terminate">Terminate</string>
<string name="section_format">Hello World from section: %1$d</string>
</resources>
Ten slotte wordt in de klasse [MainActivity] de klik op de optie [Terminate] afgehandeld:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d("menu", "onOptionsItemSelected");
// Verwerk hier klikken op items in de actiebalk. De actiebalk zal
// klikken op de knop Home/Omhoog automatisch verwerken, zolang
// je een bovenliggende activiteit opgeeft in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Log.d("menu", "action_settings selected");
return true;
}
if (id == R.id.action_terminate) {
Log.d("menu", "action_terminate selected");
//de bovenliggende activiteit wordt beëindigd
finish();
return true;
}
// bovenliggende
return super.onOptionsItemSelected(item);
}
- regels 14-19: we kopiëren en plakken regels 10-13 en passen de code aan de nieuwe optie aan;
- regel 17: de activiteit wordt beëindigd door een softwareactie;
Laten we nu deze nieuwe versie uitvoeren en zodra het eerste scherm wordt weergegeven, klikken we op de menuoptie [Terminate]. De logbestanden zien er dan als volgt uit:
- regels 1-2: klik op de optie [Terminate];
- regel 4: de methode [onDestroy] van de activiteit wordt aangeroepen;
- regels 4-5: de methode [onDestroyView] van fragment 1 wordt aangeroepen, gevolgd door de methode [onDestroy];
- regels 6-9: deze bewerking wordt herhaald voor de twee andere fragmenten;
We moeten dus onthouden dat de methode [onDestroy] van de activiteit en de fragmenten wordt aangeroepen wanneer de activiteit door het systeem, de ontwikkelaar of de gebruiker wordt verwijderd. We kunnen deze methode gebruiken om informatie op te slaan, bijvoorbeeld lokaal op de tablet, zodat deze weer terug te vinden is wanneer de gebruiker de applicatie opnieuw start.
1.9. Voorbeeld-08: een fragment bijwerken met een variabele fragmentenvolgorde
1.9.1. Het project aanmaken
We dupliceren het project [Exemple-07] naar [Exemple-08]. Hiervoor volgen we de procedure die is beschreven voor het dupliceren van [Exemple-02] naar [Exemple-03] in paragraaf 1.4.
![]() | ![]() |
1.9.2. Herschrijving van het fragment [PlaceholderFragment]
De nieuwe code voor het fragment [PlaceholderFragment] is als volgt. Deze werkt ongeacht de aangewezen nabijheid van de fragmenten (1, gedeeltelijk, volledig):
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
// een fragment is een weergave die door een fragmentcontainer wordt weergegeven
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
// onderdeel van de visuele interface
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// gegevens
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
// fragmentnummer
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public PlaceholderFragment() {
Log.d("PlaceholderFragment", "constructor");
}
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// log
Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
if (!initDone) {
// oorspronkelijke tekst
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// initialisatie voltooid
initDone = true;
}
// huidige tekstweergave
textViewInfo.setText(text);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
@Override
public void onDestroyView() {
...
}
@Override
public void onResume() {
...
}
// fragment bijwerken
public void update() {
// de uit te voeren taak hangt af van het bezoeknummer
if (numVisit > 1) {
// logboek
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// tekst gewijzigd
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// lokale informatie voor logboeken
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
}
}
- regels 34-48: de methode [@AfterViews] kan meerdere keren worden uitgevoerd. Deze werd gebruikt om de tekst van het fragment te initialiseren (regel 42). Dit doen we nog steeds, maar om ervoor te zorgen dat het slechts één keer gebeurt, beheren we een booleaanse variabele [initDone] (regel 44) om aan te geven dat de initialisatie is uitgevoerd en niet opnieuw hoeft te worden gedaan;
- regels 56-59: we introduceren de methode [onDestroyView] om aan te geven dat de volgende keer dat het fragment opnieuw wordt weergegeven, de levenscyclus ervan opnieuw wordt uitgevoerd;
- uit de logbestanden is gebleken dat er na de methode [@AfterViews] twee methoden kunnen worden uitgevoerd: de methoden [setUserVisibleHint] en [onResume]. De methode [onResume] wordt alleen uitgevoerd wanneer de levenscyclus van het fragment wordt uitgevoerd. De methode [setUserVisibleHint] wordt daarentegen niet altijd uitgevoerd na de methode [@AfterViews]. Uit de logbestanden is gebleken dat ten minste één van beide wordt uitgevoerd na de methode [@AfterViews]. Uit de logbestanden is nooit gebleken dat beide methoden samen na de methode [@AfterViews] kunnen worden uitgevoerd. Het is ofwel de ene, ofwel de andere. Uit voorzorg wordt een booleaanse waarde [updateDone] ingesteld wanneer er een update is uitgevoerd;
De methoden [setUserVisibleHint] en [onResume] zijn als volgt:
// gegevens
private boolean afterViewsDone = false;
private boolean initDone = false;
private String text;
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private int numVisit = 0;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// bovenliggend
super.setUserVisibleHint(isVisibleToUser);
// geheugen
this.isVisibleToUser = isVisibleToUser;
// log
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// aantal bezoeken
if (isVisibleToUser) {
// increment
numVisit++;
// fragment bijwerken
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// het fragment wordt verborgen
updateDone = false;
}
}
@Override
public void onResume() {
// bovenliggend
super.onResume();
// log
Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// update
if (isVisibleToUser && !updateDone) {
update();
updateDone = true;
}
}
- regel 14: de zichtbare of onzichtbare status van het fragment wordt opgeslagen;
- regels 22-25: als het fragment zichtbaar is en de methode [@AfterViews] is uitgevoerd, wordt de methode [update] uitgevoerd en wordt de booleaanse waarde [updateDone] doorgegeven aan true;
- regels 26-28: als het fragment wordt verborgen, wordt de booleaanse waarde [updateDone] teruggezet naar false. We hebben namelijk een gebeurtenis nodig om de booleaanse waarde [updateDone], die op true is gezet zodra de methode [update] wordt aangeroepen, terug te zetten naar false, zodat er nieuwe updates kunnen plaatsvinden. We maken hiervoor gebruik van het feit dat het fragment niet meer zichtbaar is. Zodra het weer zichtbaar wordt, moet het fragment opnieuw worden bijgewerkt;
- regels 32-42: uit de logbestanden blijkt dat, afhankelijk van de gekozen adjacentie voor de fragmenten, de methode [onResume] kan worden uitgevoerd terwijl het fragment niet zichtbaar is. Als het niet zichtbaar is, voeren we de update niet uit (regel 39) en net zoals we bij [setMenuVisibility] hebben gedaan, beheren we de booleaanse variabele [updateDone].
Ten slotte is de methode [onDestroyView] als volgt:
@Override
public void onDestroyView() {
// bovenliggend
super.onDestroyView();
// indicator bijwerken
afterViewsDone = false;
// log
Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}
De methode [onDestroyView] wordt uitgevoerd wanneer de levenscyclus van het fragment afloopt. Een nieuwe cyclus kan later worden hervat.
- regel 6: de methode [onDestroyView] verwijdert elke koppeling met de aan het fragment gekoppelde weergave. Deze wordt bij de volgende levenscyclus van het fragment opnieuw aangemaakt. Voorlopig moeten we de booleaanse waarde van [afterViews] instellen op false, om aan te geven dat de koppeling met de weergave niet meer bestaat;
We gaan de applicatie uitvoeren met 5 fragmenten met een adjacentie van 2. De wijzigingen worden aangebracht in [MainActivity]:
// aantal fragmenten
private final int FRAGMENTS_COUNT = 5;
// aangrenzende fragmenten
private final int OFF_SCREEN_PAGE_LIMIT=2;
// de fragmentbeheerder
private SectionsPagerAdapter mSectionsPagerAdapter;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
....
// offset van de fragmenten
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
...
}
De logbestanden bij het opstarten zijn als volgt:
05-31 06:23:07.015 32551-32551/exemples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/exemples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/exemples.android D/menu: création menu en cours
- regels 8, 10, 12: de fragmentcontainer vraagt alle fragmenten op die grenzen aan fragment 1;
- regels 9, 11, 13: de methode [setUserVisibleHint] van deze fragmenten wordt uitgevoerd met [visibleToUser] tot en met false;
- regel 14: de methode [setUserVisibleHint] van fragment 1 wordt uitgevoerd met [visibleToUser] tot en met true;
- regels 15-17: de methode [afterViews] van de 3 aangrenzende segmenten wordt aangeroepen. We zien hier dus een geval waarin deze methode wordt aangeroepen nadat een fragment zichtbaar is geworden (fragment 1, regel 14);
- regels 18-20: de methode [onResume] voor de 3 aangrenzende segmenten wordt aangeroepen;
We gaan van tabblad 1 naar tabblad 2:
05-31 06:52:36.132 32551-32551/exemples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- omdat de aangrenzende fragmenten één positie naar rechts zijn verschoven, wordt fragment 4 opgevraagd door de fragmentcontainer;
- regel 2: de methode [setUserVisibleHint] van fragment 4 wordt aangeroepen met [visibleToUser] tot en met false;
- regel 3: de methode [setUserVisibleHint] van fragment 1 wordt aangeroepen met [visibleToUser] tot en met false. Fragment 1 is nu namelijk verborgen;
- regel 4: de methode [setUserVisibleHint] van fragment 2 wordt aangeroepen met [visibleToUser] tot en met true. Fragment 2 is nu zichtbaar;
- regels 5-6: de levenscyclus van fragment 4 gaat verder;
We gaan van tabblad 2 naar tabblad 3:
05-31 06:58:16.228 32551-32551/exemples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
- omdat de volgorde van de fragmenten één positie naar rechts is verschoven, wordt fragment 5 opgevraagd door de fragmentcontainer;
- regel 2: de methode [setUserVisibleHint] van fragment 5 wordt aangeroepen met [visibleToUser] tot en met false;
- regel 3: de methode [setUserVisibleHint] van fragment 2 wordt aangeroepen met [visibleToUser] tot false. Fragment 2 is nu namelijk verborgen;
- regel 4: de methode [setUserVisibleHint] van fragment 3 wordt aangeroepen met [visibleToUser] naar true. Fragment 3 is nu zichtbaar;
- regels 5-6: de levenscyclus van fragment 5 gaat verder;
We gaan van tabblad 3 naar tabblad 4:
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- regel 1: fragment 3 is nu verborgen;
- regel 2: fragment 4 is nu zichtbaar. Merk op dat de levenscyclus van fragment 4 niet wordt uitgevoerd. Dit is al twee stappen eerder gebeurd;
- regel 3: fragment 1 verlaat de nabijheid van het weergegeven fragment 4. De methode [onDestroyView] wordt uitgevoerd. De volgende keer dat het wordt weergegeven, wordt de weergavecyclus [onCreateView, afterViews, onResume] opnieuw uitgevoerd;
We gaan van tabblad 4 naar tabblad 5:
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- regel 1: fragment 4 is nu verborgen;
- regel 2: fragment 5 is nu zichtbaar. Merk op dat de levenscyclus van fragment 5 niet wordt uitgevoerd. Dit is al twee stappen eerder gebeurd;
- regel 3: fragment 2 verlaat de nabijheid van het weergegeven fragment 5. De methode [onDestroyView] wordt uitgevoerd;
We gaan van tabblad 5 naar tabblad 1:
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
- regels 1, 4, 5, 6: de levenscyclus van fragment 1 wordt opnieuw uitgevoerd. Het had namelijk de verbinding met zijn weergave verloren;
- regels 2, 5, 8, 9: om dezelfde reden wordt de levenscyclus van fragment 2 opnieuw uitgevoerd;
- regels 10-11: fragmenten 4 en 5 verlaten de nabijheid van het weergegeven fragment;
- regel 7: fragment 1 wordt bijgewerkt;
![]() |
Uit de logbestanden is nooit gebleken dat de methoden [setUserVisibleHint] en [onResume] beide probeerden het fragment bij te werken. Het is ofwel de ene, ofwel de andere. De lezer wordt uitgenodigd om verdere tests uit te voeren en de logbestanden te volgen om het begrip ‘adjacentie’ en de levenscyclus van fragmenten goed te begrijpen.
Laten we nu uitgaan van volledige adjacentie en dezelfde tests uitvoeren.
In [MainActivity]:
// aantal fragmenten
private final int FRAGMENTS_COUNT = 5;
// aangrenzende fragmenten
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;
De logbestanden bij het opstarten zijn als volgt:
05-31 07:34:44.717 28908-28908/exemples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/exemples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/exemples.android D/menu: création menu en cours
- uit de logbestanden blijkt dat de levenscyclus van de 5 fragmenten wordt uitgevoerd;
- fragment 1 wordt weergegeven op regel 18;
We gaan van tabblad 1 naar tabblad 2:
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- regel 1: fragment 1 wordt verborgen;
- regel 2: fragment 2 wordt weergegeven;
We gaan van tabblad 2 naar tabblad 3:
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- regel 1: fragment 2 is verborgen;
- regel 2: fragment 3 wordt weergegeven;
We gaan van tabblad 3 naar tabblad 4:
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- regel 1: fragment 3 is verborgen;
- regel 2: fragment 4 wordt weergegeven;
We gaan van tabblad 4 naar tabblad 5:
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- regel 1: fragment 4 is verborgen;
- regel 2: fragment 5 wordt weergegeven;
We gaan van tabblad 5 naar tabblad 1:
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- regel 1: fragment 5 is verborgen;
- regel 2: fragment 1 wordt weergegeven;
- regel 3: fragment 1 wordt bijgewerkt;
We gaan van tabblad 1 naar tabblad 4:
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: update 4 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
- regel 1: fragment 1 is verborgen;
- regel 2: fragment 4 wordt weergegeven;
- regel 3: fragment 4 wordt bijgewerkt;
We merken op dat bij volledige adjacentie het gedrag van de fragmenten veel voorspelbaarder is.
Laten we nu eens 'nul-adjacence' instellen en kijken wat er gebeurt. De klasse [MainActivity] ontwikkelt zich als volgt:
// aantal fragmenten
private final int FRAGMENTS_COUNT = 5;
// aangrenzende fragmenten
private final int OFF_SCREEN_PAGE_LIMIT = 0;
De logbestanden bij het opstarten zijn als volgt:
06-01 03:11:52.068 5679-5679/exemples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/exemples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/exemples.android D/menu: création menu en cours
- Op regels 8 en 10 zien we dat de fragmentcontainer 2 fragmenten heeft aangevraagd, namelijk nr. 1 en nr. 2. Alles verloopt dus alsof er een adjacentie van 1 is. De adjacentie van 0 is dus genegeerd.
1.9.3. Communicatie tussen fragmenten
In de vorige architectuur hebben we één activiteit en n fragmenten. De gebruiker heeft interactie met de verschillende fragmenten. Deze interacties wijzigen de toestand van de applicatie. Onder de toestand van de applicatie verstaan we hier het geheel van informatie dat de applicatie gedurende haar hele levensduur opslaat. Het volgende probleem doet zich dan voor:
- wanneer de gebruiker interactie heeft met fragment i, gaat de applicatie van de toestand E1 over naar de toestand E2;
- een actie van de gebruiker op fragment i zorgt ervoor dat fragment j wordt weergegeven;
- hoe kan fragment j worden bijgewerkt met de huidige toestand E2 van de applicatie;
Uit eerdere voorbeelden weten we hoe we fragment j kunnen bijwerken. Maar waar vinden we de status E2 van de applicatie om het bij te werken?
Er zijn verschillende oplossingen voor dit probleem. We hebben er één gezien: fragment i kan de status E2 van de applicatie via argumenten doorgeven aan fragment j. We zijn deze methode tegengekomen in de klasse [MainActivity] bij het aanmaken van de fragmenten:
for (int i = 0; i < fragments.length; i++) {
// er wordt een fragment aangemaakt
fragments[i] = new PlaceholderFragment_();
// er kunnen argumenten aan het fragment worden doorgegeven
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
Deze oplossing is hier niet direct bruikbaar. Wanneer de gebruiker namelijk op tabblad j klikt, waardoor fragment j verschijnt, wordt onze code namelijk niet aangeroepen. Er wordt uitsluitend systeemcode uitgevoerd. In een volgend project zullen we bekijken hoe we de klik op een tabblad kunnen onderscheppen, maar voorlopig kiezen we voor een andere aanpak.
We hebben het gehad over de status van de applicatie: het geheel van gegevens dat door de applicatie in de loop van de tijd wordt beheerd. Hier bestaat de applicatie uit één activiteit en n fragmenten, die allemaal één keer worden geïnstantieerd bij het opstarten van de applicatie en waarvan de levensduur gelijk is aan die van de applicatie. Elk van deze elementen, of meerdere samen, kunnen dus in aanmerking komen om de status van de applicatie op te slaan. Elk fragment heeft via de methode [Fragment.getActivity()] toegang tot de activiteit die het heeft aangemaakt. Aangezien alle fragmenten toegang hebben tot de activiteit, lijkt het logisch om de status van de applicatie daarin op te slaan.
Het resultaat van de methode [Fragment.getActivity()] hangt echter af van het moment waarop deze in de levenscyclus wordt aangeroepen. We illustreren dit punt door enkele logboekvermeldingen toe te voegen aan de klasse [PlaceholderFragment]:
// fragment bijwerken
public void update() {
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// de uit te voeren werkzaamheden zijn afhankelijk van het bezoeknummer
if (numVisit > 1) {
// logboek
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// gewijzigde tekst
textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
}
}
// lokale informatie voor logbestanden
private String getInfos() {
return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
- regels 14-16: de methode [getInfos] geeft een deel van de status van de applicatie weer;
We starten de applicatie met een fragmentadjacentie van 2. De logbestanden bij het opstarten van de applicatie:
06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/exemples.android D/menu: création menu en cours
- regels 9, 10, 13, 14: we zien dat in de methoden [setUserVisibleHint] de methode [getActivity()==null] wordt aangeroepen als het fragment nog niet zichtbaar is (isVisibleToUser==false);
- regel 19: we zien dat wanneer de uitvoeringsstroom de methode [update] van fragment 1 bereikt, de methode [getActivity] de activiteit inderdaad weergeeft;
Wanneer de fragmentadjacentie op 4 wordt ingesteld (volledige adjacentie), zijn de logboeken als volgt:
06-01 03:35:23.553 2814-2814/exemples.android D/MainActivity: constructor
06-01 03:35:23.751 2814-2819/exemples.android I/art: Ignoring second debugger -- accepteren en verwijderen
06-01 03:35:23.900 2814-2814/exemples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.210 2814-2814/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.211 2814-2814/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.214 2814-2814/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:26.602 2814-2814/exemples.android D/menu: création menu en cours
We krijgen dezelfde resultaten. Hieruit kunnen we afleiden dat zodra het fragment zichtbaar is, de methode [getActivity] de activiteit van het fragment weergeeft. We merken ook op dat wanneer de uitvoering de methode [update] bereikt van het fragment dat zal worden weergegeven, de methode [getActivity] inderdaad een waarde weergeeft.
Om de communicatie tussen fragmenten te illustreren, bouwen we een nieuw project.
1.10. Voorbeeld-09: communicatie tussen fragmenten, swipen en scrollen
1.10.1. Het project aanmaken
We dupliceren het project [Exemple-07] naar [Exemple-08]. Hiervoor volgen we de procedure die is beschreven voor het dupliceren van [Exemple-02] naar [Exemple-03] in paragraaf 1.4.
![]() | ![]() |
1.10.2. De sessie
In dit nieuwe project willen we dat de fragmenten het totale aantal door de gebruiker weergegeven fragmenten tonen. Hiervoor moet een teller worden bijgehouden die voor alle fragmenten toegankelijk is. We noemen het object dat de door de fragmenten gedeelde gegevens omvat een ‘sessie’. Deze terminologie is afkomstig uit de webontwikkeling, waar gegevens die door verschillende, door dezelfde gebruiker opgevraagde weergaven moeten worden gedeeld, in een sessie worden geplaatst. Door de door de verschillende fragmenten gedeelde informatie in één enkel object te verpakken, wordt de code overzichtelijker.
De klasse [Session] ziet er als volgt uit:
![]() |
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// aantal bezochte fragmenten
private int numVisit;
// getters en setters
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
}
- regel 8: de sessie zal het aantal bezochte fragmenten inkapselen;
- regel 5: de annotatie [EBean] is een annotatie van AA. Het attribuut [scope] geeft het bereik (of de levensduur) aan van de aldus geannoteerde klasse. Hier zorgt het attribuut [scope = EBean.Scope.Singleton] ervoor dat de klasse [Session] een singleton is: deze wordt één keer en slechts één keer geïnstantieerd bij het opstarten van de applicatie. De referentie van een met [EBean] geannoteerde klasse kan vervolgens in een andere klasse worden geïnjecteerd. Dit is het concept van afhankelijkheidsinjectie;
1.10.3. De activiteit [MainActivity]
De activiteit [MainActivity] ontwikkelt zich als volgt:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
...
// sessie-injectie
@Bean(Session.class)
protected Session session;
// aantal fragmenten
private final int FRAGMENTS_COUNT = 5;
// aangrenzende fragmenten
private final int OFF_SCREEN_PAGE_LIMIT = 2;
@AfterInject
protected void afterInject(){
Log.d("MainActivity", "afterInject");
// sessie-initialisatie
session.setNumVisit(0);
}
...
- regels 7-8: injectie van de verwijzing naar de singleton van de sessie via de annotatie [@Bean]. De parameter van de annotatie is de klasse van de te injecteren bean. Het veld dat op deze manier is geannoteerd, mag niet het bereik [private] hebben;
- regel 15: de annotatie [@AfterInject] wordt gebruikt om een methode aan te wijzen die moet worden aangeroepen wanneer alle injecties van de klasse zijn voltooid. Wanneer de methode [afterInject] op regel 16 wordt aangeroepen, is de referentie van regel 8 dus al geïnitialiseerd;
- regel 20: de bezoekenteller wordt op nul gezet;
1.10.4. Het fragment [PlaceholderFragment]
Het fragment [PlaceholderFragment] verloopt als volgt:
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
....
// sessie
protected Session session;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// bovenliggend element
super.setUserVisibleHint(isVisibleToUser);
// geheugen
this.isVisibleToUser = isVisibleToUser;
// logboek
Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// aantal bezoeken
if (isVisibleToUser) {
// fragment bijwerken
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// het fragment wordt verborgen
updateDone = false;
}
}
// fragment bijwerken
public void update() {
// log
Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
// sessie
if (session == null) {
session = ((MainActivity) getActivity()).getSession();
}
// bezoeknummer verhogen
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// tekst gewijzigd
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- regel 7: de sessie;
- regels 35-37: we weten dat wanneer we in de methode [update] terechtkomen, de methode [getActivity] de activiteit inderdaad teruggeeft. We maken hiervan gebruik om de sessie op te halen en lokaal op te slaan (regel 36);
- regels 39-41: om het bezoeknummer te verhogen, halen we dit op uit de sessie. We hadden deze code ook in de methode [setUserVisibleHint] vanaf regel 19 kunnen plaatsen, omdat we weten dat de methode [getActivity] de activiteit dan teruggeeft. We besluiten hier om deze methode geen speciale rol te laten spelen en de fragment-specifieke code te verplaatsen naar de methode [update], die hiervoor is bedoeld;
- regel 43: geeft het bezoeknummer weer;
Wanneer we deze applicatie uitvoeren met 5 fragmenten en een aangrenzende relatie tussen 2 fragmenten, zijn de eerste logboekvermeldingen als volgt:
05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/exemples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/exemples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
...
- regels 2-3: we zien dat de methode [afterInject] van de activiteit wordt uitgevoerd vóór de methode [afterViews];
De lezer wordt uitgenodigd om deze nieuwe applicatie te testen.
1.10.5. De veegfunctie uitschakelen
In de vorige applicatie maakt de huidige weergave plaats voor de weergave aan de rechter- of linkerkant, afhankelijk van de situatie, wanneer je met de muis naar links of rechts over de Android-emulator veegt. Dit standaardgedrag is niet altijd wenselijk. We gaan leren hoe je het vegen tussen weergaven (swipe) kunt uitschakelen.
Laten we teruggaan naar het hoofdscherm XML:
![]() |
In de code XML van de weergave vinden we die van de fragmentcontainer:
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
Regel 1 verwijst naar de klasse die de pagina's van de activiteit beheert. Deze klasse is te vinden in de activiteit [MainActivity]:
import android.support.v4.view.ViewPager;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// de fragmentbeheerder
private SectionsPagerAdapter mSectionsPagerAdapter;
// de fragmentcontainer
@ViewById(R.id.container)
protected ViewPager mViewPager;
...
Op regel 12 is de fragmentcontainer van het type [android.support.v4.view.ViewPager] (regel 1). Om het scannen uit te schakelen, moet deze klasse als volgt worden afgeleid:
![]() |
package exemples.android;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// beheert de swipe
private boolean isSwipeEnabled;
// constructors
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// methoden die opnieuw moeten worden gedefinieerd om de veegbeweging te beheren
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe toegestaan?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe toegestaan?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- regel 8: de klasse [MyPager] is een uitbreiding van de Android-klasse [ViewPager] (regel 4);
- bij een veegbeweging met de hand kunnen de gebeurtenishandlers op regel 24 en 34 worden aangeroepen. Beide retourneren een booleaanse waarde. Ze hoeven alleen de booleaanse waarde [false] te retourneren om de veegbeweging te blokkeren;
- regel 11: de booleaanse waarde die aangeeft of de veegbeweging met de hand al dan niet wordt geaccepteerd.
Zodra dit is gebeurd, moeten we voortaan onze nieuwe paginabehandelaar gebruiken. Dit gebeurt in de weergave XML [activity_main.xml] en in de hoofdactiviteit [MainActivity]. In [activity_main.xml] schrijven we:
![]() |
<exemples.android.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
Op regel 1 wordt de nieuwe klasse gebruikt. In [MainActivity] verandert de code als volgt:
package exemples.android;
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
// de fragmentbeheerder
private SectionsPagerAdapter mSectionsPagerAdapter;
// de fragmentcontainer
@ViewById(R.id.container)
protected MyPager mViewPager;
@AfterViews
protected void afterViews() {
Log.d("MainActivity", "afterViews");
...
// de fragmentcontainer is gekoppeld aan de fragmentbeheerder
// dat wil zeggen dat fragment nr. i van de fragmentcontainer het fragment nr. i is dat door de fragmentbeheerder wordt geleverd
mViewPager.setAdapter(mSectionsPagerAdapter);
// het vegen tussen fragmenten wordt geblokkeerd
mViewPager.setSwipeEnabled(false);
// de tabbalk is ook gekoppeld aan de fragmentcontainer
...
- regel 12: de paginabeheerder heeft nu het type [MyPager];
- regel 23: het scrollen met de muis wordt al dan niet uitgeschakeld.
Test deze nieuwe versie. Schakel het scrollen al dan niet uit en merk het verschil in gedrag van de weergaven wanneer u ze met de muis naar rechts of naar links sleept. In alle toekomstige applicaties zal het scrollen worden uitgeschakeld. We zullen hier niet meer op terugkomen.
1.10.6. Scrollen tussen fragmenten uitschakelen
Laten we verdergaan met een verbetering van de tabbladbeheerder. Wanneer je van tabblad 1 naar tabblad 4 gaat, zie je de twee tussenliggende tabbladen 2 en 3 voorbij scrollen. Dit wordt in Android-jargon de smoothScrolling genoemd. Dit gedrag kan vervelend worden als er veel tabbladen zijn. Het kan worden uitgeschakeld door de volgende code toe te voegen aan de fragmentmanager [MyPager]:
// regelt het swipen
private boolean isSwipeEnabled;
// regelt het scrollen
private boolean isScrollingEnabled;
...
// scrollen
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setters
...
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
Omdat de tabbladmanager is gekoppeld aan de fragmentmanager [MyPager], wordt bij het klikken op tabblad nr. i fragment nr. i weergegeven door de fragmentcontainer via de bovenstaande methode [setCurrentItem] (regel 9). [position] is het nummer van het weer te geven fragment;
- regel 10: de methode [setCurrentItem] van de bovenliggende klasse wordt aangeroepen. Het tweede argument bij [false] zorgt ervoor dat er een onmiddellijke overgang plaatsvindt tussen het oude en het nieuwe fragment (zonder scrollen), bij [true] dat er een overgang plaatsvindt via scrolling. Hier is het tweede argument de waarde van het veld op regel 4, een veld dat de ontwikkelaar kan instellen met de methode op de regels 16-18;
Als men het scrollen wil uitschakelen, ziet de klasse [MainActivity] er als volgt uit:
...
// offset van de fragmenten
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// vegen tussen fragmenten wordt uitgeschakeld
mViewPager.setSwipeEnabled(false);
// geen scrollen
mViewPager.setScrollingEnabled(false);
...
Voer het project opnieuw uit en controleer of er bijvoorbeeld tussen tabbladen 1 en 4 geen scrolling meer staat. In het vervolg zullen we het scrollen altijd uitschakelen. We zullen hier niet meer op terugkomen.
1.10.7. Een nieuw fragment
In ons voorbeeld zijn alle fragmenten van hetzelfde type: [PlaceHolderFragment]. We gaan nu leren hoe we een nieuw fragment kunnen maken en weergeven.
Laten we eerst de weergave [vue1.xml] uit het project [Exemple-04] kopiëren naar het project [Exemple-09] [1]:
![]() | ![]() |
- in [1], de weergave [vue1.xml];
- in [3] vertoont de weergave fouten die het gevolg zijn van ontbrekende teksten in het bestand [res/values/strings.xml];
In [2] worden de ontbrekende teksten toegevoegd door ze over te nemen uit het bestand [res/values/strings.xml] van het project [Exemple-04]
<resources>
<string name="app_name">Exemple-07</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- weergave 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
</resources>
- hierboven hebben we de regels 6-9 toegevoegd;
Nu maken we de klasse [Vue1Fragment] aan, die het fragment zal zijn dat verantwoordelijk is voor het weergeven van de weergave [vue1.xml]:
![]() |
De klasse [Vue1Fragment] ziet er als volgt uit:
package exemples.android;
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
// elementen van de visuele interface
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// gebeurtenisbeheerder
@Click(R.id.buttonValider)
protected void doValider() {
// de ingevoerde naam wordt weergegeven
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
}
- regel 10: de annotatie [@EFragment] zorgt ervoor dat het fragment dat door de activiteit wordt gebruikt, in werkelijkheid de klasse [Vue1Fragment_] zal zijn. Dit moet je onthouden. Het fragment is gekoppeld aan de weergave [vue1.xml];
- regels 14-15: de component met de identificatie [R.id.editTextNom] wordt ingevoegd in het veld [editTextNom] op regel 15;
- regels 18-20: de methode [doValider] verwerkt de 'click'-gebeurtenis op de knop met de identificatiecode [R.id.buttonValider];
- regel 21: de eerste parameter van [Toast.makeText] is van het type [Activity]. Met de methode [Fragment.getActivity()] kan de activiteit worden opgehaald waarin het fragment zich bevindt. Dit is [MainActivity], aangezien we in deze architectuur slechts één activiteit hebben die verschillende weergaven of fragmenten toont;
In de klasse [MainActivity] ontwikkelt de fragmentmanager zich als volgt:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// de fragmenten
private Fragment[] fragments;
// fragmentnummer
private static final String ARG_SECTION_NUMBER = "section_number";
// maker
public SectionsPagerAdapter(FragmentManager fm) {
// bovenliggend
super(fm);
// initialisatie van de fragmententabel
fragments = new Fragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
// een fragment wordt aangemaakt
fragments[i] = new PlaceholderFragment_();
// er kunnen argumenten aan het fragment worden doorgegeven
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// een fragment van +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
...
}
- regel 13: er is [FRAGMENTS_COUNT] fragmenten: [FRAGMENTS_COUNT-1] fragmenten van het type [PlaceholderFragment] (regels 14-21) en een fragment van het type [Vue1Fragment_], regel 23 (let op de underscore);
Compileer en voer vervolgens het project [Exemple-09] uit. Tabblad nr. 5 moet er anders uitzien:
![]() |
1.10.8. Laat alle fragmenten afleiden van dezelfde abstracte klasse
Het nieuwe fragment [Vue1Fragment] moet ook worden bijgewerkt wanneer het wordt weergegeven. Hiervoor moeten we code maken die vergelijkbaar is met de code die is gemaakt voor het fragment [PlaceholderFragment]. Om herhaling te voorkomen, gaan we alles wat mogelijk is samenvoegen in een abstracte klasse waarvan alle fragmenten van de applicatie zullen erven.
Daarvoor maken we een nieuw project aan.
1.11. Voorbeeld-10: alle fragmenten laten afleiden van een abstracte klasse
1.11.1. Het project aanmaken
We dupliceren het project [Exemple-09] naar [Exemple-10]:
![]() | ![]() |
1.11.2. Beheer van de debugmodus
We voegen aan het project de mogelijkheid toe om de logbestanden van de debugmodus al dan niet weer te geven. Hiervoor voegen we een statische constante toe aan de klasse [MainActivity]:
// debugmodus
public static final boolean IS_DEBUG_ENABLED = false;
1.11.3. De abstracte bovenliggende klasse van alle fragmenten
![]() |
De klasse [AbstractFragment] ziet er als volgt uit:
package exemples.android;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
public abstract class AbstractFragment extends Fragment {
// privégegevens
private boolean isVisibleToUser = false;
private boolean updateDone = false;
private String className;
// gegevens toegankelijk voor onderliggende klassen
protected boolean afterViewsDone = false;
protected boolean isDebugEnabled = true;
// activiteit
protected MainActivity activity;
// sessie
protected Session session;
// constructor
public AbstractFragment() {
// init
isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// bovenliggend
super.setUserVisibleHint(isVisibleToUser);
...
}
@Override
public void onDestroyView() {
// ouder
super.onDestroyView();
...
}
@Override
public void onResume() {
// ouder
super.onResume();
...
}
// lokale informatie
protected String getParentInfos() {
return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
}
// fragment bijwerken
protected void update() {
...
// de dochterklasse wordt gevraagd zichzelf bij te werken
updateFragment();
}
protected abstract void updateFragment();
}
- regel 7: de klasse [AbstractFragment] is een uitbreiding van de Android-klasse [Fragment];
- elk fragment moet kunnen worden bijgewerkt. Daarom eist de bovenliggende klasse [AbstractFragment] dat haar onderliggende klassen beschikken over een methode [updateFragment] (regel 68) die zij aanroept (regel 65);
- regel 19: de klasse slaat een verwijzing naar de activiteit van de applicatie op;
- regel 22: de klasse slaat een verwijzing op naar de sessie waarin de gegevens zijn verzameld die door de fragmenten en de activiteit worden gedeeld;
- regels 25-33: de constructor van de abstracte klasse;
- regel 27: het aanmaken van een kopie van de constante [MainActivity.IS_DEBUG_ENABLED] in het veld van regel 16;
- regel 28: de naam van de geïnstantieerde klasse, dus de naam van een onderliggende klasse, wordt opgeslagen;
- regels 15-22: deze velden hebben het attribuut [protected], zodat de onderliggende klassen er toegang toe hebben. Opgemerkt moet worden dat de onderliggende klassen het bestaan van de booleaanse waarden [isVisibleToUser] en [updateDone] (regels 10-11) negeren;
- regel 57: de methode [getParentInfos] heeft het attribuut [protected], zodat de onderliggende klassen deze kunnen aanroepen;
De methoden [setUserVisibleHint, onDestroyView, onResume] blijven hetzelfde als in de klasse [PlaceholderFragment] van het vorige project:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// bovenliggend
super.setUserVisibleHint(isVisibleToUser);
// geheugen
this.isVisibleToUser = isVisibleToUser;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
}
// geval waarin het fragment zichtbaar wordt
if (isVisibleToUser) {
// fragment bijwerken
if (afterViewsDone && !updateDone) {
update();
updateDone = true;
}
} else {
// het fragment verlaten
updateDone = false;
}
}
@Override
public void onDestroyView() {
// bovenliggend
super.onDestroyView();
// indicator bijwerken
afterViewsDone = false;
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
}
}
@Override
public void onResume() {
// bovenliggend
super.onResume();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
}
if (isVisibleToUser) {
// update
if (!updateDone) {
update();
updateDone = true;
}
}
}
De methode [update] is als volgt:
// fragment bijwerken
protected void update() {
// de activiteit en de sessie worden opgehaald
if (activity == null) {
Activity activity = getActivity();
if (activity != null) {
this.activity = (MainActivity) activity;
this.session = this.activity.getSession();
}
}
// de dochterklasse wordt gevraagd zichzelf bij te werken
updateFragment();
}
Volgens de bovenstaande code is een fragment zichtbaar wanneer de methode [update] van dat fragment wordt uitgevoerd. Dit is belangrijk omdat het betekent dat de methode [Fragment.getActivity] vervolgens een verwijzing naar de activiteit van de applicatie retourneert (zie paragraaf 1.10.8), wat vervolgens toegang tot de sessie geeft.
- regels 4-10: de activiteit en de sessie worden geïnitialiseerd als dat nog niet is gebeurd;
- regel 12: de methode [updateFragment] van de dochterklasse wordt aangeroepen. Wanneer deze wordt uitgevoerd, zijn de velden [activity] en [session], waartoe deze toegang heeft, al geïnitialiseerd;
1.11.4. De klasse [PlaceholderFragment]
![]() |
De klasse [PlaceholderFragment] ontwikkelt zich als volgt:
package exemples.android;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
// een fragment is een weergave die door een fragmentcontainer wordt weergegeven
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
// onderdeel van de visuele interface
@ViewById(R.id.section_label)
protected TextView textViewInfo;
// gegevens
private boolean initDone;
// gegevens
private String text;
private int numVisit;
// fragmentnummer
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public PlaceholderFragment() {
super();
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", "constructor");
}
}
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
...
}
// fragment bijwerken
public void updateFragment() {
...
}
}
- regel 10: de klasse [PlaceholderFragment] is een uitbreiding van de klasse [AbstractFragment]. Met deze architectuur bestaat het schrijven van een fragment uit:
- het schrijven van de methode [@AfterViews], die dient om het fragment te initialiseren tijdens zijn eerste levenscyclus of om het te resetten als er eerder een [onDestroyView] heeft plaatsgevonden. Regel 39 is verplicht om de levenscyclus van het fragment correct te beheren;
- het schrijven van de methode [updateFragment], die het fragment vlak voor de weergave bijwerkt. Deze methode kan gebruikmaken van de sessie van de bovenliggende klasse;
- de gebeurtenishandlers van het fragment te schrijven. Dit gaan we in toekomstige projecten doen;
De methoden [@AfterViews] en [updateFragment] blijven hetzelfde als in het vorige project:
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
if (!initDone) {
// oorspronkelijke tekst
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
// initialisatie voltooid
initDone = true;
}
// huidige tekst weergeven
textViewInfo.setText(text);
}
// fragment bijwerken
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
}
// bezoeknummer verhogen
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// tekst gewijzigd
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
// lokale informatie voor logboeken
protected String getLocalInfos() {
return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
numVisit, initDone, getActivity() == null);
}
- regel 7 en 23: in de logbestanden wordt de informatie van de bovenliggende klasse weergegeven met de overgenomen methode [getParentInfos];
1.11.5. De klasse [Vue1Fragment]
![]() |
De klasse [Vue1Fragment] heeft dezelfde structuur als de klasse [PlaceholderFragment]:
package exemples.android;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementen van de visuele interface
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// gegevens
private int numVisit;
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// logboek
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
}
}
// gebeurtenisbeheerder
@Click(R.id.buttonValider)
protected void doValider() {
// de ingevoerde naam wordt weergegeven
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// lokale informatie voor logbestanden
protected String getLocalInfos() {
return String.format("numVisit=%s", numVisit);
}
// fragment bijwerken
@Override
protected void updateFragment() {
// bezoeknummer verhogen
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// het bezoeknummer wordt weergegeven
Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
- regel 9: de klasse [Vue1Fragment] is een uitbreiding van de klasse [AbstractFragment];
- regels 18-26: de methode [@AfterViews] heeft niets interessants te doen. Toch moet deze worden geschreven om de booleaanse waarde [afterViewsDone] in te stellen op true, omdat deze informatie door de bovenliggende klasse wordt gebruikt;
- regels 42-49: de methode [updateFragment] bestaat uit het weergeven van een kort bericht met het bezoeknummer (regel 48) en het verhogen van dit nummer in de sessie (regels 44-46);
De lezer wordt uitgenodigd om dit nieuwe project te testen.
We zullen deze architectuur in alle toekomstige projecten overnemen:
- één activiteit en n fragmenten;
- alle fragmenten zijn afgeleid van de klasse [AbstractFragment];
- de gegevens die tussen fragmenten onderling en tussen fragmenten en de activiteit moeten worden gedeeld, worden in de klasse [Session] geplaatst;
1.11.6. Koppeling tussen tabbladen en fragmenten
In de klasse [MainActivity], die de tabbladen beheert, staat het volgende:
// de tabbalk is ook gekoppeld aan de fragmentcontainer
// d.w.z. dat tabblad nr. i fragment nr. i van de container weergeeft
tabLayout.setupWithViewPager(mViewPager);
Regel 3 koppelt de tabbladbeheerder aan de fragmentcontainer. We hebben een gevolg van deze koppeling gezien: wanneer de gebruiker op tabblad nr. i klikt, laat de fragmentcontainer fragment nr. i weergeven. Het omgekeerde hebben we nog niet gezien: wanneer de fragmentcontainer wordt gevraagd om fragment nr. i weer te geven, wordt tabblad nr. i automatisch geselecteerd.
Om dit gedrag te illustreren, gaan we de opties [Fragment 1, Fragment 2, ...] aan het huidige menu toevoegen. Wanneer de gebruiker op de optie [Fragment i] klikt, wordt de fragmentcontainer gevraagd om fragment nr. i weer te geven. We zullen dan zien of tabblad nr. i al dan niet is geselecteerd.
Deze stap begint met het aanpassen van het menu van de applicatie:
![]() | ![]() |
De inhoud van het bestand [res / menu / menu_main.xml] verandert als volgt:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment5"
android:title="@string/fragment5"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- regels 9-28: de vijf nieuwe menuopties;
- de benamingen van de opties (regels 10, 14, 18, 22, 26) worden gedefinieerd in het bestand [res / values / strings.xml] [2]:
<resources>
<string name="app_name">Exemple-10</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- weergave 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<string name="btn_vue2">Vue n° 2</string>
<!-- menu -->
<string name="fragment1">Fragment 1</string>
<string name="fragment2">Fragment 2</string>
<string name="fragment3">Fragment 3</string>
<string name="fragment4">Fragment 4</string>
<string name="fragment5">Fragment 5</string>
</resources>
Het visuele resultaat is als volgt:
![]() |
De afhandeling van de klik op deze menuopties vindt plaats in de klasse [MainActivity]:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// logboek
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// verwerking van menuopties
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
case R.id.fragment5: {
showFragment(4);
break;
}
}
// verwerkt item
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// het weergegeven fragment wordt gewijzigd
mViewPager.setCurrentItem(i);
}
}
- regel 2: de methode [onOptionsItemSelected] wordt aangeroepen wanneer er op een van de menuopties wordt geklikt;
- regel 8: de ID van de aangeklikte optie wordt opgehaald;
- regels 9-36: de verschillende gevallen worden verwerkt door een switch;
- regels 16-36: bij een klik op de optie [Fragment i] wordt doorverwezen naar de methode [showFragment(i-1)] in de regels 41-45;
- regel 43: de fragmentcontainer wordt gevraagd het gevraagde fragment weer te geven;
- regel 42: eerst wordt gecontroleerd of dit mogelijk is (voorwaarde 1) en of het nodig is (voorwaarde 2);
De lezer wordt uitgenodigd om deze nieuwe versie te testen. We zien dat wanneer we vragen om fragment nr. i weer te geven, dit inderdaad wordt weergegeven en tabblad nr. i zelf wordt geselecteerd.
Nu we hebben gezien hoe de koppeling tussen tabbladen en fragmenten werkt, gaan we ons richten op een ander geval: namelijk wanneer het beheer van de tabbladen losstaat van dat van de fragmenten. Dit is bijvoorbeeld het geval wanneer er minder tabbladen zijn dan fragmenten. Om dit nieuwe gebruiksscenario te illustreren, bouwen we een nieuw project.
1.12. Voorbeeld 11: tabbladen losgekoppeld van fragmenten
1.12.1. Het project aanmaken
We dupliceren het project [Exemple-10] naar [Exemple-11]:
![]() | ![]() |
1.12.2. Doelstellingen
De nieuwe applicatie zal twee tabbladen hebben:
- het eerste tabblad toont altijd het fragment [Vue1];
- het tweede tabblad toont een fragment dat in het menu is geselecteerd;

- in [1], het fragment [Vue1];
- in [2], het door de gebruiker gekozen fragment van het type [PlaceholderFragment];
- in [3] worden de bezoeken verder geteld;
1.12.3. De sessie
![]() |
De nieuwe sessie ziet er als volgt uit:
package exemples.android;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// aantal bezochte fragmenten
private int numVisit;
// nummer van het fragment van het type [PlaceholderFragment] dat in het tweede tabblad wordt weergegeven
private int numFragment;
// getters en setters
...
}
- regel 10: we gaan zelf de klikken op de tabbladen afhandelen. Wanneer er op een tabblad wordt geklikt, moet het fragment worden weergegeven dat de laatste keer dat het was geselecteerd, werd getoond. Het veld [numFragment] slaat het nummer van dit fragment op voor tabblad nr. 2, een getal in [0, Fragments_COUNT-2]. Wanneer op tabblad nr. 2 wordt geklikt, zoeken we in de sessie naar het nummer van het weer te geven fragment;
1.12.4. Het menu
![]() |
Het menu [res / menu / menu_main.xml] verandert als volgt:
<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>
Tabblad nr. 2 toont een van de vier fragmenten van de regels 9-24. Het vijfde fragment is het fragment [Vue1Fragment], dat altijd in tabblad nr. 1 wordt weergegeven.
1.12.5. De klasse [MainActivity]
De klasse [MainActivity] moet voortaan de tabbladen en de navigatie daartussen beheren, wat ze tot nu toe niet deed. De code verandert als volgt:
// de tabbladbeheerder
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
...
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
...
// geen scrollen
mViewPager.setScrollingEnabled(false);
// weergave Vue1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
// in het begin is er slechts één tabblad
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// gebeurtenisbeheerder
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// er is een tabblad geselecteerd – het weergegeven fragment wordt gewijzigd door de fragmentcontainer
...
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
...
}
- regel 17: het eerste fragment dat door de fragmentcontainer wordt weergegeven, is het fragment [Vue1Fragment]. Dit is per definitie het laatste fragment in de container;
- regels 20-22: omdat er geen koppeling is gemaakt tussen tabbladen en de fragmentcontainer, moeten we de tabbladen zelf beheren. In eerste instantie heeft de tabbalk [tabLayout] in regel 3 geen tabbladen;
- regel 20: we maken het eerste tabblad aan;
- regel 21: we geven het een titel. In de voorgaande voorbeelden was de titel van de tabbladen de titel van de fragmenten. Dat is nu voorbij. Daarom verwijderen we de methode [getPageTitle] uit de fragmentbeheerder. We hebben deze niet meer nodig:
// optioneel – geef de beheerde fragmenten een titel
@Override
public CharSequence getPageTitle(int position) {
return String.format("Onglet n° %s", (position + 1));
}
- regel 22: het aangemaakte tabblad wordt toegevoegd aan de tabbalk. Onze tabbalk heeft nu één tabblad. Wat wordt er op dit tabblad weergegeven? Het is belangrijk te begrijpen dat tabbladen en fragmenten twee onafhankelijke begrippen zijn. Het weergegeven fragment is altijd het fragment dat door de fragmentcontainer is gekozen. Als we van tabblad wisselen en de fragmentcontainer niet vragen om het weergegeven fragment te wijzigen, gebeurt er niets: er wordt nog steeds hetzelfde fragment weergegeven, maar het geselecteerde tabblad is wel gewijzigd. Het weergegeven fragment is hier dus het fragment dat op regel 17 is gekozen: het fragment [Vue1Fragment];
- regels 26-30: de methode die moet worden geschreven om de tabbladwisseling door de gebruiker te verwerken;
De methode [onTabSelected] uit de regels 26-30 wordt geactiveerd zodra er een tabbladwisseling plaatsvindt (als de gebruiker op een reeds geselecteerd tabblad klikt, gebeurt er niets). De code ervan is als volgt:
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (IS_DEBUG_ENABLED) {
Log.d("onglets", "onTabSelected");
}
// er is een tabblad geselecteerd – het weergegeven fragment wordt gewijzigd door de fragmentcontainer
// positie van het tabblad
int position = tab.getPosition();
// nummer van het weer te geven fragment
int numFragment;
switch (position) {
case 0:
// fragmentnummer [Vue1Fragment]
numFragment = FRAGMENTS_COUNT - 1;
break;
default:
// fragmentnummer [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// fragment weergeven
mViewPager.setCurrentItem(numFragment);
}
- regel 8: we halen de positie op van het tabblad waarop is geklikt. Hier krijgen we een getal 0 of 1;
- regels 12-15: als het het eerste tabblad is waarop is geklikt, bereiden we ons voor op het weergeven van het fragment [Vue1Fragment];
- regels 16-18: in de andere gevallen (tabblad nr. 2 aangeklikt) bereiden we ons voor om het fragment opnieuw weer te geven dat werd weergegeven toen tabblad nr. 2 de vorige keer werd geselecteerd. Het nummer daarvan was toen in de sessie van de applicatie opgeslagen;
- regel 21: de fragmentcontainer wordt gevraagd het gewenste fragment weer te geven;
Laten we nu eens kijken naar het beheer van de menuopties (nog steeds in [MainActivity]):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// logboek
if (IS_DEBUG_ENABLED) {
Log.d("menu", "onOptionsItemSelected");
}
// verwerking van menuopties
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d("menu", "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// verwerkt item
return true;
}
- regels 16-31: beheer van de 4 menuopties. Elke handler roept de methode [showFragment] aan met het nummer van het weer te geven fragment;
De methode [showFragment] ziet er als volgt uit:
// tabblad nr. 2
private TabLayout.Tab tab2 = null;
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// als het tweede tabblad nog niet bestaat, wordt het aangemaakt
if (tab2 == null) {
tab2 = tabLayout.newTab();
tabLayout.addTab(tab2);
}
// de titel van het tweede tabblad wordt vastgelegd
tab2.setText(String.format("Fragment n° %s", (i + 1)));
// het weergegeven fragment wordt gewijzigd
mViewPager.setCurrentItem(i);
// het nummer van het weergegeven fragment wordt in de sessie opgeslagen
session.setNumFragment(i);
// tabblad 2 wordt geselecteerd – er gebeurt niets als dit al is geselecteerd
tab2.select();
}
}
- We herinneren ons dat er bij het opstarten van de applicatie slechts één tabblad is;
- regel 2: een verwijzing naar tabblad nr. 2, null bij het opstarten;
- regel 5: de weergavevoorwaarden zijn niet gewijzigd ten opzichte van de vorige versie;
- regels 7-10: als tabblad nr. 2 nog niet bestaat, wordt het aangemaakt (regel 8) en toegevoegd aan de tabbalk (regel 9);
- regel 12: in de titel van het tweede tabblad wordt het nummer van het fragment dat zal worden weergegeven, ingevuld, waarbij de nummering bij 1 begint;
- regel 14: het gewenste fragment wordt weergegeven;
- regel 16: het nummer ervan wordt in de sessie opgeslagen;
- regel 18: tabblad nr. 2 wordt geselecteerd. Als het al geselecteerd was, gebeurt er niets: de methode [onTabSelected] wordt niet uitgevoerd. Als het nog niet geselecteerd was, wordt de methode [onTabSelected] geactiveerd. Deze methode vraagt vervolgens aan de fragmentcontainer om het fragment weer te geven dat al in regel 14 werd weergegeven. Een eenvoudige test in de methode [onTabSelected] voorkomt dit geval:
// fragment wordt alleen weergegeven als dat nodig is
if (numFragment != mViewPager.getCurrentItem()) {
mViewPager.setCurrentItem(numFragment);
}
De lezer wordt uitgenodigd om deze nieuwe versie te testen.
1.12.6. Verbeteringen
We hebben nu een goed begrip van fragmenten, hun levenscyclus, het begrip 'aangrenzende fragmenten' en hun relatie met de tabbalk. Bovendien beschikken we over een robuuste architectuur die zojuist de test van voorbeeld 11 heeft doorstaan:
- één activiteit en n fragmenten;
- alle fragmenten zijn afgeleid van de klasse [AbstractFragment];
- de gegevens die tussen fragmenten onderling en tussen fragmenten en de activiteit moeten worden gedeeld, worden ondergebracht in de klasse [Session];
In een nieuw project gaan we de relaties tussen de activiteit en de fragmenten verduidelijken door een interface toe te voegen.
1.13. Voorbeeld 12: de relaties tussen de activiteit en de fragmenten coderen
In dit voorbeeld willen we de minimale relaties tussen de activiteit en de fragmenten definiëren. Hiervoor gebruiken we:
- een interface [IMainActivity] die definieert wat de fragmenten van de activiteit kunnen vragen;
- een abstracte klasse [AbstractFragment] die de status en de methoden definieert die elk fragment zou moeten hebben;
1.13.1. Het project aanmaken
We dupliceren het project [Exemple-11] naar [Exemple-12] volgens de procedure in paragraaf 1.4. We krijgen het volgende resultaat:
![]() | ![]() |
1.13.2. De interface [IMainActivity]
Uit de voorgaande voorbeelden blijkt dat de fragmenten toegang moeten hebben tot de sessie die door de activiteit is geïnstantieerd. Bovendien, hoewel dit in deze voorbeelden niet zichtbaar is, maar wel te verwachten valt: de gebeurtenishandlers van de fragmenten eindigen soms met een weergavewisseling. We zullen de activiteit vragen deze wisseling uit te voeren. De interface [IMainActivity] zou dan als volgt kunnen zijn:
![]() |
package exemples.android;
public interface IMainActivity {
// toegang tot de sessie
Session getSession();
// Weergave wijzigen
void navigateToView(int position);
// debugmodus
boolean IS_DEBUG_ENABLED = true;
}
Op regel 12 valt de aanwezigheid op van een constante die voorheen in de klasse [MainActivity] stond. We willen de koppeling tussen de fragmenten en de activiteit verminderen en deze beperken tot een koppeling tussen [AbstractFragment] en [IMainActivity]. De activiteit kan dan een andere naam krijgen dan [MainActivity]. Aangezien de constante [IS_DEBUG_ENABLED] in de fragmenten wordt gebruikt, wordt deze verplaatst naar de interface [IMainActivity].
1.13.3. De abstracte klasse [AbstractFragment]
De abstracte klasse [AbstractFragment] verandert nauwelijks:
// gegevens toegankelijk voor onderliggende klassen
protected boolean afterViewsDone = false;
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// activiteit
protected IMainActivity mainActivity;
protected Activity activity;
...
// fragment bijwerken
protected void update() {
// de activiteit en de sessie worden opgehaald
if (mainActivity == null) {
this.activity = getActivity();
if (this.activity != null) {
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
}
}
// de onderliggende klasse wordt gevraagd zichzelf bij te werken
updateFragment();
}
- regels 6 en 7: er worden twee soorten verwijzingen naar de activiteit onderhouden:
- regel 6: een verwijzing naar de activiteit die de interface [IMainActivity] implementeert;
- regel 7: een verwijzing naar de activiteit die de Android-klasse [Activity] erft. Dit geldt voor elke activiteit;
Deze twee verwijzingen verwijzen uiteraard naar hetzelfde object. Maar dit object wordt gezien als twee verschillende typen. Dit voorkomt typeconversies tijdens de uitvoering;
- regel 14: we halen een verwijzing naar de activiteit op met behulp van de methode [getActivity];
- regel 15: als deze niet-nul is, hebben we toegang tot de sessie;
- regels 16-17: we slaan de activiteit op als een implementatie van de interface [IMainActivity] en de sessie;
1.13.4. Wijziging van de fragmentmanager
De fragmentbeheerder [SectionsPagerAdapter] in de klasse [MainActivity] wordt op één punt gewijzigd: in plaats van fragmenten van het type [Fragment] te beheren, beheert deze nu fragmenten van het type [AbstractFragment]:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// de fragmenten
private AbstractFragment[] fragments;
// fragmentnummer
private static final String ARG_SECTION_NUMBER = "section_number";
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
// ouder
super(fm);
// initialisatie van de fragmentenlijst
fragments = new AbstractFragment[FRAGMENTS_COUNT];
for (int i = 0; i < fragments.length - 1; i++) {
...
}
// een fragment van +
fragments[fragments.length - 1] = new Vue1Fragment_();
}
// fragmentnr. positie
@Override
public AbstractFragment getItem(int position) {
...
}
// geeft het aantal beheerde fragmenten weer
@Override
public int getCount() {
...
}
}
1.13.5. Wijziging van de klasse [MainActivity]
De klasse [MainActivity] moet de interface [IMainActivity] implementeren:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
...
// sessie-injectie
@Bean(Session.class)
protected Session session;
...
// sessie ophalen
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// de weergave op positie wordt weergegeven
if(mViewPager.getCurrentItem()!=position){
// fragment weergeven
mViewPager.setCurrentItem(position);
}
}
- regels 10-12: de methode [getSession] bestond al;
- regels 15-22: de methode [navigateToView] zorgt ervoor dat fragment nr. [position] wordt weergegeven;
- regel 17: er wordt gekeken of er iets te doen is;
- regel 19: fragment nr. [position] wordt weergegeven;
Voer nu de toepassing uit. Deze zou nu moeten werken.
1.13.6. Wijziging van de weergave van fragmenten in [MainActivity]
Momenteel geeft de klasse [MainActivity] een fragment weer met de instructie:
// weergave van weergave 1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
Aangezien de methode [navigateToView] hetzelfde doet, vervangen we dit type instructie overal (op 2 plaatsen) door:
Voer vervolgens de applicatie uit. Deze zou nog steeds moeten werken.
1.13.7. Conclusie
Vanaf nu zullen we altijd de vorige architectuur gebruiken:
- een activiteit die de interface [IMainActivity] implementeert;
- fragmenten die de klasse [AbstractFragment] uitbreiden, waardoor ze de methode [updateFragment] moeten implementeren. Deze moeten ook een methode [@AfterViews] hebben waarin ze de booleaanse waarde [afterViewsDone] op true zetten;
- een sessie waarin de gegevens zijn ingekapseld die tussen fragmenten en activiteiten moeten worden gedeeld;
1.14. Voorbeeld-13: Voorbeeld-05 met fragmenten
In het project [Exemple-05] hebben we navigatie tussen weergaven geïntroduceerd. Het ging toen om navigatie tussen activiteiten: 1 weergave = 1 activiteit. We stellen hier voor om één enkele activiteit te hebben met meerdere weergaven van het type [AbstractFragment].
1.14.1. Het project aanmaken
We dupliceren het vorige project [Exemple-12] naar [Exemple-13] volgens de procedure in paragraaf 1.4. We krijgen het volgende resultaat:
![]() | ![]() |
1.14.2. Structurering van het project
We gaan pakketten gebruiken om de code te ordenen. Voorlopig kunnen we twee afzonderlijke domeinen onderscheiden:
- het beheer van de activiteit;
- het beheer van fragmenten;
We maken hiervoor twee pakketten aan: [exemples.android.activity] en [exemples.android.fragments]:
![]() |
![]() | ![]() |
We gaan op dezelfde manier te werk om het pakket [exemples.android.fragments] aan te maken:
![]() | ![]() |
In [8] maken we een derde pakket aan met de naam [architecture], waarin we de entiteiten [IMainActivity, AbstractFragment, Session, MyPager] plaatsen, die de basiselementen van de architectuur van onze applicatie vormen. Dit om ons eraan te herinneren dat we een specifieke architectuurkeuze hebben gemaakt. Verplaats vervolgens de bestaande elementen van het project zoals aangegeven in [9]. Elke verplaatsing moet worden bevestigd door op de knop [Refactor] te klikken.
Compileer de applicatie nu. We krijgen de volgende fouten in [MainActivity:
![]() |
Bij het verplaatsen van klassen naar pakketten heeft Android Studio de nodige wijzigingen aangebracht in de coderegels van de app (bijvoorbeeld regels 18-21). De klassen waarop de regels 15 en 17 betrekking hebben, zijn niet verplaatst. Deze worden gegenereerd door de Android Annotations-bibliotheek. Voor deze klassen moet je de imports handmatig aanpassen. Deze regels worden dus:
![]() |
Zodra dit is gebeurd, zijn er geen compilatiefouten meer. Voer de applicatie uit. Je krijgt dan de volgende foutmelding:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}:
Deze fout komt voort uit het manifest van de applicatie:
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity_"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Door de regels 3 en 12 is de aangewezen activiteit [exemples.android.MainActivity_]. Maar omdat de activiteit is verplaatst naar het pakket [activity], moet regel 12 nu als volgt luiden:
android:name=".activity.MainActivity_"
Let op de . voor [activity]. Ook hier kon Android Studio het manifest niet bijwerken omdat het verwijst naar een Android Annotations-klasse die niet is verplaatst. Het gebruik van de bibliotheek AA brengt dus een aantal ongemakken met zich mee.
1.14.3. Het project opschonen
In het nieuwe project:
- zijn er geen tabbladen, zwevende knoppen of menu’s meer;
- de fragmenten [PlaceholderFragment] zijn verdwenen. De applicatie zal twee fragmenten beheren: [Vue1Fragment], dat we al hebben, en [Vue2Fragment], dat nog aangemaakt moet worden;
- de sessie is niet meer dezelfde;
1.14.3.1. Fragmenten opschonen
Verwijder de klasse [PlaceHolderFragment] [1]:
![]() | ![]() |
Verwijder tevens de weergave [res / layout / fragment_main.xml] die gekoppeld is aan dit fragment [2].
1.14.3.2. Sessie opschonen
De sessie ziet er momenteel als volgt uit:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// aantal bezochte fragmenten
private int numVisit;
// nummer van het fragment van het type [PlaceholderFragment] dat in het tweede tabblad wordt weergegeven
private int numFragment;
// getters en setters
public int getNumVisit() {
return numVisit;
}
public void setNumVisit(int numVisit) {
this.numVisit = numVisit;
}
public int getNumFragment() {
return numFragment;
}
public void setNumFragment(int numFragment) {
this.numFragment = numFragment;
}
}
We behouden niets van deze sessie.
Compileer het project. De foutieve regels zijn die welke de inhoud van de sessie gebruikten. Verwijder deze. In de klasse [Vue1Fragment] verwijderen we ook de variabele [numVisit] uit de code, die er dan als volgt uitziet:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// de elementen van de visuele interface
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// logboek
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gebeurtenisbeheerder
@Click(R.id.buttonValider)
protected void doValider() {
// de ingevoerde naam wordt weergegeven
Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// fragment bijwerken
@Override
protected void updateFragment() {
}
}
1.14.3.3. Verwijderen van de tabbladen, de zwevende knop en het menu
Het verwijderen van de tabbladen en de zwevende knop gebeurt op twee plaatsen:
- in de weergave [res / layout / activity-main.xml], waarin deze elementen en hun plaats in de weergave worden gedefinieerd;
- in de code van de activiteit [MainActivity];
Het menu kan eveneens op twee plaatsen worden verwijderd:
- in de weergave [res / menu / menu-main.xml], waarin de menuopties worden gedefinieerd;
- in de code van de activiteit [MainActivity];
De code van de weergave [res / layout / activity-main.xml] is momenteel als volgt:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
- de regels [28-31, 41-47] worden verwijderd;
- verwijder ook de werkbalk uit de regels 18-24;
De code van het menu [res / menu / menu_main.xml] is momenteel als volgt:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
- de regels 9-24 worden verwijderd. Zo blijft er een optie over die we niet zullen gebruiken. Dit is alleen bedoeld om een voorbeeld te geven van het declareren van een menuoptie die we kunnen kopiëren en plakken;
In de klasse [MainActivity] verwijderen we alles wat verwijst naar de tabbladen, de zwevende knop, de werkbalk en het menu. De eenvoudigste manier om deze verwijzingen te vinden, is door hun declaratie te verwijderen:
// tabbladbeheer
@ViewById(R.id.tabs)
protected TabLayout tabLayout;
// de zwevende knop
@ViewById(R.id.fab)
protected FloatingActionButton fab;
en de applicatie opnieuw te compileren. De foutieve regels zijn die welke verwijzen naar de verdwenen elementen. Verwijder vervolgens al deze regels. Pas bovendien de fragmentmanager aan, zodat deze niet langer verwijst naar het fragment [PlaceholderFragment] dat we hebben verwijderd:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// de fragmenten
private AbstractFragment[] fragments;
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
// bovenliggend
super(fm);
}
// fragment nr. positie
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// geeft het aantal beheerde fragmenten weer
@Override
public int getCount() {
return fragments.length;
}
}
- regels 7-10: we hebben de volledige generatie van fragmenten verwijderd;
In dit stadium zouden er geen compilatiefouten meer moeten zijn. In de klasse [MainActivity] zijn we tot de volgende tussencode gekomen:
package exemples.android.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.IMainActivity;
import exemples.android.architecture.MyPager;
import exemples.android.architecture.Session;
import exemples.android.fragments.Vue1Fragment_;
import org.androidannotations.annotations.*;
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
// de fragmentcontainer
@ViewById(R.id.container)
protected MyPager mViewPager;
// de werkbalk
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
// sessie-injectie
@Bean(Session.class)
protected Session session;
// aantal fragmenten
private final int FRAGMENTS_COUNT = 5;
// aangrenzende fragmenten
private final int OFF_SCREEN_PAGE_LIMIT = 2;
// debugmodus
public static final boolean IS_DEBUG_ENABLED = true;
// de fragmentbeheerder
private SectionsPagerAdapter mSectionsPagerAdapter;
// constructor
public MainActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "constructor");
}
}
@AfterViews
protected void afterViews() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterViews");
}
// werkbalk – hier wordt de naam van de applicatie weergegeven
setSupportActionBar(toolbar);
// de fragmentbeheerder
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// de fragmentcontainer is gekoppeld aan de fragmentbeheerder
// d.w.z. dat fragment nr. i van de fragmentcontainer het fragment nr. i is dat door de fragmentmanager wordt geleverd
mViewPager.setAdapter(mSectionsPagerAdapter);
// offset van de fragmenten
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// het swipen tussen fragmenten wordt uitgeschakeld
mViewPager.setSwipeEnabled(false);
// geen scrollen
mViewPager.setScrollingEnabled(false);
// weergave Weergave1
navigateToView(FRAGMENTS_COUNT - 1);
}
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
}
// sessie ophalen
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position) {
// we geven de positieweergave weer
if (mViewPager.getCurrentItem() != position) {
// fragment weergeven
mViewPager.setCurrentItem(position);
}
}
// de fragmentbeheerder
// hieraan worden de fragmenten opgevraagd die in de hoofdweergave moeten worden weergegeven
// moet de methoden [getItem] en [getCount] definiëren – de overige zijn optioneel
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// de fragmenten
private AbstractFragment[] fragments;
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
// bovenliggend
super(fm);
}
// fragment nr. positie
@Override
public AbstractFragment getItem(int position) {
// log
if (IS_DEBUG_ENABLED) {
Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
}
return fragments[position];
}
// geeft het aantal beheerde fragmenten weer
@Override
public int getCount() {
return fragments.length;
}
}
}
Er moeten nog enkele wijzigingen worden aangebracht:
- verwijder regel 31, die niet meer nodig is;
- regel 33: stel de fragmentadjacentie in op 1;
- regel 76: ga naar weergave 0. Deze wordt als eerste weergegeven;
- regel 108: initialiseer de array met het fragment [Vue1Fragment_]:
// de fragmenten
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
We hebben dus maar één fragment. Voer de applicatie uit. U zou het volgende resultaat moeten krijgen:

De knop [Valider] moet werken.
1.14.4. Het aanmaken van fragmenten en bijbehorende weergaven
De applicatie krijgt twee weergaven, namelijk die van het project [Exemple-05]. We hebben de weergave [vue1.xml] al in het huidige project. We dupliceren nu [vue2.xml] van [Exemple-05] naar [Exemple-12] (open beide projecten en kopieer en plak tussen beide).
![]() | ![]() |
- in [1], de nieuwe weergave. Wanneer je probeert dit te bewerken, verschijnen er fouten in [2]. We moeten het bestand [strings.xml] [3] aanpassen om de strings toe te voegen waarnaar deze nieuwe weergave verwijst:
<resources>
<string name="app_name">Exemple-13</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- weergave 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="txt_nom">Quel est votre nom ?</string>
<string name="btn_valider">Valider</string>
<!-- weergave 2 -->
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
</resources>
We dupliceren de klasse [Vue1Fragment] naar [Vue2Fragment]:
![]() |
en wij passen de gekopieerde code als volgt aan:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// logboek
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// fragmentupdate
@Override
protected void updateFragment() {
}
}
- regel 9: het fragment is gekoppeld aan de weergave [res / layout / vue2.xml];
- regel 10: de klasse is een uitbreiding van de abstracte klasse [AbstractFragment];
- regels 12-20: de verplichte methode [@AfterViews];
- regels 23-25: de methode [updateFragment] is verplicht;
1.14.5. Implementatie van fragmenten en navigatie daartussen
De activiteit zal voortaan twee fragmenten beheren. De bijbehorende klasse [SectionsPagerAdapter] wordt als volgt aangepast:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// de fragmenten
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
}
De interface [IMainActivity] zorgt voor de navigatie tussen weergaven met de methode [navigateToView]. We gaan de klik op de knop [Vue n° 2] van het fragment [Vue1Fragment] afhandelen:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// de elementen van de visuele interface
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gebeurtenisverwerkers ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// de ingevoerde naam wordt weergegeven
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
mainActivity.navigateToView(1);
}
// fragment bijwerken
@Override
protected void updateFragment() {
}
}
- regels 37-40: de methode [showVue2] verwerkt de 'klik'-gebeurtenis op de knop [Vue n° 2];
- regel 39: er wordt genavigeerd met de methode [navigateToView] van de activiteit. Ter herinnering: de activiteit is in de bovenliggende klasse opgeslagen in de vorm:
// activiteit
protected IMainActivity mainActivity;
en dat deze activiteit al is geïnitialiseerd wanneer men in een willekeurige gebeurtenishandler terechtkomt.
- regel 34: de instructie gebruikt de variabele [activity] van de bovenliggende klasse, die een verwijzing is naar de activiteit als een instantie van het Android-type [Activity];
protected Activity activity;
We vinden vergelijkbare code voor het fragment [Vue2Fragment]:
package exemples.android.fragments;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// logboek
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gebeurtenisbeheerders ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// fragmentupdate
@Override
protected void updateFragment() {
}
}
- regels 24-27: de methode [showVue1] verwerkt de 'klik'-gebeurtenis op de knop [Vue n° 1];
Voer het project uit en controleer of het navigeren tussen weergaven werkt.
1.14.6. Definitie van de sessie
De applicatie werkt als volgt:
- een naam invoeren in weergave nr. 1;
- weergave van deze naam in weergave nr. 2;
Om ervoor te zorgen dat weergave nr. 1 de ingevoerde naam kan doorgeven aan weergave nr. 2, gebruiken we de volgende sessie;
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// naam
private String nom;
// getters en setters
...
}
- regel 8: de ingevoerde naam;
De klasse [MainActivity] initialiseert de sessie als volgt:
// sessie-injectie
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// logboek
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// sessie initialiseren
session.setNom("");
}
1.14.7. Definitieve opmaak van de fragmenten
In het fragment [Vue1Fragment] wijzigen we de code van de klikhandler voor de knop [Valider]:
package exemples.android.fragments;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementen van de visuele interface
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
...
// gebeurtenishandlers ----------------------------------
@Click(R.id.buttonValider)
protected void doValider() {
// de ingevoerde naam wordt opgeslagen
String nom = editTextNom.getText().toString();
// deze wordt weergegeven
Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
}
@Click(R.id.buttonVue2)
protected void showVue2() {
// de ingevoerde naam wordt in de sessie opgeslagen
session.setNom(editTextNom.getText().toString());
// er wordt doorgeschakeld naar weergave nr. 2
mainActivity.navigateToView(1);
}
// fragment bijwerken
@Override
protected void updateFragment() {
}
}
- regels: 31-37: we verwerken de klik op de knop [Vue n° 2];
- regel 34: voordat we naar weergave nr. 2 navigeren, slaan we de ingevoerde naam op in de sessie, zodat de nieuwe weergave er toegang toe heeft;
De weergave [Vue2Fragment] verloopt als volgt:
package exemples.android.fragments;
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// componenten van de visuele interface
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// logboek
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// gebeurtenisverwerkers ----------------------------------------------
@Click(R.id.buttonVue1)
protected void showVue1() {
mainActivity.navigateToView(0);
}
// fragment bijwerken
@Override
protected void updateFragment() {
// de in de sessie ingevoerde naam wordt opgehaald
String nom = session.getNom();
// deze wordt weergegeven
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
}
Wanneer weergave nr. 2 wordt weergegeven, moet de in weergave nr. 1 ingevoerde naam worden weergegeven. We weten dat direct na de weergave de methode [updateFragment] wordt uitgevoerd. Daarom kunnen we in deze methode (regels 36-42) de code plaatsen om de naam weer te geven.
- regels 16-17: declaratie van het enige visuele onderdeel van de weergave;
- regel 39: de in weergave nr. 1 ingevoerde naam wordt uit de sessie opgehaald;
- regel 41: de tekst [textViewBonjour] wordt gewijzigd;
Voer het project uit en controleer of het werkt.
1.14.8. Beheer van de levenscyclus van fragmenten
In het fragment [Vue1Fragment] is de methode [@AfterViews] als volgt:
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
}
Deze methode is onvolledig. Er moet namelijk altijd rekening worden gehouden met het geval waarin het fragment wordt hergebruikt na een bewerking met [onDestroyView]. In dat geval wordt de weergave van fragment 1 opnieuw gegenereerd en verdwijnt de naam die eerder mogelijk was ingevoerd uit de weergave. Dat willen we niet. Momenteel blijft de ingevoerde naam zichtbaar omdat de fragmenten van 1 naast elkaar liggen, waardoor de levenscyclus van het fragment [Vue1Fragment] slechts één keer wordt uitgevoerd. Het is echter beter om rekening te houden met het hergebruik van het fragment.
Er zijn verschillende manieren om dit probleem op te lossen:
- we kunnen gebruikmaken van het feit dat de methode [update] systematisch wordt uitgevoerd telkens wanneer het fragment wordt weergegeven, om de ingevoerde naam bij te werken;
- men kan deze bijwerking alleen uitvoeren wanneer de methode [@AfterViews] opnieuw wordt uitgevoerd. Wij kiezen voor deze laatste aanpak;
We passen de code van [Vue1Fragment] als volgt aan:
// de elementen van de visuele interface
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// gegevens
private String nom;
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// de weergegeven tekst wordt (opnieuw) geïnitialiseerd
editTextNom.setText(nom);
}
// gebeurtenishandlers ----------------------------------
...
@Click(R.id.buttonVue2)
protected void showVue2() {
// de ingevoerde naam wordt opgeslagen, zodat deze kan worden opgehaald als het fragment wordt hergebruikt
nom = editTextNom.getText().toString();
// de ingevoerde naam wordt in de sessie opgeslagen
session.setNom(nom);
// er wordt doorgeschakeld naar weergave nr. 2
activity.navigateToView(1);
}
- regel 27: wanneer we op het punt staan om van weergave 1 naar weergave 2 over te schakelen, slaan we de ingevoerde naam op;
- regel 17: bij elke nieuwe uitvoering van de levenscyclus van het fragment wordt de laatst ingevoerde naam opnieuw weergegeven;
Voor het fragment [Vue2Fragment] volstaat de bestaande code:
// componenten van de visuele interface
@ViewById(R.id.textViewBonjour)
protected TextView textViewBonjour;
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// logboek
if (isDebugEnabled) {
Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
}
}
// fragment bijwerken
@Override
protected void updateFragment() {
// de in de sessie ingevoerde naam wordt opgehaald
String nom = session.getNom();
// deze wordt weergegeven
textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
- het enige visuele onderdeel van de weergave (regel 3) wordt bij elke weergave van de weergave bijgewerkt (regel 21). De methode [@AfterViews] hoeft dus niets toe te voegen;
1.14.9. Conclusie
Op dit punt hebben we opnieuw de relevantie van onze architectuur aangetoond:
- een activiteit die de interface [IMainActivity] implementeert;
- fragmenten die de klasse [AbstractFragment] uitbreiden, waardoor ze verplicht zijn de methode [updateFragment] te implementeren. Deze moeten ook beschikken over een methode [@AfterViews] waarin ze de booleaanse waarde [afterViewsDone] instellen op true;
- een sessie waarin de gegevens zijn ingekapseld die tussen fragmenten en activiteiten moeten worden gedeeld;
1.15. Voorbeeld 14: een architectuur met twee lagen
We gaan een applicatie met één weergave bouwen met de volgende architectuur:
![]() |
1.15.1. Het project aanmaken
We dupliceren het vorige project [Exemple-12] naar [Exemple-13] volgens de procedure in paragraaf 1.4. We krijgen het volgende resultaat:
![]() | ![]() |
1.15.2. De weergave [vue1]
De applicatie zal slechts één weergave hebben: [vue1.xml]. Daarom verwijderen we de andere weergave, [vue2.xml], samen met het bijbehorende fragment:
![]() | ![]() |
Compileer de applicatie. Er verschijnen fouten in [MainActivity]:
![]() |
Corrigeer de onderstaande regel 4 in de fragmentmanager [SectionsPagerAdapter]
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// de fragmenten
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...
De bovenstaande regel 4 wordt:
// de fragmenten
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};
Verwijder de overbodig geworden imports [Ctrl-Shift-O]. Er mogen nu geen compilatiefouten meer zijn. Voer het project uit: weergave nr. 1 moet verschijnen. Deze gaan we nu aanpassen.
We gaan de weergave [vue1.xml] aanmaken waarmee willekeurige getallen kunnen worden gegenereerd:
![]() |
De componenten hiervan zijn als volgt:
De code XML is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:orientation="vertical" >
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/txt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_Titre2"
android:layout_marginTop="20dp"
android:text="@string/txt_nbaleas" />
<EditText
android:id="@+id/edt_nbaleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_nbaleas"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorNbAleas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_nbaleas"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_nbaleas"
android:text="@string/txt_errorNbAleas"
android:textColor="@color/red" />
<TextView
android:id="@+id/txt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_nbaleas"
android:layout_marginTop="20dp"
android:text="@string/txt_a" />
<EditText
android:id="@+id/edt_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_a"
android:inputType="number" />
<TextView
android:id="@+id/txt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_a"
android:text="@string/txt_b" />
<EditText
android:id="@+id/edt_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_a"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_b"
android:inputType="number" />
<TextView
android:id="@+id/txt_errorIntervalle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edt_b"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/edt_b"
android:text="@string/txt_errorIntervalle"
android:textColor="@color/red" />
<Button
android:id="@+id/btn_Executer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_a"
android:layout_marginTop="20dp"
android:text="@string/btn_executer" />
<TextView
android:id="@+id/txt_Reponses"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btn_Executer"
android:layout_marginTop="30dp"
android:text="@string/list_reponses"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/blue" />
<ListView
android:id="@+id/lst_reponses"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_below="@+id/txt_Reponses"
android:layout_marginTop="40dp"
android:background="@color/wheat"
android:clickable="true"
tools:listitem="@android:layout/simple_list_item_1" >
</ListView>
</RelativeLayout>
In de vorige weergave worden labels gebruikt die zijn gedefinieerd in het bestand [res / values / strings.xml]:
<resources>
<string name="app_name">Exemple-14</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- weergave 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="list_reponses">Liste des réponses</string>
<string name="btn_executer">Exécuter</string>
<string name="aleas">Génération de N nombres aléatoires</string>
<string name="txt_nbaleas">Valeur de N :</string>
<string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
<string name="txt_b">"b : "</string>
<string name="txt_dummy">Dummy</string>
<string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
<string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>
De kleuren die in [vue1.xml] worden gebruikt, zijn gedefinieerd in het bestand [res / values / colors.xml]:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<!-- kleuren app -->
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
</resources>
1.15.3. De sessie
![]() |
Aangezien er hier slechts één fragment is, hoeft er geen communicatie tussen fragmenten te worden voorzien. De sessie zal dus leeg zijn:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}
Compileer de applicatie nu. Er verschijnen fouten op de regels die gebruik maakten van elementen uit de nu lege sessie. Verwijder deze regels en controleer of de compilatie geen fouten meer oplevert.
1.15.4. Het fragment [Vue1Fragment]
![]() |
We passen het bestaande fragment [Vue1Fragment] als volgt aan:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// de elementen van de visuele interface
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// lijst met reacties op een opdracht
private List<String> reponses = new ArrayList<>();
// listview-adapter
private ArrayAdapter<String> adapterReponses;
// de invoervelden
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
// geheugen
afterViewsDone = true;
// logboek
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
}
// foutmeldingen worden verborgen
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
}
@Click(R.id.btn_Executer)
void doExecuter() {
// eventuele eerdere foutmeldingen worden verborgen
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// de geldigheid van de invoer wordt gecontroleerd
if (!isPageValid()) {
return;
}
}
// de geldigheid van de ingevoerde gegevens wordt gecontroleerd
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
}
}
- hier is er slechts één fragment waarvan de levenscyclus slechts één keer wordt uitgevoerd, bij het opstarten van de applicatie. Om deze reden worden de methoden [@AfterViews] (regels 46-57) en [udateFragment] (regels 75-81) slechts één keer uitgevoerd bij het opstarten van de applicatie;
- regels 55-56: de twee foutmeldingen van de weergave (hieronder weergegeven) [1-2] worden verborgen;
![]() |
- regels 59-60: de methode die wordt uitgevoerd wanneer op de knop [Exécuter] wordt geklikt;
- regels 71-73: de geldigheid van de invoer wordt gecontroleerd;
De methode [isPageValid] is als volgt:
// de invoer
private int nbAleas;
private int a;
private int b;
...
// de geldigheid van de ingevoerde gegevens wordt gecontroleerd
private boolean isPageValid() {
// het aantal willekeurige getallen invoeren
nbAleas = 0;
Boolean erreur;
int nbErreurs = 0;
try {
nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
erreur = (nbAleas < 1);
} catch (Exception ex) {
erreur = true;
}
// fout?
if (erreur) {
nbErreurs++;
txtErrorAleas.setVisibility(View.VISIBLE);
}
// invoer van a
a = 0;
erreur = false;
try {
a = Integer.parseInt(edtA.getText().toString());
} catch (Exception ex) {
erreur = true;
}
// fout?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// invoer van b
b = 0;
erreur = false;
try {
b = Integer.parseInt(edtB.getText().toString());
erreur = b < a;
} catch (Exception ex) {
erreur = true;
}
// fout?
if (erreur) {
nbErreurs++;
txtErrorIntervalle.setVisibility(View.VISIBLE);
}
// terug
return (nbErreurs == 0);
}
- regels 2-4: deze drie velden worden geïnitialiseerd door de methode [isPageValid]. Bovendien retourneert deze methode true als alle invoer geldig is, en false in het tegenovergestelde geval. Als er ongeldige invoer is, worden de bijbehorende foutmeldingen weergegeven;
Op dit moment is de applicatie uitvoerbaar. Controleer de werking van de methode [isPageValid] door onjuiste gegevens in te voeren.
1.15.5. De laag [métier]
![]() |
![]() |
De laag [métier] heeft de volgende interface [IMetier]:
package exemples.android.metier;
import java.util.List;
public interface IMetier {
List<Object> getAleas(int a, int b, int n);
}
De methode [getAleas(a,b,n)] retourneert normaal gesproken n willekeurige gehele getallen in het interval [a,b]. Er is ook voorzien dat de methode één op de drie keer een uitzondering retourneert, die eveneens wordt opgenomen in de door de methode geretourneerde antwoorden. Uiteindelijk retourneert de methode een lijst met objecten van het type [Exception] of [Integer].
De implementatie [Metier] van deze interface is als volgt:
package exemples.android.metier;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@EBean(scope = EBean.Scope.Singleton)
public class Metier implements IMetier {
public List<Object> getAleas(int a, int b, int n) {
// de lijst met objecten
List<Object> réponses = new ArrayList<Object>();
// enkele controles
if (n < 1) {
réponses.add(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));
}
if (a < 0) {
réponses.add(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));
}
if (b < 0) {
réponses.add(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));
}
if (a >= b) {
réponses.add(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));
}
// fout?
if (réponses.size() != 0) {
return réponses;
}
// er worden willekeurige getallen gegenereerd
Random random = new Random();
for (int i = 0; i < n; i++) {
// er wordt 1 keer op de 3 een willekeurige uitzondering gegenereerd
int nombre = random.nextInt(3);
if (nombre == 0) {
réponses.add(new AleaException("Exception aléatoire"));
} else {
// anders wordt een willekeurig getal tussen twee grenzen geretourneerd [a,b]
réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
}
}
// resultaat
return réponses;
}
}
- regel 9: de annotatie AA [@EBean] wordt toegepast op de klasse [Metier], zodat verwijzingen naar deze klasse kunnen worden ingevoegd in de laag [Présentation]. Het attribuut (scope = EBean.Scope.Singleton) zorgt ervoor dat er slechts één exemplaar van de klasse [Metier] wordt geïnstantieerd. Er wordt dus altijd dezelfde referentie geïnjecteerd, ook al wordt deze meerdere keren in de laag [Présentation] geïnjecteerd;
- de rest van de code is standaard;
Het type [AleaException] dat door de klasse [Metier] wordt gebruikt, is als volgt:
package exemples.android.metier;
public class AleaException extends RuntimeException {
private static final long serialVersionUID = 1L;
public AleaException() {
}
public AleaException(String detailMessage) {
super(detailMessage);
}
public AleaException(Throwable throwable) {
super(throwable);
}
public AleaException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
}
- regel 3: de klasse [AleaException] is een uitbreiding van de systeemklasse [RuntimeException], waardoor het een ongecontroleerde uitzondering is: deze hoeft niet in een try/catch-blok te worden afgehandeld, noch in de methodesignatuur te worden opgenomen;
1.15.6. De activiteit [MainActivity] herzien
![]() |
Laag
[metier]
Activiteit
Weergave
Gebruiker
De activiteit implementeert de interface [IMetier] van de laag [métier]. Het fragment/de weergave heeft dus alleen de activiteit als gesprekspartner.
De activiteit [MainActivity] implementeert al de interface [IMainActivity]. Om ervoor te zorgen dat deze ook de interface [IMetier] implementeert, kan men:
- de interface [IMetier] toevoegen aan de interfaces die door de activiteit worden geïmplementeerd;
- ervoor zorgen dat de interface [IMainActivity] zelf de interface [IMetier] uitbreidt. Dit is de aanpak die we kiezen;
De interface [IMainActivity] ziet er dan als volgt uit:
![]() |
package exemples.android.architecture;
import exemples.android.metier.IMetier;
public interface IMainActivity extends IMetier {
// toegang tot de sessie
Session getSession();
// van weergave wisselen
void navigateToView(int position);
// debugmodus
public static final boolean IS_DEBUG_ENABLED = true;
}
- regel 5: de interface [IMainActivity] breidt de interface [IMetier] uit
De klasse [MainActivity] verandert als volgt:
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
...
// sessie-injectie
@Bean(Session.class)
protected Session session;
// business-injectie
@Bean(Metier.class)
protected IMetier metier;
...
// implementatie IMetier --------------------------------------------------------------------
@Override
public List<Object> getAleas(int a, int b, int n) {
return metier.getAleas(a, b, n);
}
- regels 11-12: de laag [métier] wordt in de activiteit geïnjecteerd. Hiervoor wordt de annotatie AA [@Bean] gebruikt, waarvan de parameter de klasse is die de annotatie AA [@EBean] draagt;
- regel 2: de activiteit implementeert de interface [IMainActivity] en daarmee ook de interface [IMetier] van de laag [métier];
- regels 16-19: implementatie van de enige methode van de interface [IMetier]. We delegeren de aanroep gewoon naar de laag [métier];
1.15.7. Het fragment [Vue1Fragment] opnieuw bekeken
![]() |
De code van de klasse [Vue1Fragment] verandert als volgt:
package exemples.android.fragments;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// elementen van de visuele interface
@ViewById(R.id.lst_reponses)
protected ListView listReponses;
@ViewById(R.id.edt_nbaleas)
protected EditText edtNbAleas;
@ViewById(R.id.edt_a)
protected EditText edtA;
@ViewById(R.id.edt_b)
protected EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
protected TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
protected TextView txtErrorIntervalle;
// lijst met reacties op een opdracht
private List<String> reponses = new ArrayList<>();
// ListView-adapter
private ArrayAdapter<String> adapterReponses;
// de invoergegevens
private int nbAleas;
private int a;
private int b;
@AfterViews
protected void afterViews() {
...
}
@Click(R.id.btn_Executer)
void doExecuter() {
...
}
// de geldigheid van de ingevoerde gegevens wordt gecontroleerd
private boolean isPageValid() {
...
}
@Override
protected void updateFragment() {
// logboek
if (isDebugEnabled) {
Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
}
// wordt slechts één keer uitgevoerd bij het opstarten van de applicatie
// de adapter van ListView wordt aangemaakt – hiervoor moet de variabele [activity] geïnitialiseerd zijn
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
}
- regels 69-70: de adapter van de component van het type [ListView] wordt ingesteld;
De component [ListView] dient om een lijst met elementen weer te geven. Dit gebeurt via een adapter van het type [ListAdapter], die op zijn beurt is gekoppeld aan de gegevensbron die de [ListView] moet voeden. Om de adapter van een [ListView] te definiëren, is de volgende methode [ListView.setAdapter] beschikbaar:
public void setAdapter (ListAdapter adapter)
[ListAdapter] is een interface. De klasse [ArrayAdapter] is een klasse die deze interface implementeert. De constructor die in regel 69 hierboven wordt gebruikt, is de volgende:
- [context] is de activiteit die [ListView] weergeeft;
- [resource] is het gehele getal dat de weergave identificeert die wordt gebruikt om een element van [ListView] weer te geven. Deze weergave kan willekeurig complex zijn. De ontwikkelaar bouwt deze op basis van zijn behoeften;
- [textViewResourceId] is het gehele getal dat een component [TextView] in de weergave [resource] identificeert. De weergegeven tekenreeks wordt door deze component weergegeven;
- [objects]: de lijst met objecten die door [ListView] worden weergegeven. De methode [toString] van de objecten wordt gebruikt om het object in de [TextView], geïdentificeerd door [textViewResourceId], weer te geven in de weergave die wordt geïdentificeerd door [resource].
Het is de taak van de ontwikkelaar om de weergave [resource] te maken die elk element van [ListView] weergeeft. Voor het eenvoudige geval waarin men, zoals hier, slechts een eenvoudige tekenreeks wil weergeven, biedt Android de weergave met de identificatie [android.R.layout.simple_list_item_1]. Deze bevat een component [TextView] met de identificatie [android.R.id.text1]. Dit is de methode die in regel 69 wordt gebruikt om de adapter van [ListView] aan te maken. Deze adapter hoeft slechts één keer te worden gedefinieerd. Om hergebruik mogelijk te maken, is deze gedefinieerd als een instantievariabele van de klasse (regel 39). Laten we nogmaals naar regel 69 kijken:
adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
De eerste parameter van de constructor [ArrayAdapter] is de activiteit die in een fragment is verkregen via [getActivity] en die hier is opgeslagen in de variabele [activity] van de bovenliggende klasse. Dit veld heeft niet altijd een waarde. Uit de logbestanden blijkt dus dat wanneer de methode [@AfterViews] wordt aangeroepen, deze nog niet is geïnitialiseerd en dat de regels 69-70 daarom niet in deze methode kunnen worden geplaatst. In de methode [updateFragment] is dit wel mogelijk, omdat we weten dat wanneer deze methode wordt uitgevoerd, [activity!=null] onvermijdelijk aanwezig is. De adapter is hier gekoppeld aan de gegevensbron [reponses], gedefinieerd op regel 37;
De methode [doExecuter] verwerkt de klik op de knop [Exécuter]. De code ervan is als volgt:
@Click(R.id.btn_Executer)
void doExecuter() {
// eventuele eerdere foutmeldingen worden in de cache opgeslagen
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
// eerdere antwoorden worden gewist
reponses.clear();
adapterReponses.notifyDataSetChanged();
// de geldigheid van de invoer wordt gecontroleerd
if (!isPageValid()) {
return;
}
// er worden willekeurige getallen opgevraagd bij de activiteit
List<Object> data = mainActivity.getAleas(a, b, nbAleas);
// er wordt een lijst met strings aangemaakt op basis van deze gegevens
for (Object o : data) {
if (o instanceof Exception) {
reponses.add(((Exception) o).getMessage());
} else {
reponses.add(o.toString());
}
}
// de lijstweergave vernieuwen
adapterReponses.notifyDataSetChanged();
}
- regels 7-8: we willen de ListView leegmaken. Hiervoor maken we de gegevensbron [reponses] leeg en vragen we de adapter die aan de ListView is gekoppeld om te verversen;
- regels 10-12: voordat de gevraagde actie wordt uitgevoerd, wordt gecontroleerd of de ingevoerde waarden correct zijn;
- regel 14: de lijst met willekeurige getallen wordt opgevraagd bij de activiteit. We krijgen een lijst met objecten waarbij elk object van het type [Integer] of [AleaException] is;
- regels 16-22: op basis van de verkregen lijst met objecten wordt de gegevensbron [reponses] bijgewerkt die door ListView wordt weergegeven;
- regel 24: de adapter van ListView wordt gevraagd om te verversen;
1.15.8. Uitvoering
Voer het project uit en controleer of het correct werkt.
1.16. Voorbeeld-15: client/server-architectuur
We bespreken een veelvoorkomende architectuur voor een Android-app, waarbij de Android-app communiceert met externe webservices. We hebben nu de volgende architectuur:
![]() |
Aan de Android-app is een laag [DAO] toegevoegd om te communiceren met de externe server. Deze laag communiceert met de server die de willekeurige getallen genereert die op de Android-tablet worden weergegeven. Deze server heeft de volgende tweelaagse architectuur:
![]() |
Klanten vragen bepaalde URL-gegevens op uit de laag [web / jSON] en ontvangen een tekstantwoord in het formaat jSON (JavaScript Object Notation). Hier verwerkt onze webservice één enkele URL van het type [/a/b], die een willekeurig getal binnen het bereik [a,b] retourneert. We zullen de toepassing in de volgende volgorde beschrijven:
De server
- de [métier]-laag;
- de service [web / jSON], geïmplementeerd met Spring MVC;
De client
- de [DAO]-laag. Er zal geen [métier]-laag zijn;
1.16.1. De server [web / jSON]
We willen de volgende architectuur opbouwen:
![]() |
1.16.1.1. Het project opzetten
We gaan de webservice bouwen met het Spring-ecosysteem [http://spring.io/]. We gaan naar de website [http://start.spring.io/] (juni 2016), waarmee we een Gradle-project kunnen genereren met de benodigde afhankelijkheden voor ons project. Dit is geen Android-project en Android Studio biedt hiervoor dan ook geen ondersteuning:
![]() |
- in [1]: kies een Gradle-project;
- in [2-3]: de kenmerken van de door het project gegenereerde afhankelijkheid jar (zie hieronder);
- in [4]: kies de webafhankelijkheid [5] zodat de benodigde binaire bestanden voor onze webservice beschikbaar zijn;
- in [6]: genereer het project. Er wordt dan een zip-bestand van een Gradle-skeletproject gegenereerd en aangeboden om te downloaden;
Wat moet er in [2-3] worden ingevuld? We hebben al gebruikgemaakt van Gradle-afhankelijkheden. Die van het vorige project was bijvoorbeeld als volgt:
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Sinds Android's Gradle-plugin 0.11 moet je android-apt >= 1.3 gebruiken
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
...
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
- regel 22: een afhankelijkheid heeft de vorm [groupId:artifactId:version]. Wat op het formulier van de website wordt gevraagd voor [http://start.spring.io/]:
- in [2] is [groupId];
- in [3] is [artifactId];
Pak het bovenstaande zip-bestand uit in de map met de andere projecten:
![]() | ![]() ![]() | ![]() |
Open met Android Studio het Gradle-project [server-01] [1-2]. Het geopende project is [3] (perspectief Project).
1.16.1.2. Gradle-configuratie
![]() |
Het gegenereerde Gradle-bestand (juni 2016) ziet er als volgt uit:
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
eclipse {
classpath {
containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
}
}
- de regels 14 en 34-38 zijn voor de Eclipse-versie IDE. We verwijderen deze;
- de regels 1-11 en 15 dienen om een plug-in met de naam [spring-boot] aan ons Gradle-project toe te voegen. Spring Boot is een project binnen het Spring-ecosysteem [http://projects.spring.io/spring-boot/]. Deze plug-in definieert de versies van de afhankelijkheden die het meest worden gebruikt met Spring. Hierdoor hoeven we hun versies niet specifiek op te geven (regels 30 en 31). De versie is dan de versie die wordt bepaald door de gebruikte Spring Boot-versie (regel 3);
- regels 22-23: de te gebruiken Java-versie, in dit geval versie 1.8;
- regels 25-27: de binaire repositories die moeten worden gebruikt om de afhankelijkheden te downloaden;
- regel 26: verwijst naar de centrale Maven-repository. Dit is momenteel de grootste beschikbare repository voor open-source-binaries;
- regels 29-32: de benodigde afhankelijkheden voor het project:
- regel 30: deze afhankelijkheid bevat alle binaire bestanden die nodig zijn om een Spring-webservice te bouwen;
- regel 31: deze afhankelijkheid bevat alle binaire bestanden die nodig zijn voor het testen, met name voor de JUnit-tests;
- een afhankelijkheid [compile] geeft aan dat de afhankelijkheid nodig is om het project te compileren. Een afhankelijkheid [testCompile] geeft aan dat de afhankelijkheid alleen nodig is voor het uitvoeren van de tests. Deze wordt dan niet opgenomen in het binaire bestand van het project;
We voeren een eerste opschoning van het Gradle-bestand uit:
// Spring Boot
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// plugins
apply plugin: 'java'
apply plugin: 'spring-boot'
// projectbinair
jar {
baseName = 'server-01'
version = '0.0.1-SNAPSHOT'
}
// Java-versies
sourceCompatibility = 1.8
targetCompatibility = 1.8
// Maven-repositories
repositories {
mavenLocal()
mavenCentral()
}
// afhankelijkheden
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- regel 30: we hebben de lokale Maven-repository van de ontwikkelingswerkplek toegevoegd. Deze wordt aangemaakt bij de installatie van Maven (zie paragraaf 6.10). Als de gevraagde afhankelijkheid al in de lokale Maven-repository aanwezig is, wordt deze niet opgevraagd bij de centrale Maven-repository;
- regels 19-22: een Gradle-taak waarmee het binaire bestand van het project kan worden gegenereerd. We gaan deze gebruiken om te zien wat er gebeurt;
![]() | ![]() | ![]() |
- in [1-4] voert u de taak [jar] uit die is gedefinieerd in het bestand [build.gradle] ([1] bevindt zich rechtsboven en naast IDE);
De vorige stap maakt het jar-archief van het project aan en plaatst dit in de map [build / libs] [5]:
![]() |
De naam van het archief is rechtstreeks afgeleid van de informatie die is opgegeven voor de taak [jar] in het bestand [build.gradle] (regels 19-22).
Alle afhankelijkheden van het project kunnen als volgt worden weergegeven:
![]() |
In [1] is te zien dat de enige afhankelijkheid van het project [compile('org.springframework.boot:spring-boot-starter-web')] tientallen binaire bestanden met zich mee heeft gebracht. Spring Boot voor het web heeft de afhankelijkheden opgenomen die een Spring-webapplicatie MVC waarschijnlijk nodig zal hebben. Dit betekent dat sommige misschien overbodig zijn. Spring Boot is ideaal voor een tutorial:
- het levert de afhankelijkheden die we waarschijnlijk nodig zullen hebben;
- het bevat een ingebouwde Tomcat-server [1], waardoor we de applicatie niet op een externe webserver hoeven te implementeren;
Op de website van het Spring-ecosysteem zijn talrijke voorbeelden te vinden waarin Spring Boot wordt gebruikt [http://spring.io/guides].
We vullen het bestand [build.gradle] nu als volgt aan:
// Spring Boot
...
// afhankelijkheden
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
// plug-in om een binaire versie volgens de Maven-standaarden te maken in de lokale Maven-repository
apply plugin: 'maven-publish'
publishing {
publications {
maven(MavenPublication) {
groupId 'istia.st.exemples.android'
artifactId 'server-01'
version '0.0.1-SNAPSHOT'
from components.java
}
}
repositories {
maven {
// pas dit aan zodat het naar uw repository verwijst: e.g. http://my.org/repo
url 'file://D:\\maven'
}
}
}
- regel 10: we importeren een Gradle-plugin met de naam [maven-publish] waarmee het binaire bestand van het project in een Maven-repository kan worden gepubliceerd volgens de Maven-standaarden;
- regel 11: een Gradle-taak met de naam [publishing];
- regels 14-15: de kenmerken van het Maven-binaire bestand dat zal worden aangemaakt;
- regel 23: de Maven-repository waarin het zal worden gepubliceerd, in dit geval een lokale Maven-repository;
Door de toevoeging van de plug-in [maven-publish] zijn er nieuwe taken in het Gradle-project aangemaakt:
![]() | ![]() |
Als we in [2] de taak [publish] uitvoeren, wordt het binaire bestand van het project aangemaakt en geïnstalleerd in de map die is aangegeven in regel 23 van het bestand [build.gradle]:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() |
Met de taak [jar] kan het binaire bestand van het project worden gegenereerd. Dit binaire bestand bevat geen afhankelijkheden en is dus niet uitvoerbaar. Het is mogelijk om een binaire uitvoer te genereren met alle afhankelijkheden, die wel uitvoerbaar is. Hiervoor voegen we de volgende code toe aan het bestand [build.gradle]:
// een binair bestand maken met alle bijbehorende afhankelijkheden
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'istia.st.exemples.android.Server01Application'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
- regel 6: hier moet de volledige naam van de uitvoerbare klasse van het project worden ingevoerd:
![]() |
De code van deze klasse ziet er als volgt uit:
package istia.st.exemples.android;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Server01Application {
public static void main(String[] args) {
System.out.println("Server01Application running");
//SpringApplication.run(Server01Application.class, args);
}
}
Ververs het Gradle-project en voer vervolgens de taak [fatJar] uit:
![]() | ![]() |
Het binaire bestand wordt gegenereerd in de map [build / libs] en kan worden uitgevoerd als [1-7]:
![]() | ![]() |
1.16.1.3. Projectconfiguratie
De Gradle-configuratie is niet voldoende. We moeten ook het project configureren. Aangezien dit geen Android-project is dat door IDE is gegenereerd, moet deze configuratie, die we tot nu toe niet hebben uitgevoerd, hier worden uitgevoerd.
![]() | ![]() |
- in [3-4]: gebruik een JDK 1.8;
Om het project te compileren, is de knop die beschikbaar is voor Android-projecten niet meer aanwezig. We gebruiken een optie uit het menu [1-2]:
![]() | ![]() |
Vervolgens wordt de lezer gevraagd het volgende project aan te maken. We geven uitleg bij de uiteindelijke code van het project [3].
1.16.1.4. De laag [métier]
![]() |
![]() |
De laag [métier] volgt dezelfde opzet als de laag [métier] uit het vorige voorbeeld. Deze laag heeft de volgende interface [IMetier]:
package exemples.android.server.metier;
public interface IMetier {
// willekeurig getal in [a,b]
int getAlea(int a, int b);
}
- regel 5: de methode die 1 willekeurig getal genereert in [a,b]
De code van de klasse [Metier] die deze interface implementeert, is als volgt:
package exemples.android.server.metier;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class Metier implements IMetier {
@Override
public int getAlea(int a, int b) {
// enkele controles
if (a < 0) {
throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0", 2);
}
if (b < 0) {
throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0", 3);
}
if (a >= b) {
throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b", 4);
}
// resultaat genereren
Random random=new Random();
random.setSeed(new Date().getTime());
return a + random.nextInt(b - a + 1);
}
}
We geven geen toelichting op de klasse: deze is vergelijkbaar met die uit het vorige voorbeeld, behalve dat er geen willekeurige uitzonderingen worden gegenereerd. Let vooral op de Spring-annotatie [@Service] op regel 8, die ervoor zorgt dat Spring slechts één exemplaar (singleton) van de klasse instantiëert en de referentie ervan beschikbaar stelt voor andere Spring-componenten. Hier hadden ook andere Spring-annotaties kunnen worden gebruikt om hetzelfde effect te bereiken. Spring-componenten hebben standaardnamen die als attribuut van de gebruikte annotatie kunnen worden opgegeven. Zonder dit attribuut, zoals hier, krijgt de Spring-component de naam van de klasse met het eerste letterteken in kleine letters. Hier draagt de Spring-component dus standaard de naam [metier];
De klasse [Metier] genereert uitzonderingen van het type [AleaException]:
package exemples.android.server.metier;
public class AleaException extends RuntimeException {
// foutcode
private int code;
// constructors
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// getters en setters
....
}
- regel 3: [AleaException] is een uitbreiding van de klasse [RuntimeException]. Het is dus een ongecontroleerde uitzondering (hoeft niet met een try/catch te worden afgehandeld);
- regel 6: er wordt een foutcode toegevoegd aan de klasse [RuntimeException];
1.16.1.5. De webservice / jSON
![]() |
![]() |
De webservice / jSON wordt geïmplementeerd door Spring MVC. Spring MVC implementeert het zogenaamde MVC-architectuurmodel (Model – View – Controller) als volgt:
![]() |
De verwerking van een verzoek van een klant verloopt als volgt:
- verzoek – de aangevraagde URL hebben de vorm http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... De [Dispatcher Servlet] is de Spring-klasse die de binnenkomende URL verwerkt. Deze „routeert“ de URL naar de actie die deze moet verwerken. Deze acties zijn methoden van specifieke klassen die [Contrôleurs] worden genoemd. De C van MVC is hier de tekenreeks [Dispatcher Servlet, Contrôleur, Action]. Als er geen actie is geconfigureerd om de binnenkomende URL te verwerken, zal de servlet [Dispatcher Servlet] antwoorden dat de gevraagde URL niet is gevonden (fout 404 NOT FOUND);
- verwerking
- de gekozen actie kan gebruikmaken van de parameters parami die de servlet [Dispatcher Servlet] aan haar heeft doorgegeven. Deze kunnen uit verschillende bronnen afkomstig zijn:
- het pad [/param1/param2/...] van de URL,
- de parameters [p1=v1&p2=v2] van de URL,
- van parameters die door de browser bij het verzoek zijn meegestuurd;
- Bij de verwerking van het verzoek van de gebruiker kan de actie de laag [metier] [2b] nodig hebben. Zodra het verzoek van de klant is verwerkt, kan dit verschillende reacties oproepen. Een klassiek voorbeeld is:
- een foutpagina als het verzoek niet correct kon worden verwerkt
- een bevestigingspagina in het andere geval
- de actie vraagt om een bepaalde weergave [3] weer te geven. Deze weergave toont gegevens die het weergavemodel worden genoemd. Dit is de M van MVC. De actie zal dit model M [2c] aanmaken en vragen om een weergave V weer te geven [3];
- antwoord – de gekozen weergave V gebruikt het door de actie opgebouwde model M om de dynamische delen van het antwoord HTML te initialiseren dat zij naar de client moet verzenden, en verstuurt vervolgens dit antwoord.
Voor een webservice / jSON is de voorgaande architectuur enigszins aangepast:
![]() |
- in [4a] wordt het model, dat een Java-klasse is, door een bibliotheek jSON omgezet in de tekenreeks jSON;
- in [4b] wordt deze tekenreeks jSON naar de browser verzonden;
Een voorbeeld van het serialiseren van een Java-object naar de tekenreeks jSON en het deserialiseren van de tekenreeks jSON naar een Java-object wordt gegeven in de bijlagen bij paragraaf 6.14.
Laten we terugkeren naar de [web]-laag van onze applicatie:
![]() |
In onze applicatie is er slechts één controller:
![]() |
De webservice / jSON stuurt zijn clients het volgende antwoord van het type [Response]:
package exemples.android.server.web;
import java.util.List;
public class Response<T> {
// ----------------- eigenschappen
// status van de bewerking
private int status;
// eventuele foutmeldingen
private List<String> messages;
// de inhoud van het antwoord
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters en setters
...
}
- regel 13: het veld [T body] is het antwoord dat de client verwacht. We hebben ervoor gekozen om hier een generiek antwoord van het type T te gebruiken, in plaats van het type Integer met het verwachte willekeurige getal. We willen deze klasse in andere situaties kunnen hergebruiken. Tijdens de verwerking van het verzoek van de klant kan de server een probleem tegenkomen dat vervolgens in de twee andere velden wordt samengevat;
- regel 8: een statuscode (0 als er geen fout is);
- regel 9: indien status!=0, een lijst met foutmeldingen, meestal die uit de uitzonderingsstack als er een uitzondering is opgetreden, null indien er geen fouten zijn;
De controller [WebController] is als volgt:
package exemples.android.server.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import exemples.android.server.metier.IMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;
@Controller
public class WebController {
// bedrijfslaag
@Autowired
private IMetier metier;
// mapper JSON
@Autowired
private ObjectMapper mapper;
// willekeurige getallen
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
// het antwoord
Response<Integer> response = new Response<>();
// we gebruiken de businesslaag
try {
response.setBody(metier.getAlea(a, b));
response.setStatus(0);
} catch (AleaException e) {
response.setStatus(e.getCode());
response.setMessages(getMessagesFromException(e));
}
// we geven het antwoord terug
return mapper.writeValueAsString(response);
}
private List<String> getMessagesFromException(Throwable e) {
// lijst met berichten
List<String> messages = new ArrayList<String>();
// de uitzonderingsstapel wordt doorlopen
Throwable th = e;
while (th != null) {
messages.add(e.getMessage());
th = th.getCause();
}
// het resultaat wordt weergegeven
return messages;
}
}
- regel 17: de annotatie [@Controller] geeft aan dat de klasse een controller is (MVC) waarvan de methoden verzoeken voor bepaalde URL van de webapplicatie verwerken;
- regels 21-22: de annotatie [@Autowired] vraagt Spring om een component van het type [IMetier] in het veld te injecteren. Dit is de eerder genoemde klasse [Metier]. Omdat we hier de annotatie [@Service] aan hebben toegevoegd, wordt deze behandeld als een Spring-component;
- regels 24-25: we doen hetzelfde met een mapper van het type jSON die we later zullen definiëren. Onze webservice zal zijn antwoord verzenden in de vorm van een tekenreeks jSON. Deze mapper zorgt voor de serialisatie van het antwoord naar jSON;
- regel 30: de methode die het willekeurige getal genereert. De naam ervan doet er niet toe. Wanneer deze wordt uitgevoerd, zijn de parameters ervan door Spring geïnitialiseerd MVC. We zullen zien hoe dat gebeurt. Overigens wordt deze methode uitgevoerd omdat de webserver een verzoek HTTP GET heeft ontvangen voor de URL uit regel 28;
- regel 28: de annotatie [@RequestMapping] definieert bepaalde eigenschappen van de geannoteerde methode:
- [value]: de URL die door de methode wordt geaccepteerd;
- [method]: de methode HTTP die door de methode wordt geaccepteerd. Er zijn er hoofdzakelijk twee, GET en POST. De methode [POST] wordt gebruikt wanneer de klant een document aan zijn verzoek HTTP wil toevoegen;
- [produces]: stelt een van de headers vast van het antwoord HTTP dat aan de klant wordt verzonden. Hier zal er in de HTTP-headers die samen met het antwoord aan de klant worden verzonden, één zijn die de klant meedeelt dat het antwoord in de vorm van een jSON-string wordt verzonden. Deze header is niet verplicht. Deze wordt ter informatie aan de klant verstrekt indien deze antwoorden verwacht die verschillende vormen kunnen aannemen;
- [consumes]: is hier niet aanwezig. Hiermee worden de HTTP-headers aangegeven die het verzoek HTTP van de klant moeten vergezellen om te worden geaccepteerd;
- regel 29: de annotatie [@ResponseBody] geeft aan dat het door de methode gegenereerde resultaat naar de klant moet worden verzonden. Zonder deze annotatie wordt het antwoord van de methode beschouwd als een sleutel waarmee de pagina HTML kan worden geselecteerd om naar de klant te verzenden. In een webservice / jSON zijn er geen pagina’s HTML;
- regel 28: de verwerkte URL heeft de vorm /{a}/{b}, waarbij {x} een variabele vertegenwoordigt. De variabelen {a} en {b} worden op regel 30 toegewezen aan de parameters van de methode. Dit gebeurt via de annotatie @PathVariable("x"). Merk op dat {a} en {b} componenten zijn van een URL en dus van het type String zijn. De conversie van String naar het type van de parameters kan mislukken. Spring MVC genereert dan een uitzondering. Samenvattend: als ik met een browser de URL /100/200 opvraag, wordt de methode getAlea uit regel 30 uitgevoerd met de gehele getallen a=100, b=200;
- regel 36: de laag [métier] wordt gevraagd om een willekeurig getal in het interval [a,b]. We herinneren ons dat de methode [metier].getAlea een uitzondering kan genereren;
- regel 37: geen fout;
- regel 39: foutcode;
- regel 40: de lijst met berichten in het antwoord is die van de uitzonderingsstack (regels 46-57). Hier weten we dat de stack slechts één uitzondering bevat, maar we wilden een meer generieke methode laten zien;
- regel 43: het antwoord van het type [Response<Integer>] wordt weergegeven in de vorm van een tekenreeks jSON;
1.16.1.6. Configuratie van het Spring-project
![]() |
Er zijn verschillende manieren om Spring te configureren:
- met bestanden XML;
- met Java-code;
- met een combinatie van beide;
We kiezen ervoor om onze webapplicatie met Java-code te configureren. De volgende klasse [Config] zorgt voor deze configuratie:
package exemples.android.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(basePackages = { "exemples.android.server.metier", "exemples.android.server.web" })
@EnableWebMvc
public class Config {
// webconfiguratie ------------------------------------
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mapper jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- regel 12: we geven aan Spring door in welke pakketten het de twee componenten kan vinden die het moet beheren:
- de component [Metier], geannoteerd met [@Service], in het pakket [exemples.android.server.metier];
- de component [WebController], geannoteerd als [@Controller], in het pakket [exemples.android.server.web];
- regel 13: de annotatie [@EnableWebMvc] stelt Spring Boot in staat om zelf een aantal standaardconfiguraties uit te voeren voor een Spring-applicatie MVC. Dit ontlast de ontwikkelaar;
- regels 16, 22, 27 en 33: de annotatie [@Bean] definieert eveneens Spring-componenten (beans), net als de twee eerder genoemde annotaties (@Service, @Controller). Hier annotateert de annotatie [@Bean] een methode in plaats van een klasse, en is het resultaat van de methode de Spring-component. Aangezien er geen naamgevingsattribuut in de annotatie [@Bean] is opgenomen, krijgt de aangemaakte Spring-component de naam van de geannoteerde methode;
- regels 16-20: definiëren de bean [dispatcherServlet]. Dit is een vooraf gedefinieerde Spring-naam (MVC) die de frontcontroller van de applicatie MVC definieert, een object waar alle verzoeken van clients doorheen lopen en dat deze (vandaar de naam) doorstuurt naar de verschillende [@Controller]’en van de Spring-applicatie MVC;
- regel 18: de bean [dispatcherServlet] is een instantie van de klasse [DispatcherServlet] die door Spring MVC wordt geleverd;
- regels 22-25: de bean [servletRegistrationBean] wordt gebruikt om te definiëren welke URL door de applicatie worden geaccepteerd. Op regel 24 worden alle URL geaccepteerd;
- regels 27-30: de bean [embeddedServletContainerFactory] wordt gebruikt om de ingebouwde server in de projectafhankelijkheden te definiëren die de webapplicatie moet hosten. Regel 29 geeft aan dat het om een Tomcat-server gaat en dat deze op poort 8080 zal draaien. Standaard worden de binaire bestanden van deze webserver geleverd via de afhankelijkheid [org.springframework.boot:spring-boot-starter-web] in het Gradle-bestand;
1.16.1.7. Uitvoering van de webservice / jSON
![]() |
Het project wordt uitgevoerd vanuit de volgende uitvoerbare klasse [Boot]:
package exemples.android.server.boot;
import exemples.android.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Boot {
public static void main(String[] args) {
// toepassing uitvoeren
SpringApplication.run(Config.class, args);
}
}
- de klasse [Boot] is een uitvoerbare klasse (regels 7-10);
- regel 9: de statische methode [SpringApplication.run] is een methode van [spring Boot] (regel 4) die de toepassing zal starten. De eerste parameter is de Java-klasse die het project configureert. In dit geval is dat de klasse [Config] die we zojuist hebben beschreven. De tweede parameter is de argumentenarray die wordt doorgegeven aan de methode [main] (regel 7);
De webapplicatie kan op verschillende manieren worden gestart, waaronder de volgende:
![]() |
In de console verschijnen dan een aantal logberichten:
- regels 12-14: de ingebouwde Tomcat-server wordt gestart;
- regels 15-19: de Spring-servlet [DispatcherServlet] wordt geladen en geconfigureerd;
- regel 20: de URL [/{a}/{b}] van de webserver wordt gedetecteerd;
Laten we nu een browser openen en de webdienst /URL /jSON testen:
![]() |
![]() |
![]() |
![]() |
We krijgen telkens de weergave jSON van een object van het type [Response<Integer>].
In plaats van een standaardbrowser gebruiken we nu de extensie [Advanced Rest Client] van de Chrome-browser (zie bijlagen, paragraaf 6.13):

- in [1], de gevraagde URL;
- in [2], door middel van een GET;
- naar [3], waarna het verzoek wordt verzonden;

- in [4], de headers HTTP van het antwoord van de server. Merk op dat hierin wordt aangegeven dat het verzonden document een tekenreeks jSON is;
- in [5], de ontvangen tekenreeks jSON;
1.16.1.8. Het genereren van het uitvoerbare JAR-bestand van het project
In paragraaf 1.16.1.2 hebben we laten zien hoe het Gradle-bestand moet worden geconfigureerd om een uitvoerbaar bestand van de applicatie te genereren met alle bijbehorende afhankelijkheden. Aangepast aan de huidige applicatie ziet deze configuratie er als volgt uit:
// een binair bestand maken met alle bijbehorende afhankelijkheden
version = '1.0'
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
attributes 'Main-Class': 'exemples.android.server.boot.Boot'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
Om dit uitvoerbare bestand te genereren, kunt u als volgt te werk gaan [1-5]:
![]() | ![]() |
Om het uit te voeren, stoppen we de webservice als deze actief is ([1]) en voeren we vervolgens het archief uit ([2-4]):
![]() | ![]() |
Open een browser en vraag de bestanden URL en [localhost:8080/100/200] op. U zou dezelfde resultaten moeten krijgen als eerder.
1.16.1.9. Logboekbeheer
Wanneer je het uitvoerbare archief uitvoert, zie je dat je niet dezelfde logbestanden krijgt als wanneer je het project uitvoert vanuit het IDE. Je krijgt logbestanden in de modus [DEBUG]:
...
09:32:03.741 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletConfigInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletContextInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:32:03.810 [main] INFO org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:32:03.813 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.984 seconds (JVM running for 2.206)
Je kunt het logniveau beheren door een bestand [logback.xml] toe te voegen aan de map [resources] van het project:
![]() |
Dit bestand zou de volgende inhoud kunnen hebben:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!---encoders wordt standaard het type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- logniveau-instelling -->
<root level="info"> <!-- info, debug, waarschuwing -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
Het logniveau wordt bepaald in regel 12. Als we nu het uitvoerbare archief opnieuw genereren en dit uitvoeren, krijgen we alleen logs van niveau [info]:
...
09:36:52.433 [main] INFO o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:36:52.923 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:36:52.926 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.865 seconds (JVM running for 2.203)
1.16.2. De Android-client van de webserver / jSON
De Android-client krijgt de volgende architectuur:
![]() |
De client zal uit twee componenten bestaan:
- een [Présentation]-laag (weergave + activiteit), vergelijkbaar met die welke we in het voorbeeld [Exemple-14] hebben bestudeerd;
- de laag [DAO] die communiceert met de service [web / jSON] die we eerder hebben besproken.
1.16.2.1. Het project aanmaken
We dupliceren het vorige project [Exemple-14] naar [Exemple-15] volgens de procedure in paragraaf 1.4. We krijgen het volgende resultaat:
![]() | ![]() |
Vervolgens wordt de lezer gevraagd het volgende project aan te maken.
1.16.2.2. Gradle-configuratie
![]() |
Het bestand [build.gradle] ziet er als volgt uit:
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Sinds Android's Gradle-plugin 0.11 moet je android-apt >= 1.3 gebruiken
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "exemples.android"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// benodigde verpakkingsopties om de APK te kunnen genereren
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
We geven alleen uitleg bij zaken die nog niet aan bod zijn gekomen:
- regels 46-47: toevoeging van een plug-in AA. Met de plug-in [rest-spring-api] kunnen de client-servercommunicaties worden gedelegeerd aan de bibliotheek AA;
- regel 50: de bibliotheek [spring-android-rest-template] is de bibliotheek die door AA wordt gebruikt om de communicatie tussen client en server te verzorgen. De versie [2.0.0.M3] is een zogenaamde 'milestone'-versie die niet in de gebruikelijke Maven-repositories te vinden is. Daarom moet in de regels 56-59 worden aangegeven welke repository (regel 58) moet worden gebruikt om de bibliotheek te vinden;
- regel 51: een bibliotheek jSON;
- regels 33-39: zonder deze eigenschap treden er fouten op bij het genereren van het binaire bestand APK van het project;
1.16.2.3. Het manifest van de Android-app
![]() |
Het bestand [AndroidManifest.xml] moet worden aangepast. Standaard is internettoegang namelijk uitgeschakeld. Dit moet worden ingeschakeld met een speciale instructie:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="exemples.android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity_"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- regel 5: internettoegang is toegestaan;
1.16.2.4. De laag [DAO]
![]() |
![]() |
1.16.2.4.1. De interface [IDao] van de laag [DAO]
De interface van de laag [DAO] ziet er als volgt uit:
package exemples.android.dao;
public interface IDao {
// willekeurig getal
int getAlea(int a, int b);
// URL van de webservice
void setUrlServiceWebJson(String url);
// maximale wachttijd (ms) voor het antwoord van de server
void setTimeout(int timeout);
// wachttijd in milliseconden van de client vóór het verzoek
void setDelay(int delay);
}
- regel 6: de methode van de webservice / jSON om een willekeurig getal te verkrijgen binnen het interval [a,b] van deze webservice;
- regel 9: de URL van de webservice / jSON voor het genereren van willekeurige getallen;
- regel 12: we stellen een maximale wachttijd in voor het wachten op het antwoord van de server;
- regel 15: we willen een wachttijd instellen voordat het verzoek naar de server wordt verzonden, zodat de gebruiker de tijd heeft om zijn verzoek te annuleren;
1.16.2.4.2. De interface [WebClient]
![]() |
De interface [WebClient] zorgt voor de communicatie met de webservice. De code ervan is als volgt:
package exemples.android.dao;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// 1 willekeurig getal in het interval [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- regel 12: [WebClient] is een interface die de bibliotheek AA zelf zal implementeren aan de hand van de annotaties die we erin zullen opnemen. Deze interface moet de aanroepen naar URL implementeren die door de webservice / jSON worden blootgesteld:
// willekeurig getal
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
- regel 11: de annotatie [@Rest] is een annotatie van het type AA. De waarde van het attribuut [converters] is een array van converters. Hier zorgt de converter [MappingJackson2HttpMessageConverter.class] ervoor dat wanneer de server een tekenreeks jSON verstuurt, deze automatisch wordt gedeserialiseerd. Zo zien we in regel (d) dat de URL [/{a}/{b}] een type String retourneert, wat in feite een tekenreeks jSON is (regel b). Met deze informatie en die van het verwachte type op regel 16 zal de [WebClient]-instantie van de client de tekenreeks die hij ontvangt deserialiseren tot een type [Response<Integer>];
- regel 15: een annotatie AA die aangeeft dat de URL moet worden aangeroepen met een methode HTTP GET. De parameter van de annotatie [@Get] is de vorm van de URL die door de webservice wordt verwacht. Het volstaat om de parameter [value] uit de annotatie [@RequestMapping] (regel b) over te nemen in de aangeroepen methode in de controller [WebController] van de server. De accolades {} omringen de parameters van de URL die moeten worden overgenomen in de parameters van de methode op regel 16. Door de syntaxis van [@Path("a") int a] wordt de parameter [a] van de methode toegewezen aan de waarde {a} van URL. Wanneer de parameter van URL en die van de methode dezelfde naam hebben, zoals hier, kan men eenvoudiger [@Path int a] schrijven;
In het geval van een aanroep HTTP POST zou de aanroepmethode de volgende handtekening hebben:
@Post("/{a}/{b}")
Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);
Het is de annotatie [@Body] die de verzonden waarde aangeeft. Deze wordt automatisch geserialiseerd tot jSON. Aan de serverzijde hebben we de volgende handtekening:
// willekeurige getallen
@RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
- regel 2: hier wordt aangegeven dat een verzoek met de code HTTP POST wordt verwacht en dat de inhoud van dit verzoek (het verzonden object) moet worden doorgegeven in de vorm van een tekenreeks jSON (attribuut consumes);
- regel 4: de verzonden waarde wordt opgehaald uit de parameter [@RequestBody T body] van de methode;
Laten we teruggaan naar de code van de klasse [WebClient]:
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
- we moeten de URL van de webdienst waarmee contact moet worden gelegd, kunnen opgeven. Dit wordt bereikt door de interface [RestClientRootUrl], die door AA wordt geleverd, uit te breiden. Deze interface biedt een methode [setRootUrl(urlServiceWeb] waarmee de URL van de webdienst die moet worden benaderd, kan worden ingesteld;
- bovendien willen we de aanroep naar de webservice controleren, omdat we de wachttijd voor het antwoord willen beperken. Hiervoor breiden we de interface [RestClientSupport] uit, die de methode [setRestTemplate] beschikbaar stelt waarmee we:
- zelf het object [RestTemplate] aanmaken, dat dient voor het beheer van de communicatie tussen client en server;
- dit object configureren om de maximale wachttijd voor het antwoord in te stellen;
1.16.2.4.3. De klasse [Response]
De methode [getAlea] van de interface [IDao] retourneert een antwoord van het type [Response] als volgt:
package exemples.android.dao;
import java.util.List;
public class Response<T> {
// ----------------- eigenschappen
// status van de bewerking
private int status;
// eventuele foutmeldingen
private List<String> messages;
// de inhoud van het antwoord
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters en setters
...
}
Dit is de klasse [Response] die al aan de serverzijde wordt gebruikt (paragraaf 1.16.1.5). In feite verloopt alles vanuit programmeertechnisch oogpunt alsof de [DAO]-laag van de client rechtstreeks communiceert met de [WebController]-controller van de webservice:
![]() |
De netwerkcommunicatie tussen client en server en de serialisatie/deserialisatie van Java-objecten aan de clientzijde zijn transparant voor de programmeur.
1.16.2.4.4. Implementatie van de laag [DAO]
![]() |
De interface [IDao] wordt geïmplementeerd met de volgende klasse [Dao]:
package exemples.android.dao;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@EBean
public class Dao implements IDao {
// service-client REST
@RestService
protected WebClient webClient;
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// wachttijd voor het uitvoeren van een verzoek
private int delay;
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setDelay(int delay) {
this.delay = delay;
}
}
- regel 15: we voorzien de klasse [Dao] van de annotatie [@EBean] om er een bean AA van te maken die we elders kunnen injecteren;
- regels 19-20: we injecteren de implementatie van de interface [WebClient] die we hebben beschreven. De annotatie [@RestService] zorgt voor deze injectie;
- de overige methoden implementeren de interface [IDao] (regels 27-46);
Methode [setTimeout]
De methode [setTimeout] is als volgt:
@Override
public void setTimeout(int timeout) {
// de time-out voor verzoeken van de client wordt ingesteld REST
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
// restTemplate wordt opgebouwd
RestTemplate restTemplate = new RestTemplate(factory);
// de converter wordt ingesteld jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// de restTemplate van de webclient wordt ingesteld
webClient.setRestTemplate(restTemplate);
}
- de interface [WebClient] zal worden geïmplementeerd door een klasse AA die gebruikmaakt van de Gradle-afhankelijkheid [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] implementeert de communicatie tussen de client en de webserver / jSON door middel van een klasse van het type [RestTemplate];
- regel 4: de klasse [SimpleClientHttpRequestFactory] wordt geleverd door de afhankelijkheid [spring-android-rest-template]. Hiermee kunnen we de maximale wachttijd voor het antwoord van de server instellen (regels 5-6);
- regel 8: we maken het object van het type [RestTemplate] aan, dat als drager voor de communicatie met de webservice zal dienen. We geven het object [factory], dat zojuist is aangemaakt, als parameter door;
- regel 10: de dialoog tussen client en server kan verschillende vormen aannemen. De uitwisselingen vinden plaats via tekstregels en we moeten het object van het type [RestTemplate] aangeven wat het met deze tekstregel moet doen. Hiervoor voorzien we het van converters, klassen die tekstregels kunnen verwerken. De keuze van de converter gebeurt doorgaans via de headers HTTP die bij de tekstregel horen. In dit geval weten we dat we uitsluitend tekstregels in het formaat jSON ontvangen. Bovendien hebben we in paragraaf 1.16.1.7 gezien dat de server de header HTTP verstuurde:
Content-Type: application/json;charset=UTF-8
Regel 10: de enige converter voor [RestTemplate] is een jSON-converter die is geïmplementeerd met de bibliotheek [Jackson]. Er is iets vreemds aan de hand met deze converters: AA dwingt ons om deze ook op te nemen in de annotatie van de webclient [WebClient]:
@Rest(converters = {MappingJacksonHttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
Op regel 1 moeten we een converter specificeren, terwijl we die al via de programmering specificeren.
- regel 12: het zo geconstrueerde object [RestTemplate] wordt geïnjecteerd in de implementatie van de interface [WebClient] en het is dit object dat de communicatie tussen client en server zal verzorgen;
Methode [getAlea]
De methode [getAlea] is als volgt:
@Override
public int getAlea(int a, int b) {
// dienst wordt uitgevoerd
Response<Integer> info;
DaoException ex;
try {
// in afwachting
waitSomeTime(delay);
// service wordt uitgevoerd
info = webClient.getAlea(a, b);
int status = info.getStatus();
if (status == 0) {
// het resultaat wordt teruggegeven
return info.getBody();
} else {
// de uitzondering wordt geregistreerd
ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
}
} catch (JsonProcessingException | RuntimeException e) {
// de uitzondering wordt geregistreerd
ex = new DaoException(e, 100);
}
// de uitzondering wordt gegenereerd
throw ex;
}
...
// privé-methoden -------------------
private void waitSomeTime(int delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- regel 8: er wordt gewacht gedurende [delay] milliseconden;
- regel 10: we roepen gewoon de methode met dezelfde handtekening aan in de klasse die de interface [WebClient] implementeert;
- regel 11: het antwoord van de server wordt geanalyseerd door te kijken naar de [status];
- regels 12-14: als er geen fout aan de serverzijde is opgetreden (status=0), dan wordt het resultaat van de methode teruggegeven;
- regel 17: als er een fout aan de serverzijde is opgetreden (status!=0), dan wordt er een uitzondering voorbereid zonder deze af te vuren. De server heeft een lijst met foutmeldingen doorgegeven. We maken een uitzondering aan met als enige bericht de tekenreeks jSON uit de lijst met serverberichten;
- regels 19-22: andere uitzonderingsgevallen;
- regel 24: wanneer we hier aankomen, is er onvermijdelijk een uitzondering opgetreden. Deze wordt dan afgevuurd;
De uitzondering [DaoException] die in deze code wordt gebruikt, is de volgende:
package exemples.android.dao;
import java.util.ArrayList;
import java.util.List;
public class DaoException extends RuntimeException {
// foutcode
private int code;
// constructors
public DaoException() {
}
public DaoException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public DaoException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
// getters en setters
...
}
- regel 6: de uitzondering [DaoException] is een ongecontroleerde uitzondering;
Methode [setUrlServiceWebJson]
De methode [setUrlServiceWebJson] is als volgt:
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// de URL van de service REST wordt ingesteld
webClient.setRootUrl(urlServiceWebJson);
}
- regel 4: de URL van de webservice wordt ingesteld via de methode [setRootUrl] van de interface [WebClient]. Deze methode bestaat omdat deze interface een uitbreiding is van de interface [RestClientRootUrl];
1.16.2.5. Het pakket [architecture]
Het pakket [architecture] bevat de elementen die de structuur van de applicatie vormen:
![]() |
![]() |
1.16.2.5.1. De interface [IMainActivity]
De interface [IMainActivity] geeft een overzicht van de methoden die door de activiteit van de applicatie moeten worden geïmplementeerd:
package exemples.android.architecture;
import exemples.android.dao.IDao;
public interface IMainActivity extends IDao {
// toegang tot de sessie
Session getSession();
// van weergave wisselen
void navigateToView(int position);
// wachten
void beginWaiting();
void cancelWaiting();
// debugmodus
boolean IS_DEBUG_ENABLED = true;
// wachttijd voor het antwoord
int TIMEOUT = 1000;
// aangrenzende fragmenten
int OFF_SCREEN_PAGE_LIMIT = 1;
}
- regel 5: de interface [IMainActivity] breidt de interface [IDao] uit;
- regels 13-16: aan de methoden die al in de voorgaande voorbeelden stonden (regels 7-11), hebben we twee methoden toegevoegd om de laadafbeelding van de applicatie te beheren (regels 14, 16);
- regel 21: we stellen een maximale wachttijd voor het antwoord van de server in op 1 seconde;
1.16.2.5.2. De klasse [Utils]
In de klasse [Utils] zijn statische hulpprogramma's verzameld die vanaf verschillende plaatsen in de applicatiearchitectuur kunnen worden aangeroepen:
package exemples.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// lijst met foutmeldingen van een uitzondering - versie 1
static public List<String> getMessagesFromException(Throwable ex) {
// er wordt een lijst aangemaakt met de foutmeldingen uit de uitzonderingsstapel
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// lijst met foutmeldingen van een uitzondering - versie 2
static public String getMessagesForAlert(Throwable th) {
// de weer te geven tekst wordt samengesteld
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--;
}
// resultaat
return texte.toString();
}
}
- regels 9-18: maakt de lijst met foutmeldingen aan die in een Throwable is opgenomen;
- regels 21-32: maakt gebruik van de vorige methode om op basis van de verkregen lijst met meldingen de tekst samen te stellen die in een Android-waarschuwingsbericht moet worden weergegeven;
- regels 27-28: de berichten worden genummerd. Het laagste nummer (1) komt overeen met de eerste uitzondering en het hoogste nummer met de meest recente uitzondering in de uitzonderingsstapel;
1.16.2.5.3. De abstracte klasse [AbstractFragment]
De klasse [AbstractFragment] heeft twee functies:
- ervoor zorgen dat de methode [updateFragments] van de dochterklassen altijd wordt aangeroepen bij het weergeven van het fragment, en wel slechts één keer;
- de status en de methoden van de dochterklassen die daarvoor in aanmerking komen, te factoriseren;
Het is doel 2 dat ervoor zorgt dat we in deze klasse de bewerkingen voor het beheer van de laadafbeelding onderbrengen: alle fragmenten van een asynchrone Android-applicatie hebben te maken met dit soort problemen:
// afhandeling van de wachttijd
protected void beginWaiting() {
// de zandloper wordt weergegeven
mainActivity.beginWaiting();
}
protected void cancelWaiting() {
// de zandloper wordt verwijderd
mainActivity.cancelWaiting();
}
1.16.2.6. De weergave
![]() |
1.16.2.6.1. De weergave [vue1.xml]
![]() |
In vergelijking met het vorige voorbeeld verandert de weergave [vue1.xml] als volgt:
![]() |
![]() |
- in [1] moet de gebruiker de URL van de webservice en de wachttijd [2] opgeven vóór elke aanroep van de webservice;
- in [3] worden de antwoorden geteld;
- in [4] kan de gebruiker zijn verzoek annuleren;
- in [5] wordt een wachtindicator weergegeven wanneer de getallen worden opgevraagd. Deze verdwijnt zodra alle getallen zijn ontvangen of de bewerking is geannuleerd;

- in [6] wordt de geldigheid van de invoer gecontroleerd;
De gebruiker wordt gevraagd het bestand [vue1.xml] uit de voorbeelden te laden. Hieronder volgen de ID's van de nieuwe componenten:

De knoppen [10-11] bevinden zich fysiek boven elkaar. Op een bepaald moment zal slechts één van de twee zichtbaar zijn.
1.16.2.6.2. Het fragment [Vue1Fragment]
![]() |
De structuur van het fragment [Vue1Fragment] is als volgt:
package exemples.android.fragments;
import android.app.AlertDialog;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.*;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.*;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.api.BackgroundExecutor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// de elementen van de visuele interface
@ViewById(R.id.editTextUrlServiceWeb)
EditText edtUrlServiceRest;
@ViewById(R.id.textViewErreurUrl)
TextView txtMsgErreurUrlServiceWeb;
@ViewById(R.id.editTextDelay)
EditText edtDelay;
@ViewById(R.id.textViewErreurDelay)
TextView textViewErreurDelay;
@ViewById(R.id.lst_reponses)
ListView listReponses;
@ViewById(R.id.txt_Reponses)
TextView infoReponses;
@ViewById(R.id.edt_nbaleas)
EditText edtNbAleas;
@ViewById(R.id.edt_a)
EditText edtA;
@ViewById(R.id.edt_b)
EditText edtB;
@ViewById(R.id.txt_errorNbAleas)
TextView txtErrorAleas;
@ViewById(R.id.txt_errorIntervalle)
TextView txtErrorIntervalle;
@ViewById(R.id.btn_Executer)
Button btnExecuter;
@ViewById(R.id.btn_Annuler)
Button btnAnnuler;
...
// lokale gegevens
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
@AfterViews
void afterViews() {
// geheugen
afterViewsDone=true;
// in eerste instantie geen foutmeldingen
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// knop [Annuler] verborgen
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
// lijst met antwoorden
reponses = new ArrayList<>();
}
...
- regels 24-49: de verwijzingen naar de componenten van de weergave [vue1.xml] (regel 20);
- regels 55-69: de methode [@AfterViews] die wordt uitgevoerd wanneer de verwijzingen uit de regels 24-49 zijn geïnitialiseerd;
- regel 58: niet vergeten – noodzakelijk voor de levenscyclus van het fragment;
- regels 60-63: de foutmeldingen worden verborgen;
- regels 65-66: de knop [Annuler] (regel 65) wordt verborgen en de knop [Exécuter] (regel 66) wordt weergegeven. Ter herinnering: ze bevinden zich fysiek boven elkaar;
- regel 68: het veld van regel 52 bevat de lijst met tekenreeksen die door de ListView van de antwoorden moeten worden weergegeven;
Direct na de methode [@AfterViews] wordt de volgende methode [updateFragment] uitgevoerd:
@Override
protected void updateFragment() {
// de adapter voor de lijst met antwoorden wordt aangemaakt
adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
listReponses.setAdapter(adapterReponses);
}
- regels 4-5: we maken de adapter ListView voor de antwoorden aan. Deze wordt opgeslagen in een instantievariabele, zodat hij beschikbaar is voor de andere methoden van de klasse;
Als er op de knop [Exécuter] wordt 'geklikt', wordt de volgende methode uitgevoerd:
// de invoergegevens
private int nbAleas;
private int a;
private int b;
private String urlServiceWebJson;
private int delay;
// lokale gegevens
private int nbInfos;
private List<String> reponses;
private ArrayAdapter<String> adapterReponses;
private boolean hasBeenCanceled;
@Click(R.id.btn_Executer)
protected void doExecuter() {
// eerdere antwoorden worden gewist
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// de antwoorteller wordt op 0 gezet
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// de geldigheid van de invoer wordt gecontroleerd
if (!isPageValid()) {
return;
}
// activiteit initialiseren
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// willekeurige getallen opvragen
for (int i = 0; i < nbAleas; i++) {
getAlea(a, b);
}
// de wachttijd begint
beginWaiting();
}
@Background(id = "alea")
void getAlea(int a, int b) {
// hier moet zo min mogelijk gebeuren
// in ieder geval geen weergave – deze moeten plaatsvinden in de UiThead
try {
// we geven het resultaat weer in het UiThread
showInfo(mainActivity.getAlea(a, b));
} catch (RuntimeException e) {
// de uitzondering wordt weergegeven in de UiThread
showAlert(e);
}
}
- regels 17-18: de vorige lijst met antwoorden van de server wordt gewist. Hiervoor wordt in regel 17 de gegevensbron [reponses], die gekoppeld is aan de adapter van de ListView, leeggemaakt;
- regel 19: een booleaanse waarde die we gebruiken om te bepalen of de gebruiker zijn verzoek al dan niet heeft geannuleerd;
- regels 21-22: er wordt een teller met waarde nul weergegeven voor het aantal antwoorden;
- regels 24-26: we halen de invoer uit de regels [2-6] op en controleren of deze geldig is. Als een van de gegevens ongeldig is, wordt de methode afgebroken (regel 25) en keert de gebruiker terug naar de visuele interface;
- regels 28-29: als alle ingevoerde gegevens geldig zijn, worden de URL van de webservice (regel 28) en de wachttijd voor elke aanroep van de service (regel 29) doorgegeven aan de activiteit. Deze informatie is nodig voor de laag [DAO] en we herinneren eraan dat het de activiteit is die hiermee communiceert;
- regels 31-33: de willekeurige getallen worden één voor één opgevraagd bij de methode [getAlea] in regel 39;
- regel 38: de methode [getAlea] is voorzien van de annotatie AA [@Background], waardoor deze in een andere thread (uitvoeringsstroom, proces) dan die waarin de visuele interface wordt uitgevoerd. Het is namelijk verplicht om elke internetaanroep uit te voeren in een andere thread dan die van de visuele interface. Zo kunnen er op een bepaald moment meerdere threads zijn:
- de thread die de visuele interface UI (User Interface) weergeeft en de bijbehorende gebeurtenissen afhandelt,
- de [nbAleas]-threads die elk een willekeurig getal opvragen bij de webservice. Deze threads worden asynchroon gestart: de UI-thread start een [getAlea]-thread (regel 32) die een willekeurig getal opvraagt bij de webservice en niet wacht tot deze is voltooid. De voltooiing wordt aan deze thread gemeld via een gebeurtenis. Zo worden de [nbAleas]-threads parallel gestart. Het is mogelijk om de applicatie zo te configureren dat er slechts één thread tegelijk wordt gestart. Er ontstaat dan een wachtrij van uit te voeren threads;
Regel 38: de parameter [id] geeft de gegenereerde thread een naam. Hier hebben de [nbAleas]-threads allemaal dezelfde naam: [alea]. Hierdoor kunnen we ze allemaal tegelijkertijd annuleren. Deze parameter is optioneel als het annuleren van de thread niet wordt beheerd;
- regel 44: de methode [getAlea] van de activiteit wordt aangeroepen. Dit gebeurt dus in een andere thread dan die van UI. Deze laatste roept de webservice aan en wacht niet op het antwoord. Hij wordt later via een gebeurtenis gewaarschuwd dat het antwoord beschikbaar is. Op dat moment wordt in regel 44 de methode [showInfo] aangeroepen met het ontvangen antwoord als parameter;
- regels 45-47: de uitvoering van het webverzoek kan een uitzondering veroorzaken. Er wordt dan gevraagd om de foutmeldingen van de uitzondering weer te geven in een waarschuwingsbericht;
- regel 35: er wordt gewacht op de resultaten:
- er wordt een wachtindicator weergegeven;
- de knop [Annuler] vervangt de knop [Exécuter]. Omdat de gestarte threads asynchroon zijn, wacht de thread van UI niet op hen en wordt regel 35 uitgevoerd voordat ze zijn voltooid. Zodra de methode [beginWaiting] is voltooid, kan de UI weer reageren op verzoeken van de gebruiker, zoals het klikken op de knop [Annuler]. Als de gestarte threads synchroon waren geweest, zou regel 35 pas worden bereikt nadat alle threads waren voltooid. Het annuleren ervan zou dan geen zin meer hebben;
De methode [showInfo] is als volgt:
@UiThread
protected void showInfo(int alea) {
if (!hasBeenCanceled) {
// nog wat extra informatie
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// zijn we klaar?
if (nbInfos == nbAleas) {
// we beëindigen het wachten
cancelWaiting();
}
// de informatie wordt toegevoegd aan de lijst met antwoorden
reponses.add(0, String.valueOf(alea));
// we geven de antwoorden weer
adapterReponses.notifyDataSetChanged();
}
}
- de methode [showInfo] wordt aangeroepen binnen de thread [getAlea], geannoteerd door [@Background]. Deze methode werkt de visuele interface UI bij. Dit kan alleen gebeuren als de methode wordt uitgevoerd binnen de thread van UI. Dit is de betekenis van de annotatie [@UiThread] in regel 1;
- regel 2: de methode ontvangt een willekeurig getal;
- regel 3: de hoofdcode van de methode wordt alleen uitgevoerd als de gebruiker zijn verzoek niet heeft geannuleerd;
- regels 5-6: de antwoords teller wordt verhoogd en weergegeven;
- regels 8-11: als alle verwachte antwoorden zijn ontvangen, wordt het wachten beëindigd (einde van het wachtsignaal, de knop [Exécuter] vervangt de knop [Annuler]);
- regels 12-15: het ontvangen willekeurige getal wordt toegevoegd aan de lijst met antwoorden die wordt weergegeven door de component [ListView listReponses] en deze wordt vernieuwd;
De methode [showAlert] is als volgt:
@UiThread
protected void showAlert(Throwable th) {
if (!hasBeenCanceled) {
// we annuleren alles
doAnnuler();
// we geven het weer
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
De logica is vergelijkbaar met die van de methode [showInfo]:
- regel 1: de annotatie [@UiThread] is verplicht;
- regel 2: de methode ontvangt de opgetreden uitzondering;
- regel 3: de methode wordt alleen uitgevoerd als de gebruiker zijn verzoek niet heeft geannuleerd;
- regel 5: het verzoek van de gebruiker wordt geannuleerd alsof hij zelf op de knop [Annuler] had geklikt;
- regel 7: de waarschuwing wordt weergegeven met behulp van de Android-klasse [AlertDialog]:
- [activity]: is de activiteit van het type [Activity] die is opgeslagen in de bovenliggende klasse [AbstractFragment];
- [setTitle]: stelt de titel van het waarschuwingsvenster [1] in;
- [setMessage]: stelt het bericht in dat wordt weergegeven in het waarschuwingsvenster [2];
- [setNeutral]: stelt de knop in waarmee het waarschuwingsvenster wordt gesloten [3];
- [show]: vraagt om het waarschuwingsvenster weer te geven;
![]() |
Het 'klikken' op de knop [Annuler] wordt afgehandeld met de volgende methode:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// geheugen
hasBeenCanceled=true;
// de asynchrone taak wordt geannuleerd
BackgroundExecutor.cancelAll("alea", true);
// einde van het wachten
cancelWaiting();
}
- regel 4: er wordt genoteerd dat de gebruiker zijn verzoek heeft geannuleerd;
- regel 6: annuleert alle taken die worden aangeduid met de tekenreeks [alea]. De tweede parameter [true] betekent dat ze moeten worden geannuleerd, zelfs als ze al zijn gestart. De identificatiecode [alea] wordt gebruikt om de methode [getAlea] van het fragment (regel 1 hieronder) te specificeren:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
Opmerking: het bleek dat regel 6 van de coderegel van de methode [doAnnuler] niet correct werkte. Daarom is de booleaanse waarde [hasBeenCanceled] toegevoegd. Bij een uitzondering (server niet beschikbaar) verscheen het waarschuwingsvenster namelijk n keer als er n willekeurige getallen waren aangevraagd.
1.16.2.7. De activiteit [MainActivity]
![]() |
1.16.2.7.1. De weergave [activity-main.xml]
![]() |
In vergelijking met het vorige voorbeeld hebben we een laadafbeelding toegevoegd aan de weergave die bij de activiteit [MainActivity] hoort:
...
<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">
<!-- wachtbeeld -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- plaatshouderafbeelding -->
</android.support.design.widget.AppBarLayout>
...
- regels 17-21: de laadafbeelding;
1.16.2.7.2. De activiteit [MainActivity]
De activiteit [MainActivity] verschilt nauwelijks van wat het was in [Exemple-14]. Allereerst wordt de laag [DAO] eraan toegevoegd:
// DAO-injectie
@Bean(Dao.class)
protected IDao dao;
...
@AfterInject
protected void afterInject() {
// logboek
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// de laag wordt geconfigureerd [DAO]
setTimeout(TIMEOUT);
}
- regels 2-3: invoeging van de laag [DAO] via een annotatie AA;
- regels 5-13: code die na deze injectie wordt uitgevoerd;
- regel 12: de timeout van de laag [DAO] wordt vastgelegd
Bovendien moet de activiteit [MainActivity] de interface [IMainActivity] implementeren, die op haar beurt de interface [IDao] uitbreidt:
// implementatie IMainActivity --------------------------------------------------------------------
@Override
public void navigateToView(int position) {
// de positieweergave wordt weergegeven
if (mViewPager.getCurrentItem() != position) {
// weergave van een fragment
mViewPager.setCurrentItem(position);
}
}
// beheer van de wachtafbeelding
public void cancelWaiting() {
loadingPanel.setVisibility(View.INVISIBLE);
}
public void beginWaiting() {
loadingPanel.setVisibility(View.VISIBLE);
}
// implementatie IDao --------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// uitvoering
return dao.getAlea(a, b);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
1.16.2.8. Het project uitvoeren
Start de webservice (paragraaf 1.16.1.7) en start vervolgens de Android-client:

Om te weten wat je in [1] moet invoeren, ga je als volgt te werk. Open een opdrachtvenster en typ de volgende opdracht:
C:\Program Files\Console2>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 3 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet VirtualBox Host-Only Network :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::e481:1583:cd2a:c47%27
Adresse IPv4. . . . . . . . . . . . . .: 192.168.82.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet VirtualBox Host-Only Network #2 :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::8191:14ad:407d:b840%54
Adresse IPv4. . . . . . . . . . . . . .: 192.168.64.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet Ethernet :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::d972:ad53:3b8a:263f%28
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . : uang ad.univ-angers.fr univ-angers.fr
Als u [GenyMotion] hebt geïnstalleerd, heeft de virtuele machine VirtualBox adressen IP aan uw computer toegevoegd (regels 10 en 18). Deze adressen zijn bijzonder handig omdat ze niet door de Windows-firewall worden geblokkeerd. Regel 30 geeft het adres IP van uw computer op een lokaal netwerk weer. Om dit adres te gebruiken, moet u doorgaans de Windows-firewall uitschakelen. Als u verbonden bent met een wifi-netwerk, gebruik dan het wifi-adres en schakel ook hier de firewall uit als u die heeft.
Test de applicatie in de volgende gevallen:
- 100 willekeurige getallen in het interval [1000, 2000] zonder wachttijd;
- 2000 willekeurige getallen in het bereik [10000, 20000] zonder wachttijd en annuleer de wachttijd voordat het genereren is voltooid;
- 5 willekeurige getallen in het interval [100, 200] met een wachttijd van 5000 ms en annuleer de wachttijd voordat het genereren is voltooid;
1.16.2.9. Afbrekingsbeheer
Om bij te houden wat er gebeurt wanneer de gebruiker om annulering vraagt of wanneer annulering wordt aangevraagd omdat er een uitzondering is opgetreden, voegen we de volgende methode toe aan de interface [IDao] (zie paragraaf 1.16.2.4.1):
package exemples.android.dao;
public interface IDao {
...
// debugmodus
void setDebugMode(boolean isDebugEnabled);
}
In de klasse [Dao] voegen we de volgende code toe:
// debugmodus
private boolean isDebugEnabled;
// klassenaam
private String className;
..
// constructor
public Dao() {
// naam van de klasse
className = getClass().getSimpleName();
}
...
// interface IDao -------------------------------------------------------------------
@Override
public int getAlea(int a, int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// dienstuitvoering
Response<Integer> info;
...
@Override
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
- regel 9: we noteren de naam van de klasse;
- regels 16-18: we schrijven een logboekbericht telkens wanneer de methode [getAlea] wordt aangeroepen;
Verder voegen we in het fragment [Vue1Fragment] de volgende logberichten toe:
@UiThread
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
....
}
@UiThread
protected void showAlert(Throwable th) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
...
}
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
...
}
Telkens wanneer het fragment [Vue1Fragment] informatie ontvangt van de laag [DAO], wordt er een logbericht gegenereerd. Bovendien wordt de gebeurtenis gelogd wanneer de methode [doAnnuler] wordt aangeroepen.
Test 1
Er worden 5 getallen opgevraagd terwijl de server niet is gestart. We krijgen de volgende logboekvermeldingen:
- regels 1-5: de methode [getAlea] van de klasse [Dao] wordt vijf keer aangeroepen. Ter herinnering: dit zijn asynchrone aanroepen die worden gedaan door het fragment [VueFragment] en dit fragment wacht niet op het resultaat van zijn aanroep;
- regel 7: het eerste verzoek HTTP heeft plaatsgevonden en het fragment [VueFragment] heeft zijn eerste uitzondering ontvangen;
- regel 8: het vraagt vervolgens om alle verzoeken te annuleren;
- regels 9-12: we zien echter dat hij de volgende vier uitzonderingen ontvangt. De asynchrone verzoeken die in de wachtrij stonden, zijn dus allemaal uitgevoerd;
Test 2
Laten we nu de server starten en 5 getallen opvragen met een wachttijd van 5 seconden, en klikken we op [Annuler] voordat deze wachttijd is verstreken. De logbestanden zien er als volgt uit:
- regels 1-5: de methode [getAlea] van de klasse [Dao] wordt vijf keer aangeroepen;
- regel 7: de gebruiker heeft gevraagd om de verzoeken te annuleren;
- regel 8: we zien dat [Vue1_Fragment] vijf waarden ontvangt. Ook hier zijn alle asynchrone verzoeken die in de wachtrij stonden, uitgevoerd;
Dit is de reden waarom we een booleaanse variabele [hasBeenCanceled] moesten beheren om te voorkomen dat er iets werd weergegeven terwijl er een annulering was aangevraagd. In de code voor de annulering:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// geheugen
hasBeenCanceled = true;
// de asynchrone taak wordt geannuleerd
BackgroundExecutor.cancelAll("alea",true);
// einde van het wachten
cancelWaiting();
}
doet de code op regel 10 niet wat verwacht wordt. Mogelijk komt dit doordat de asynchrone taken dezelfde geannoteerde methode [@Background] delen:
@Background(id = "alea")
void getAlea(int a, int b) {
...
}
1.17. Voorbeeld-16: asynchrone verwerking beheren met RxAndroid
We gaan nu de asynchroniteit die nodig is voor Android-applicaties beheren met een bibliotheek genaamd RxJava [http://reactivex.io/] en de daarvan afgeleide versie voor de Android-omgeving [RxAndroid]. Hiervoor gebruiken we de cursus [Introduction à RxJava. Application aux environnements Swing et Android].
1.17.1. Het project aanmaken
We dupliceren het project [Exemple-1] naar [Exemple-16]:
![]() | ![]() |
1.17.2. Gradle-configuratie
![]() |
In [build.gradle] voegen we de afhankelijkheid van de bibliotheek [RxAndroid] toe:
dependencies {
...
compile 'io.reactivex:rxandroid:1.2.0'
}
1.17.3. De laag [DAO]
![]() |
1.17.4. De interface [IDao]
De interface [IDao] ziet er als volgt uit:
package exemples.android.dao;
import rx.Observable;
public interface IDao {
// willekeurig getal
Observable<Integer> getAlea(int a, int b);
// URL van de webservice
void setUrlServiceWebJson(String url);
// maximale wachttijd (ms) voor het antwoord van de server
void setTimeout(int timeout);
// wachttijd in milliseconden van de client vóór het verzoek
void setDelay(int delay);
// debugmodus
void setDebugMode(boolean isDebugEnabled);
}
- regel 8: de methode [getAlea] retourneert nu een type [Observable] uit de bibliotheek RxJava (regel 3). Het principe is als volgt:
Een stroom van elementen van het type Observable<T> wordt geobserveerd door een of meer abonnees (observeren, consumenten) van het type Subscriber<T>. De bibliotheek RxJava maakt het mogelijk dat de Observable<T>-stroom wordt uitgevoerd in een thread van het type T1 en de bijbehorende waarnemer van het type Subscriber<T> in een thread van het type T2, zonder dat de ontwikkelaarzich zorgen hoeft te maken over het beheer van de levenscyclus van deze threads en over van nature lastige problemen, zoals het delen van gegevens tussen threads en de synchronisatie daarvan om een globale taak uit te voeren. Het vergemakkelijkt dus asynchroon programmeren.
1.17.5. De klasse [AbstractDao]
We zullen de klasse [Dao] laten afleiden van de volgende klasse [AbstractDao]:
package exemples.android.dao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// beschermde methoden ----------------------------------------------------------
// generieke interface
protected interface IRequest<T> {
Response<T> getResponse();
}
// generieke aanvraag
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// service-uitvoering
return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// service-uitvoering
try {
// de synchrone aanvraag wordt uitgevoerd en het antwoord wordt doorgestuurd naar de abonnee
Response<T> response = request.getResponse();
// fout?
int status = response.getStatus();
if (status != 0) {
// de uitzondering wordt geregistreerd
ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
} else {
// het antwoord wordt verzonden
subscriber.onNext(response.getBody());
// het einde van de observable wordt gemeld
subscriber.onCompleted();
}
} catch (JsonProcessingException | RuntimeException e) {
// de uitzondering wordt geregistreerd
ex = new DaoException(e, 100);
}
// uitzondering?
if (ex != null) {
// de uitzondering wordt gegenereerd
subscriber.onError(ex);
}
}
});
}
}
- De klasse [AbstractDao] heeft als belangrijkste element een generieke methode [getResponse] die dient om van de server een type [Response<T>] op te halen, waarbij T het type is van het door de client gewenste resultaat HTTP (hier Integer);
- regel 20: de enige parameter van de generieke methode [getResponse] is een instantie van de generieke interface [IRequest<T>] uit de regels 15-17. Deze heeft slechts één methode, [getResponse], en het is deze methode die het gewenste antwoord [Response<T>] levert;
- dankzij de twee voorgaande elementen kan de klasse [AbstractDao] dienen als bovenliggende klasse voor elke [Dao]-klantlaag van een server die antwoorden van het type [Response<T>] verstuurt;
- regel 20: de generieke methode [getResponse] retourneert een type [Observable<T>] dat het daadwerkelijk door de client HTTP verwachte resultaat vertegenwoordigt (in dit geval een type Observable<Integer>);
- regels 22-51: de statische methode [rx.Observable.create] creëert een type [Observable];
- regel 22: de enige parameter van deze methode is een instantie van het type [rx.Observable.OnSubscribe<T>], een interface die de volgende methoden bevat:
- [onNext(T element)]: maakt het mogelijk om een element van het type T naar een waarnemer te verzenden;
- [onError(Throwable th)]: maakt het mogelijk om een uitzondering naar een waarnemer te verzenden;
- [onCompleted]: hiermee kan aan een waarnemer worden aangegeven dat de verzendingen zijn beëindigd;
Een type [Observable<T>] voldoet aan bepaalde voorwaarden:
- het verzendt zijn elementen met de methode [onNext(T element)];
- de methode [onCompleted] moet één keer worden aangeroepen zodra er geen elementen meer zijn om naar de waarnemer te verzenden;
- de methode [onCompleted] wordt niet aangeroepen als de methode [onError(Throwable th)] al is aangeroepen;
In ons voorbeeld:
- is de observer het fragment [Vue1Fragment]. Dit fragment verwerkt de door [Observable<T>] verzonden elementen (elementen of uitzonderingen);
- het aangemaakte type [Observable<T>] zal slechts één enkel element uitzenden (regel 37);
- regel 29: voert een synchrone HTTP-aanvraag uit naar de server en ontvangt het type [Response<T>]. Dit verzoek HTTP wordt afgehandeld door het type [IRequest] dat als parameter wordt doorgegeven aan de generieke methode [getResponse];
- regel 31: het status uit het antwoord wordt opgehaald;
- regels 32-34: als dit status een foutcode is, wordt er een uitzondering voorbereid;
- regels 36-39: als deze status niet bij een fout hoort, wordt het antwoord verzonden dat de klant daadwerkelijk verwacht (regel 37) en wordt aan de waarnemer aangegeven dat er geen verdere verzendingen meer zullen plaatsvinden (regel 39);
- regels 41-44: als het verzoek HTTP eindigt met een uitzondering, wordt deze geregistreerd;
- regels 46-49: als de uitzondering [ex] verschilt van null, dan wordt deze naar de waarnemer verzonden. Het is hier niet nodig om de methode [onCompleted] aan te roepen om de observer te laten weten dat er geen elementen meer zullen worden verzonden. Dit is impliciet;
Uit deze uitleg kunnen we het volgende onthouden:
- de generieke methode [<T> Observable<T> getResponse(final IRequest<T> request)] een type [Observable<T>] retourneert dat ofwel één element van het type T, ofwel een uitzondering retourneert;
- dat deze methode als enige parameter een type [IRequest<T>] accepteert, waarvan de enige methode [getResponse()] de toegang HTTP uitvoert, die het type [Response<T>] retourneert;
1.17.6. De klasse [Dao]
De klasse [Dao] ontwikkelt zich als volgt:
@EBean
public class Dao extends AbstractDao implements IDao {
// client van de service REST
@RestService
protected WebClient webClient;
// wachttijd voor het uitvoeren van het verzoek
private int delay;
// debugmodus
private boolean isDebugEnabled;
// naam van de klasse
private String className;
// constructor
public Dao() {
// naam van de klasse
className = getClass().getSimpleName();
}
// interface IDao -------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// uitvoering webclient
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// wachten
waitSomeTime(delay);
// synchrone aanroep van HTTP
return webClient.getAlea(a, b);
}
});
}
...
- regel 2: de klasse [Dao] is een uitbreiding van de klasse [AbstractDao];
- regel 24: de methode [getAlea] retourneert nu een type [Observable<Integer>];
- regel 30: aanroep van de generieke methode [getResponse] van de bovenliggende klasse. Hieraan wordt een parameter van het type [IRequest<Integer>] doorgegeven;
- regels 32-37: implementatie van de interface [IRequest<Integer>];
- regel 36: de aanvraag HTTP wordt uitgevoerd via de interface AA [webClient], net zoals eerder is gedaan. We weten dat we een type [Response<Integer>] zullen ontvangen, wat inderdaad het type is dat de methode [IRequest<Integer>.getReponse()] moet retourneren;
- regel 36: hier wordt een eigenschap gebruikt met de naam closure: de mogelijkheid om bij het aanmaken van een instantie waarden van buitenaf in die instantie in te kapselen, in dit geval de waarden van [a, b] uit regel 24. Hierdoor hoeft de methode [IRequest<Integer>.getReponse()] geen parameters te hebben. Deze zijn in de body van de methode vastgelegd. En waar men normaal gesproken de parameters van de methode zou wijzigen (a,b) -> (x,y), maakt men hier een nieuwe instantie van [IRequest<Integer>] aan waarin de waarden van x en y zijn ingekapseld;
1.17.7. De klasse [MainActivity]
De klasse [MainActivity], die de interface [IDao] implementeert, verandert als volgt:
// implementatie IDao --------------------------------------------------------------------
@Override
public Observable<Integer> getAlea(int a, int b) {
// uitvoering
return dao.getAlea(a, b);
}
1.17.8. De klasse [Vue1Fragment]
De klasse [Vue1Fragment] verandert als volgt:
@Click(R.id.btn_Executer)
protected void doExecuter() {
// eerdere antwoorden worden gewist
reponses.clear();
adapterReponses.notifyDataSetChanged();
hasBeenCanceled = false;
// de antwoords teller wordt op 0 gezet
nbInfos = 0;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// de geldigheid van de invoer wordt gecontroleerd
if (!isPageValid()) {
return;
}
// activiteit initialiseren
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// willekeurige getallen opvragen
getAleasInBackground(a, b);
// de wachttijd begint
beginWaiting();
}
- regel 18: de willekeurige getallen worden opgevraagd bij de methode [getAleasInBackground], die zo heet omdat de getallen worden opgevraagd in een andere thread dan die van de UI;
private int nbReponses = 0;
// abonnementen op observables
private List<Subscription> abonnements;
// annotatie [Background] is overbodig
void getAleasInBackground(int a, int b) {
// in eerste instantie geen reacties en geen abonnementen
nbReponses = 0;
abonnements.clear();
// de observable wordt voorbereid
Observable<Integer> response = Observable.empty();
// de resultaten van de verschillende aanroepen worden samengevoegd HTTP
// ze worden uitgevoerd op een I/O-thread
for (int i = 0; i < nbAleas; i++) {
response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
}
// de gecumuleerde observable wordt geobserveerd op de thread van de UI
response = response.observeOn(AndroidSchedulers.mainThread());
try {
// de observable wordt uitgevoerd
abonnements.add(response.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
// de informatie wordt toegevoegd aan de lijst met antwoorden
showInfo(alea);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// foutmelding
showAlert(th);
// einde wachttijd
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// einde wachten
cancelWaiting();
}
}));
} catch (RuntimeException e) {
// de uitzondering wordt weergegeven in de UiThread
showAlert(e);
}
}
- regel 3: een observable heeft abonnees. De koppeling tussen een abonnee en het proces dat hij observeert, wordt een abonnement (Subscription) genoemd. Hier hebben we slechts één geobserveerd proces en één abonnee. We hebben dus slechts één abonnement. In principe doen we alsof we meerdere geobserveerde processen zouden kunnen hebben die door verschillende waarnemers worden geobserveerd, wat zou leiden tot meerdere abonnementen;
- regels 11-18: hier wordt het geobserveerde proces (observable) geconfigureerd. Het is belangrijk te beseffen dat dit slechts een configuratie is: het proces wordt niet uitgevoerd;
- regel 11: we beginnen met een lege observable, een observable die niets uitzendt;
- regels 14-16: aan deze lege observable voegen we de [nbAleas] observables toe, die de [nbAleas] verzoeken zullen zijn die HTTP willekeurige getallen zullen opleveren;
- regel 15: net als eerder wordt het willekeurige getal nr. i opgevraagd bij de klasse [MainActivity]. Het is belangrijk om te begrijpen dat hier nog geen verzoek HTTP wordt uitgevoerd. De methode [mainActivity.getAlea(a, b)] wordt uitgevoerd en retourneert een type [Observable<Integer>]. Dit is een proces dat zal worden waargenomen zodra het wordt gestart;
- regel 15: de methode [subscribeOn(Schedulers.io())] vraagt dat het proces (wanneer het wordt gestart) op een I/O-thread wordt uitgevoerd. De bibliotheek RxJava biedt verschillende soorten threads. De I/O-thread is geschikt voor HTTP-aanroepen;
- regel 15: observable nr. i wordt samengevoegd met de oorspronkelijke observable uit regel 11: uit [nbAleas]-observables die elk één element uitzenden, wordt een observable gecreëerd die [nbAleas] elementen zal uitzenden. Deze zal worden geobserveerd. Deze observable verzendt de melding [onCompleted] wanneer alle observables waaruit deze is samengesteld hun eigen melding [onCompleted] hebben verzonden. Zo hoeven we de antwoorden niet te tellen, zoals we in de vorige versie deden, om te weten of we alle verwachte getallen hebben ontvangen;
- regel 18: wanneer we hier aankomen, hebben we een observable geconfigureerd die bestaat uit [nbAleas] observables die elk op een I/O-thread worden uitgevoerd;
- regel 18: de methode [observeOn(AndroidSchedulers.mainThread())] wordt gebruikt om aan te geven op welke thread de waarneming van de door de observable uitgezonden waarden moet plaatsvinden. Hier behoort de thread [AndroidSchedulers.mainThread())] tot de bibliotheek RxAndroid en niet tot RxJava. Deze verwijst naar de UI-thread, ook wel event loop genoemd. Dit is belangrijk: in een Android-app kan een UI-component alleen in de UI-thread worden gewijzigd, anders treedt er een uitzondering op;
- regels 19-45: nu het te observeren proces is geconfigureerd, wordt het uitgevoerd;
- regel 21: het is de bewerking [Observable.subscribe] die de uitvoering van het geobserveerde proces start. Deze bewerking start de eerder geconfigureerde asynchrone processen [nbAleas]. De resultaten hiervan worden automatisch ter beschikking gesteld aan de observer op de UI-thread;
- we herinneren ons dat de observable drie soorten gebeurtenissen uitzendt:
- [onNext]: wanneer het een element verzendt;
- [onError]: wanneer er een uitzondering is opgetreden;
- [onCompleted]: wanneer het aangeeft dat het geen elementen meer zal verzenden;
De methode [Observable.subscribe] heeft als parameters drie objecten van het type [Action1<Integer>, Action1<Throwable>, Action0], waarvan de methoden [call] dienen om elk van deze drie gebeurtenissen te verwerken;
- regels 21-27: de eerste parameter van het type [Action1<Integer>] dient om de gebeurtenis [onNext] te verwerken. De bijbehorende methode [call] ontvangt het element dat door de observable is verzonden (regel 23);
- regel 25: de methode [showInfo] uit het vorige voorbeeld wordt opnieuw gebruikt;
- regels 27-35: de tweede parameter van het type [Action1<Throwable>] dient voor de verwerking van de gebeurtenis [onError]. De methode [call] ontvangt de uitzondering die door de observable is gegenereerd (regel 29);
- regel 31: we gebruiken opnieuw de methode [showAlert] uit het vorige voorbeeld;
- regel 33: de procedure voor het annuleren van het verzoek van de gebruiker wordt gestart. Dit houdt in dat alle observables die momenteel worden uitgevoerd, worden geannuleerd;
- regels 35-41: de derde parameter van het type [Action0] dient voor de verwerking van de gebeurtenis [onCompleted]. De bijbehorende methode [call] ontvangt geen parameters;
- regel 39: het wachten wordt geannuleerd;
De methode [showInfo] verloopt als volgt:
// aantekening [UiThread] overbodig
protected void showInfo(int alea) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
}
if (!hasBeenCanceled) {
// nog een stukje informatie
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// de informatie wordt toegevoegd aan de lijst met antwoorden
reponses.add(0, String.valueOf(alea));
// de antwoorden worden weergegeven
adapterReponses.notifyDataSetChanged();
}
}
De methode vertoont twee wijzigingen:
- regel 1: de annotatie AA [@UiThread] is verwijderd;
- er worden geen antwoorden meer geteld om te bepalen of het wachten al dan niet moet worden beëindigd. Voortaan is het de gebeurtenis [onCompleted] van de observable die ons deze informatie geeft;
De methode [showAlert] verandert als volgt:
// opmerking [UiThread] overbodig
protected void showAlert(Throwable th) {
// logboek
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Exception reçue");
}
if (!hasBeenCanceled) {
// alles wordt geannuleerd
doAnnuler();
// we plaatsen het
new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
}
}
- de enige wijziging is in regel 1: de annotatie AA [@UiThread] is verwijderd;
Ten slotte verandert de methode [doAnnuler] als volgt:
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), "Annulation demandée");
}
// geheugen
hasBeenCanceled = true;
// asynchrone taken annuleren
if (abonnements != null) {
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
}
// einde van het wachten
cancelWaiting();
}
- regel 12: een abonnement opzeggen en daarmee de observatie van het bijbehorende proces;
1.17.9. Uitvoering
Start de webservice (paragraaf 1.16.1.7), start de Android-client en herhaal de tests die u bij het vorige voorbeeld hebt uitgevoerd (paragraaf 1.16.2.8).
1.17.10. Beheer van de annulering
We voeren dezelfde tests uit als bij het vorige voorbeeld (paragraaf 1.16.2.9).
Test 1
Er worden 5 getallen opgevraagd terwijl de server niet is gestart. We krijgen de volgende logberichten:
Na regel 7 zijn er geen logboekvermeldingen meer, wat aantoont dat de waarnemer (Vue1Fragment) geen meldingen meer ontvangt van het waargenomen proces.
Test 2
Laten we nu de server starten en 5 getallen opvragen met een vertraging van 5 seconden, en klikken we op [Annuler] voordat deze vertraging is verstreken. De logbestanden zijn als volgt:
Na regel 6 zijn er geen logboekvermeldingen meer, wat aantoont dat de waarnemer (Vue1Fragment) geen meldingen meer ontvangt van het waargenomen proces.
Dit is het verwachte gedrag bij een annulering. We kunnen dus in de code van [Vue1Fragment] de booleaanse variabele [hasBeenCanceled] verwijderen die we in het vorige voorbeeld hadden toegevoegd, omdat de annulering niet deed wat we ervan verwachtten.
Het feit dat de waarnemer na het annuleren van de waargenomene geen meldingen meer ontvangt, betekent niet dat de verzoeken HTTP zelf worden geannuleerd. Om dit te zien, passen we de klasse [Dao] als volgt aan:
@Override
public Observable<Integer> getAlea(final int a, final int b) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
}
// uitvoering webclient
return getResponse(new IRequest<Integer>() {
@Override
public Response<Integer> getResponse() {
// wachten
waitSomeTime(delay);
// synchrone aanroep van HTTP
Response<Integer> response= webClient.getAlea(a, b);
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
} catch (JsonProcessingException e) {
Log.d(String.format("%s", className),"erreur désérialisation jSON");
}
}
return response;
}
});
}
- regels 15-21: we loggen het resultaat van de aanvraag HTTP uit regel 14;
De logberichten voor test nr. 2 zijn dan als volgt:
- regels 1-5: de 5 verzoeken zijn uitgevoerd;
- regel 6: de gebruiker heeft de aanvraag geannuleerd;
- regels 7-11: we ontvangen inderdaad de antwoorden op de vijf verzoeken HTTP. Alleen worden deze elementen, vanwege de annulering van de observabel, niet doorgegeven aan de waarnemer;
1.17.11. Conclusie
In het vervolg van dit document zullen client/server-toepassingen worden gerealiseerd met de bibliotheek RxAndroid in plaats van met de bibliotheek AA, en wel om de volgende redenen:
- RxAndroid kan worden gebruikt in een Android-toepassing die geen gebruik maakt van AA;
- RxAndroid doet meer dan alleen asynchrone bewerkingen vergemakkelijken. Het biedt talrijke methoden om een nieuwe observable te maken op basis van een andere. Deze methoden hebben geen equivalent in AA;
- zodra men een klasse wil afleiden die is geannoteerd met AA, zoals een fragment, stuit men op ernstige problemen. Men ziet zich dan genoodzaakt AA op te geven en oplossing 1 te gebruiken voor asynchrone programmering;
De lezer die zich wil verdiepen in de mogelijkheden van de bibliotheek RxAndroid kan het document [Introduction à RxJava. Application aux environnements Swing et Android] raadplegen. Daarin wordt RxAndroid gebruikt zonder de bibliotheek AA.
1.18. Voorbeeld 17: componenten voor gegevensinvoer
We gaan een nieuw project schrijven om enkele veelgebruikte componenten in formulieren voor gegevensinvoer te presenteren.
1.18.1. Het project aanmaken
We dupliceren het project [Exemple-13] naar [Exemple-17]:
![]() | ![]() |
Het nieuwe project zal slechts één weergave bevatten: [vue1.xml]. Daarom verwijderen we de weergave [vue2.xml] en de bijbehorende fragmenten [Vue2Fragment] en [2]. We verwerken deze wijziging in de fragmentbeheerder van [Mainactivity]:
// onze fragmentmanager moet voor elke toepassing opnieuw worden gedefinieerd
// moet de volgende methoden definiëren: getItem, getCount, getPageTitle
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// de fragmenten
private final Fragment[] fragments = {new Vue1Fragment_()};
....
}
Voer het project opnieuw uit. Weergave nr. 1 moet nu net als eerder verschijnen. We gaan verder werken vanuit dit project.
1.18.2. De weergave XML van het formulier
![]() |
De weergave die door het bestand [vue1.xml] wordt gegenereerd, is als volgt:

De tekst XML van de weergave is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="30dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textViewFormulaireTitre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
<Button
android:id="@+id/formulaireButtonValider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/TextViewFormulaireCombo"
android:layout_below="@+id/TextViewFormulaireCombo"
android:layout_marginTop="30dp"
android:text="@string/formulaire_valider"/>
<TextView
android:id="@+id/textViewFormulaireCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTitre"
android:layout_below="@+id/textViewFormulaireTitre"
android:layout_marginTop="30dp"
android:text="@string/formulaire_checkbox"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireCheckBox"
android:layout_below="@+id/textViewFormulaireCheckBox"
android:layout_marginTop="30dp"
android:text="@string/formulaire_radioButton"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireRadioButton"
android:layout_below="@+id/textViewFormulaireRadioButton"
android:layout_marginTop="30dp"
android:text="@string/formulaire_seekBar"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireEdtText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireSeekBar"
android:layout_below="@+id/textViewFormulaireSeekBar"
android:layout_marginTop="30dp"
android:text="@string/formulaire_saisie"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireBool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireEdtText"
android:layout_below="@+id/textViewFormulaireEdtText"
android:layout_marginTop="30dp"
android:text="@string/formulaire_bool"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireDate"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_alignLeft="@+id/textViewFormulaireBool"
android:layout_below="@+id/textViewFormulaireBool"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@string/formulaire_date"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireMultilignes"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center"
android:layout_alignBaseline="@+id/textViewFormulaireTitre"
android:layout_alignParentTop="true"
android:layout_marginLeft="400dp"
android:layout_toRightOf="@+id/textViewFormulaireTitre"
android:text="@string/formulaire_multilignes"
android:textSize="20sp"/>
<TextView
android:id="@+id/textViewFormulaireTime"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:gravity="center"
android:layout_alignLeft="@+id/textViewFormulaireMultilignes"
android:layout_below="@+id/textViewFormulaireMultilignes"
android:layout_marginTop="30dp"
android:text="@string/formulaire_time"
android:textSize="20sp"/>
<TextView
android:id="@+id/TextViewFormulaireCombo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textViewFormulaireTime"
android:layout_below="@+id/textViewFormulaireTime"
android:layout_marginTop="30dp"
android:text="@string/formulaire_combo"
android:textSize="20sp"/>
<CheckBox
android:id="@+id/formulaireCheckBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"
android:layout_marginLeft="100dp"
android:layout_toRightOf="@+id/textViewFormulaireCheckBox"
android:text="@string/formulaire_checkbox1"/>
<RadioGroup
android:id="@+id/formulaireRadioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:orientation="horizontal">
<RadioButton
android:id="@+id/formulaireRadioButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton1"/>
<RadioButton
android:id="@+id/formulaireRadioButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radionbutton2"/>
<RadioButton
android:id="@+id/formulaireRadionButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/formulaire_radiobutton3"/>
</RadioGroup>
<SeekBar
android:id="@+id/formulaireSeekBar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_alignLeft="@+id/formulaireCheckBox1"/>
<EditText
android:id="@+id/formulaireEditText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:ems="10"
android:inputType="text">
</EditText>
<Switch
android:id="@+id/formulaireSwitch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireBool"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:text="@string/formulaire_switch"
android:textOff="Non"
android:textOn="Oui"/>
<TimePicker
android:id="@+id/formulaireTimePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireTime"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes"
android:timePickerMode="spinner"
/>
<EditText
android:id="@+id/formulaireEditTextMultiLignes"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"
android:layout_alignBottom="@+id/textViewFormulaireMultilignes"
android:layout_marginLeft="50dp"
android:layout_toRightOf="@+id/textViewFormulaireMultilignes"
android:ems="10"
android:inputType="textMultiLine">
</EditText>
<Spinner
android:id="@+id/formulaireDropDownList"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_alignBottom="@+id/TextViewFormulaireCombo"
android:layout_alignLeft="@+id/formulaireEditTextMultiLignes">
</Spinner>
<DatePicker
android:id="@+id/formulaireDatePicker1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textViewFormulaireDate"
android:layout_alignLeft="@+id/formulaireCheckBox1"
android:datePickerMode="spinner"
android:calendarViewShown="false">
</DatePicker>
<TextView
android:id="@+id/textViewSeekBarValue"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@+id/formulaireSeekBar"
android:text=""/>
</RelativeLayout>
De belangrijkste onderdelen van het formulier zijn:
| |
| |
| |
| |
| |
| |
| ![]() |
| ![]() |
| ![]() |
| ![]() |
|
1.18.3. De tekenreeksen van het formulier
De tekenreeksen van het formulier zijn gedefinieerd in het volgende bestand [res / values / strings.xml]:
![]() |
<resources>
<string name="app_name">Exemple-17</string>
<string name="action_settings">Settings</string>
<string name="section_format">Hello World from section: %1$d</string>
<!-- weergave 1 -->
<string name="titre_vue1">Vue n° 1</string>
<string name="formulaire_checkbox">Cases à cocher</string>
<string name="formulaire_radioButton">Boutons Radio</string>
<string name="formulaire_seekBar">Seek Bar</string>
<string name="formulaire_saisie">Champ de saisie</string>
<string name="formulaire_bool">Booléen</string>
<string name="formulaire_date">Date</string>
<string name="formulaire_time">Heure</string>
<string name="formulaire_multilignes">Champ de saisie multilignes</string>
<string name="formulaire_listview">Liste</string>
<string name="formulaire_combo">Liste déroulante</string>
<string name="formulaire_checkbox1">1</string>
<string name="formulaire_checkbox2">2</string>
<string name="formulaire_radiobutton1">1</string>
<string name="formulaire_radionbutton2">2</string>
<string name="formulaire_radiobutton3">3</string>
<string name="formulaire_switch"></string>
<string name="formulaire_valider">Valider</string>
</resources>
1.18.4. Het fragment van het formulier
![]() |
De klasse [Vue1Fragment] is als volgt:
package exemples.android.fragments;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.ArrayList;
import java.util.List;
// een fragment is een weergave die door een fragmentcontainer wordt weergegeven
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// de velden van de weergave die door het fragment wordt weergegeven
@ViewById(R.id.formulaireDropDownList)
Spinner dropDownList;
@ViewById(R.id.formulaireButtonValider)
Button buttonValider;
@ViewById(R.id.formulaireCheckBox1)
CheckBox checkBox1;
@ViewById(R.id.formulaireRadioGroup)
RadioGroup radioGroup;
@ViewById(R.id.formulaireSeekBar)
SeekBar seekBar;
@ViewById(R.id.formulaireEditText1)
EditText saisie;
@ViewById(R.id.formulaireSwitch1)
Switch switch1;
@ViewById(R.id.formulaireDatePicker1)
DatePicker datePicker1;
@ViewById(R.id.formulaireTimePicker1)
TimePicker timePicker1;
@ViewById(R.id.formulaireEditTextMultiLignes)
EditText multiLignes;
@ViewById(R.id.formulaireRadioButton1)
RadioButton radioButton1;
@ViewById(R.id.formulaireRadioButton2)
RadioButton radioButton2;
@ViewById(R.id.formulaireRadionButton3)
RadioButton radioButton3;
@ViewById(R.id.textViewSeekBarValue)
TextView seekBarValue;
// keuzelijst
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// de eerste knop wordt aangevinkt
radioButton1.setChecked(true);
// de kalender
datePicker1.setCalendarViewShown(false);
// de 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));
}
});
// de vervolgkeuzelijst
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
@SuppressLint("DefaultLocale")
@Click(R.id.formulaireButtonValider)
protected void doValider() {
...
}
@Override
protected void updateFragment() {
// initialisatie van de adapter van de vervolgkeuzelijst
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
}
- regels 22-49: we halen de referenties op van alle componenten van het formulier XML [vue1] (regel 18);
- regel 58: met de methode [setChecked] kan een keuzerondje of een selectievakje worden aangevinkt;
- regel 60: standaard geeft de component [DatePicker] zowel een datumveld als een kalender weer. Regel 60 verwijdert de kalender;
- regel 62: met [SeekBar].setMax() kan de maximale waarde van de schuifbalk worden ingesteld. De minimale waarde is 0;
- regels 63-74: hier worden de gebeurtenissen van de schuifbalk afgehandeld. Bij elke wijziging die de gebruiker aanbrengt, moet de waarde van de schuifbalk worden weergegeven in [TextView] van regel 49;
- regel 71: de parameter [progress] vertegenwoordigt de waarde van de schuifbalk;
- regels 76-79: een lijst met [String] die we aan de vervolgkeuzelijst gaan koppelen;
- regel 90: de methode [updateFragment] van het fragment. Wanneer deze wordt uitgevoerd, is de variabele [activity] van de bovenliggende klasse geïnitialiseerd;
- regel 92: de gegevensbron [list] wordt gekoppeld aan de adapter van de vervolgkeuzelijst;
- regels 93-94: de adapter [dataAdapter] wordt gekoppeld aan de keuzelijst [dropDownList];
- regel 84: de methode [doValider] wordt gekoppeld aan het klikken op de knop [Valider];
De methode [doValider] is bedoeld om de door de gebruiker ingevoerde waarden weer te geven. De code ervan is als volgt:
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// lijst met weer te geven berichten
List<String> messages = new ArrayList<>();
// selectievakje
boolean isChecked = checkBox1.isChecked();
messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
// de keuzerondjes
int id = radioGroup.getCheckedRadioButtonId();
String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
// de SeekBar
int progress = seekBar.getProgress();
messages.add(String.format("SeekBar [value=%d]", progress));
// het invoerveld
String texte = String.valueOf(saisie.getText());
messages.add(String.format("Saisie simple [value=%s]", texte));
// de schakelaar
boolean état = switch1.isChecked();
messages.add(String.format("Switch [value=%s]", état));
// de datum
int an = datePicker1.getYear();
int mois = datePicker1.getMonth() + 1;
int jour = datePicker1.getDayOfMonth();
messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
// de tekst met meerdere regels
String lignes = String.valueOf(multiLignes.getText());
messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
// de tijd
int heure = timePicker1.getHour();
int minutes = timePicker1.getMinute();
messages.add(String.format("Heure [%d, %d]", heure, minutes));
// de vervolgkeuzelijst
int position = dropDownList.getSelectedItemPosition();
String selectedItem = String.valueOf(dropDownList.getSelectedItem());
messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
// weergave
doAfficher(messages);
}
- regel 4: de ingevoerde waarden worden verzameld in een lijst met berichten;
- regel 6: met de methode [CheckBox].isCkecked() kan worden vastgesteld of een vakje is aangevinkt of niet;
- regel 9: met de methode [RadioGroup].getCheckedButtonId() kan de id worden opgehaald van de selectieknop die is aangevinkt, of -1 als er geen is aangevinkt;
- regel 10: met de code [activity.findViewById(id)] kun je het aangevinkte keuzerondje vinden en zo de bijbehorende tekst ophalen;
- regel 13: met de methode [SeekBar].getProgress() kun je de waarde van een schuifbalk ophalen;
- regel 19: met de methode [Switch].isChecked() kun je nagaan of een schakelaar On (true) of Off (false) is;
- regel 22: met de methode [DatePicker].getYear() kan het geselecteerde jaar worden opgehaald met een [DatePicker]-object;
- regel 23: met de methode [DatePicker].getMonth() kan de gekozen maand worden verkregen met een object [DatePicker] binnen het interval [0,11];
- regel 24: met de methode [DatePicker].getDayOfMonh() kan de gekozen dag van de maand worden verkregen met een object [DatePicker] binnen het interval [1,31];
- regel 30: met de methode [TimePicker].getHour() kan het gekozen uur worden verkregen met een object [TimePicker];
- regel 31: met de methode [TimePicker].getMinute() kun je de gekozen minuten ophalen met een object [TimePicker];
- regel 34: met de methode [Spinner].getSelectedItemPosition() kun je de positie van het geselecteerde element in een vervolgkeuzelijst ophalen;
- regel 35: met de methode [Spinner].getSelectedItem() kan het geselecteerde object in een vervolgkeuzelijst worden opgehaald;
De methode [doAfficher], die de lijst met ingevoerde waarden weergeeft, is als volgt:
private void doAfficher(List<String> messages) {
// de weer te geven tekst wordt samengesteld
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// deze wordt weergegeven
new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
- regel 1: de methode ontvangt een lijst met weer te geven berichten;
- regels 3-6: op basis van deze berichten wordt een object van het type [StringBuilder] samengesteld. Voor het samenvoegen van tekenreeksen is het type [StringBuilder] efficiënter dan het type [String];
- regel 8: een dialoogvenster geeft de tekst van regel 3 weer:

1.18.5. Het project uitvoeren
Voer het project uit en test de verschillende invoercomponenten.
1.19. Voorbeeld 18: gebruik van een weergavesjabloon
1.19.1. Het project aanmaken
We maken een nieuw project [Exemple-18] aan door het project [Exemple-13] te kopiëren.
![]() | ![]() |
1.19.2. De sjabloon voor weergaven
We willen de twee weergaven uit het project overnemen en deze in een sjabloon opnemen:
![]() |

Beide weergaven krijgen dezelfde structuur:
- in [1], een koptekst;
- in [2], een linkerkolom die links kan bevatten;
- in [3], een voettekst;
- in [4], de inhoud.
Dit wordt bereikt door de basisweergave [activity_main.xml] van de activiteit aan te passen;
![]() | ![]() |
De code XML van de weergave [main] is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="75dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/lavenderblushh2">
<TextView
android:id="@+id/textViewHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:text="@string/txt_header"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="0.8"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/left"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@color/lightcyan2">
<TextView
android:id="@+id/txt_left"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_left"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
<exemples.android.architecture.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/floral_white"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</LinearLayout>
<LinearLayout
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="0.1"
android:background="@color/wheat1">
<TextView
android:id="@+id/textViewBottom"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:text="@string/txt_bottom"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/red"/>
</LinearLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
- de koptekst [1] wordt verkregen met de regels 38-54;
- de linkerband [2] wordt verkregen uit de regels 56-84;
- de voettekst [3] wordt verkregen uit de regels 86-101;
- de inhoud [4] wordt verkregen uit de regels 78-84;
De weergave XML [main] maakt gebruik van informatie uit de bestanden [res / values / colors.xml] en [res / values / strings.xml]:
![]() |
Het bestand [colors.xml] is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
<color name="floral_white">#FFFAF0</color>
<color name="lavenderblushh2">#EEE0E5</color>
<color name="lightcyan2">#D1EEEE</color>
<color name="wheat1">#FFE7BA</color>
</resources>
en het bestand [strings.xml] is het volgende:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">exemple-12</string>
<string name="action_settings">Settings</string>
<string name="titre_vue1">Vue n° 1</string>
<string name="textView_nom">Quel est votre nom :</string>
<string name="btn_Valider">Validez</string>
<string name="btn_vue2">Vue n° 2</string>
<string name="titre_vue2">Vue n° 2</string>
<string name="btn_vue1">Vue n° 1</string>
<string name="textView_bonjour">"Bonjour "</string>
<string name="txt_header">Header</string>
<string name="txt_left">Left</string>
<string name="txt_bottom">Bottom</string>
</resources>
Maak een uitvoeringscontext voor dit project aan en voer het uit.
1.20. Voorbeeld 19: de component [ListView]
Met de component [ListView] kunt u een bepaalde weergave herhalen voor elk element in een lijst. De herhaalde weergave kan willekeurig complex zijn, van een eenvoudige tekenreeks tot een weergave waarmee informatie voor elk element in de lijst kan worden ingevoerd. We gaan de volgende [ListView] aanmaken:

Elke weergave in de lijst bestaat uit drie onderdelen:
- een [TextView] met informatie;
- een [CheckBox];
- een aanklikbare [TextView];
1.20.1. Het project aanmaken
We maken een nieuw project [Exemple-19] aan door het project [Exemple-18] te kopiëren.
![]() | ![]() |
![]() |
We gaan het project verder ontwikkelen zoals aangegeven in [3].
1.20.2. De sessie
![]() |
De sessie slaat de gegevens op die door activiteiten en fragmenten worden gedeeld:
package exemples.android.architecture;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// een lijst met gegevens
private List<Data> liste=new ArrayList<>();
// getters en setters
...
}
- regel 11: de gegevenslijst die door beide weergaven wordt gebruikt;
De klasse [Data] is als volgt:
package exemples.android.architecture;
public class Data {
// gegevens
private String texte;
private boolean isChecked;
// constructor
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
// getters en setters
...
}
- regel 6: de tekst die het eerste [TextView] van elk element in de lijst zal vullen;
- regel 7: de booleaanse waarde die wordt gebruikt om het [checkBox] van elk element in de lijst al dan niet aan te vinken;
1.20.3. De activiteit [MainActivity]
De code van de methode [@AfterInject] wordt als volgt:
// sessie-injectie
@Bean(Session.class)
protected Session session;
...
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// we maken een lijst met gegevens aan
List<Data> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new Data("Texte n° " + i, false));
}
}
- regels 12-15: initialisatie van de lijst met gegevens die in de sessie aanwezig zijn;
1.20.4. De oorspronkelijke weergave [Vue1]
![]() | ![]() |
De weergave XML [vue1.xml] toont het bovenstaande veld [1]. De code ervan is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue1"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView_titre"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:text="@string/btn_vue2" />
<ListView
android:id="@+id/listView1"
android:layout_width="600dp"
android:layout_height="200dp"
android:layout_alignParentLeft="true"
android:layout_below="@+id/button_vue2"
android:layout_marginLeft="30dp"
android:layout_marginTop="50dp" >
</ListView>
</RelativeLayout>
- regels 7-16: de component [TextView] [2];
- regels 27-35: de component [ListView] [4];
- regels 18-25: de component [Button] [3];
1.20.5. De weergave herhaald door [ListView]
![]() |
De weergave die wordt herhaald door [ListView] is de volgende weergave [list_data]:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Libellé"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:text="@string/txt_dummy" />
<CheckBox
android:id="@+id/checkBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="37dp"
android:layout_toRightOf="@+id/txt_Libellé"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/textViewRetirer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Libellé"
android:layout_alignBottom="@+id/txt_Libellé"
android:layout_marginLeft="68dp"
android:layout_toRightOf="@+id/checkBox1"
android:text="@string/txt_retirer"
android:textColor="@color/blue"
android:textSize="20sp" />
</RelativeLayout>
- regels 8-14: de component [TextView] [1];
- regels 16-23: de component [CheckBox] [2];
- regels 25-35: de component [TextView] [3];
1.20.6. Het fragment [Vue1Fragment]
![]() |
Het fragment [Vue1Fragment] beheert de weergave XML [vue1]. De code ervan is als volgt:
package exemples.android.fragments;
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
import java.util.List;
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
// de velden van de weergave die door het fragment wordt getoond
@ViewById(R.id.listView1)
protected ListView listView;
// de lijstadapter
private ListAdapter adapter;
// initialisatie voltooid
private boolean initDone = false;
@AfterViews
void afterViews() {
// geheugen
afterViewsDone = true;
}
@Click(R.id.button_vue2)
void navigateToView2() {
// er wordt naar weergave 2 genavigeerd
mainActivity.navigateToView(1);
}
public void doRetirer(int position) {
...
}
@Override
protected void updateFragment() {
if (!initDone) {
// er worden gegevens gekoppeld aan [ListView]
adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
initDone = true;
}
// geval waarin het fragment is (her)gegenereerd – in dit geval moet ListView opnieuw aan zijn adapter worden gekoppeld
listView.setAdapter(adapter);
// in het geval dat andere fragmenten de gegevensbron hebben gewijzigd – in dit geval moet ListView worden vernieuwd
adapter.notifyDataSetChanged();
}
}
- regel 15: de weergave XML [vue1] is aan het fragment gekoppeld;
- regels 26-30: de methode [@AfterViews] doet niets. Deze is echter nodig om de variabele [afterViewsDone] in te stellen op true, omdat deze wordt gebruikt door de bovenliggende klasse [AbstractFragment];
- regels 42-53: de methode [updateFragment], die wordt aangeroepen telkens wanneer het fragment zichtbaar wordt. De methode is hier geschreven alsof het fragment uit de nabijheid van het weergegeven fragment zou kunnen verdwijnen en daarmee zijn levenscyclus zou kunnen resetten. Dat is hier niet het geval, maar het zou wel het geval zijn als de applicatie drie fragmenten zou hebben met een nabijheid van 1;
- regel 44: de adapter van [ListView] hoeft slechts één keer te worden geïnitialiseerd;
- regel 46: aan deze [ListView] wordt een adapter van het type [ListAdapter] gekoppeld. We gaan deze klasse bouwen. Deze is afgeleid van de klasse [ArrayAdapter], die we al eerder hebben gebruikt om gegevens aan een [ListView] te koppelen. We geven diverse gegevens door aan de constructor van [ListAdapter]:
- een verwijzing naar de huidige activiteit,
- de ID van de weergave die voor elk element van de lijst zal worden geïnstantieerd,
- een gegevensbron om de lijst te vullen,
- een verwijzing naar het fragment. Deze wordt gebruikt om de klik op een [Retirer]-link van de [ListView] te verwerken via de methode [doRetirer] op regel 38;
- regel 50: de adapter wordt gekoppeld aan [ListView]. Tegelijkertijd wordt de gegevensbron [listes] gekoppeld aan [ListView]. Deze bewerking wordt hier telkens uitgevoerd wanneer weergave nr. 1 wordt weergegeven. In werkelijkheid zou dit alleen hoeven te gebeuren wanneer de methode [@AfterViews] is uitgevoerd. Hier wordt de instructie te vaak uitgevoerd. Er is behoefte aan een booleaanse variabele die aangeeft dat de methode [@AfterViews] zojuist is uitgevoerd en dat [ListView] dus opnieuw aan zijn adapter moet worden gekoppeld;
- regel 52: we verversen de [ListView]. In dit voorbeeld heeft dit geen zin, omdat alleen weergave nr. 1 de gegevensbron van de [ListView] kan wijzigen. Laten we eens kijken naar een algemener geval waarin ook weergave nr. 2 de gegevensbron van de [ListView] zou kunnen wijzigen. Dergelijke voorbeelden komen verderop in dit document aan bod. In dit geval moet, wanneer men van weergave nr. 2 naar weergave nr. 1 overschakelt, de [ListView] van weergave nr. 1 worden vernieuwd;
1.20.7. De adapter [ListAdapter] van de [ListView]
![]() |
De klasse [ListAdapter]
- configureert de gegevensbron van de [ListView];
- beheert de weergave van de verschillende elementen van [ListView];
- beheert de gebeurtenissen van deze elementen;
De code ervan is als volgt:
package exemples.android.fragments;
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
// de uitvoeringscontext
private Context context;
// de id van de weergavelay-out van een rij in de lijst
private int layoutResourceId;
// de gegevens van de lijst
private List<Data> data;
// het fragment dat de [ListView] weergeeft
private Vue1Fragment fragment;
// de adapter
final ListAdapter adapter = this;
// fabrikant
public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
super(context, layoutResourceId, data);
// de gegevens worden opgeslagen
this.context = context;
this.layoutResourceId = layoutResourceId;
this.data = data;
this.fragment = fragment;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
}
- regel 5: de klasse [ListAdapter] is een uitbreiding van de klasse [ArrayAdapter];
- regel 19: de constructor;
- regel 20: vergeet niet de constructor van de bovenliggende klasse [ArrayAdapter] aan te roepen met de eerste drie parameters;
- regels 22-25: de gegevens van de constructor worden opgeslagen;
- regel 29: de methode [getView] wordt herhaaldelijk aangeroepen door [ListView] om de weergave van element nr. [position] te genereren. Het weergegeven resultaat [View] is een verwijzing naar de aangemaakte weergave.
De code van de methode [getView] is als volgt:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// de huidige regel van ListView wordt aangemaakt
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// de tekst
TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
textView.setText(data.get(position).getTexte());
// het selectievakje
CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
checkBox.setChecked(data.get(position).isChecked());
// de link [Retirer]
TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
txtRetirer.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
fragment.doRetirer(position);
}
});
// het klikken op het selectievakje wordt afgehandeld
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.get(position).setChecked(isChecked);
}
});
// de regel wordt weergegeven
return row;
}
- regel 2: de methode ontvangt drie parameters. We gebruiken alleen de eerste;
- regel 4: we maken de weergave van element nr. [position] aan. Dit is de weergave [list_data] waarvan de id als tweede parameter aan de constructor is doorgegeven. Vervolgens halen we de verwijzingen op van de componenten van de weergave die we zojuist hebben geïnstantieerd;
- regel 6: we halen de referentie op van [TextView] nr. 1;
- regel 7: er wordt een tekst aan toegewezen uit de gegevensbron die als derde parameter aan de constructor is doorgegeven;
- regel 9: de referentie van [CheckBox] nr. 2 wordt opgehaald;
- regel 10: deze wordt al dan niet aangevinkt met een waarde uit de gegevensbron van [ListView];
- regel 12: de referentie van [TextView] nr. 3 wordt opgehaald;
- regels 13-18: de klik op de link [Retirer] wordt verwerkt;
- regel 16: de methode [Vue1Fragment].doRetirer verwerkt deze klik. Het lijkt inderdaad logischer om deze gebeurtenis te laten verwerken door het fragment dat [ListView] weergeeft. Dit fragment heeft een totaaloverzicht dat de klasse [ListAdapter] niet heeft. De referentie van het fragment [Vue1Fragment] was als vierde parameter doorgegeven aan de constructor van de klasse;
- regels 20-25: hier wordt de klik op het selectievakje afgehandeld. De actie die hierop wordt uitgevoerd, wordt doorgegeven aan de gegevens die het weergeeft. Dit om de volgende reden. De [ListView] is een lijst die slechts een deel van deze elementen weergeeft. Daardoor is een element van de lijst soms verborgen, soms weergegeven. Wanneer element nr. i moet worden weergegeven, wordt de methode [getView] uit regel 2 hierboven aangeroepen voor positie nr. i. Regel 10 berekent de status van het selectievakje opnieuw op basis van de gegevens waaraan het is gekoppeld. Het is dus noodzakelijk dat deze de status van het selectievakje in de loop van de tijd onthoudt;
1.20.8. Een element uit de lijst verwijderen
Een klik op de link [Retirer] wordt in het fragment [Vue1Fragment] afgehandeld door de volgende methode [doRetirer]:
public void doRetirer(int position) {
// element nr. [position] uit de lijst verwijderen
List<Data> liste = mainActivity.getListe();
liste.remove(position);
// de scrollpositie wordt opgeslagen om ernaar terug te keren
// lezen
// [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// positie van het eerste element: volledig zichtbaar of niet
int firstPosition = listView.getFirstVisiblePosition();
// Y-offset van dit element ten opzichte van de bovenkant van het ListView
// meet de hoogte van het eventueel verborgen gedeelte
View v = listView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// het [ListView] wordt vernieuwd
adapter.notifyDataSetChanged();
// we positioneren ons op de juiste plek van de ListView
listView.setSelectionFromTop(firstPosition, top);
}
- regel 1: we ontvangen de positie in [ListView] van de link [Retirer] waarop is geklikt;
- regel 3: de gegevenslijst wordt opgehaald;
- regel 4: het element met nummer [position] wordt verwijderd;
- regel 15: we verversen de [ListView]. Zonder dit verandert er visueel niets.
- regels 5-13, 17: een vrij ingewikkelde procedure. Zonder deze procedure gebeurt het volgende:
- [ListView] geeft de regels 15-18 van de gegevenslijst weer,
- regel 16 wordt verwijderd,
- de regel 15 hierboven reset het geheel volledig en de [ListView] geeft vervolgens de regels 0-3 van de gegevenslijst weer;
Met de bovenstaande regels wordt de verwijdering uitgevoerd en blijft de [ListView] op de regel staan die volgt op de verwijderde regel.
1.20.9. De weergave XML [Vue2]
![]() | ![]() |
De code XML van de weergave is als volgt:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView_titre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:text="@string/titre_vue2"
android:textSize="50sp" />
<Button
android:id="@+id/button_vue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textViewResultats"
android:layout_marginTop="25dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="@string/btn_vue1" />
<TextView
android:id="@+id/textViewResultats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView_titre"
android:layout_marginTop="50dp"
android:layout_alignLeft="@+id/textView_titre"
android:text="" />
</RelativeLayout>
- regels 6-15: component [TextView] nr. 1;
- regels 26-33: component [TextView] nr. 2;
- regels 17-24: component [Button] nr. 3;
1.20.10. Het fragment [Vue2Fragment]
![]() | 123 ![]() |
Het fragment [Vue2Fragment] beheert de weergave XML [vue2]. De code ervan is als volgt:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
// de velden van het scherm
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@AfterViews
void initFragment(){
// geheugen
afterViewsDone=true;
}
@Click(R.id.button_vue1)
void navigateToView1() {
// we navigeren naar weergave 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// de items uit de lijst die in weergave 1 zijn geselecteerd, worden weergegeven
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (Data data : mainActivity.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
}
}
De belangrijke code staat in de methode [updateFragment] op regel 32:
- regel 34: de tekst die moet worden weergegeven in [TextView] nr. 2 wordt berekend;
- regels 35-39: de lijst met gegevens die door de [ListView] wordt weergegeven, wordt doorlopen. Deze lijst is opgeslagen in de activiteit;
- regel 36: als gegeven nr. i is aangevinkt, wordt de bijbehorende omschrijving toegevoegd aan een type [StringBuilder];
- regel 41: de [TextView] geeft de berekende tekst weer;
1.20.11. Uitvoering
Maak een uitvoeringsconfiguratie voor dit project aan en voer deze uit.
1.20.12. Verbetering
In het vorige voorbeeld hebben we een gegevensbron van het type List<Data> gebruikt, waarbij de klasse [Data] als volgt was:
package exemples.android.fragments;
public class Data {
// gegevens
private String texte;
private boolean isChecked;
// fabrikant
public Data(String texte, boolean isCkecked) {
this.texte = texte;
this.isChecked = isCkecked;
}
...
}
Op regel 7 hadden we een booleaanse waarde gebruikt om het selectievakje van de elementen van de [ListView] te beheren. Vaak moet de [ListView] gegevens weergeven die kunnen worden geselecteerd door een selectievakje aan te vinken, zonder dat het element in de gegevensbron een booleaanse waarde heeft die overeenkomt met dit selectievakje. We kunnen dan als volgt te werk gaan:
De klasse [Data] ziet er dan als volgt uit:
package exemples.android.fragments;
public class Data {
// gegevens
private String texte;
// constructor
public Data(String texte) {
this.texte = texte;
}
// getters en setters
...
}
We maken een klasse [CheckedData] die is afgeleid van de vorige:
package exemples.android.fragments;
public class CheckedData extends Data {
// aangevinkt element
private boolean isChecked;
// constructor
public CheckedData(String text, boolean isChecked) {
// bovenliggend element
super(text);
// lokaal
this.isChecked = isChecked;
}
// getters en setters
...
}
Vervolgens hoeft men alleen maar overal in de code (MainActivity, ListAdapter, Vue1Fragment, Vue2Fragment) het type [Data] door het type [CheckedData] te vervangen. Bijvoorbeeld in [MainActivity]:
@AfterInject
protected void afterInject() {
// log
if (IS_DEBUG_ENABLED) {
Log.d("MainActivity", "afterInject");
}
// we maken een lijst met gegevens aan
List<CheckedData> liste = session.getListe();
for (int i = 0; i < 20; i++) {
liste.add(new CheckedData("Texte n° " + i, false));
}
}
Het project van deze versie wordt u geleverd onder de naam [Exemple-19B].
1.21. Voorbeeld-20: een menu gebruiken
1.21.1. Het project aanmaken
We dupliceren het project [Exemple-19B] in het project [Exemple-20]:
![]() | ![]() |
![]() | 3 ![]() |
We gaan de knoppen uit weergaven 1 en 2 verwijderen en vervangen door menuopties [1-2].
1.21.2. De definitie XML van de menu’s
![]() |
Het bestand [res / menu / menu_vue1] definieert het menu van weergave nr. 1:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuOptions"
app:showAsAction="ifRoom"
android:title="@string/menuOptions">
<menu>
<item
android:id="@+id/actionCacherMontrerTout"
android:title="@string/actionCacherMontrerTout"/>
<item
android:id="@+id/actionCacherMontrerActions"
android:title="@string/actionCacherMontrerActions"/>
<item
android:id="@+id/actionCacherMontrerActionsValider"
android:title="@string/actionCacherMontrerActionsValider"/>
</menu>
</item>
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue2"
android:title="@string/navigationVue2"/>
</menu>
</item>
</menu>
De menu-items worden gedefinieerd door de volgende gegevens:
- android:id: de ID van het element;
- android:title: de tekst van het menu-item;
- app:showsAsAction: geeft aan of het menu-item in de actiebalk van de activiteit kan worden geplaatst. [ifRoom] geeft aan dat het item in de actiebalk moet worden geplaatst als er ruimte voor is;
- een menuoptie kan zelf een submenu zijn (tag <menu>, regels 25, 29);
Het bestand [res / menu / menu_vue2] definieert het menu van weergave nr. 2:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationVue1"
android:title="@string/navigationVue1"/>
</menu>
</item>
</menu>
1.21.3. Het beheer van het menu in de abstracte klasse [AbstractFragment]
We gaan het beheer van het menu samenvoegen in de bovenliggende klasse [AbstractFragment] van beide weergaven:
package exemples.android.architecture;
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// gegevens toegankelijk voor dochterklassen
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
protected String className;
// activiteit
protected IMainActivity mainActivity;
protected Activity activity;
// sessie
protected Session session;
// menu
private Menu menu;
private int[] menuOptions;
private boolean initDone;
// constructor
public AbstractFragment() {
// init
className = getClass().getSimpleName();
// log
if (isDebugEnabled) {
Log.d("AbstractFragment", String.format("constructor %s", className));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// geheugen
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
// de # menuopties worden opgehaald als dit nog niet is gebeurd
if (!initDone) {
// de # menuopties worden opgehaald
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// de lijst met opties wordt naar een array overgebracht
menuOptions = new int[menuOptionsIds.size()];
for (int i = 0; i < menuOptions.length; i++) {
menuOptions[i] = menuOptionsIds.get(i);
}
// activiteit
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = this.mainActivity.getSession();
// geheugen
initDone = true;
}
// het dochterfragment wordt gevraagd om te worden geactiveerd
updateFragment();
}
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
...
}
// menuopties weergeven -----------------------------------
protected void setAllMenuOptions(boolean isVisible) {
....
}
protected void setMenuOptions(MenuItemState[] menuItemStates) {
...
}
// dochterklasse bijwerken
protected abstract void updateFragment();
}
- regel 42: uit de logs blijkt dat de methode [onCreateOptionsMenu] wordt aangeroepen telkens wanneer het fragment wordt weergegeven. Deze wordt pas heel laat aangeroepen, namelijk nadat de methode [updateFragment] is aangeroepen. Dit suggereert dat deze methode gebruikt zou kunnen worden om het fragment bij te werken. Dat is wat we hier gaan doen (regel 63);
- regel 42: de methode heeft twee parameters:
- [menu]: dit is een leeg menu;
- [inflater]: een hulpmiddel waarmee het menu kan worden aangemaakt op basis van de oorspronkelijke beschrijving. We maken hier geen gebruik van deze mogelijkheid, omdat we een annotatie AA zullen gebruiken die dit voor ons doet;
- regel 44: we slaan het menu op. We hebben dit later nodig;
- regels 52-53: we slaan in de array van regel 28 de ID’s van alle menu-items op;
- regels 55-57: uit de logs blijkt dat wanneer de methode [onCreateOptionsMenu] wordt aangeroepen, de methode [Fragment.getActivity()] de activiteit teruggeeft die aan het fragment is gekoppeld;
- regel 55: we slaan de activiteit op als een instantie van de Android-klasse [Activity];
- regel 56: we slaan de activiteit op als een instantie van de interface [IMainActivity];
- regel 57: we slaan de sessie op;
- regel 59: we merken op dat de klasse al is geïnitialiseerd, zodat we dit niet opnieuw hoeven te doen (regel 50);
- regel 63: we vragen het onderliggende fragment om zichzelf bij te werken. Dit is mogelijk omdat het fragment zowel zichtbaar is als gekoppeld aan zijn weergave en aan zijn menu;
De methode [getMenuOptions] waarmee de identificatiecodes van de menu-elementen kunnen worden opgehaald, is als volgt:
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// alle menu-items doorlopen
for (int i = 0; i < menu.size(); i++) {
// item nr. i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// als item nr. i een submenu is, dan beginnen we opnieuw
if (menuItem.hasSubMenu()) {
// recursie
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
Met de methode [setAllMenuOptions] kun je alle menuopties verbergen of weergeven;
protected void setAllMenuOptions(boolean isVisible) {
// alle menuopties worden bijgewerkt
for (int menuItemId : menuOptions) {
menu.findItem(menuItemId).setVisible(isVisible);
}
}
Met de methode [setMenuOptions] kun je bepaalde menuopties verbergen of weergeven;
protected void setMenuOptions(MenuItemState[] menuItemStates) {
// bepaalde menuopties worden bijgewerkt
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
De klasse [MenuItemState] is als volgt:
![]() |
package exemples.android.architecture;
public class MenuItemState {
// ID van de menuoptie
private int menuItemId;
// zichtbaarheid van de optie
private boolean isVisible;
// constructors
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters en setters
...
}
1.21.4. Het beheer van het menu in het fragment [Vue1Fragment]
De klasse [Vue1Fragment] wordt als volgt:
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
...
@OptionsItem(R.id.navigationVue2)
void navigateToView2() {
// we navigeren naar weergave 2
mainActivity.navigateToView(1);
}
@OptionsItem(R.id.actionValider)
void valider() {
// er wordt een bericht weergegeven
Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
}
private boolean actionCacherMontrerTout = true;
@OptionsItem(R.id.actionCacherMontrerTout)
void cacherMontrerTout() {
// de status wordt gewijzigd
actionCacherMontrerTout = !actionCacherMontrerTout;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionCacherMontrerTout), new MenuItemState(R.id.menuActions, actionCacherMontrerTout)});
}
private boolean actionCacherMontrerActions = true;
@OptionsItem(R.id.actionCacherMontrerActions)
void actionCacherMontrerActions() {
// de status wordt gewijzigd
actionCacherMontrerActions = !actionCacherMontrerActions;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
}
private boolean actionCacherMontrerActionsValider = true;
@OptionsItem(R.id.actionCacherMontrerActionsValider)
void actionCacherMontrerActionsValider() {
// de status wordt gewijzigd
actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
}
...
@Override
protected void updateFragment() {
....
// het menu wordt bijgewerkt
//setMenuOptions(...)
}
}
- regel 2: het menu [res / menu / menu_vue1.xml] wordt aan het fragment gekoppeld;
- regel 48: wanneer de methode [updateFragment] wordt uitgevoerd, kan ook het menu worden bijgewerkt om de nieuwe status van het fragment weer te geven;
- regel 7: de annotatie [@OptionsItem(R.id.navigationVue2)] geeft aan welke methode moet worden uitgevoerd wanneer er op de menuoptie [Navigation / Vue 2] wordt geklikt;
- regels 19-25: om een tak van het menu te verbergen, volstaat het om de hoofdoptie ervan te verbergen;
- regel 24: de hoofdoptie [menuNavigation, menuActions] wordt weergegeven/verborgen;
- regel 40: om een optie van een menutak weer te geven, moet niet alleen deze optie worden weergegeven, maar ook alle opties die men tegenkomt bij het teruggaan van de bladoptie naar de hoofdoptie van het menu;
1.21.5. Het beheer van het menu in fragment [Vue2Fragment]
We vinden vergelijkbare code terug in het fragment van weergave nr. 2:
package exemples.android.fragments;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {
// de velden van de weergave
@ViewById(R.id.textViewResultats)
TextView txtResultats;
@OptionsItem(R.id.navigationVue1)
void navigateToView1() {
// we navigeren naar weergave 1
mainActivity.navigateToView(0);
}
@Override
protected void updateFragment() {
// de items uit de lijst weergeven die in weergave 1 zijn geselecteerd
StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
for (CheckedData data : session.getListe()) {
if (data.isChecked()) {
texte.append(String.format("(%s)", data.getTexte()));
}
}
texte.append("]");
txtResultats.setText(texte);
// het menu wordt bijgewerkt
// setMenuOptions(...)
}
}
- regel 35: de optie [Navigation / Vue 1] wordt weergegeven;
- regels 17-20: wanneer op de optie [Navigation / Vue1] wordt geklikt, wordt de methode [navigateToView1] aangeroepen;
1.21.6. Uitvoering
Maak een uitvoeringscontext voor dit project aan en voer het uit.
1.22. Voorbeeld-21: refactoring van de abstracte klasse [AbstractFragment]
Het vorige voorbeeld heeft ons laten zien dat wanneer het fragment een menu heeft, de methode [onCreateOptionsMenu] een goede plek is om het fragment te vragen zichzelf bij te werken:
- deze wordt precies één keer aangeroepen wanneer het fragment wordt weergegeven;
- wanneer deze wordt aangeroepen, zijn de koppelingen van het fragment met zijn activiteit, zijn weergave en zijn menu tot stand gebracht;
Om dit te illustreren, nemen we voorbeeld 12, dat kenmerkend is door het grote aantal fragmenten waarvan de nabijheid kan worden gewijzigd. In dit voorbeeld hadden de fragmenten geen menu. We gaan er een leeg menu aan koppelen.
1.22.1. Het project aanmaken
We dupliceren het project [Exemple-12] in het project [Exemple-21]:
![]() | ![]() |
1.22.2. Het menu van de fragmenten
![]() |
Het toegevoegde menu voor de fragmenten is leeg:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
</menu>
Wat je hier moet begrijpen, is dat de activiteit al een eigen menu heeft [menu_main]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
Wanneer een activiteit al een menu heeft, wordt het menu dat bij de fragmenten hoort toegevoegd aan dat van de activiteit: je hebt dus de opties van twee menu's. In dit geval zal het menu van de fragmenten leeg zijn. Je ziet dus alleen het menu van de activiteit.
1.22.3. De fragmenten
![]() |
We gebruiken opnieuw de abstracte klasse [AbstractFragment] uit het vorige voorbeeld (zie paragraaf 1.21.3). We koppelen het menu [menu_fragment] aan de twee fragmenten:
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
In de twee fragmenten [PlaceholderFragment] en [Vue1Fragment] verwijderen we alles wat verwijst naar de oude abstracte klasse [AbstractFragment].
1.22.4. Uitvoering
Voer de applicatie uit en controleer of deze werkt. Volg de logbestanden om te zien wanneer de methode [onCreateOptionsMenu] van de klasse [AbstractFragment] wordt uitgevoerd. Deze methode roept nu de methode [updateFragment] van de onderliggende fragmenten aan.
1.23. Voorbeeld 22: de status van de activiteit en de fragmenten opslaan en herstellen
1.23.1. Het probleem
We behandelen hier het probleem van het draaien van het Android-apparaat (staand <--> liggend). Om dit te illustreren, nemen we het vorige voorbeeld 21 weer op:

Als we het apparaat [1] draaien, krijgen we de volgende nieuwe weergave:

We zien dat:
- in [1] is het tabblad [Fragment n° 3] verdwenen;
- in [2] is de weergegeven tekst weliswaar die van fragment nr. 3, maar de bezoekers teller klopt niet;
Tijdens deze rotatie zijn de logs als volgt:
- regel 1: we zien dat de activiteit volledig opnieuw is opgebouwd;
- regels 3-7: hetzelfde geldt voor de vijf fragmenten die door de activiteit worden beheerd;
- regel 21: fragment nr. 3 wordt weergegeven. We zien dat het bezoeknummer vóór de incrementatie 0 is;
Het resultaat na de rotatie kan dan als volgt worden verklaard:
- de klasse [MainActivity] maakt in eerste instantie een tabbalk aan met één enkel tabblad, met de naam [Vue 1]. Dit is het tabblad dat we zien;
- na het draaien van het apparaat geeft de paginabeheerder [mViewPager] hetzelfde fragment opnieuw weer, in dit geval dus fragment nr. 3. Hierbij moet men in gedachten houden dat tabbladen en fragmenten verschillende begrippen zijn en een verschillende levenscyclus hebben. De methode [updateFragment] van fragment nr. 3 wordt uitgevoerd:
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// bezoeknummer verhogen
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// tekst gewijzigd
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
- regel 7: het laatste bezoeknummer wordt uit de sessie gelezen. Deze is echter, net als al het andere, opnieuw opgebouwd en het bezoeknummer is op nul gezet. Dit verklaart het resultaat dat in fragment nr. 3 wordt weergegeven;
1.23.2. Methoden voor het opslaan en herstellen van de activiteit en de fragmenten
1.23.2.1. Oplossing 1: handmatig opslaan
Bij het draaien van het apparaat worden twee methoden van de activiteit aangeroepen:
// beheer van het opslaan/herstellen van de activiteit ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// bovenliggend
super.onSaveInstanceState(outState);
// back-up van de activiteitsstatus
// ....
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// bovenliggend
super.onCreate(savedInstanceState);
// activiteit herstellen
// ...
}
- regels 2-8: de methode [onSaveInstanceState] wordt tijdens het draaien door het systeem aangeroepen. Hier kan de activiteit worden opgeslagen. Als er niets wordt gedaan, wordt er niets opgeslagen. De status van de activiteit moet worden opgeslagen in de parameter [Bundle outState] die aan de methode wordt doorgegeven. De klasse [Bundle] lijkt op een woordenboek. Deze beschikt over de methode [putString, putInt, putLong, putBoolean, putChar, ...] met twee parameters: void putT(String key, T value);
- regels 10-16: de methode [onCreate] wordt aangeroepen bij het aanmaken van de activiteit. Als de status van de activiteit is opgeslagen, wordt deze opslag doorgegeven via de parameter [Bundle savedInstanceState]. Om de opgeslagen waarden op te halen, zijn er methoden beschikbaar zoals [getString, getInt, getLong, geBoolean, getChar, ...] met één parameter: T getT(String key);
De fragmenten beschikken over dezelfde twee methoden om hun status op te slaan.
We gaan deze informatie gebruiken om de status van voorbeeld 21 op te slaan en te herstellen. Hiervoor dupliceren we het project [Exemple-21] naar [Exemple-22].
1.23.2.2. Oplossing 2: automatisch opslaan
In de Android-documentatie staat dat bij het draaien van het apparaat het verlies van een fragment kan worden voorkomen door de instructie [Fragment].setRetainInstance(true) te gebruiken. In verschillende artikelen van [StackOverflow] wordt aanbevolen om deze instructie alleen te gebruiken voor fragmenten zonder visuele interface [http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, http://stackoverflow.com/questions/12640316/further-understanding-setretaininstancetrue, http://stackoverflow.com/questions/21203948/setretaininstancetrue-in-oncreate-fragment-in-android]. Ik heb deze instructie getest aan de hand van twee voorbeelden: Voorbeeld-17 (paragraaf 1.18 – een applicatie met één fragment dat een formulier weergeeft) en Voorbeeld-21 (paragraaf 1.22), een applicatie met vijf fragmenten. In beide gevallen bleek deze enkele instructie, toegepast op alle fragmenten van de applicatie, onvoldoende om de weergave die tijdens het draaien van het apparaat te zien was, correct te herstellen. In plaats van twee modellen te bouwen, één gebaseerd op [setRetainInstance(true)] en een ander op [setRetainInstance(false)] (de standaardwaarde), heb ik besloten de aanbevelingen van [StackOverflow] op te volgen en de standaardwaarde false van de methode [setRetainInstance(boolean )] te behouden. De instructie: [Fragment].setRetainInstance(true) is verderop in dit document nooit gebruikt.
1.23.3. De methode voor het opslaan en herstellen van het project [Exemple-22]
Het project [Exemple-22] ontwikkelt zich als volgt:
![]() |
Hierin verschijnen twee nieuwe klassen:
- [PlaceHolderFragmentState], die de status van een fragment van het type [PlaceHolderFragment] opslaat;
- [Vue1FragmentState], die de status van het fragment van het type [Vue1Fragment] zal opslaan;
Dit zijn de volgende klassen:
package exemples.android;
public class Vue1FragmentState {
// status Vue1Fragment
private boolean hasBeenVisited=false;
// getters en setters
...
}
- regel 5: de booleaanse waarde [hasBeenVisited] is waar als het fragment [Vue1Fragment] minstens één keer is bezocht (weergegeven). Dit veld is voor het voorbeeld aangemaakt omdat het fragment [Vue1Fragment] niets te bewaren heeft;
De klasse [PlaceHolderFragmentState] is als volgt:
package exemples.android;
public class PlaceHolderFragmentState {
// status: bezocht of niet bezocht
private boolean hasBeenVisited;
// weergegeven tekst
private String text;
// getters en setters
...
}
- regel 5: hier vinden we de booleaanse waarde [hasBeenVisited];
- regel 7: de tekst die door het fragment wordt weergegeven op het moment dat het moet worden opgeslagen. We hebben gezien dat deze tekst tijdens het roteren verloren was gegaan;
De status van de fragmenten wordt opgeslagen in de sessie en het is de activiteit die verantwoordelijk is voor het opslaan en herstellen van deze sessie. De sessie verloopt als volgt:
package exemples.android;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// aantal bezochte fragmenten
private int numVisit;
// nummer van het fragment van het type [PlaceholderFragment] dat in het tweede tabblad wordt weergegeven
private int numFragment = -1;
// nummer van het geselecteerde tabblad
private int selectedTab = 0;
// huidige weergavenummer
private int currentView;
// opgeslagen fragmenten ---------------
private Vue1FragmentState vue1FragmentState;
private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
// constructor
public Session() {
for (int i = 0; i < placeHolderFragmentStates.length; i++) {
placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
}
vue1FragmentState = new Vue1FragmentState();
}
// getters en setters
...
}
- regel 18: de status van het fragment [Vue1Fragment];
- regel 19: de status van de fragmenten van het type [PlaceHolderFragment];
- regels 22-27: in de sessieconstructor worden de velden van de regels 18 en 19 geïnitialiseerd;
- regels 12-15: er verschijnen twee nieuwe velden:
- regel 13: het nummer van het laatst geselecteerde tabblad;
- regel 15: het nummer van het laatst weergegeven fragment;
De activiteit slaat de sessie op en herstelt deze als volgt:
// beheer opslaan/herstellen van de activiteit ----------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// bovenliggend
super.onSaveInstanceState(outState);
// sessie opslaan
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// logboek
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// bovenliggend
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// sessieherstel
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();
}
}
}
}
- regel 8: de sessie wordt opgeslagen in de vorm van de tekenreeks jSON;
- regel 29: de sessie wordt hersteld op basis van de tekenreeks jSON;
Om het opslaan en herstellen van fragmenten te beheren, wordt de abstracte klasse [AbstractFragment] als volgt aangepast:
// beheer opslaan/herstellen -----------------------------------------------
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// bovenliggend
super.setUserVisibleHint(isVisibleToUser);
// back-up?
if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
// het fragment wordt verborgen - we maken er een back-up van
saveFragment();
saveFragmentDone = true;
}
// geheugen
this.isVisibleToUser = isVisibleToUser;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// bovenliggend
super.onActivityCreated(savedInstanceState);
// logboek
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// het fragment moet worden hersteld
fragmentHasToBeInitialized = true;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// logboek
if (isDebugEnabled) {
Log.d(className, "onSaveInstanceState");
}
// bovenliggend
super.onSaveInstanceState(outState);
// het fragment wordt alleen opgeslagen als het zichtbaar is
if (isVisibleToUser && !saveFragmentDone) {
saveFragment();
saveFragmentDone = true;
}
}
// onderliggende klassen
protected abstract void updateFragment();
protected abstract void saveFragment();
- er wordt besloten om de status van de fragmenten op twee momenten in de sessie op te slaan:
- regels 2-14: wanneer het fragment van zichtbaar naar verborgen gaat;
- regels 29-42: wanneer het systeem aangeeft dat er een back-up van het fragment moet worden gemaakt en dit fragment zichtbaar is (regel 38);
Dit mechanisme voorkomt dat er vaker dan nodig back-ups worden gemaakt. Aangezien de status van fragment i is opgeslagen toen het van zichtbaar naar verborgen ging, is het namelijk niet nodig om fragment i opnieuw op te slaan wanneer fragment j wordt weergegeven en er een rotatie plaatsvindt. Als het sinds de laatste opslag niet opnieuw is weergegeven, is de status ervan niet veranderd. Alleen de toestand van fragment j hoeft te worden opgeslagen. Deze werkwijze heeft nog een ander voordeel: het opslaan van de toestand van een fragment is niet alleen nodig bij het draaien van het apparaat. Er is ook het geval van pure navigatie tussen fragmenten, bijvoorbeeld in een systeem met tabbladen. Men wil dan een fragment terugvinden in de toestand waarin het zich bevond toen het voor het laatst werd weergegeven. Deze toestand kan gedeeltelijk verdwenen zijn als het fragment op een bepaald moment niet meer in de nabijheid van de weergegeven fragmenten was. Het fragment wordt dan niet in zijn geheel gereconstrueerd, maar de bijbehorende weergave wel. De back-up die is gemaakt toen het fragment verborgen raakte, wordt gebruikt om de laatste toestand van deze weergave te herstellen;
- regels 10, 40: om te voorkomen dat er twee opeenvolgende back-ups worden gemaakt, wordt de booleaanse variabele [saveFragmentDone] gebruikt om aan te geven dat er een back-up is gemaakt;
- regels 9, 39: het dochterfragment wordt gevraagd zijn toestand op te slaan. De methode [saveFragment] is abstract (regel 47). Het is dus aan de dochterklassen om deze te implementeren;
- regels 16-26: de methode [onActivityCreated] wordt gebruikt om de booleaanse waarde [fragmentHasToBeInitialized] op ‘waar’ te zetten. Het dochterfragment moet namelijk weten dat het de status van het fragment volledig moet resetten op basis van een status die het in de sessie aantreft;
Nog steeds in de klasse [AbstractFragment] verandert de methode [onCreateOptionsMenu] als volgt:
// het fragment bijwerken
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// geheugen
this.menu = menu;
// log
if (isDebugEnabled) {
Log.d(className, String.format("création menu en cours"));
}
...
// het onderliggende fragment wordt gevraagd zichzelf bij te werken
updateFragment();
// back-up moet worden gemaakt
saveFragmentDone = false;
}
- regel 14: we hebben gezien dat de booleaanse waarde [saveFragmentDone] was gewijzigd in vrai toen er een opslag plaatsvond. Op een bepaald moment moet deze weer terugkeren naar faux. Wanneer de methode [updateFragment] (regel 12) van het dochterfragment wordt uitgevoerd, wordt dit fragment zichtbaar. Een fragment moet echter worden opgeslagen op het moment dat het zichtbaar is, namelijk op het specifieke moment waarop het van de zichtbare naar de verborgen toestand overgaat. We stellen dan de booleaanse variabele [saveFragmentDone] in op false, zodat de opslag kan plaatsvinden;
1.23.4. Opslaan van het fragment [Vue1Fragment]
Het opslaan van de fragmenten gebeurt in de methode [saveFragment], die wordt aangeroepen door de bovenliggende klasse [AbstractFragment]:
// back-up van de status van het fragment
@Override
public void saveFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
}
// de status van het fragment wordt tijdens de sessie opgeslagen
Vue1FragmentState state = new Vue1FragmentState();
state.setHasBeenVisited(true);
session.setVue1FragmentState(state);
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- regels 9-11: opslaan van de status van het fragment in de sessie. Wanneer de methode [saveFragment] wordt aangeroepen, is het fragment zichtbaar. Daarom moet de booleaanse waarde [hasBeenVisited] worden ingesteld op vrai (regel 10);
1.23.5. Opslaan van het fragment [PlaceHolderFragment]
Het opslaan van de fragmenten gebeurt in de methode [saveFragment], die wordt aangeroepen door de bovenliggende klasse [AbstractFragment]:
@Override
public void saveFragment() {
// de status van het fragment wordt tijdens de sessie opgeslagen
PlaceHolderFragmentState state = new PlaceHolderFragmentState();
state.setText(textViewInfo.getText().toString());
state.setHasBeenVisited(true);
session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- regels 4-7: de status van het fragment wordt tijdens de sessie opgeslagen;
- regel 5: de tekst die momenteel wordt weergegeven door [TextView] textViewInfo wordt opgeslagen;
- regel 6: de booleaanse waarde [hasBeenVisited] van het fragment wordt gewijzigd in vrai;
- regel 7: de status van het fragment wordt in de tabel [placeHolderFragmentStates] vastgelegd. Het nummer van het te initialiseren element is het sectienummer van het fragment min één;
1.23.6. Herstel van het fragment [Vue1Fragment]
Het herstellen van de fragmenten gebeurt in de methode [updateFragment]:
@Override
protected void updateFragment() {
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
}
// herstel?
if (fragmentHasToBeInitialized) {
// status herstellen
hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
fragmentHasToBeInitialized = false;
}
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
}
// navigatie?
boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
if (navigation) {
// bezoeknummer verhogen
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// bezoeknummer weergeven
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
// huidige weergavenummer wijzigen
session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
}
- regels 8-12: herstel van de status van het fragment. De booleaanse waarde [fragmentHasToBeInitialized] is geïnitialiseerd door de bovenliggende klasse [AbstractFragment]. Wanneer deze op vrai staat, is het fragment zojuist gereconstrueerd en moet het opnieuw worden geïnitialiseerd. Dit gebeurt hier. In dit specifieke voorbeeld hoeft er niets te gebeuren. We hebben alleen laten zien dat de waarde van de booleaanse variabele [hasBeenVisited] kan worden teruggevonden in de opgeslagen status van het fragment (regel 10);
- regel 11: vergeet niet om [fragmentHasToBeInitialized] terug te zetten naar faux, zodat wanneer we later terugkeren naar dit fragment zonder dat het apparaat is gedraaid, het fragment niet onnodig opnieuw wordt geïnitialiseerd;
- regels 18-26: de bezoekenteller wordt verhoogd. Hier doet zich een probleem voor: wanneer het fragment wordt hersteld, willen we deze teller niet verhogen. We moeten hier onderscheid maken tussen:
- een gewone navigatie die de gebruiker terugbrengt naar het tabblad [Vue 1];
- een herstel wanneer de gebruiker zijn apparaat draait terwijl het tabblad [Vue 1] wordt weergegeven;
We onderscheiden deze twee gevallen aan de hand van het weergavenummer dat in de sessie is opgeslagen. Dit nummer is dat van de laatst weergegeven weergave (regel 28).
- regel 18: er vindt navigatie plaats en geen herstel als het nummer van de laatste weergave verschilt van dat van de huidige weergave;
- regels 21-25: de bezoekenteller wordt verhoogd en weergegeven;
1.23.7. Herstellen van het fragment [PlaceHolderFragment]
Het laden van de fragmenten gebeurt in de methode [updateFragment]:
// gegevens
private String text;
private int numVisit;
private String newText;
private boolean hasBeenVisited = false;
private ObjectMapper jsonMapper = new ObjectMapper();
...
public void updateFragment() {
// log
if (isDebugEnabled) {
Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
}
// om welk fragment gaat het?
int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
int numView = numSection - 1;
// moet het fragment worden geïnitialiseerd?
if (fragmentHasToBeInitialized) {
// initiële tekst
text = getString(R.string.section_format, numSection);
fragmentHasToBeInitialized = false;
}
// navigatie?
boolean navigation = session.getCurrentView() != numView;
if (navigation) {
// bezoeknummer verhogen
numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// gewijzigde tekst
newText = String.format("%s, visite %s", text, numVisit);
} else {
// het gaat om een herstel
PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
newText = state.getText();
}
// tekstweergave
textViewInfo.setText(newText);
// huidige weergave
session.setCurrentView(numView);
}
- regels 15-16: hier wordt het nummer bepaald van de weergave die op dat moment wordt bijgewerkt;
- regels 18-22: het geval waarin het fragment zich in een cyclus van opslaan/herstellen bevindt na een verandering van de oriëntatie van het apparaat. Hier moet het worden hersteld. Meestal gaat het om het herstellen van bepaalde velden van het fragment;
- regel 20: het veld [text] op regel 2 moet de oorspronkelijke tekst bevatten die door het fragment wordt weergegeven: [Hello world from section i]. Dit veld moet hier opnieuw worden gegenereerd;
- regel 21: we zien dat het fragment is geïnitialiseerd;
- regels 24-36: net als eerder bij het fragment [Vue1Fragment] mag de bezoekenteller niet worden verhoogd tijdens een herstel. Net als eerder moeten we onderscheid maken tussen navigatie en herstel;
- regels 32-36: het geval van herstel;
- regel 34: de status van het fragment vóór het draaien van het apparaat wordt uit de sessie opgehaald;
- regel 35: hier wordt de tekst opgehaald die op dat moment werd weergegeven;
- regel 38: deze tekst wordt opnieuw weergegeven;
- regel 40: het nummer van de nieuwe weergegeven weergave wordt in de sessie genoteerd;
1.23.8. Beheer van tabbladen
In de voorgaande paragrafen is het beheer van tabbladen niet aan de orde gekomen. In voorbeeld 21 zagen we echter een probleem bij het draaien van het apparaat: alleen het eerste tabblad [Vue 1] bleef behouden. Het tweede tabblad ging verloren.
We lossen dit probleem in de klasse [MainActivity] als volgt op:
@AfterViews
protected void afterViews() {
// logboek
if (IS_DEBUG_ENABLED) {
Log.d(className, "afterViews");
}
// werkbalk
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
// 1e tabblad
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// 2e tabblad?
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
// welk tabblad moet je kiezen?
tabLayout.getTabAt(session.getSelectedTab()).select();
...
}
- regels 14-16: aanmaken van het eerste tabblad;
- regels 18-23: aanmaken van het tweede tabblad. Om te bepalen of dit moet worden aangemaakt, kijken we in de sessie naar het nummer van het fragment dat in tabblad 2 wordt weergegeven. Als dit nummer verschilt van -1, de beginwaarde, dan wordt het tweede tabblad aangemaakt. Op dit moment hebben we twee tabbladen, waarvan het eerste standaard is geselecteerd;
- regel 26: we zoeken in de sessie naar het nummer van het tabblad dat geselecteerd was vóór het opslaan/herstellen en selecteren dit opnieuw. Als het veld [selectedTab] nog niet door de code is geïnitialiseerd, wordt de beginwaarde 0 gebruikt;











































































































































































































































































































































