Skip to content

2. Basisframework voor een Android-client die communiceert met een webservice / jSON

We bieden nu een basisstructuur voor een Android-app die communiceert met een of meer webservices / jSON. Dit is het project [client-android-skel] dat te vinden is in de map [architecture] met voorbeelden:

  

Het bestuderen van deze basisstructuur biedt de gelegenheid om bepaalde punten die we in de vorige voorbeelden zijn tegengekomen nog eens door te nemen. Deze applicatie zal als basis dienen voor alle toekomstige applicaties. Ze is na talrijke iteraties tot stand gekomen. Het doel is om zoveel mogelijk elementen van de applicaties die we binnenkort gaan bouwen in abstracte klassen onder te brengen, om te voorkomen dat we steeds hetzelfde soort code moeten schrijven dat alleen in details verschilt. De kenmerken zijn als volgt:

  • de asynchrone communicatie met de webserver / jSON verloopt via de bibliotheek RxJava;
  • de levenscyclus van een fragment (update, save, restore) wordt beheerd door de bovenliggende klasse [AbstractFragment], die op specifieke momenten bepaalde methoden van de onderliggende klassen aanroept. De onderliggende klasse hoeft zich dus geen zorgen te maken over de fasen van de levenscyclus, maar hoeft alleen bepaalde methoden te implementeren die door de bovenliggende klasse worden opgelegd;
  • de levenscyclus van de activiteit (opslaan / herstellen) wordt beheerd door een abstracte klasse [AbstractActivity], die op haar beurt de dochteractiviteit verplicht bepaalde methoden te implementeren;
  • de klasse [AbstractActivity] kan een applicatie beheren met of zonder tabbladen, met of zonder laadafbeelding, met of zonder basisauthenticatie bij de webserver / jSON. Of deze elementen al dan niet aanwezig zijn, wordt bepaald via de configuratie;

Dit skelet is gebruikt voor alle volgende voorbeelden. Vanwege de diversiteit daarvan werkte wat voor het ene voorbeeld werkte, mogelijk niet voor het volgende. Aangezien het skelet voor in totaal zeven voorbeelden is gebruikt, hebben er talrijke iteraties plaatsgevonden. Als we het zouden gebruiken voor een achtste voorbeeld, is het mogelijk dat we opnieuw zouden merken dat de specifieke kenmerken van dit nieuwe voorbeeld nieuwe fouten veroorzaken. Niettemin zal het gebruik van dit skelet het schrijven van de komende voorbeelden aanzienlijk vereenvoudigen. Het beheer van de levenscyclus van een fragment (update, save, restore), in combinatie met het begrip ‘aangrenzende fragmenten’, is namelijk bijzonder complex. Hier is dit volledig verborgen in de klasse [AbstractFragment].

2.1. Architectuur van de Android-client

De voorgestelde Android-client is gebaseerd op de volgende architectuur:

  • de laag [DAO] implementeert een interface [IDao]. Deze communiceert met de webserver / jSON;
  • er is slechts één activiteit die tevens de interface [IDao] implementeert. De weergaven maken hier gebruik van om de server te bereiken;
  • de weergaven worden geïmplementeerd door fragmenten;

Het Android-project weerspiegelt deze architectuur:

  

We zullen de verschillende onderdelen van dit project een voor een bespreken.

2.2. De Gradle-configuratie

 

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 {
    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 'io.reactivex:rxandroid:1.2.0'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}

repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}
  • Alle versienummers zijn onder voorbehoud van wijzigingen. We kunnen echter uitgaan van de huidige nummers als we Android Studio zo configureren dat deze versies van de Android-tools (regels 15-16, 47-48) aanwezig zijn (zie paragraaf 6.11);

2.3. Het manifest van de applicatie

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="client.android">

  <uses-permission android:name="android.permission.INTERNET"/>

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".activity.MainActivity_"
      android:label="@string/app_name"
      android:windowSoftInputMode="stateHidden"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
  • regel 3: we wijzigen het pakket van de app;
  • regels 10, 15: we stellen de waarde van het item [app_name] in het bestand [res / values / strings.xml] vast. Op dit moment is deze als volgt:

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- naam van de applicatie -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. De structuur van de Java-code

  
  • [architecture] bevat de belangrijkste organisatorische elementen van de code;
  • [activity] bevat de enige activiteit van de applicatie;
  • [fragments] groepeert de fragmenten of weergaven van de applicatie;
  • [dao] groepeert de elementen voor communicatie met de webserver / jSON;

2.5. Elementen van de activiteit

 

Image

2.5.1. Het aan de activiteit gekoppelde overzicht

De weergave [activity_main.xml] die aan de activiteit is gekoppeld, ziet er als volgt uit:


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

  <!-- fragmentcontainer -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
  • regel 29: er wordt een specifieke fragmentcontainer gebruikt;

De activiteit heeft ook een menu [res / menu / menu_main.xml] voor de weergave:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
</menu>

Op dit moment is dit menu leeg. De ontwikkelaar zal het indien nodig aanvullen.

2.5.2. De fragmentcontainer [MyPager]

  

package client.android.architecture;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class MyPager extends ViewPager {

  // regelt het vegen
  private boolean isSwipeEnabled;
  // scrollbesturing
  private boolean isScrollingEnabled;

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

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

  // te herdefiniëren methoden voor het beheren van het vegen
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipen toegestaan?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // swipen toegestaan?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

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

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

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

Deze klasse breidt de standaard Android-klasse [ViewPager] uitsluitend uit om het vegen (regel 11) en het scrollen (regel 13) tussen weergaven te beheren.

  • regels 26-43: de methoden die het swipen uitschakelen als dit is uitgeschakeld;
  • regels 46-49: herdefinitie van de methode [setCurrentItem] die dient om de weergegeven weergave te wijzigen. Als het scrollen is uitgeschakeld, vindt de weergavewisseling plaats zonder scrollen. Let op: de ontwikkelaar kan deze werkwijze omzeilen door de methode [setCurrentItem(int position, boolean smoothScrolling)] te gebruiken, waarmee hij kan aangeven welk scrollgedrag hij wenst;

2.5.3. De klasse [CoreState]

  

De klasse [CoreState] is de bovenliggende klasse van de statussen van de verschillende fragmenten:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo: voeg hier de subklassen van [CoreState] toe
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragment bezocht of niet
  protected boolean hasBeenVisited = false;
  // status van het eventuele menu van het fragment
  protected MenuItemState[] menuOptionsState;

  // getters en setters
...
}
  • regel 16: elk fragment heeft in zijn status een booleaanse waarde [hasBeenVisited] die aangeeft of het al dan niet is bezocht. Dit is nodig omdat er soms, bij de eerste weergave van een fragment, specifieke handelingen moeten worden uitgevoerd;
  • regel 18: het project [client-android-skel] slaat de menu’s van de fragmenten automatisch op en herstelt deze automatisch als ze er een hebben. In de tabel MenuItemState[] menuOptionsState wordt de status (zichtbaar of niet) van alle menuopties opgeslagen;
  • regels 10-13: net zoals in [Exemple-22] gebeurt, wordt de status van de activiteit en de bijbehorende fragmenten opgeslagen in de sessie, die op haar beurt wordt opgeslagen in de vorm van een tekenreeks jSON. We zullen zien dat de sessie een array met elementen van het type [CoreState] opslaat. Als we niets doen, wordt de tekenreeks jSON van het type [CoreState] opgeslagen. Wij willen echter de statussen van de fragmenten opslaan, statussen die zijn afgeleid van [CoreState]. Om ervoor te zorgen dat de reeks jSON van het afgeleide type wordt gegenereerd en niet die van het bovenliggende type, moeten de afgeleide typen worden gedeclareerd zoals aangegeven in de regels 10-13. De klasse [CoreState] is een van de klassen van de architectuur die de ontwikkelaar voor elke nieuwe toepassing moet aanpassen (regels 10-13);

2.5.4. De interface [IMainActivity]

  

De interface [IMainActivity] bepaalt wat de fragmenten van de activiteit mogen opvragen in de volgende architectuur:

Image


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // toegang tot de sessie
  ISession getSession();

  // wijziging van weergave
  void navigateToView(int position, ISession.Action action);

  // afhandeling van wachtrijen
  void beginWaiting();

  void cancelWaiting();

  // applicatieconstanten (aan te passen) -------------------------------------

  // debugmodus
  boolean IS_DEBUG_ENABLED = true;

  // maximale wachttijd voor het antwoord van de server
  int TIMEOUT = 1000;

  // wachttijd voordat de clientverzoek wordt uitgevoerd
  int DELAY = 0;

  // basisauthenticatie
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // aaneenschakeling van fragmenten
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // tabbladbalk
  boolean ARE_TABS_NEEDED = false;

  // laadafbeelding
  boolean IS_WAITING_ICON_NEEDED = false;

  // aantal fragmenten van de applicatie
  int FRAGMENTS_COUNT = 0;

  // todo voeg hier uw constanten en andere methoden toe
}
  • regel 6: de interface [IMainActivity] breidt de interface [IDao] uit vanuit de laag [DAO];
  • regel 9: dit is de activiteit die toegang geeft tot de sessie in de vorm van een instantie van de interface [ISession];
  • regel 12: via deze activiteit wordt van weergave gewisseld. De tweede parameter is de actie die deze weergavewisseling veroorzaakt, een van de waarden SUBMIT, NAVIGATION, RESTORE;
  • regels 15-17: dit is de activiteit die het laadbeeld beheert;
  • regel 22: voor het debuggen van de applicatie;
  • regel 25: om niet te lang te wachten als de server niet meer reageert;
  • regel 28: tijdens het debuggen stellen we een waarde van enkele seconden in, zodat we de tijd hebben om de bewerking met de server te annuleren en te zien wat er gebeurt;
  • regel 31: naar true als de service jSON om basisauthenticatie vraagt;
  • regel 34: fragmenten naast elkaar;
  • regel 37: naar vrai als de applicatie tabbladen heeft;
  • regel 39: naar vrai als de applicatie communiceert met een webserver / jSON en men tijdens de communicatie een laadafbeelding wil tonen;
  • regel 43: het aantal fragmenten dat door de applicatie wordt beheerd;

De interface [IMainActivity] is het tweede onderdeel van de architectuur dat de ontwikkelaar moet invullen (regel 45).

2.5.5. De interface [IDao]

De interface [IMainActivity] breidt de volgende interface [IDao] uit:

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL van de webservice
  void setUrlServiceWebJson(String url);

  // gebruiker
  void setUser(String user, String mdp);

  // time-out van de client
  void setTimeout(int timeout);

  // basisauthenticatie
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // debugmodus
  void setDebugMode(boolean isDebugEnabled);

  // Wachttijd van de client in milliseconden vóór het verzoek
  void setDelay(int delay);

  // todo: definieer hier uw interface
}
  • regel 24: de ontwikkelaar vult de interface hier aan;

2.5.6. De sessie

  

De klasse [Session] omvat de elementen die door de activiteit en de fragmenten worden gedeeld. Deze klasse implementeert de volgende interface [ISession]:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // nummer van de laatst weergegeven weergave
  int getPreviousView();

  void setPreviousView(int numView);

  // laatste status van een weergave
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // actie in uitvoering
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // statussen van alle weergaven -
  // wordt niet door de code gebruikt, maar is nodig voor serialisatie/deserialisatie jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // nummer van het laatst geselecteerde tabblad
  int getPreviousTab();

  void setPreviousTab(int position);

  // navigatie bij selectie van tabblad
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

We introduceren de interface [ISession] om de aanwezigheid van bepaalde methoden in de sessie verplicht te stellen:

  • regels 7-10: het nummer van de laatst weergegeven weergave (fragment);
  • regels 12-15: de status van een bepaalde weergave;
  • regels 17-24: we introduceren het begrip 'actie in uitvoering'. Er zijn er vier (regel 17):
    • RESTORE: er vindt een opslag/herstel plaats. Er is geen wijziging van weergave;
    • NAVIGATION: er vindt een navigatie plaats. We noemen hier navigatie een weergavewisseling waarbij de nieuwe weergave kan worden hersteld op basis van de laatste status die tijdens de sessie is opgeslagen;
    • SUBMIT: we kennen het type [SUBMIT] toe aan een lopende actie wanneer er een weergavewisseling plaatsvindt en de nieuwe weergave afhankelijk is van de algemene status van de activiteit en niet alleen van haar eigen status. Soms is het moeilijk om onderscheid te maken tussen NAVIGATION en SUBMIT. In dat geval wordt het meest algemene geval, SUBMIT, gekozen;
    • NONE: waarde van de actie wanneer deze nog geen eerste waarde heeft gekregen;
  • regels 26-30: de statussen van de activiteit en de fragmenten worden opgeslagen in een array van het type CoreState[]. Om ervoor te zorgen dat deze correct wordt beheerd tijdens het serialiseren en deserialiseren van jSON, moet deze een getter en een setter hebben;
  • regels 32-35: nummer van het laatst geselecteerde tabblad. Wordt gebruikt tijdens het opslaan/herstellen om het tabblad opnieuw te selecteren dat geselecteerd was vóór het draaien van het apparaat;
  • regels 37-40: beheer van een booleaanse waarde die aangeeft of de selectie van een tabblad gepaard moet gaan met een wijziging van het fragment;

De interface [ISession] wordt geïmplementeerd door de volgende abstracte klasse [AbstractSession]:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class AbstractSession implements ISession {
  // nummer van het vorige overzicht
  private int preViousView;

  // status van de weergaven
  private CoreState[] coreStates = new CoreState[0];

  // actie in uitvoering
  private Action action = Action.NONE;

  // voorheen geselecteerd tabblad
  private int previousTab;

  // navigeren op basis van tabbladselectie
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // constructor
  public AbstractSession() {
    // de tabel met fragmentstatussen wordt geïnitialiseerd
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // interface ISession ---------------------------------------------------------
  @Override
  public int getPreviousView() {
    return preViousView;
  }

  @Override
  public void setPreviousView(int numView) {
    this.preViousView = numView;
  }

  @Override
  public CoreState getCoreState(int numView) {
    return coreStates[numView];
  }

  @Override
  public void setCoreState(int numView, CoreState coreState) {
    coreStates[numView] = coreState;
  }

  @Override
  public Action getAction() {
    return action;
  }

  @Override
  public void setAction(Action action) {
    this.action = action;
  }

  @Override
  public CoreState[] getCoreStates() {
    return coreStates;
  }

  @Override
  public void setCoreStates(CoreState[] coreStates) {
    this.coreStates = coreStates;
  }

  @Override
  public int getPreviousTab() {
    return previousTab;
  }

  @Override
  public void setPreviousTab(int position) {
    this.previousTab = position;
  }

  @Override
  public boolean isNavigationOnTabSelectionNeeded() {
    return navigationOnTabSelectionNeeded;
  }

  @Override
  public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
    this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
  }
}
  • regel 9: het nummer van de weergave die werd weergegeven vóór de huidige weergave. Deze informatie is nuttig wanneer een weergave vanuit meerdere locaties kan worden geopend. Dit is typisch het geval bij navigatie via tabbladen. De weergegeven weergave kan dan weten wat de vorige weergave was;
  • regel 12: de tabel met de statussen van alle fragmenten die door de activiteit worden weergegeven;
  • regel 18: het nummer van het eerder geselecteerde tabblad. Speelt een vergelijkbare rol als het nummer van de vorige weergave in regel 9. Deze informatie is nuttig wanneer het apparaat wordt gedraaid en men terug moet gaan naar het tabblad dat vóór de draaiing was geselecteerd;
  • regel 22: een booleaanse waarde die aangeeft of de selectie van een tabblad gepaard moet gaan met een wijziging van het weergegeven fragment. Het is belangrijk te weten dat het project [client-android-skel] tabbladen en fragmenten afzonderlijk beheert, zodat het kan worden gebruikt in gevallen waarin het aantal tabbladen kleiner is dan het aantal fragmenten. Er zijn twee soorten selecties:
    • een selectie door de gebruiker wanneer deze op een tabblad klikt. In dit geval moet het weergegeven fragment doorgaans veranderen;
    • een selectie door de software via de methode [Tablayout.Tab.select()]. In dit geval is het niet altijd wenselijk dat het weergegeven fragment verandert. Hier volgen twee voorbeelden:
      • bij het draaien van het apparaat wordt de activiteit opnieuw aangemaakt, evenals de tabbladen. Wanneer het eerste tabblad wordt aangemaakt, ondergaat het echter automatisch een softwarebewerking [select]. Het is dan niet wenselijk om het weergegeven fragment te wijzigen, omdat men zich in een fase van het opnieuw aanmaken van de activiteit bevindt, waarbij het uiteindelijk weergegeven fragment niet noodzakelijkerwijs het fragment is dat aan het eerste tabblad is gekoppeld;
      • aangezien het beheer van de tabbladen losstaat van dat van de fragmenten, kan men de tabbladen willen bijwerken (verwijderen, toevoegen) zonder de bijbehorende fragmenten te beïnvloeden. Sommige van deze bewerkingen kunnen echter ook hier weer een impliciete softwarebewerking [select] op een van de tabbladen activeren. Deze selectie hoeft dan niet noodzakelijkerwijs te leiden tot een navigatie naar het bijbehorende fragment;
  • regel 21: het veld [navigationOnTabSelectionNeeded] is niet bedoeld om te worden opgeslagen bij het opslaan van de activiteit en de bijbehorende fragmenten. De annotatie [@JsonIgnore] zorgt ervoor dat het veld wordt genegeerd tijdens jSON-serialisaties/deserialisaties;
  • regels 25-31: de constructor initialiseert de statuslijst van de [FRAGMENTS_COUNT]-fragmenten van de applicatie. De elementen van deze lijst worden geïnitialiseerd met het veld [hasBeeenVisited=false]. Deze informatie wordt gebruikt om te bepalen of het al dan niet om het eerste bezoek aan het fragment gaat;

De klasse [Session] is als volgt:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // gegevens die tussen fragmenten onderling en tussen fragmenten en activiteiten moeten worden gedeeld
  // elementen die niet kunnen worden geserialiseerd in jSON moeten de annotatie @JsonIgnore hebben
  // vergeet niet de getters en setters die nodig zijn voor serialisatie/deserialisatie in jSON
}
  • regel 5: de klasse [Session] is een uitbreiding van de klasse [AbstractSession] die we zojuist hebben bekeken. De ontwikkelaar plaatst hierin de elementen die tussen de fragmenten onderling en tussen fragmenten en activiteiten moeten worden gedeeld. Merk op dat de klasse [Session] niet langer wordt geannoteerd door de annotatie AA [@EBean]. Het is een normale klasse geworden;

2.5.7. De abstracte klasse [AbstractActivity]

  

2.5.7.1. Squelette

De klasse [AbstractActivity] bestaat uit meer dan 300 regels. We gaan deze stap voor stap bestuderen. De structuur ervan is als volgt:


package client.android.architecture;

import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // laag [DAO]
  private IDao dao;
  // de sessie
  protected Session session;

  // de fragmentcontainer
  protected MyPager mViewPager;
  // de werkbalk
  private Toolbar toolbar;
  // de laadafbeelding
  private ProgressBar loadingPanel;
  // tabbladbalk
  protected TabLayout tabLayout;

  // de fragment- of sectiebeheerder
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // klassenaam
  protected String className;
  // mapper jSON
  private ObjectMapper jsonMapper;

  // constructor
  public AbstractActivity() {
    // naam van de klasse
    className = getClass().getSimpleName();
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // implementatie IMainActivity --------------------------------------------------------------------
  ...

  // levenscyclus - back-up / herstel van de activiteit ------------------------------------
  ...

  // beheer van de stand-by-image ---------------------------------
  ...

  // interface IDao -----------------------------------------------------
  ...

  // de fragmentmanager --------------------------------
  ...

  // afgeleide klassen
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

  protected abstract int getFirstView();

}

De klasse [AbstractActivity]:

  • implementeert de interface [IMainActivity] (regels 21, 55);
  • beheert het opslaan en herstellen van de activiteit en de bijbehorende fragmenten bij het draaien van het apparaat (regel 58);
  • beheert de wachtafbeelding tijdens een uitwisseling met de webserver / jSON (regel 61);
  • implementeert de interface IDao van de laag [DAO] (regel 64);
  • implementeert de fragmentmanager (regel 67);
  • legt aan zijn onderliggende klassen de aanwezigheid van zes methoden op (regels 71-81);

2.5.7.2. De interface [IMainActivity] implementeren

De implementatie van de interface [IMainActivity] (zie paragraaf 2.5.4) is als volgt:


  // implementatie IMainActivity --------------------------------------------------------------------
  @Override
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position, ISession.Action action) {
    if (IS_DEBUG_ENABLED) {
      Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
    }
    // weergave nieuw fragment
    mViewPager.setCurrentItem(position);
    // de huidige actie wordt genoteerd bij deze weergavewisseling
    session.setAction(action);
}

2.5.7.3. De status van de activiteit en de fragmenten ervan opslaan

De status van de activiteit en de fragmenten ervan bevindt zich volledig in de sessie. Het gaat er dus om deze op te slaan. We nemen hier over wat er in het project [Exemple-22] is gedaan (zie paragraaf 1.23):


  // beheer van het opslaan en herstellen van de activiteit ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // bovenliggend
    super.onSaveInstanceState(outState);
    // sessie opslaan in de vorm van een tekenreeks jSON
    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();
      }
    }
}

2.5.7.4. Herstel van de status van de activiteit en de bijbehorende fragmenten

Het gaat erom de sessie te herstellen. We gaan te werk zoals aangetoond in [Exemple-22]:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // bovenliggend
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // iets om te herstellen?
    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();
        }
      }
    } else {
      // sessie
      session = new Session();
    }
...
  • regels 10-26: als de parameter [Bundle savedInstanceState] in regel 2 niet null is, dan wordt de sessie hersteld (regels 12-17);
  • regels 26-29: het geval waarin de parameter [Bundle savedInstanceState] van regel 2 gelijk is aan null, komt overeen met de eerste start van de activiteit. Er wordt dan een lege sessie aangemaakt;

2.5.7.5. Initialisatie van de laag [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // bovenliggend
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // laag [DAO]
    dao = getDao();
    if (dao != null) {
      // laagconfiguratie [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // dochterklassen
  protected abstract IDao getDao();
....
}
  • regel 11: er wordt een verwijzing naar de laag [DAO] opgevraagd bij de dochteractiviteit (regel 21);
  • regels 14-17: als de laag [DAO] bestaat, wordt deze geconfigureerd op basis van de informatie in de interface [IMainActivity];

2.5.7.6. Initialisatie van de aan de activiteit gekoppelde weergave

De weergave die bij de activiteit hoort, is beschreven in paragraaf 2.5.1:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <!-- fragmentcontainer -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>

Deze weergave wordt geïnitialiseerd met de volgende code:


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // bovenliggende klasse
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // gekoppelde weergave
    setContentView(R.layout.activity_main);
    // weergavecomponenten ---------------------
    // werkbalk
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // laadafbeelding?
    if (IS_WAITING_ICON_NEEDED) {
      // de laadafbeelding wordt toegevoegd
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // aanmaken van ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // ProgressBar aan de werkbalk toevoegen
      toolbar.addView(loadingPanel);
    }
...
  • regel 11: de weergave XML [activity_main] is gekoppeld aan de activiteit;
  • regels 14-15: de werkbalk is geïntegreerd en wordt ondersteund;
  • regels 17-27: eventuele toevoeging van een laadafbeelding: als de booleaanse waarde [IS_WAITING_ICON_NEEDED] op waar staat in de interface [IMainActivity];
  • regel 23: aanmaken van de wachtafbeelding van het type [ProgressBar], waarnaar wordt verwezen door het veld [loadingPanel];
  • regel 24: deze afbeelding is aanvankelijk verborgen;
  • regel 26: het wordt toegevoegd aan de werkbalk;

2.5.7.7. Beheer van tabbladen

De interface [IMainActivity] kan een tabbalk vereisen. Deze wordt als volgt toegevoegd en beheerd:


// tabbladbalk
  protected TabLayout tabLayout;
...

    // tabbladbalk?
    if (ARE_TABS_NEEDED) {
      // de tabbalk wordt toegevoegd
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // geen navigatie op basis van selectie totdat een fragment wordt weergegeven
      session.setNavigationOnTabSelectionNeeded(false);
      // tabbladbalk aanmaken
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // de tabbalk toevoegen aan de applicatiebalk
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // gebeurtenisbeheerder van de tabbalk
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // er is een tabblad geselecteerd
          if (IS_DEBUG_ENABLED) {
            Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
              tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
          }
          if (session.isNavigationOnTabSelectionNeeded()) {
            // positie van het tabblad
            int position = tab.getPosition();
            // geheugen
            session.setPreviousTab(position);
            // gerelateerd fragment weergeven?
            navigateOnTabSelected(position);
          }
        }

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

        }

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

        }
      });
    }

...
  // onderliggende klassen
  protected abstract void navigateOnTabSelected(int position);
...
  • regels 12-48: toevoegen en beheren van een tabbladbalk;
  • regel 6: de tabbalk wordt toegevoegd als de constante [ARE_TABS_NEEDED] is ingesteld op vrai in de interface [IMainActivity];
  • regel 12: bij het aanmaken van de tabbalk kunnen er impliciete bewerkingen [Tablayout.Tab.select] plaatsvinden (deze worden niet door de gebruiker geactiveerd). We zetten de booleaanse waarde [session.navigationOnTabSelectionNeeded] op faux om navigatie tijdens deze valse selecties te voorkomen. Het is aan de ontwikkelaar om het weer te geven fragment te selecteren met de methode [navigateToView]. De booleaanse waarde [session.navigationOnTabSelectionNeeded] wordt teruggezet naar vrai wanneer dit fragment wordt weergegeven (zie klasse AbstractFragment);
  • regel 14: aanmaken van een tabbalk waarnaar wordt verwezen door het veld [tabLayout]. We gebruiken een aangepaste tabbalk [CustomTabLayout], waar we later op terugkomen;
  • regel 15: we stellen de kleuren van de tabtitels in. Deze zijn te vinden in het volgende bestand [res / color / tab_txt.xml]:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_selected="true" android:color="#FFFF00" />
  <item android:state_selected="false" android:color="#FFFFFF" />
</selector>
    • regel (c): de kleur van de tabbladtitel wanneer deze is geselecteerd;
    • regel (d): de kleur van de tabbladtitel wanneer deze niet is geselecteerd;

Dit bestand kan uiteraard worden aangepast. De hexadecimale kleurcodes zijn bijvoorbeeld hier te vinden.

  • regels 17-18: toevoeging van deze tabbalk aan de applicatiebalk in de weergave XML [activity_main];
  • regels 20-47: gebeurtenisbeheer voor de tabbalk;
  • regels 22-36: alleen de gebeurtenis [onTabSelected] wordt afgehandeld. Deze komt overeen met een klik op het tabblad [Tab tab] dat als parameter aan de methode is doorgegeven, of met een softwarebewerking [TabLayout.Tab.select];
  • regel 30: positie van het geselecteerde tabblad;
  • regel 32: deze positie wordt tijdens de sessie opgeslagen;
  • regel 34: nu moet het fragment worden weergegeven dat aan dit tabblad is gekoppeld. Alleen de dochterklasse (regel 52) kan deze koppeling maken. Merk op dat de tabbalk niet wordt gekoppeld aan de fragmentcontainer [mViewPager], zoals in sommige bestudeerde voorbeelden is gedaan. Hier wordt het beheer van de tabbalk volledig losgekoppeld van dat van de fragmenten. Daarom moet men, wanneer er op een tabblad wordt geklikt, aangeven welke weergave men wil zien;
  • regel 28: er wordt onderscheid gemaakt tussen tabbladsselectie met of zonder navigatie. Over het algemeen wil men navigatie wanneer de gebruiker op een tabblad klikt, en wil men dat niet bij een programmatische selectie. Het is de ontwikkelaar die deze twee gevallen onderscheidt met het element [session.navigationOnTabSelectionNeeded]. Wanneer er geen navigatie plaatsvindt, wordt het nummer van het laatst geselecteerde tabblad niet in de sessie opgeslagen. Het is aan de ontwikkelaar om dit te doen;

2.5.7.8. De tabbladbeheerder [CustomTabLayout]

  

We gebruiken een aangepast tabbladbeheer om de titels van de tabbladen in verschillende lettertypen weer te geven. De klasse [CustomTabLayout] is als volgt:


package client.android.architecture.custom;

import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class CustomTabLayout extends TabLayout {
  private Typeface mTypeface;

  public CustomTabLayout(Context context) {
    super(context);
    init();
  }

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

  public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  private void init() {
    mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
  }

  @Override
  public void addTab(Tab tab) {
    super.addTab(tab);

    ViewGroup mainView = (ViewGroup) getChildAt(0);
    ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());

    int tabChildCount = tabView.getChildCount();
    for (int i = 0; i < tabChildCount; i++) {
      View tabViewChild = tabView.getChildAt(i);
      if (tabViewChild instanceof TextView) {
        ((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
      }
    }
  }

}
  • het aanpassen van het lettertype van de tabtitels gebeurt op regel 30 en 44;

Het bestand [fonts] ziet er als volgt uit:

  

Bronnen:

  • de code van de klasse [CustomTabLayout] is gevonden in URL en [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
  • de lettertypen zijn gevonden in URL en [https://www.fontsquirrel.com/fonts/roboto];

2.5.7.9. Laatste initialisaties


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // bovenliggend element
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // instantiëring van de fragmentbeheerder
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // de fragmentcontainer is gekoppeld aan de fragmentmanager
    // d.w.z. dat fragment nr. i van de fragmentcontainer het fragment nr. i is dat door de fragmentmanager wordt geleverd
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // het vegen tussen fragmenten wordt geblokkeerd
    mViewPager.setSwipeEnabled(false);
    // fragmenten liggen naast elkaar
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // de eerste weergave wordt getoond
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // het stokje wordt doorgegeven aan de onderliggende activiteit
    onCreateActivity();
  }
...
  // dochterklassen
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • regels 10-19: hier zien we code die vaak voorkomt in de bestudeerde voorbeelden;
  • regels 21-23: weergave van de allereerste weergave. Er zijn ongetwijfeld meerdere manieren om dit geval te onderscheiden. Hier hebben we gebruikgemaakt van het feit dat voor de allereerste weergave de waarde van de actie die de weergavewisseling veroorzaakt NONE is;
  • regel 22: we doen geen aanname over het eerste fragment dat moet worden weergegeven. In onze voorbeelden was dit vaak fragment nr. 0, maar niet altijd (zie Voorbeeld-22). We zullen de dochteractiviteit (regel 30) dus vragen om aan te geven wat deze eerste weergave is;
  • regel 25: hier hebben we alles wat mogelijk was gefactoreerd. Nu moet de dochterklasse haar eigen initialisaties uitvoeren (regel 29);

2.5.7.10. Beheer van de laadafbeelding

In de klasse [AbstractActivity] wordt de laadafbeelding beheerd door de volgende twee methoden:


  // beheer van de wachtafbeelding ---------------------------------
  public void cancelWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.INVISIBLE);
    }
  }

  public void beginWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.VISIBLE);
    }
}

2.5.7.11. Implementatie van de interface [IDao]

In de klasse [AbstractActivity] wordt de interface [IDao] (zie paragraaf 2.5.5) als volgt geïmplementeerd:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // laag [DAO]
  private IDao dao;
...
  // interface IDao -----------------------------------------------------
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    dao.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    dao.setBasicAuthentification(isBasicAuthentificationNeeded);
  }

  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    dao.setDebugMode(isDebugEnabled);
  }

  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
}
  • regel 3: ter herinnering: de waarde van dit veld is door de dochteractiviteit in de methode [onCreate] aangeleverd;

2.5.7.12. Implementatie van de fragmentmanager

In de klasse [AbstractActivity] is de fragmentmanager als volgt geïmplementeerd:


...
  // de fragmentmanager --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // fragmenten van de dochterklasse
      fragments = getFragments();
    }

    // moet fragment nr. positie weergeven
    @Override
    public AbstractFragment getItem(int position) {
      // het fragment wordt weergegeven
      return fragments[position];
    }

    // geeft het aantal te beheren fragmenten weer
    @Override
    public int getCount() {
      return fragments.length;
    }

    // geeft de titel van fragment nr. positie weer
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  // dochterklassen
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • regel 5: de array met fragmenten die aan de activiteit zijn gekoppeld. Alle fragmenten worden afgeleid van de klasse [AbstractFragment];
  • regels 8-12: dit is de constructor die de array met fragmenten initialiseert. Deze vraagt de fragmenten op bij de dochterklasse van de activiteit (regel 35);
  • regels 28-31: de titels van de fragmenten kunnen worden gebruikt in een applicatie waarin er evenveel tabbladen zijn als fragmenten. In dat geval kan het tabblad de titel van het fragment krijgen. Hier worden deze titels opgevraagd bij de dochterklasse (regel 37);

2.5.7.13. De methode [onResume]

De methode [onResume] wordt uitgevoerd kort voordat de weergave die bij de activiteit hoort, zichtbaar wordt. Deze wordt hier gebruikt om een tabblad te selecteren na het opslaan of herstellen:


  @Override
  public void onResume() {
    // bovenliggend
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // als er een herstel plaatsvindt, moet het laatst geselecteerde tabblad worden hersteld
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • regel 10: selectie van het tabblad dat geselecteerd was vóór het opslag-/herstelproces. Houd er hierbij rekening mee dat in de methode [onCreate], die in de levenscyclus van de activiteit vóór de methode [onResume] wordt uitgevoerd, de navigatie bij het selecteren van een tabblad is uitgeschakeld. Hier vindt dus wel een tabbladsselectie plaats, maar er vindt geen fragmentwisseling plaats;

2.5.7.14. Résumé

De abstracte klasse [AbstractActivity] wordt de bovenliggende klasse van de enige activiteit van de applicatie.

De onderliggende activiteit moet de volgende zes methoden implementeren:


  // onderliggende klassen
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

protected abstract int getFirstView();

De dochteractiviteit heeft bovendien toegang tot de volgende beschermde elementen van haar bovenliggende klasse:


  // de sessie
  protected ISession session;
  // de fragmentcontainer
  protected MyPager mViewPager;
  // tabbladbalk
  protected CustomTabLayout tabLayout;
  // naam van de klasse
protected String className;

2.5.8. De activiteit [MainActivity]

  

De klasse [MainActivity] kan een andere naam hebben. De enige vereiste is dat deze de interface [IMainActivity] implementeert. De standaardklasse is als volgt:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // laag [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessie
  private Session session;

  // methoden van de bovenliggende klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessie
    this.session = (Session) super.session;
    // todo: we gaan verder met de initialisaties die door de bovenliggende klasse zijn gestart
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // todo: definieer hier de fragmenten
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo: de titels van de fragmenten hier definiëren
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // todo: navigatie via tabbladen – de weer te geven weergave definiëren
  }

  @Override
  protected int getFirstView() {
    // todo: navigatie via tabbladen – de eerste weer te geven weergave definiëren
    return 0;
  }
}
  • regel 14: om de notatie AA [@Bean] op regel 19 te kunnen begrijpen, moet de activiteit de notatie AA [@EActivity] hebben;
  • regel 15: de activiteit is gekoppeld aan het menu XML [menu_main]. Momenteel is dit menu leeg. De ontwikkelaar moet het aanvullen indien nodig;
  • regel 16: de klasse is een uitbreiding van de klasse [AbstractActivity];
  • regels 19-20: een verwijzing naar de laag [DAO]. Deze wordt geïnstantieerd door de bibliotheek AA voordat dit veld wordt geïnitialiseerd. Dit houdt in dat de bean AA [Dao] moet bestaan. Dit is altijd het geval bij de door ons geleverde skeletapplicatie. Zelfs in een applicatie zonder de laag [DAO] kan het pakket [dao] blijven bestaan. Dit leidt niet tot complicaties;
  • regel 22: de sessie als instantie van het type [Session]. De sessie bestaat in de bovenliggende klasse [AbstractActivity], maar als instantie van de interface [ISession] (regel 32);
  • regels 24-63: de zes methoden die door de bovenliggende klasse [AbstractActivity] worden opgelegd;
  • regels 36-39: de methode [getDao] retourneert een verwijzing naar de laag [DAO]. Hier is deze verwijzing nooit null. In de bovenliggende klasse [AbstractActivity] is echter rekening gehouden met het geval waarin de onderliggende klasse een verwijzing null zou retourneren om aan te geven dat er geen laag [DAO] was. Als men van deze mogelijkheid gebruik wil maken (wat naar mijn mening niet erg nuttig is), moet hier de pointer null worden geretourneerd;

2.6. De laag [DAO]

Image

  

2.6.1. De interface IDao

Deze is beschreven in paragraaf 2.5.5:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL van de webservice
  void setUrlServiceWebJson(String url);

  // gebruiker
  void setUser(String user, String mdp);

  // time-out van de client
  void setTimeout(int timeout);

  // basisauthenticatie
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // debugmodus
  void setDebugMode(boolean isDebugEnabled);

  // wachttijd van de client in milliseconden vóór het verzoek
  void setDelay(int delay);

  // todo: definieer hier uw interface
}

De ontwikkelaar voegt de methoden van zijn laag [DAO] toe vanaf regel 24.

2.6.2. De interface [WebClient]

  

De interface [WebClient] is als volgt:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // todo: definieer hier de te bereiken URL
}

De ontwikkelaar voegt de methoden toe die communiceren met de URL die door de server jSON worden blootgesteld, vanaf regel 17.

2.6.3. De authenticatie-interceptor [MyAuthInterceptor]

  

De klasse [MyAuthInterceptor] ziet er als volgt uit:


package client.android.dao.service;

import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // gebruiker
  private String user;
  // wachtwoord
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // HTTP-headers van het onderschepte verzoek HTTP
    HttpHeaders headers = request.getHeaders();
    // de header HTTP voor basisauthenticatie
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // toevoeging aan de headers van HTTP
    headers.setAuthorization(auth);
    // de levenscyclus van het verzoek HTTP wordt voortgezet
    return execution.execute(request, body);
  }

  // authenticatie-elementen
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

Deze klasse genereert de volgende authenticatie-header HTTP:

Authorization: Basic code

waarbij [code] de Base64-code is van de tekenreeks 'user:mp'. Deze klasse wordt alleen gebruikt als de server jSON deze vorm van authenticatie verwacht. Er bestaan nog andere klassen.

Opmerking: het gebruik van deze klasse wordt geïllustreerd in paragraaf 3.6.3.1.

2.6.4. De klasse [AbstractDao]

  

De klasse [AbstractDao] is als volgt:


package client.android.dao.service;

import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;

public abstract class AbstractDao {

  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
  // debugmodus
  protected boolean isDebugEnabled;
  // naam van de klasse
  protected String className;
  // wachttijd voor het uitvoeren van een verzoek
  private int delay;

  // constructor
  public AbstractDao() {
    // naam van de klasse
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }

  // beschermde methoden ----------------------------------------------------------
  // generieke interface
  protected interface IRequest<T> {
    T getResponse();
  }

  // generieke aanvraag naar een webservice / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // logboek
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // uitvoering van de service – er wordt één antwoord verwacht
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // uitvoering van een webservice
        try {
          // in afwachting?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // de synchrone aanvraag wordt uitgevoerd
          T response = request.getResponse();
          // log
          if (isDebugEnabled) {
            String log;
            if (response instanceof String) {
              log = (String) response;
            } else {
              log = mapper.writeValueAsString(response);
            }
            Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
          }
          // het antwoord wordt naar de waarnemer verzonden
          subscriber.onNext(response);
          // het einde van de observable wordt gemeld
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // log
          if (isDebugEnabled) {
            try {
              Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
            } catch (JsonProcessingException e1) {
              Log.d(className, String.format("Erreur jSON imprévue"));
            }
          }
          // er wordt een uitzondering gegenereerd
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // debugmodus
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • regels 35-81: de methode [getResponse] gebruikt de bibliotheek RxAndroid om een type [Observable<T>] weer te geven. In tegenstelling tot sommige eerder genoemde voorbeelden wordt hier geen type [Response<T>] weergegeven – dat is een eigen type – maar een willekeurig type T;
  • regel 35: de methode [getResponse] ontvangt als parameter een instantie van het type [IRequest<T>] uit de regels 30-32, waarvan de methode [IRequest.getReponse()] het type T verkrijgt via een synchrone bewerking HTTP;
  • regels 48-50: kunstmatig wordt er [delay] milliseconden gewacht. In de productieomgeving wordt [delay=0] ingesteld. Tijdens het debuggen wordt [delay=qqs secondes] ingesteld om de gebruiker de kans te geven de asynchrone bewerking te annuleren en zo te zien hoe de code zich dan gedraagt;
  • regel 52: het verwachte antwoord wordt opgevraagd met een synchrone aanvraag;
  • regel 64: zodra het antwoord is ontvangen, wordt het doorgegeven aan de observer;
  • regel 66: er wordt aangegeven dat er geen verdere uitzendingen meer zullen plaatsvinden. We hebben hier te maken met het specifieke geval van een asynchrone actie die slechts één element retourneert;
  • regels 67-78: in geval van een uitzondering wordt de uitzondering doorgegeven aan de waarnemer (regel 77);

2.6.5. De klasse [Dao]

  

De klasse [Dao] is als volgt:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // webserviceklant
  @RestService
  protected WebClient webClient;
  // beveiliging
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // de RestTemplate
  private RestTemplate restTemplate;
  // fabriek van de RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // logboek
    Log.d(className, "afterInject");
    // de restTemplate wordt gebouwd
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // de converter jSON wordt geïnstalleerd
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // we stellen de restTemplate van de webclient in
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // de URL van de webservice wordt ingesteld
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // de gebruiker wordt geregistreerd in de interceptor
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // fabrieksconfiguratie
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authenticatie-interceptor?
    if (isBasicAuthentificationNeeded) {
      // de authenticatie-interceptor wordt toegevoegd
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // privé-methoden -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // todo: implementatie IDao
}
  • regels 21-22: injectie van de bean AA [WebClient] die de communicatie met de webserver / jSON zal verzorgen;
  • regels 24-25: injectie van de authenticatie-interceptor;
  • regels 31-42: methode die wordt uitgevoerd na het invoeren van de velden uit de regels 21-25;
  • regel 37: het object [RestTemplate], dat de communicatie tussen client en server verzorgt, wordt aangemaakt op basis van een factory. Dit is niet strikt noodzakelijk, maar via het object factory kunnen de time-outs voor de communicatie worden geconfigureerd. Daarom gebruiken we de constructor zonder parameters [RestTemplate()] niet;
  • regel 39: we voegen een converter jSON toe aan de converters van de [RestTemplate]. Dit zal de enige converter zijn. Wanneer een methode van de client [WebClient] een string jSON van de server ontvangt, wordt deze automatisch gedeserialiseerd naar het object dat de methode moet retourneren;
  • regel 41: het aldus geconfigureerde object [RestTemplate] wordt doorgegeven aan de webclient, die hiermee de communicatie tussen client en server zal verzorgen;
  • regels 44-48: de hoofdmap van de webserver / jSON wordt ingesteld op URL. Alle URL die in de klasse [WebClient] zijn gedeclareerd, zijn URL die betrekking hebben op deze URL-root;
  • regels 50-54: met deze methode kan de eigenaar van de verbinding worden gespecificeerd wanneer deze wordt gecontroleerd door een basisautorisatie (zie paragraaf 2.6.3);
  • regels 56-64: hierin worden de timeouts voor de communicatie tussen client en server vastgelegd. Dit gebeurt via de factory van het object [RestTemplate] dat de communicatie regelt;
  • regels 66-78: met deze methode kan worden aangegeven dat de server wordt beveiligd door basisauthenticatie;
  • regels 72-77: als basisauthenticatie wordt gevraagd, wordt de authenticatie-interceptor die op regel 25 is ingevoegd, toegevoegd aan de interceptors van het object [RestTemplate]. Deze interceptor voegt automatisch aan alle verzoeken van de webclient de regel HTTP met basisauthenticatie toe, zoals verwacht door de server;
  • de ontwikkelaar implementeert de interface [IDao] vanaf regel 87;

2.7. De fragmenten

  

2.7.1. De klasse [MenuItemState]

De klasse [MenuItemState] kapselt de status van een menuoptie in:


package client.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
...
}

2.7.2. De klasse [Utils]

De klasse [Utils] bevat statische hulpprogramma's:


package client.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 uitzonderingsstack
    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 getMessageForAlert(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();
  }

  // lijst met foutmeldingen van een uitzondering – versie 3
  static public String getMessageForAlert(List<String> messages) {
    // de weer te geven tekst wordt samengesteld
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // resultaat
    return texte.toString();
  }
}

2.7.3. De bovenliggende klasse [AbstractFragment]

De klasse [AbstractFragment] bevat de elementen die alle fragmenten van de applicatie gemeen hebben. Net als in de klasse [AbstractActivity] is de code complex. Ook hier zullen we deze stapsgewijs analyseren.

2.7.3.1. Het raamwerk


package client.android.architecture.core;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

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

public abstract class AbstractFragment extends Fragment {

  // vertrouwelijke gegevens ------------------------------------------------------------
  // abonnementen op observables
  private List<Subscription> abonnements = new ArrayList<>();
  // fragmentmenu
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // levenscyclus van het fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // status van het fragment
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // levenscyclus van het fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // asynchrone taken
  private boolean runningTasksHaveBeenCanceled;

  // gegevens  toegankelijk voor onderliggende klassen ---------------------------------------
  // debugmodus
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // naam van de klasse
  protected String className;
  // asynchrone taken
  protected int numberOfRunningTasks;
  // activiteit
  protected IMainActivity mainActivity;
  protected Activity activity;
  // sessie
  protected Session session;


  // Fragment bijwerken ----------------------------------------------------------------------------------
 ...

  // menubeheer ------------------------------------------
  ...

  // wachtrijbeheer -------------------------------------------------------------
...

  // beheer van asynchrone bewerkingen --------------------------------------------------------------------
...

  // uitzonderingsbeheer -------------------------------------------------------------------
....

  // beheer van de levenscyclus van het fragment --------------------------------------------------------
...

  // afgeleide klassen -----------------------------------------------------
  public abstract CoreState saveFragment();

  protected abstract int getNumView();

  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();

  protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);

}
  • regels 28-45: de privégegevens van de klasse;
  • regels 47-58: de beschermde gegevens die toegankelijk zijn voor de onderliggende klassen;
  • regels 61-62: code die het fragment bijwerkt dat zal worden weergegeven;
  • regels 64-65: hulpprogrammacode om het eventuele menu te beheren;
  • regels 67-68: hulpprogrammacode voor het beheren van de wachttijd tijdens een asynchrone bewerking;
  • regels 70-71: code om de communicatie van het fragment met de laag [DAO] te vergemakkelijken;
  • regels 73-74: hulpprogrammacode om eventuele uitzonderingen op standaardwijze af te handelen;
  • regels 76-77: code die de levenscyclus van het fragment beheert;
  • regels 80-94: de bovenliggende klasse schrijft 8 methoden voor aan haar onderliggende klassen;

2.7.3.2. De constructor

De constructor van de klasse is als volgt:


  // naam van de klasse
  protected String className;
  // levenscyclus van het fragment
  private boolean fragmentHasToBeInitialized = false;
...
  // constructor ----------------------
  public AbstractFragment() {
    // init
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // log
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • regel 9: hier wordt de naam van de dochterklasse genoteerd die hier wordt geïnstantieerd. Deze naam wordt gebruikt in alle logboeken van de bovenliggende klasse;
  • regel 10: er wordt genoteerd dat het fragment wordt geconstrueerd. Deze informatie wordt gebruikt wanneer het dochterfragment wordt gevraagd om zichzelf bij te werken;

2.7.3.3. Beheer van het menu

In onze architectuur moet elk fragment een menu hebben, zelfs als het leeg is. Uit de logbestanden is namelijk gebleken dat wanneer de methode [onCreateOptionsMenu] wordt uitgevoerd – wat gebeurt wanneer het fragment een menu heeft – het fragment al is gekoppeld aan zijn activiteit, zijn weergave en zijn menu en zichtbaar zal worden. Dit is dus het moment waarop de visuele interface en het menu kunnen worden bijgewerkt. In deze methode [onCreateOptionsMenu] vragen we het onderliggende fragment om zichzelf bij te werken.

Het menubeheer omvat hulpprogramma's waarmee het dochterfragment menu-items al dan niet kan weergeven:


  // fragmentmenu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // menu beheer ------------------------------------------
  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);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // resultaat
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // we halen de ID's van de menuopties op
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // we zetten de menuopties in een array
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // optie-ID
      int id = menuOptionsIds.get(i);
      // status van de optie
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // resultaat
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  // statussen van de menuopties
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // status
      MenuItemState state = this.menuOptionsStates[i];
      // menu-ID
      int id = state.getMenuItemId();
      // status initialiseren
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // resultaat
    return menuOptionsStates;
  }

  // menuopties weergeven -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // alle menuopties worden bijgewerkt
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // bepaalde menuopties worden bijgewerkt
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • regel 6-18: met deze methode kunnen de numerieke identificatiecodes van alle menuopties worden opgehaald;
  • regel 6: de methode [getMenuOptions] ontvangt twee parameters:
    • [Menu menu]: het menu van het fragment;
    • [List<Integer> menuOptionsIds]: de lijst met Android-identificatiecodes van de menuopties. In eerste instantie is deze lijst leeg. Vervolgens wordt deze gevuld door een recursieve doorloop (regel 15) van de menustructuur;
  • regels 20-40: op basis van het menu wordt de tabel met statussen (ID, zichtbaarheid) van de menuopties samengesteld. Deze tabel wordt opgeslagen in regel 3. De klasse [MenuItemState] is beschreven in paragraaf 2.7.1;
  • regels 43-55: een variant op de vorige methode. Deze doet hetzelfde, maar in plaats van de identificatiecodes van alle menuopties opnieuw te berekenen – wat al is gebeurd – gebruikt deze de identificatiecodes uit de statuslijst van regel 3;
  • regels 58-63: met de methode [setAllMenuOptionsStates] kunnen alle menuopties van het fragment worden verborgen of weergegeven;
  • regels 65-69: met de methode [setMenuOptionsStates] kunnen bepaalde menuopties selectief worden weergegeven of verborgen;
  • de methoden [getMenuOptions, getMenuOptionsStates] zijn als privé gedeclareerd omdat ze uitsluitend in [AbstractFragment] worden gebruikt. De methoden [setAllMenuOptionsStates] (regel 58) en [setMenuOptionsStates] (regel 65) zijn als ‘protected’ gedeclareerd, zodat ze beschikbaar zijn voor afgeleide klassen;

2.7.3.4. Beheer van het wachten op het einde van een asynchrone taak


   // abonnementen op observables
  private List<Subscription> abonnements = new ArrayList<>();
// asynchrone taken
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // beheer van het wachten op het einde van een asynchrone bewerking -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // het aantal taken dat zal worden uitgevoerd wordt genoteerd
    this.numberOfRunningTasks = numberOfRunningTasks;
    // de wachtstatus wordt ingesteld
    mainActivity.beginWaiting();
    // de lijst met abonnementen wordt leeggemaakt
    abonnements.clear();
    // nog geen annulering
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // de wachtafbeelding wordt verborgen
    mainActivity.cancelWaiting();
  }

  • regels 9-18: om een of meer asynchrone bewerkingen te starten, roept het dochterfragment de bovenliggende methode [beginRunningTasks] aan. De parameter van deze methode is het aantal asynchrone taken dat het dochterfragment gaat starten;
  • regel 11: de parameter van de methode wordt opgeslagen;
  • regel 13: de wachtafbeelding wordt weergegeven;
  • regel 15: de lijst met abonnementen op asynchrone bewerkingen wordt gewist. Deze zijn nog niet aangemaakt door het dochterfragment;
  • regel 17: er wordt een booleaanse variabele bijgehouden om aan te geven dat de door het dochterfragment aangevraagde asynchrone taken zijn geannuleerd. In eerste instantie heeft deze booleaanse variabele de waarde false;
  • regels 20-25: het dochterfragment roept de bovenliggende methode [cancelWaitingTasks] aan om aan te geven dat het de taken die het heeft gestart, wil annuleren;
  • regel 22: de wachtafbeelding wordt verborgen;

2.7.3.5. Uitzonderingsafhandeling


  // uitzonderingsafhandeling -------------------------------------------------------------------

  // waarschuwing bij uitzondering weergeven
  protected void showAlert(Throwable th) {
    // berichten uit de uitzonderingsstapel van de Throwable th weergeven
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  // lijst met berichten weergeven
  protected void showAlert(List<String> messages) {
    // de lijst met berichten wordt weergegeven
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • regels 4-7: met de methode [showAlert(Throwable)] kan een dochterfragment de berichten uit de uitzonderingsstack van de als parameter doorgegeven Throwable in een venster weergeven;
  • regels 10-13: de methode [showAlert(List<String>] stelt een onderliggend fragment in staat om de als parameter doorgegeven lijst met berichten in een venster weer te geven;
  • de klasse [Utils] die in de regels 6 en 12 wordt gebruikt, is beschreven in paragraaf 2.7.2;

2.7.3.6. Beheer van asynchrone bewerkingen


...
  // abonnementen op observables
  private List<Subscription> abonnements = new ArrayList<>();
  // asynchrone taken
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // uitvoering van een asynchrone taak met RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // proces: de uit te voeren / te observeren observable
    // consumeResult: de methode die de verkregen respons verwerkt
    // 
    // er worden alleen nieuwe abonnementen aangemaakt als er geen annulering heeft plaatsgevonden
    if (!runningTasksHaveBeenCanceled) {
      // uitvoering op de I/O-thread en observatie op de UI-thread
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // de observable wordt uitgevoerd
      try {
        abonnements.add(process.subscribe(
          // het resultaat wordt verbruikt
          consumeResult,
          // verwerking van uitzondering
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // einde van de taak
          new Action0() {

            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }

  private void endOfTask() {
...
  }

  // een asynchrone bewerking heeft een uitzondering gegenereerd
  // of er is een uitzondering opgetreden tijdens de uitvoering van een asynchrone bewerking
  private void consumeThrowable(Throwable th) {
...
  }

  • regels 9-41: voeren een asynchrone taak uit;
  • regel 9: de methode [executeInBackground] verwacht twee parameters:
    • [Observable<T> process]: het uit te voeren asynchrone proces;
    • [Action1<T> consumeResult]: de methode van het dochterfragment die moet worden aangeroepen om de door het proces gegenereerde elementen door te geven. In onze voorgaande voorbeelden hebben de processen altijd slechts één element gegenereerd. Het type T van [Action1<T>] is het type T van het resultaat dat door het geobserveerde proces wordt geretourneerd;
  • regel 14: de asynchrone taak wordt alleen gestart als er nog geen annulering door de gebruiker of door het programma (als gevolg van een uitzondering) heeft plaatsgevonden;
  • regel 16: het proces is geconfigureerd om op een I/O-thread te draaien en wordt geobserveerd op de UI-thread;
  • regel 16: de instructie [process.subscribe] start de uitvoering van het proces in de I/O-thread. Binnen deze thread verlopen de bewerkingen synchroon, omdat we gebruikmaken van een bibliotheek HTTP die synchroon is;
  • regel 19: de methode [process.subscribe] heeft drie parameters:
    • regel 21: [consumeResult]: de methode van het dochterfragment die de door het proces uitgegeven elementen zal verwerken;
    • regels 22-28: de methode die wordt uitgevoerd wanneer er een uitzondering is opgetreden tijdens de verwerking van de asynchrone taak. De verwerking wordt gedelegeerd aan de methode [consumeThrowable] op regel 49;
    • regels 29-36: de methode die wordt uitgevoerd wanneer de taak de melding van het einde van de verzending verstuurt. De verwerking wordt gedelegeerd aan de methode [endOfTask] op regel 43;
  • regel 19: de zojuist gestarte asynchrone taak wordt geregistreerd in het veld [abonnements], waarin alle gestarte asynchrone taken worden bijgehouden. Hierdoor kunnen ze indien nodig worden geannuleerd;
  • regels 37-39: methode die wordt uitgevoerd wanneer er een uitzondering is opgetreden tijdens de verwerking van de asynchrone taak. De verwerking wordt gedelegeerd aan de methode [consumeThrowable] op regel 49;

De methode [endOfTask] is als volgt:


  // asynchrone taken
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // één taak minder om op te wachten
    numberOfRunningTasks--;
    // klaar?
    if (numberOfRunningTasks == 0) {
      // wachten voltooid
      cancelWaitingTasks();
      // het einde van de taken wordt doorgegeven aan de dochterklasse
      notifyEndOfTasks(false);
    }
  }
...
  // dochterklassen -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • regel 6: een asynchrone taak is zojuist voltooid. De teller van actieve taken wordt verlaagd;
  • regel 8: als er geen actieve taken meer zijn, heeft het dochterfragment al zijn antwoorden ontvangen;
  • regel 10: het wachten wordt geannuleerd;
  • regel 12: het dochterfragment wordt geïnformeerd dat alle door haar gestarte taken zijn voltooid door haar methode [notifyEndOfTasks] aan te roepen. De parameter van deze methode geeft aan hoe de taken zijn beëindigd: normaal, of door annulering door de gebruiker of door de code vanwege een opgetreden uitzondering. In regel 12 wordt een normale beëindiging gemeld. Merk op dat het dochterfragment zich geen zorgen hoeft te maken over het bijhouden van de nog actieve taken. De bovenliggende klasse doet dit voor haar;

De methode [consumeThrowable] is als volgt:


  // asynchrone taken
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // er is een uitzondering opgetreden bij een asynchrone bewerking
  // of er is een uitzondering opgetreden tijdens de uitvoering van een asynchrone bewerking
  private void consumeThrowable(Throwable th) {
    // th: de te verwerken uitzondering
    // 
    // log
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // reeds gestarte taken worden geannuleerd
    cancelRunningTasks();
    // de foutmeldingen worden weergegeven
    showAlert(th);
  }

  // taken annuleren
  protected void cancelRunningTasks() {
    // log
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // alle geregistreerde asynchrone taken worden geannuleerd
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // de annulering wordt geregistreerd
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // einde van het wachten
    cancelWaitingTasks();
    // de annulering van de taken wordt doorgegeven aan het dochterfragment
    notifyEndOfTasks(true);
}

...
  // dochterklassen -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • regel 3: de methode [consumeThrowable] ontvangt de uitzondering die zich heeft voorgedaan;
  • regel 15: alle nog actieve taken worden geannuleerd;
  • regel 17: de tekst van de uitzondering wordt weergegeven;
  • regels 21-37: alle taken worden geannuleerd;
  • regels 27-29: alle abonnementen worden geannuleerd;
  • regel 31: er wordt genoteerd dat er een annulering heeft plaatsgevonden;
  • regel 32: de taakenteller wordt op nul gezet;
  • regel 34: het wachten wordt geannuleerd;
  • regel 36: het dochterfragment wordt geïnformeerd over het einde van de taken bij annulering;

2.7.3.7. Beheer van de levenscyclus van het fragment


  // levenscyclus --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // bovenliggend
    super.onDestroyView();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // bovenliggend
    super.onDestroy();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroy");
    }
  }

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

  private void saveState() {
...
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
...
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
...
}
  • regels 2-20: de methoden [onDestroyView, onDestroy] zijn uitsluitend bedoeld voor de logbestanden. Deze stellen de ontwikkelaar in staat om de levenscyclus van de fragmenten beter te begrijpen;

Het opslaan van het fragment bij het draaien van het apparaat gebeurt via de volgende methoden [setUserVisibleHint, onSaveInstanceState, saveState]:


  // levenscyclus van het fragment
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // bovenliggend
    super.setUserVisibleHint(isVisibleToUser);
    // opslaan?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // het fragment wordt verborgen – we maken er een back-up van
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // geheugen
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // logboek
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // bovenliggend
    super.onSaveInstanceState(outState);
    // het fragment wordt alleen opgeslagen als het zichtbaar is
    if (isVisibleToUser) {
      // misschien is het fragment al opgeslagen
      if (!saveFragmentDone) {
        saveState();
      }
      // herstel moet in elk geval worden uitgevoerd
      session.setAction(ISession.Action.RESTORE);
    }
}
  • regels 6-19: het fragment wordt opgeslagen als het van de weergegeven naar de verborgen status overgaat (regel 11). De methode [setUserVisibleHint] geeft ons deze informatie;
  • regel 14: het opslaan gebeurt via de privémethode van de regels 21-23;
  • regels 25-41: bij het draaien van het apparaat wordt de methode [onSaveInstanceState] aangeroepen. Het fragment wordt onder twee voorwaarden opgeslagen:
    • het is zichtbaar (regel 34);
    • het is nog niet opgeslagen (regel 36). Het is mogelijk dat de methoden [setUserVisibleHint, onSaveInstanceState] niet allebei kunnen worden uitgevoerd wanneer het fragment zichtbaar is, en dat het beheer van de booleaanse variabele [saveFragmentDone] dus overbodig is. Bij twijfel heb ik ervoor gekozen deze te gebruiken;
  • regel 40: na het opslaan volgt het terugzetten. Merk op dat, wanneer het fragment de volgende keer moet worden bijgewerkt, dit moet gebeuren via een bewerking [RESTORE];

Let op de twee momenten waarop een opslag van het fragment wordt aangevraagd:

  1. wanneer het fragment van de zichtbare naar de verborgen status overgaat;
  2. wanneer het apparaat wordt gedraaid;

De privémethode [saveState] is als volgt:


...
  private void saveState() {
    // taken annuleren?
    if (numberOfRunningTasks != 0) {
      // taken worden geannuleerd
      cancelRunningTasks();
    }
    // de status van het fragment wordt opgeslagen
    CoreState currentState = saveFragment();
    // het fragment is bezocht
    currentState.setHasBeenVisited(true);
    // status van het menu opslaan
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // inloggen
    session.setCoreState(getNumView(), currentState);
    // opslag voltooid
    saveFragmentDone = true;
    // logboek
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  // dochterklassen -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • regel 4-7: het apparaat kan worden gedraaid terwijl er asynchrone bewerkingen aan de gang zijn. Hier wordt besloten om ze allemaal te annuleren. Dit is geen goede beslissing voor de gebruiker, die een nieuwe, mogelijk tijdrovende aanvraag moet indienen terwijl hij alleen maar zijn telefoon of tablet heeft verplaatst of een telefoontje heeft ontvangen. Het is mogelijk om de netwerkverbindingen te behouden tijdens een back-up-/herstelcyclus. De oplossingen zijn echter niet voor de hand liggend en ik heb besloten ze in deze cursus voor beginners niet te behandelen. De juiste aanpak is om deze netwerkverbindingen tot stand te brengen via een fragment zonder gekoppelde visuele interface, dat niet wordt vernietigd tijdens de back-up-/herstelcyclus. Hiervoor volstaat het om de instructie [Fragment.setRetainInstance(true)] te gebruiken;
  • regel 9: we vragen het dochterfragment om zijn status op te slaan in een type dat is afgeleid van [CoreState] (regel 31);
  • regel 11: er wordt genoteerd dat het fragment is bezocht. Deze informatie is nuttig. Wanneer een fragment voor de eerste keer wordt bezocht, kan de update ervan afwijken van de volgende, omdat het dan geen eerdere status in de sessie heeft;
  • regel 13: de status van het menu wordt opgeslagen, waardoor we het automatisch kunnen herstellen;
  • regel 15: deze huidige status wordt opgeslagen in de sessie. Hierin worden de statussen gegroepeerd per weergave/fragment, waarbij elk een eigen status heeft. Het nummer van de weergave wordt geleverd door het onderliggende fragment (regel 33);
  • regel 17: er wordt genoteerd dat het fragment is opgeslagen. Dit omdat twee methoden de methode [saveState] kunnen aanroepen en het zinloos is om twee keer op te slaan;

Het opnieuw genereren van de weergave die aan het fragment is gekoppeld, wordt verzorgd door de volgende methode:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // bovenliggende klasse
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // de weergave moet worden hersteld
    viewHasToBeInitialized = true;
}

In de levenscyclus wordt de methode [onActivityCreated] direct na de methode [onCreateView] uitgevoerd. Het aanroepen van deze laatste methode geeft aan dat de aan het fragment gekoppelde weergave opnieuw moet worden opgebouwd. Dit wordt eenvoudigweg genoteerd in regel 10.

2.7.3.8. Het fragment bijwerken

Het bijwerken van het fragment is de laatste bewerking die op het fragment wordt uitgevoerd voordat het zichtbaar wordt en in afwachting gaat van acties van de gebruiker. Dit wordt verzorgd door de volgende code:


  // fragmentmenu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // levenscyclus van het fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragmentstatussen
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // levenscyclus van het fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // Fragment bijwerken ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // logboek
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // geheugen
    this.menu = menu;
    // de # menuopties worden opgehaald als dit nog niet is gebeurd
    if (fragmentHasToBeInitialized) {
      // de # menuopties worden opgehaald
      getMenuOptionsStates(menu);
      // activiteit
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // de vorige status van het fragment wordt opgehaald (de allereerste keer heeft alleen de booleaanse waarde hasBeenVisited betekenis)
    previousState = session.getCoreState(getNumView());
    // het dochterfragment in meerdere stappen bijwerken
    // stap 1 – is dit het eerste bezoek?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // dit is niet het eerste bezoek
      // stap 2: moet het fragment worden geïnitialiseerd?
      ...
      // stap 3: moet de weergave worden geïnitialiseerd?
      ...
    }
    // stap 4: een verzending, een navigatie, een herstel?
    ...

    // stap 5: eindupdates ----------------------
...
  }
...
  // onderliggende klassen -----------------------------------------------------
  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();
  • regel 19: de methode [onCreateOptionsMenu] wordt gebruikt om het fragment bij te werken. Daarom moet het fragment een menu hebben, dat indien nodig leeg mag zijn. Wanneer deze methode wordt uitgevoerd, is het fragment gekoppeld aan zijn weergave en zijn activiteit en is het bovendien zichtbaar;
  • regel 25: het menu dat als parameter (regel 22) aan de methode is doorgegeven, wordt opgeslagen;
  • regels 27-34: als het fragment moet worden geïnitialiseerd:
    • regel 29: de statussen van de menuopties worden opgeslagen in de array [menuOptionsStates] uit regel 3;
    • regel 31: de activiteit wordt opgeslagen als een instantie van het Android-type [Activity];
    • regel 32: de activiteit wordt opgeslagen als een instantie van de interface [IMainActivity];
    • regel 33: de sessie wordt opgeslagen. De typewijziging is nodig, omdat de methode [mainActivity.getSession()] een type [ISession] retourneert;
  • regel 36: de vorige status van het fragment wordt uit de sessie opgehaald. Als dit het eerste bezoek aan het fragment is, is alleen de booleaanse waarde [previousState.hasBeenVisited] van betekenis;
  • regels 39-44: code die wordt uitgevoerd wanneer het fragment voor het eerst wordt bezocht. In dit geval is de vorige status niet relevant;
  • regels 44-50: code die wordt uitgevoerd wanneer dit niet het eerste bezoek aan het fragment is;
  • regels 46-47: code die wordt uitgevoerd als de constructor van het fragment is aangeroepen (fragmentHasToBeInitialized==true);
  • regels 48-49: code die wordt uitgevoerd als de aan het fragment gekoppelde weergave opnieuw is opgebouwd (viewHasToBeInitialized==true);
  • regels 51-52: code wordt uitgevoerd afhankelijk van de huidige actie (SUBMIT, NAVIGATION, RESTORE);
  • regels 54-55: code wordt nog steeds uitgevoerd;

De vijf stappen van de update zijn als volgt:

stap 1


  // fragmentmenu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // levenscyclus van het fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragmentstatussen
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // levenscyclus van het fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // de vorige status van het fragment wordt opgehaald (de allereerste keer heeft alleen de booleaanse waarde hasBeenVisited betekenis)
    previousState = session.getCoreState(getNumView());
    // het dochterfragment in meerdere stappen bijwerken
    // stap 1 – is dit het eerste bezoek?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // initialisatie van fragment en weergave
      initFragment(null);
      initView(null);
      // previousState resetten voor het vervolg
      previousState = null;
    } else {
      // dit is niet het eerste bezoek
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • regel 19: de vorige status van het fragment wordt opgehaald uit de sessie;
  • regels 22-31: code wordt uitgevoerd als het fragment nog nooit is bezocht;
  • regel 27: de dochterklasse wordt gevraagd het fragment te initialiseren. De parameter van de methode [initFragment] op regel 35 is de vorige status van het fragment. Hier wordt null doorgegeven om het dochterfragment aan te geven dat dit het eerste bezoek is;
  • regel 28: de dochterklasse wordt gevraagd de bij het fragment behorende weergave te initialiseren. De parameter van de methode [initView] in regel 37 is de vorige status van het fragment. Hier wordt null doorgegeven om aan het dochterfragment aan te geven dat dit het eerste bezoek is;
  • regel 30: de vorige status wordt ingesteld op null voor de volgende stappen;

stappen 2 en 3


// fragmentmenu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // levenscyclus van het fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // statussen van het fragment
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // levenscyclus van het fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // de vorige status van het fragment wordt opgehaald (de allereerste keer heeft alleen de booleaanse waarde hasBeenVisited betekenis)
    previousState = session.getCoreState(getNumView());
    // het dochterfragment in meerdere stappen bijwerken
    // stap 1 – is dit het eerste bezoek?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // dit is niet het eerste bezoek
      // stap 2: moet het fragment worden geïnitialiseerd?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // dochterfragment
        initFragment(previousState);
      }
      // stap 3: moet de weergave worden geïnitialiseerd?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // dochterfragment
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • regels 24-42: worden uitgevoerd wanneer dit niet het eerste bezoek aan het fragment is;
  • regels 27-33: als het fragment zojuist is gereconstrueerd, wordt het gereset door de methode [initFragment] van de dochterklasse aan te roepen (regels 32, 46). De vorige status van het fragment wordt hieraan doorgegeven;
  • regels 35-51: als de aan het fragment gekoppelde weergave moet worden geïnitialiseerd of opnieuw geïnitialiseerd, wordt het dochterfragment gevraagd dit te doen (regels 40, 48). Ook hier wordt de laatst bekende status van het fragment doorgegeven;

stap 4


// menu van het fragment
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // levenscyclus van het fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // statussen van het fragment
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // levenscyclus van het fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // de vorige status van het fragment wordt opgehaald (de allereerste keer heeft alleen de booleaanse waarde hasBeenVisited betekenis)
    previousState = session.getCoreState(getNumView());
    // het dochterfragment in meerdere stappen bijwerken
 ...

    // stap 4: een verzending, een navigatie, een herstel?
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
        Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // actie bezig
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // dochterfragment
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // menu herstellen
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // dochterfragment
          updateOnRestore(previousState);
        } else {
          // dit is een eerste bezoek – niets te doen
        }
        break;
      case RESTORE:
        // herstel
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // menu herstellen (previousState mag niet null zijn)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // dochterfragment
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • regels 34-66: de huidige actie wordt verwerkt; dit kan een van de volgende drie zijn:
    • RESTORE: het fragment wordt hersteld na een rotatie van het apparaat;
    • NAVIGATION: men keert terug naar het fragment met de bedoeling het terug te vinden in de toestand waarin het was achtergelaten bij het laatste gebruik;
    • SUBMIT: alle andere gevallen;
  • regel 34: de lopende actie wordt opgehaald;
  • regels 36-42: voor een actie van het type SUBMIT roepen we de methode [updateOnSubmit] van het onderliggende fragment aan (regels 41, 68) en geven we de laatst bekende status van het fragment door;
  • regels 43-55: voor een actie van het type NAVIGATION;
  • regels 47-54: we willen het fragment terugzetten naar de laatst bekende status. De bewerking van NAVIGATION kan samenvallen met een eerste bezoek. Dit zou bijvoorbeeld het geval zijn in een applicatie met tabbladen: als ik van tabblad 1 naar tabblad 4 ga:
    • moet ik het fragment van tabblad 4 initialiseren als het om het eerste bezoek gaat;
    • het fragment van tabblad 4 in de vorige toestand herstellen als het niet het eerste bezoek is;
  • regels 52-54: er gebeurt niets als het om een eerste bezoek gaat. Het is aan de dochtermethode [initView(CoreState previousState)] om deze initialisatie uit te voeren. Het eerste bezoek wordt gekenmerkt door de voorwaarde [previousState==null];
  • regel 49: als dit niet het eerste bezoek aan het fragment is, wordt het menu weergegeven;
  • regel 51: de dochterklasse wordt gevraagd zichzelf bij te werken door de methode op regel 70 aan te roepen. De vorige status van het fragment wordt aan haar doorgegeven, zodat ze haar werk kan doen;
  • regels 56-66: in het geval van een herstelbewerking van het fragment doen we hetzelfde als bij navigatie buiten het eerste bezoek;

stap 5


// fragmentmenu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // levenscyclus van het fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // status van het fragment
  private CoreState previousState;
  // mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // levenscyclus van het fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // stap 5: eindupdates ----------------------
    // we zijn van weergave veranderd
    session.setPreviousView(getNumView());
    // er is geen actie meer gaande
    session.setAction(ISession.Action.NONE);
    // wanneer dit fragment wordt verlaten, moet het worden opgeslagen
    saveFragmentDone = false;
    // zolang het fragment niet opnieuw is opgebouwd, hoeft het niet te worden geïnitialiseerd
    fragmentHasToBeInitialized = false;
    // zolang de weergave niet opnieuw is opgebouwd, hoeft deze niet te worden geïnitialiseerd
    viewHasToBeInitialized = false;
    // de normale werking van de tabbladenkeuze wordt hersteld
    session.setNavigationOnTabSelectionNeeded(true);

    // er wordt aan het fragment gemeld dat de weergave gereed is
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • regels 18-30: wanneer we hier aankomen, is het fragment geïnitialiseerd en klaar om te worden weergegeven. We zetten dan alle indicatoren die worden gebruikt bij het beheer van de levenscyclus van het fragment terug naar de begintoestand;
  • regel 20: er is van weergave gewisseld: dit wordt in de sessie genoteerd;
  • regel 22: er is geen actie meer gaande;
  • regel 24: wanneer we het nu weergegeven fragment gaan verlaten, moet het worden opgeslagen bij het verlaten;
  • regel 26: het fragment hoeft niet meer opnieuw te worden opgebouwd. Deze indicator wordt teruggezet naar vrai wanneer de constructor van het fragment opnieuw wordt uitgevoerd;
  • regel 28: de aan het fragment gekoppelde weergave hoeft niet meer te worden geïnitialiseerd. Deze indicator wordt teruggezet naar vrai wanneer de methode [onActivityCreated] opnieuw wordt uitgevoerd;
  • regel 30: het fragment wordt mogelijk weergegeven in een applicatie met tabbladen. In dat geval moet er een fragmentwisseling plaatsvinden wanneer de gebruiker op een van de tabbladen klikt;
  • regel 36: aan de onderliggende klasse wordt aangegeven dat het fragment gereed is. Deze kan in de methode [notifyEndOfUpdates] updates opnemen die in alle gevallen moeten worden uitgevoerd, een asynchrone bewerking starten om nieuwe gegevens op te halen, ...

2.7.4. Een voorbeeld van een fragment

  

In het project [client-android-skel] is een voorbeeldfragment opgenomen om de lezer de typische structuur van een fragment van een op dit project gebaseerde applicatie te laten zien.

De klasse [DummyFragment] ziet er als volgt uit:


package client.android.fragments.behavior;

import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;

public class DummyFragment extends AbstractFragment {

  // velden overgenomen van de bovenliggende klasse -------------------------------------------------------

  // debugmodus
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // klassenaam
  //-- beschermde String className;
  // asynchrone taken
  //-- protected int numberOfRunningTasks;
  // activiteit
  //-- protected IMainActivity mainActivity;
  //-- beschermde activiteit;
  // sessie
  //-- protected Session session;

  // van de bovenliggende klasse geërfde methoden -------------------------------------------------------

  // menuopties weergeven
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // beheer van het wachten op de voltooiing van een reeks asynchrone taken
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // uitvoering van een asynchrone taak met RxAndroid
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // taken annuleren
  //-- protected void cancelRunningTasks() {
  // waarschuwing weergeven bij uitzondering
  //-- protected void showAlert(Throwable th) {
  // lijst met berichten weergeven
  //-- protected void showAlert(List<String> messages) {

  // door de bovenliggende klasse opgelegde methoden -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // het fragment moet worden opgeslagen
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // alser niets is om op te slaan, voer dan [return new CoreState();] uit en verwijder de klasse [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // het fragmentnummer moet worden teruggestuurd naar de tabel met fragmenten die door de activiteit worden beheerd (zie MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // het fragment wordt zichtbaar en is in deze stap of een eerdere stap opgebouwd
    // dit gebeurt bij het opstarten van de applicatie en bij elke rotatie van het Android-apparaat
    // wordt onvermijdelijk gevolgd door de uitvoering van [initView]
    // de velden van het opnieuw opgebouwde fragment moeten worden geïnitialiseerd
    // previousState is de laatste opslag van het fragment – heeft de waarde null als dit het eerste bezoek aan het fragment is
  }

  @Override
  protected void initView(CoreState previousState) {
    // het fragment wordt zichtbaar en de bijbehorende weergave is in deze stap of een eerdere stap opnieuw opgebouwd
    // dit gebeurt telkens wanneer [initFragment] wordt uitgevoerd en telkens wanneer het fragment de nabijheid van het weergegeven fragment verlaat
    // de componenten van de opnieuw opgebouwde weergave moeten worden geïnitialiseerd
    // previousState is de laatste opgeslagen versie van het fragment – heeft de waarde null als het de eerste keer is dat het fragment wordt bezocht

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // wordt uitgevoerd na [initFragment, initView] als deze methoden worden uitgevoerd
    // de weergave wordt getoond na een bewerking van het type SUBMIT
    // moeten het fragment en de bijbehorende weergave doorgaans vanuit de sessie worden geïnitialiseerd
    // previousState is de laatste opslag van het fragment – is null als het de eerste keer is dat het fragment wordt bezocht
    // er hoeft niets te gebeuren als het fragment niet via een bewerking SUBMIT kan worden bereikt
    // als het fragment via bewerkingen SUBMIT vanuit verschillende fragmenten kan worden bereikt, kan de vorige weergave worden achterhaald via [session.getPreviousView]
    // als men via meerdere bewerkingen SUBMIT vanuit hetzelfde fragment bij het fragment kan komen, dan moet er een indicator in de sessie worden opgenomen om de verschillende soorten SUBMIT vanuit dit fragment van elkaar te onderscheiden
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // wordt uitgevoerd na [initFragment, initView] als deze methoden worden uitgevoerd
    // wordt de weergave getoond na een bewerking van het type RESTORE of NAVIGATION
    // previousState is de laatste opgeslagen versie van het fragment – is nooit null
    // de weergave moet in de vorige toestand worden teruggezet

  }

  @Override
  protected void notifyEndOfUpdates() {
    // wordt uitgevoerd na de methoden [updateOnSubmit, updateOnRestore]
    // op dit punt is de weergave al opgebouwd en geïnitialiseerd
    // vaak valt hier niets te doen, maar men kan hier ook acties samenvoegen die moeten worden uitgevoerd, ongeacht de manier waarop men op deze weergave terechtkomt
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // wordt aangeroepen wanneer de door het fragment gestarte asynchrone taken zijn voltooid of geannuleerd
    // deze twee gevallen kunnen worden onderscheiden aan de hand van de parameter runningTasksHaveBeenCanceled
    // over het algemeen moet de weergave in een andere toestand worden teruggezet dan waarin deze zich bevond terwijl deze wachtte op de antwoorden van de asynchrone taken

  }
}

De klasse [DummyFragment] hoeft geen status te hebben. Hier hebben we er een toegevoegd om te laten zien wat erin verwacht wordt:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  // status van het fragment [DummyFragment]
  // alleen serialiseerbare velden opnemen in jSON
  // voeg de annotatie @JsonIgnore toe aan de andere, maar het is onduidelijk waarvoor ze zouden kunnen dienen
  // Vergeet de getters/setters niet – deze worden gebruikt voor serialisatie/deserialisatie
}

Om het gebruik van het project [client-android-skel] te illustreren, zullen we eerst enkele eenvoudige voorbeelden gebruiken, voordat we overgaan tot een uitgebreidere casestudy.

2.8. Illustratieve oefeningen

We beginnen met het herstructureren van reeds geschreven voorbeelden.

2.8.1. Voorbeeld-17B

We nemen voorbeeld 17 uit paragraaf 1.18 opnieuw onder de loep. Dit is een applicatie met één fragment, zonder asynchrone taken en zonder tabbladen. We onderzoeken hoe deze zich gedraagt wanneer het apparaat wordt gedraaid. We voeren de volgende gegevens in:

Image

Vervolgens draaien we in [1] het apparaat twee keer. Het nieuwe scherm ziet er dan als volgt uit:

Image

Als we de weergaven vergelijken, is alles behouden gebleven, behalve de lijst [2], die nu leeg is.

Als we bovendien op de knop [Valider] klikken, verschijnt er een dialoogvenster met de gegevens die in het formulier zijn ingevuld. Als we op dat moment het apparaat draaien, verdwijnt het dialoogvenster.

We moeten dus bij een rotatie het volgende opnieuw genereren:

  • de vervolgkeuzelijst en het geselecteerde item;
  • het dialoogvenster, als dit tijdens het draaien werd weergegeven;

2.8.1.1. Het project [Exemple-17B]

We dupliceren het project [client-android-skel] in voorbeelden/Voorbeeld-17B. Vervolgens laden we het nieuwe project [1]:

  • in [2-3], in de map [behavior], plakken we het fragment [Vue1Fragment] uit het project [Exemple-17];
  • in [4-5], in de map [layout] van [Exemple-17B], plakken we de weergave [vue1.xml] van [Exemple-17]. Dit is de weergave die bij het fragment hoort;
  • in [6] wordt de map [values] van [Exemple-17B] vervangen door de map [values] van [Exemple-17];

De bovenmarge van weergave [vue1.xml] wordt gewijzigd in 80 dp:


    <TextView
      android:id="@+id/textViewFormulaireTitre"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="10dp"
      android:layout_marginTop="80dp"
      android:text="@string/titre_vue1"
android:textSize="30sp"/>

In dit stadium kunnen we een eerste compilatie uitvoeren om te zien welke fouten er zijn. De eerste gemelde fouten komen voort uit imports, waar pakketten van plaats zijn gewisseld. We corrigeren deze (Ctrl-Shift-O). Andere fouten komen doordat de view [Vue1Fragment] niet alle methoden implementeert die door de bovenliggende klasse [AbstractParent] worden opgelegd:

Image

We genereren de ontbrekende methoden (Alt-Enter).

Een andere gemelde compilatiefout is de volgende:

Image

Dit wordt gecorrigeerd in het bestand [build.gradle] van de module (regel 20 hieronder):

 

In dit stadium kunnen we opnieuw compileren om te zien welke fouten er nog over zijn. De enige gemelde fout betreft de methode [Vue1Fragment.updateFragment]:

 

De annotatie [@Override] op regel 135 moet worden verwijderd. Er zijn nu geen fouten meer. We gaan vanaf hier verder met het aanpassen van het project.

2.8.1.2. De status van het fragment [Vue1Fragment]

Het fragment [Vue1Fragment] moet informatie opslaan wanneer het apparaat wordt gedraaid, zodat het volledig kan worden hersteld. Hiervoor maken we een klasse [Vue1FragmentState] aan:

  

Op dit moment is deze klasse leeg:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {
  
}

2.8.1.3. Het project aanpassen

  

In de map [custom] bevinden zich de architectuurelementen die door de ontwikkelaar kunnen worden aangepast.

De constanten van de interface [IMainActivity] zijn als volgt:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // toegang tot de sessie
  ISession getSession();

  // van weergave wisselen
  void navigateToView(int position, ISession.Action action);

  // afhandeling van wachtrijen
  void beginWaiting();

  void cancelWaiting();

  // applicatieconstanten -------------------------------------

  // debugmodus
  boolean IS_DEBUG_ENABLED = true;

  // maximale wachttijd voor het antwoord van de server
  int TIMEOUT = 1000;

  // wachttijd voordat de clientverzoek wordt uitgevoerd
  int DELAY = 0;

  // basisauthenticatie
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // aaneenschakeling van fragmenten
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // tabbladbalk
  boolean ARE_TABS_NEEDED = false;

  // laadafbeelding
  boolean IS_WAITING_ICON_NEEDED = false;

  // aantal fragmenten van de applicatie
  int FRAGMENTS_COUNT = 1;

}
  • regels 24-31: de applicatie maakt hier geen gebruik van de [DAO]-laag. Deze constanten worden niet gebruikt;
  • regel 34: een fragmentadjacentie van 1, wat de standaardwaarde is. Aangezien de applicatie slechts één fragment heeft (regel 43), is deze waarde niet van belang;
  • regels 39-40: aangezien er geen bewerkingen plaatsvinden met de laag [DAO], is een wachtbeeld overbodig;
  • regel 37: dit is geen applicatie met tabbladen;
  • regel 43: er is slechts één fragment;

De klasse [Session] is als volgt:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // elementen die niet in jSON kunnen worden geserialiseerd, moeten de annotatie @JsonIgnore hebben

}

Deze is leeg. Aangezien er slechts één fragment is, is er geen reden om te voorzien in communicatie tussen fragmenten met een sessie.

Ten slotte is de klasse [CoreState] als volgt:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // al dan niet bezocht fragment
  protected boolean hasBeenVisited = false;
  // status van het eventuele menu van het fragment
  protected MenuItemState[] menuOptionsState;

  // getters en setters
...
}
  • regels 11-13: we moeten alle klassen vermelden die zijn afgeleid van [CoreState] en die de status van de verschillende fragmenten opslaan. Hier is er slechts één (regel 12);

2.8.1.4. De activiteit [MainActivity]

De activiteit [MainActivity] ziet er momenteel als volgt uit:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // laag [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessie
  private Session session;

  // methoden van de bovenliggende klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessie
    this.session = (Session) super.session;
    // todo: we gaan verder met de initialisaties die door de bovenliggende klasse zijn gestart
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // todo: definieer hier de fragmenten
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo: de titels van de fragmenten hier definiëren
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // todo: navigatie via tabbladen – de weergave definiëren die moet worden getoond wanneer tabblad nr. [position] is geselecteerd
  }

  @Override
  protected int getFirstView() {
    // todo: het nummer van de eerste weer te geven weergave (fragment) instellen
    return 0;
  }
}

De opmerkingen [//todo] geven aan wat de ontwikkelaar moet doen. De klasse [MainActivity] ontwikkelt zich als volgt:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // laag [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessie
  private Session session;

  // methoden van de bovenliggende klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessie
    this.session = (Session) super.session;
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new Vue1Fragment_()};
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return 0;
  }
}

Alleen de methode op de regels 41-44 moet worden aangepast. Deze moet de array met fragmenten van de applicatie retourneren. Vergeet op regel 43 niet de underscore achter de naam van het fragment te plaatsen.

2.8.1.5. De status van het fragment [FragmentState]

Na de rotatietests die zijn uitgevoerd op het project [Exemple-17], is besloten om de volgende elementen van het fragment op te slaan:

  • de lijst met waarden van de vervolgkeuzelijst;
  • de positie van het geselecteerde element in deze lijst;
  • het bericht dat door het dialoogvenster wordt weergegeven als dit op het moment van de rotatie aanwezig is;

De klasse [Vue1FragmentState] ziet er als volgt uit:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

  // de waarden van de vervolgkeuzelijst
  private List<String> list;
  // het geselecteerde item in de vervolgkeuzelijst
  private int listSelectedPosition;
  // het bericht dat in het dialoogvenster wordt weergegeven
  private String message;

  // getters en setters
...
}

2.8.1.6. Het fragment [AbstractFragment]

Momenteel wordt de levenscyclus van het fragment beheerd door twee methoden (regels 6 en 32):


// 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");
  }
...
  protected void updateFragment() {
    // initialisatie van de adapter voor 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);
  }

De code van deze twee methoden wordt als volgt gemigreerd naar de methoden die worden voorgeschreven door de klasse [AbstractFragment]:


// beheer van de levenscyclus van het fragment ---------------------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    Vue1FragmentState state = new Vue1FragmentState();
    state.setList(list);
    state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
    state.setMessage(message);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // Eerste bezoek?
    if (previousState == null) {
      // de waarden van de vervolgkeuzelijst worden aangemaakt
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // de waarden van de vervolgkeuzelijst worden weergegeven
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // en de tekst in het dialoogvenster
      message = state.getMessage();
    }
    // 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);
  }

  @Override
  protected void initView(CoreState previousState) {
    // 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));
      }
    });
    // initialisatie van de adapter voor de vervolgkeuzelijst
    dropDownList.setAdapter(dataAdapter);
    // Eerste bezoek?
    if (previousState == null) {
      // de eerste knop wordt aangevinkt
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // waarde schuifbalk
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // geselecteerd item in de vervolgkeuzelijst
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // dialoogvenster zichtbaar?
    if (message != null) {
      // wordt weergegeven
      showMessage();
    }
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

}
  • regels 2-9: de methode [saveFragment] moet de elementen van het fragment die moeten worden opgeslagen, in een van [CoreState] afgeleide klasse plaatsen en de instantie daarvan retourneren;
  • regels 11-14: de methode [getNumView] moet het nummer van het fragment retourneren. Hier is er slechts één fragment met nummer 0;
  • regels 16-34: de methode [initFragment] moet de velden van het fragment initialiseren. Deze methode ontvangt de vorige status van het fragment. Als [previousState] gelijk is aan null, dan is dit het eerste bezoek;
  • regels 19-25: bij het eerste bezoek worden de waarden van de vervolgkeuzelijst aangemaakt;
  • regels 26-30: als het niet het eerste bezoek betreft, worden de velden [list, message] van het fragment hersteld op basis van de vorige status;
  • regels 33-34: initialisatie van het veld [dataAdapter] van het fragment. Dit is de gegevensbron van de vervolgkeuzelijst;
  • regels 37-62: de methode [initView] dient om de componenten van de visuele interface te initialiseren. Deze methode ontvangt als parameter de vorige toestand [previousState]. Als [previousState==null], dan is dit het eerste bezoek;
  • hier vinden we terug wat eerder in de methode [@AfterViews] stond;
  • regels 57-61: bij het eerste bezoek wordt gecontroleerd of het eerste keuzerondje is aangevinkt;
  • regels 64-67: de methode [updateOnSubmit] wordt uitgevoerd wanneer de huidige actie [SUBMIT] is. Hier is er geen navigatie tussen fragmenten en dus geen huidige actie;
  • regels 69-81: de methode [updateOnRestore] wordt uitgevoerd wanneer de huidige actie [NAVIGATION] of [RESTORE] is. Hier vindt er geen navigatie tussen fragmenten plaats en is er dus geen actie [NAVIGATION] mogelijk;
  • regel 72: de waarde van TextView seekBarValue wordt opnieuw berekend (niet hersteld). Bij rotaties ging de waarde namelijk soms verloren;
  • regels 74-75: de lijst wordt op het element geplaatst dat vóór de rotatie was geselecteerd. Zonder deze aanpassing zou de lijst op het eerste element worden geplaatst;
  • regels 76-80: het dialoogvenster wordt opnieuw weergegeven als het bericht van de vorige status niet null is. We komen later terug op de methode [showMessage] (regel 79);
  • regels 83-86: de methode [notifyEndOfUpdates] is de laatste methode die door de bovenliggende klasse wordt aangeroepen voordat het onderliggende fragment met rust wordt gelaten. Hier valt niets te doen;
  • regels 88-91: de methode [notifyEndOfTasks] geeft aan dat de door het fragment gestarte asynchrone taken zijn voltooid. Hier zijn er geen;

Het dialoogvenster wordt als volgt hersteld:


  // de tekst van het dialoogvenster
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // lijst met weer te geven berichten
    List<String> messages = new ArrayList<>();
    ...
    // weergave
    doAfficher(messages);
  }

  private void doAfficher(final 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));
    }
    // de melding wordt opgeslagen
    message = texte.toString();
    // we geven het weer
    showMessage();
  }

  private void showMessage() {
    // we geven het weer
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // het bericht wordt gereset
        message = null;
      }
    }).show();
}

Wanneer de gebruiker het formulier bevestigt, stelt de methode [doValider] (regel 5) een lijst met berichten samen, die vervolgens (regel 10) in het dialoogvenster wordt weergegeven.

  • regels 14-20: de lijst met berichten wordt samengevoegd tot één bericht dat in regel 2 wordt opgeslagen;
  • regels 25-33: dit bericht wordt weergegeven in het dialoogvenster en het is hetzelfde bericht dat door de methode [updateOnRestore] wordt weergegeven;
  • regel 27: de tweede parameter van de methode [setNeutralButton] is de methode die wordt uitgevoerd wanneer de gebruiker op de knop [Fermer] in het dialoogvenster klikt;
  • regel 31: bij het sluiten van het dialoogvenster wordt de melding teruggegeven aan null om aan te geven dat het dialoogvenster niet meer aanwezig is;

2.8.1.7. Tests

De lezer wordt verzocht dit project te testen en te controleren of het fragment na één of meerdere opeenvolgende rotaties inderdaad behouden blijft.

2.8.2. Voorbeeld 23: weersclient

Op sommige websites kun je weerinformatie vinden in de vorm van reeksen zoals jSON. Hier is een voorbeeld:

Image

De URL heeft de volgende vorm: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} waarbij:

  • city: de stad waarvan we het weer willen weten, in dit geval Angers;
  • country: het land van de stad, in dit geval Frankrijk (fr);
  • APPID: een sleutel die je krijgt door je te registreren op de website [https://home.openweathermap.org/users/sign_up];

2.8.2.1. Het project

  

Het project is gebaseerd op het project [client-android-skel]. Het heeft de volgende kenmerken:

  • het bestaat uit slechts één fragment waarvan de status niet hoeft te worden bijgehouden;
  • het voert asynchrone verzoeken uit;

2.8.2.2. Aanpassing van het project

  

Via de interface [IMainActivity] kunnen bepaalde kenmerken van het project worden gespecificeerd:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // toegang tot de sessie
  ISession getSession();

  // weergave wijzigen
  void navigateToView(int position, ISession.Action action);

  // wachtrijbeheer
  void beginWaiting();

  void cancelWaiting();

  // applicatieconstanten -------------------------------------

  // debugmodus
  boolean IS_DEBUG_ENABLED = true;

  // maximale wachttijd voor het antwoord van de server
  int TIMEOUT = 1000;

  // wachttijd voordat de clientverzoek wordt uitgevoerd
  int DELAY = 5000;

  // basisauthenticatie
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // aaneenschakeling van fragmenten
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // tabbladbalk
  boolean ARE_TABS_NEEDED = false;

  // laadafbeelding
  boolean IS_WAITING_ICON_NEEDED = true;

  // aantal fragmenten van de applicatie
  int FRAGMENTS_COUNT = 1;

}
  • regels 25, 28, 31, 40: kenmerken van de laag [DAO]. Regel 31: er is geen basisauthenticatie nodig;
  • regel 34: aangrenzende fragmenten. Hier is deze constante niet van belang, aangezien er slechts één fragment is;
  • regel 37: dit is geen toepassing met tabbladen;
  • regel 43: er is slechts één fragment;

De klasse [CoreState], die de status van de fragmenten opslaat, ziet er als volgt uit:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo: voeg hier de subklassen van [CoreState] toe
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragment al dan niet bezocht
  protected boolean hasBeenVisited = false;
  // status van het eventuele menu van het fragment
  protected MenuItemState[] menuOptionsState;

  // getters en setters
...
}
  • regels 10-13: er valt niets te declareren, aangezien er in deze applicatie slechts één fragment is waarvan de status niet wordt bewaard;

De klasse [Session] ziet er als volgt uit:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // elementen die niet kunnen worden geserialiseerd in jSON moeten de annotatie @JsonIgnore hebben
}

Deze is leeg, aangezien er in deze applicatie geen communicatie tussen fragmenten plaatsvindt.

2.8.2.3. De laag [DAO]

  

In de laag [DAO] moeten drie klassen worden aangepast:

  • de interface IDao;
  • de Dao-implementatie ervan;
  • de interface WebClient voor communicatie met de webserver / jSON;

De interface [WebClient] ziet er als volgt uit:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // weerdienst
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • regels 18-19: de URL van de weerdienst. Ter herinnering: deze heeft betrekking op de hoofd-URL (RestClientRootUrl, regel 12) van de klant. Hier zal deze hoofd-URL [http://api.openweathermap.org/] zijn;

De interface [IDao] ziet er als volgt uit:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL van de webservice
  void setUrlServiceWebJson(String url);

  // gebruiker
  void setUser(String user, String mdp);

  // time-out van de client
  void setTimeout(int timeout);

  // basisauthenticatie
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // debugmodus
  void setDebugMode(boolean isDebugEnabled);

  // Wachttijd van de client in milliseconden vóór het verzoek
  void setDelay(int delay);

  // weerdienst
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • ter herinnering: de methoden in de regels 6-22 zijn standaard aanwezig in de interface IDao van het project [client-android-skel];
  • regel 25: met de methode [getWeatherForecast] kan de tekenreeks jSON worden verkregen voor het weer in de stad [city] in het land [country]. De derde parameter is de sleutel die is verkregen op de website [https://home.openweathermap.org/users/sign_up];

De interface [IDao] wordt geïmplementeerd door de volgende klasse [Dao]:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // webserviceclient
  @RestService
  protected WebClient webClient;
  // beveiliging
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // de RestTemplate
  private RestTemplate restTemplate;
  // fabriek van RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // time-out
  private int timeout;

  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we bouwen de restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // de converter jSON wordt ingesteld
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // de restTemplate van de webclient wordt ingesteld
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // de URL van de webservice wordt ingesteld
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // de gebruiker wordt geregistreerd in de interceptor
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // geheugen
    this.timeout = timeout;
    // configuratiefabriek
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authenticatie-interceptor?
    if (isBasicAuthentificationNeeded) {
      // de authenticatie-interceptor wordt toegevoegd
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


  // privé-methoden -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // weerdienst ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // logboek
    if (isDebugEnabled) {
      Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
    }
    // resultaat
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • Ter herinnering: de regels 17-90 zijn standaard aanwezig in de klasse [Dao] van het project [client-android-skel]. U hoeft alleen de implementatiemethoden van de interface [IDao] toe te voegen, die specifiek zijn voor de toepassing (regel 92);
  • regels 93-105: implementatie van de methode [getWeatherForecast]. Deze is heel eenvoudig en bestaat uit 6 regels, regels 100-105;
  • regel 100: de methode [getResponse] is een methode van de bovenliggende klasse [AbstractDao]. Deze verwacht een parameter van het type [IRequest<T>], waarbij T het type is van het verwachte antwoord van de server, in dit geval een String, aangezien we een tekenreeks van het type jSON verwachten. Het type T van [IRequest<T>] moet het type T zijn van de methode [Observable<T> getWeatherForecast];
  • de interface [IRequest<T>] heeft slechts één methode: getResponse. Deze heeft als taak het antwoord van type T te leveren dat de methode [Observable<T> getWeatherForecast] moet retourneren;
  • regel 103: het is de interface [WebClient] die dit antwoord levert. De drie parameters die op regel 94 zijn ontvangen, worden hieraan doorgegeven. Om deze reden moeten deze het attribuut final hebben;

2.8.2.4. De activiteit [MainActivity]

  

De activiteit [MainActivity] is als volgt:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // laag [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // methoden van de bovenliggende klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new MeteoFragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // interface IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • Ter herinnering: de regels 15-55 zijn standaard aanwezig in het project [client-android-skel]. U hoeft ze alleen maar aan te passen;
  • regels 37-40: de tabel met fragmenten. Er is hier slechts één fragment;
  • regels 43-46: er zijn geen fragmenttitels nodig;
  • regels 48-50: hier zijn geen tabbladen;
  • regels 52-55: de eerste weergave die moet worden getoond is weergave nr. 0, die van [MeteoFragment];
  • regels 58-61: implementatie van de interface [IDao]. Hier hoeft niets anders te gebeuren dan het werk te delegeren aan de laag [DAO] uit regel 21;

2.8.2.5. Het fragment [MeteoFragment]

  

Het fragment [MeteoFragment] doet een verzoek aan de weerswebservice / jSON. De structuur ervan is als volgt:


package client.android.fragments;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
  • regel 14: de weergave [res / layout / meteo_fragment.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">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Construisez votre interface visuelle"
    android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
    android:layout_marginTop="120dp"/>
</RelativeLayout>

De weergave toont alleen de tekst van regel 10;

  • regel 15: het menu [res / menu / menu_meteo.xml] ziet er als volgt uit:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionMeteo"
        android:title="@string/actionMeteo"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionTerminer"
        android:title="@string/actionTerminer"/>
    </menu>
  </item>
</menu>
  • regels 10-12: deze menuoptie dient om het weer van een stad op te vragen;
  • regels 14-15: deze menuoptie dient om deze aanvraag te annuleren als deze nog bezig is;
  • regels 16-18: deze menuoptie sluit de applicatie af;

De volledige code van het fragment is als volgt:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {

  // lokale gegevens
  private int nbReponsesRecues;

  // gebeurtenisbeheer ---------------------------------------------------------------------------------------
  // steden waarvan we het weer willen weten
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // zijn land
    String country = "fr";
    // verkrijg een API-ID door een account aan te maken [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL van de webservice / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // begin van het wachten op de asynchrone taken [paysDeLoire.length]
    beginWaiting(paysDeLoire.length);
    // aantal ontvangen antwoorden
    nbReponsesRecues = 0;
    // de asynchrone oproepen worden parallel uitgevoerd
    for (String city : paysDeLoire) {
      // weer
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // verwerking van het antwoord
          consumeResponse(response);
          // een positief antwoord
          nbReponsesRecues++;
        }
      });
    }
  }

  // verwerking van het antwoord van de server
  private void consumeResponse(String response) {
    // log
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // begin van de wachttijd
  protected void beginWaiting(int numberOfRunningTasks) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // bovenliggend
    beginRunningTasks(numberOfRunningTasks);
    // de optie wordt weergegeven [Annuler]
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // weergave resultaten
    String message;
    switch (nbReponsesRecues) {
      case 0:
        message = "Aucune réponse n'a été reçue";
        break;
      case 1:
        message = "Une réponse a été reçue. Consultez vos logs...";
        break;
      default:
        message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
        break;
    }
    Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
  }

  // privé-methoden -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // levenscyclusbeheer ---------------------------------------------------------------------------------------
...
}
  • regels 25-50: afhandeling van de klik op de menuoptie [Météo];
  • regel 32: opbouw van de URL van de webservice / jSON van de weerservice. Deze wordt vervolgens via de activiteit doorgegeven aan de laag [DAO];
  • regel 34: we beginnen met wachten. We geven het aantal taken door dat zal worden gestart, zodat de bovenliggende klasse ons kan melden wanneer deze zijn voltooid. Hier zijn er vijf taken, omdat we het weer van de vijf steden uit regel 23 gaan opvragen;
  • regel 16: we tellen het aantal ontvangen antwoorden om dit te kunnen weergeven;
  • regels 38-50: we doorlopen de steden waarvan we het weer willen weten;
  • regel 40: we voeren 5 HTTP-verzoeken parallel uit;
  • regel 40: we vragen de bovenliggende klasse [AbstractParent] om de webservice /jSON te raadplegen;
  • regels 40-48: de methode [executeInBackground] verwacht twee parameters:
    • regel 40: het te observeren en uit te voeren proces wordt geleverd door de methode [mainActivity.getWeatherForecast];
    • regels 40-48: de instantie [Action1] die moet worden uitgevoerd wanneer het antwoord van de asynchrone service wordt ontvangen. Het type T van [Action1<T>] moet het type T zijn van het resultaat van de methode [getWeatherForecast];
  • regel 44: er is een antwoord ontvangen. Dit wordt doorgegeven aan de methode [consumeResponse] op regel 53;
  • regel 46: de teller van de ontvangen antwoorden wordt verhoogd;
  • regels 53-56: verwerking van een antwoord jSON van de weerdienst;
  • regel 55: de tekenreeks jSON wordt alleen gelogd;
  • regels 59-72: code die wordt uitgevoerd voordat de asynchrone taken worden gestart;
  • regel 65: het aantal uit te voeren taken wordt doorgegeven aan de bovenliggende klasse [AbstractParent]. Hierdoor kan deze klasse ons waarschuwen wanneer alle taken zijn voltooid;
  • regels 67-70: voorbereiding van het menu voor een wachttijd. We behouden alleen de optie [Actions/Annuler], waarmee de gebruiker de gestarte taken kan annuleren;
  • regels 74-92: code die wordt uitgevoerd wanneer de bovenliggende klasse ons laat weten dat alle gestarte taken zijn voltooid;
  • regel 77: we zetten het menu terug in de oorspronkelijke staat. De methode [initMenu] (regels 95-102) geeft het menu weer met alle opties, behalve de optie [Actions/Annuler], die verborgen is;
  • regels 80-91: het aantal ontvangen antwoorden wordt weergegeven;

Het klikken op de menuoptie [Annuler] wordt afgehandeld door de volgende code:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchrone taken annuleren
    cancelRunningTasks();
}
  • regel 7: de bovenliggende klasse wordt gevraagd om de nog actieve taken te annuleren;

Een klik op de menuoptie [Terminer] wordt afgehandeld door de volgende code:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // alles stoppen
    System.exit(0);
}

Het beheer van de levenscyclus van het fragment wordt verzorgd door de volgende methoden:


  // levenscyclusbeheer ---------------------------------------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    // Eerste bezoek?
    if (previousState == null) {
      initMenu();
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

}
  • regels 3-6: dienen om de status van het fragment op te slaan in een klasse die is afgeleid van [CoreState]. Als het fragment geen status heeft die moet worden opgeslagen, zoals hier het geval is, volstaat het om een instantie van [CoreState] terug te geven. null mag niet worden geretourneerd, omdat dit later tot een crash zou leiden;
  • regels 8-11: moeten het nummer van de weergave retourneren. Hier heeft het fragment [MeteoFragment] het nummer 0;
  • regels 13-16: dienen om het fragment te initialiseren zodra het is opgebouwd (previousState==null) of opnieuw is opgebouwd (previousState!=null). Hier hoeft niets te gebeuren. Het enige veld dat kan worden geïnitialiseerd, is het volgende:

  // steden waarvan we het weer willen weten
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

maar dit wordt vanzelf geïnitialiseerd;

  • regels 18-24: dienen om de weergave die aan het fragment is gekoppeld te initialiseren zodra deze is opgebouwd (previousState==null) of opnieuw is opgebouwd (previousState!=null);
  • regels 21-23: als dit het eerste bezoek aan het fragment is, wordt het menu geïnitialiseerd om de optie [Annuler] te verbergen;
  • regels 27-30: worden aangeroepen als er, om bij het fragment te komen, is genavigeerd met een actie van het type [SUBMIT]. Hier is er geen navigatie tussen fragmenten, aangezien er slechts één fragment is;
  • regels 32-35: worden aangeroepen tijdens een back-up-/herstelcyclus als gevolg van het draaien van het apparaat of een andere reden. Aangezien er hier geen status is opgeslagen, hoeft er niets te gebeuren;
  • regels 37-40: worden aangeroepen wanneer alle voorgaande updates zijn uitgevoerd. Hier hoeft er niets te gebeuren;

2.8.2.6. Tests

We voeren nu het voorbeeld uit:

Image

Image

De logbestanden zien er dan als volgt uit:


07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructeur
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructeur, thread=main
07-23 13:24:32.861 2642-2642/client.android D/client.android.dao.service.Dao_: afterInject
07-23 13:24:32.950 2642-2642/client.android D/MainActivity_: onCreate
07-23 13:24:32.951 2642-2642/client.android D/client.android.dao.service.Dao_: setTimeout thread=main, timeout=1000
07-23 13:24:32.952 2642-2642/client.android D/client.android.dao.service.Dao_: setBasicAuthentification thread=main, isBasicAuthentificationNeeded=false
07-23 13:24:33.041 2642-2642/client.android D/MainActivity_: adding loadingPanel
07-23 13:24:33.043 2642-2642/client.android D/MeteoFragment_: constructeur
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigation vers vue 0 sur action NONE
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: onCreateActivity
07-23 13:24:33.080 2642-2642/client.android D/MainActivity_: onResume
07-23 13:24:33.325 2642-2642/client.android D/MeteoFragment_: onActivityCreated
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: onCreateOptionsMenu
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: getMenuOptionsStates(Menu)
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: Nombre d'options de menu=4
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initFragment initView updateForFirstVisit
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initMenu
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: session={"action":"NONE","coreStates":[{"@type":"CoreState","hasBeenVisited":false,"menuOptionsState":null}],"previousTab":0,"previousView":0}
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: état précédent=null
07-23 13:24:33.558 2642-2642/client.android D/MeteoFragment_: notifyEndOfUpdates
07-23 13:24:39.766 2642-2642/client.android D/MeteoFragment_: beginWaiting
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=angers, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=le mans, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=nantes, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=laval, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=la roche sur yon, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:45.035 2642-2961/client.android D/client.android.dao.service.Dao_: response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200} sur thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-3]
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: initMenu
  • regels 32-36: de antwoorden jSON worden verkregen via I/O-threads
  • regels 37-41: het fragment haalt de 5 antwoorden op via de UI-thread;

Nu voeren we de aanvraag uit met een onjuiste identificatiecode API:


    String APIID = "";

Image

De logbestanden zien er dan als volgt uit:


07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
  • regels 3-6, 10: de 5 aanroepen van HTTP hebben 5 uitzonderingen gegenereerd;
  • regel 7: het fragment [MeteoFragment] ontvangt de eerste uitzondering. Het zal vervolgens alle taken annuleren;

Laten we nu een wachttijd van 5 seconden instellen met [IMainActivity.DELAY] en de bewerking annuleren. De logbestanden zien er dan als volgt uit:


07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
  • regel 3: annuleringsverzoek;
  • regel 4: de wachttijd wordt geannuleerd omdat er een annulering heeft plaatsgevonden;
  • regels 6-10: het annuleren van de taken leidt tot een uitzondering in elk van de threads van de vijf taken. Het type uitzondering hangt af van de toepassingen. De uitzondering is hier [java.lang.InterruptedException] omdat de taken werden onderbroken terwijl ze de instructie [Thread.sleep(delay)] uitvoerden, waardoor ze kunstmatig [delay] milliseconden moeten wachten;

2.8.3. Voorbeeld-16B

We herstructureren hier voorbeeld 16 uit paragraaf 1.17. Dit voorbeeld bevat een fragment dat asynchrone oproepen doet naar een server voor willekeurige getallen. Laten we eens kijken hoe dit zich gedraagt tijdens een rotatie van het apparaat:

Image

  • in [1] wordt het apparaat twee keer gedraaid;

Image

We zien dat alle foutmeldingen verloren zijn gegaan. We gaan proberen dit te verbeteren.

2.8.3.1. Het project Voorbeeld-16B

We kopiëren het project [client-android-skel] naar het project [exemples/Exemple-16B] en laden vervolgens het nieuwe project:

  

Uit het oorspronkelijke project [Exemple-16] kopiëren we de volgende elementen naar [Exemple-16B]:

  • het bestand [res/layout/vue1.xml], de map [res/values]:
  

We passen de bovenmarge van weergave [vue1.xml] aan naar 80 dp:


  <TextView
    android:id="@+id/txt_Titre2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="80dp"
    android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
  • het fragment [Vue1Fragment]:
  
  • de klasse [dao / service / Response]:
  

In dit stadium kunnen we een eerste compilatie proberen:

  • een eerste soort fouten betreft de imports. Bij de migratie naar [Exemple-16B] zijn klassen van pakket gewisseld. We beginnen met het corrigeren van dit soort fouten;
  • een tweede type fout wordt gemeld voor de klasse [Vue1Fragment], omdat deze de methoden die door de bovenliggende klasse [AbstractParent] worden opgelegd, niet implementeert. We genereren deze automatisch;

We proberen een tweede compilatie:

  • alle resterende fouten hebben zich nu geconcentreerd op de klasse [Vue1Fragment], de klasse die de meeste wijzigingen zal ondergaan;

2.8.3.2. Een rapport aanmaken voor het fragment [Vue1Fragment]

We hebben gezien dat bepaalde gegevens van het fragment tijdens een rotatie moeten worden opgeslagen om het fragment te kunnen herstellen zoals het was vóór de rotatie. We maken daarom een status [Vue1FragmentState] aan, die voorlopig leeg is:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. Aanpassing van het project

  

Via de interface [IMainActivity] kunt u bepaalde kenmerken van het project specificeren:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // toegang tot de sessie
  ISession getSession();

  // weergave wijzigen
  void navigateToView(int position, ISession.Action action);

  // beheer van de wachtrij
  void beginWaiting();

  void cancelWaiting();

  // applicatieconstanten -------------------------------------

  // debugmodus
  boolean IS_DEBUG_ENABLED = true;

  // maximale wachttijd voor het antwoord van de server
  int TIMEOUT = 1000;

  // wachttijd voordat de clientverzoek wordt uitgevoerd
  int DELAY = 5000;

  // basisauthenticatie
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // aaneenschakeling van fragmenten
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // tabbladbalk
  boolean ARE_TABS_NEEDED = false;

  // laadafbeelding
  boolean IS_WAITING_ICON_NEEDED = true;

  // aantal fragmenten van de applicatie
  int FRAGMENTS_COUNT = 1;

}
  • regels 25, 28, 31, 40: kenmerken van de laag [DAO]. Er is geen basisauthenticatie nodig;
  • regel 34: aangrenzendheid van de fragmenten. Hier is deze constante niet van belang, aangezien er slechts één fragment is;
  • regel 37: dit is geen toepassing met tabbladen;
  • regel 43: er is slechts één fragment;

De klasse [CoreState], die de status van de fragmenten opslaat, ziet er als volgt uit:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // al dan niet bezocht fragment
  protected boolean hasBeenVisited = false;
  // status van het eventuele menu van het fragment
  protected MenuItemState[] menuOptionsState;

  // getters en setters
...
}
  • regel 12: we definiëren de klasse voor de status van het fragment [Vue1Fragment];

De klasse [Session] is als volgt:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // elementen die niet in jSON kunnen worden geserialiseerd, moeten de annotatie @JsonIgnore hebben
}

Deze is leeg, aangezien er in deze toepassing geen communicatie tussen fragmenten plaatsvindt.

2.8.3.4. De laag [DAO]

  

In de laag [DAO] moeten drie klassen worden aangepast:

  • de interface IDao;
  • de Dao-implementatie ervan;
  • de interface WebClient voor communicatie met de webserver / jSON;

De klasse [Response] is afkomstig uit het project [Exemple-16], dat deze klasse gebruikt:


package client.android.dao.service;

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

De interface [WebClient] ziet er als volgt uit:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // 1 willekeurig getal in het interval [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • regels 18-19: de URL van de dienst voor willekeurige getallen. Ter herinnering: deze heeft betrekking op de hoofd-URL (RestClientRootUrl, regel 12) van de klant. Hier zal deze hoofd-URL [http://localhost:8080] zijn;

De interface [IDao] ziet er als volgt uit:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL van de webservice
  void setUrlServiceWebJson(String url);

  // gebruiker
  void setUser(String user, String mdp);

  // time-out van de client
  void setTimeout(int timeout);

  // basisauthenticatie
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // debugmodus
  void setDebugMode(boolean isDebugEnabled);

  // Wachttijd van de client in milliseconden vóór het verzoek
  void setDelay(int delay);

  // dienst voor willekeurige getallen
  Observable<Response<Integer>> getAlea(int a, int b);

}
  • Ter herinnering: de methoden in de regels 6-22 zijn standaard aanwezig in de interface IDao van het project [client-android-skel];
  • regel 25: met de methode [getAlea] kan een willekeurig getal worden verkregen binnen het interval [a,b]. Dit getal wordt verkregen in een antwoord van het type [Response<Integer>], waarbij het willekeurige getal zich in het veld [body] van dit type bevindt;

De interface [IDao] wordt geïmplementeerd door de volgende klasse [Dao]:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // webserviceclient
  @RestService
  protected WebClient webClient;
  // beveiliging
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // de RestTemplate
  private RestTemplate restTemplate;
  // factory van RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // logboek
    Log.d(className, "afterInject");
    // de restTemplate wordt gebouwd
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // de converter jSON wordt bevestigd
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // we stellen de restTemplate van de webclient in
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // de URL van de webservice wordt ingesteld
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // de gebruiker wordt geregistreerd in de interceptor
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // fabrieksconfiguratie
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authenticatie-interceptor?
    if (isBasicAuthentificationNeeded) {
      // de authenticatie-interceptor wordt toegevoegd
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // privé-methoden -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // dienst voor willekeurige getallen
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // uitvoering van de webclient
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }

}
  • Ter herinnering: de regels 17-85 zijn standaard aanwezig in de klasse [Dao] van het project [client-android-skel]. U hoeft alleen de implementatiemethoden van de interface [IDao] toe te voegen;
  • regels 88-97: implementatie van de methode [getAlea]. Deze is heel eenvoudig en bestaat uit 6 regels, regels 91-96;
  • regel 91: de methode [getResponse] is een methode van de bovenliggende klasse [AbstractDao]. Deze verwacht een parameter van het type [IRequest<T>], waarbij T het type is van het verwachte antwoord, in dit geval een type Response<Integer>. Het type T van [IRequest<T>] (regel 91) moet hetzelfde zijn als het type T van de methode [Observable<T> getAlea] (regel 89);
  • De interface [IRequest<T>] heeft slechts één methode: getResponse. Deze methode heeft als taak het antwoord van het type T te leveren dat door de methode [Observable<T> getAlea] moet worden geretourneerd;
  • regel 94: het is de interface [WebClient] die dit antwoord levert. De twee parameters die op regel 89 zijn ontvangen, worden hieraan doorgegeven. Daarom moeten deze het attribuut ‘final’ hebben;

2.8.3.5. De activiteit [MainActivity]

  

De activiteit [MainActivity] is als volgt:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // laag [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // methoden van de bovenliggende klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // logboek
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // we gaan verder met de initialisaties die door de bovenliggende klasse zijn gestart
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // definieer hier de fragmenten
    return new AbstractFragment[]{new Vue1Fragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // definieer hier de titels van de fragmenten
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // navigatie via tabbladen - de weer te geven weergave definiëren
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // interface IDao ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }

}
  • Ter herinnering: de regels 15-61 zijn standaard aanwezig in het project [client-android-skel]. U hoeft ze alleen maar aan te passen;
  • regels 40-44: de tabel met fragmenten. Er is hier slechts één fragment;
  • regels 47-51: er zijn geen fragmenttitels nodig;
  • regels 53-56: hier zijn geen tabbladen;
  • regels 58-61: de eerste weergave die moet worden getoond is weergave nr. 0, die van [Vue1Fragment];
  • regels 64-67: implementatie van de interface [IDao]. Hier hoeft niets anders te gebeuren dan het werk te delegeren aan de laag [DAO] uit regel 23;

2.8.3.6. De status van het fragment [Vue1Fragment]

  

De klasse [Vue1FragmentState] ziet er als volgt uit:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

public class Vue1FragmentState extends CoreState {

  // fragmentstatus ------------------------
  // lijst met antwoorden
  private List<String> reponses = new ArrayList<>();
  // weergavestatus ------------------------
  // foutmelding over het aantal aangevraagde willekeurige getallen
  private boolean txtErrorAleasVisible = false;
  // foutmelding over het generatie-interval [a,b]
  private boolean txtErrorIntervalleVisible = false;
  // foutmelding over de URL van de webservice
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // foutmelding over de wachttijd
  private boolean textViewErreurDelayVisible = false;
  // Zichtbare of onzichtbare status van de knop 'Uitvoeren'
  private boolean btnExecuterVisible = true;

  // getters en setters
...
}

Om te bepalen wat er in het fragment moest worden opgeslagen, hebben we het apparaat in verschillende situaties laten draaien en gekeken wat er bij het herstellen was verdwenen. We kwamen tot de conclusie dat de informatie van de regels 10-23 moest worden opgeslagen.

2.8.3.7. Het fragment [Vue1Fragment]

  

Momenteel vertoont de weergave [Vue1Fragment] diverse fouten doordat de bovenliggende klasse [AbstractFragment], waarvan deze is afgeleid, is gewijzigd. In plaats van de aan te brengen wijzigingen één voor één te beschrijven, zullen we direct de definitieve versie toelichten.

De structuur van het fragment is als volgt:


package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {

...
}
  • regel 26: er wordt aan herinnerd dat elk fragment een menu moet hebben, zelfs als het leeg is. Dat is hier het geval.

2.8.3.7.1. Verwerking van de klik op de knop [Exécuter]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // de ingevoerde gegevens worden gecontroleerd
    if (!isPageValid()) {
      return;
    }
    // eerdere antwoorden worden gewist
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // de antwoords teller wordt op 0 gezet
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // activiteit initialiseren
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // de willekeurige taak wordt voorbereid
    beginWaiting(1);
    // willekeurige getallen opvragen
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    // het te observeren proces wordt aangemaakt
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // willekeurige getallen opvragen
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // het antwoord wordt verwerkt
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // log
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // een antwoord van +
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // het antwoord wordt geanalyseerd
    // fout?
    if (response.getStatus() != 0) {
      // weergave
      showAlert(response.getMessages());
      // annuleren
      doAnnuler();
      // terug naar de gebruikersinterface
      return;
    }
    // de informatie wordt toegevoegd aan de lijst met antwoorden
    reponses.add(0, String.valueOf(response.getBody()));
    // de antwoorden worden vernieuwd
    dataAdapterReponses.notifyDataSetChanged();
  }

  // annulering ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchrone taken worden geannuleerd
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // het zandloperpictogram wordt weergegeven
    beginRunningTasks(nbRunningTasks);
    // de knop [Annuler] vervangt de knop [Exécuter]
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • regels 4-6: eerst wordt gecontroleerd of de invoer geldig is. Er kunnen dan foutmeldingen verschijnen;
  • regels 8-9: de lijst met antwoorden wordt leeggemaakt. Deze wijziging wordt doorgevoerd in de ListView die ze weergeeft;
  • regels 11-12: het aantal ontvangen antwoorden wordt op nul gezet;
  • regel 14: de URL van de dienst voor willekeurige getallen wordt ingesteld. Deze informatie wordt doorgegeven aan de laag [DAO];
  • regel 15: de wachttijd wordt ingesteld voordat het verzoek aan de dienst voor willekeurige getallen wordt verzonden. Deze informatie wordt doorgegeven aan de laag [DAO];
  • regel 17: we bereiden ons voor op het starten van 1 asynchrone taak (en niet N, we zullen zien waarom);
  • regels 24-27: van de N asynchrone taken wordt er één per reeks bewerkingen uitgevoerd [merge];
  • regels 29-36: de bovenliggende klasse [AbstractParent] wordt gevraagd om de webservice / jSON te raadplegen voor willekeurige getallen;
  • regels 29-36: de methode [executeInBackground] verwacht twee parameters:
    • regel 29: het proces dat moet worden geobserveerd en uitgevoerd, is het proces dat in de voorgaande regels is berekend;
    • regels 29-36: de instantie [Action1] die moet worden uitgevoerd wanneer het antwoord van de asynchrone service wordt ontvangen. Het type T van [Action1<T>] moet het type T zijn van het resultaat van de methode [getAlea], d.w.z. een type [Response<Integer>];
  • regel 34: wanneer er een antwoord binnenkomt (een willekeurig getal), wordt dit verwerkt in de methode op regel 39;
  • regels 49-50: er wordt genoteerd en gemeld dat er een nieuw antwoord is ontvangen;
  • regels 53-60: het type [Response<T>] heeft een veld [status] dat een foutcode is. Als deze code niet nul is, heeft de server een probleem ondervonden;
  • regel 55: er wordt een foutmelding weergegeven. De methode [showAlert] behoort tot de bovenliggende klasse;
  • regel 57: de methode op de regels 68-75 wordt aangeroepen. Deze zal de nog actieve taken annuleren (regel 74);
  • regel 62: het antwoord wordt toegevoegd aan de lijst met antwoorden, die de gegevensbron is voor ListView;
  • regel 64: de ListView wordt vernieuwd;
  • regels 77-83: de methode [beginWaiting(int nbRunningTasks)] bereidt de weergave voor op het wachten (regels 81-82) en geeft aan de bovenliggende klasse door dat de taken van [nbRunningTasks] binnenkort zullen worden uitgevoerd (regel 79);

2.8.3.7.2. De levenscyclus van het fragment

De levenscyclus van het fragment wordt verzorgd door de volgende methoden:


  // lokale gegevens
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // levenscyclusbeheer ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // huidige status van de weergave
    Vue1FragmentState state = new Vue1FragmentState();
    state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
    state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
    state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
    state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
    state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
    state.setReponses(reponses);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // Eerste bezoek?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // gegevensbron van listView
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // aantal antwoorden
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // koppeling tussen lijstweergave en adapter
    listReponses.setAdapter(dataAdapterReponses);
    // Eerste bezoek?
    if (previousState == null) {
      // foutmeldingen worden verborgen
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // de knoppen
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // vorige status van de weergave
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // foutmeldingen weergeven / verbergen
    txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
    textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
    // knoppen
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // aantal antwoorden
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // de knop [Exécuter] vervangt de knop [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • regels 7-18: zorgen ervoor dat het fragment wordt opgeslagen wanneer de bovenliggende klasse daarom vraagt;
  • regel 11: weergave van het foutbericht over de wachttijd;
  • regel 12: weergave van de foutmelding over het aantal aangevraagde willekeurige getallen;
  • regel 13: weergeeft de foutmelding over de URL van de webservice / jSON;
  • regel 14: zichtbaarheid van de foutmelding over het interval [a,b] voor het genereren van willekeurige getallen;
  • regel 15: zichtbaarheid van de knop [Exécuter];
  • regel 16: de lijst met ontvangen antwoorden;
  • regels 20-23: moeten het nummer van de weergave weergeven. Het fragmentnummer is hier 0, aangezien er maar één is;
  • regels 25-38: initialisatie van de velden van het fragment, hetzij bij een eerste bezoek (previousState==null), hetzij bij een volgend bezoek;
    • regels 29-30: als dit niet het eerste bezoek is, wordt het veld [reponses] hersteld op basis van de vorige status van het fragment;
    • regels 31-33: als het het eerste bezoek is, wordt het veld [reponses] geïnitialiseerd met een lege lijst;
    • regels 34-37: op basis van het veld [reponses] kunnen de gegevensbron van het fragment ListView (regel 35) en het aantal antwoorden (regel 37) worden samengesteld;
  • regels 40-55: worden uitgevoerd om de aan het fragment gekoppelde weergave te initialiseren, hetzij bij een eerste bezoek (previousState==null), hetzij bij een volgend bezoek;
    • regel 43: de ListView van het fragment wordt gekoppeld aan de gegevensbron die zojuist is opgebouwd in de methode [initFragment];
    • regels 45-54: als het het eerste bezoek is, wordt de weergave voorbereid voor de eerste weergave;
  • regels 57-60: worden uitgevoerd bij navigatie tussen fragmenten in verband met een actie van het type [SUBMIT]. Hier is er slechts één fragment en dus geen navigatie tussen fragmenten;
  • regels 63-76: worden uitgevoerd bij navigatie tussen fragmenten in combinatie met een actie van het type [NAVIGATION] of bij een opslag-/herstelcyclus als gevolg van het draaien van het apparaat of een andere reden. Hier kan alleen het laatste geval zich voordoen. Men moet in gedachten houden dat hier, in alle gevallen, [previousState] altijd niet null is;
  • regel 65: de vorige status wordt gecast naar het type van de fragmentstatus;
  • regels 66-75: de inhoud van de vorige toestand wordt gebruikt om de weergave te herstellen;
  • regels 78-81: worden aangeroepen wanneer alle voorgaande updates zijn uitgevoerd. Hier hoeft niets te gebeuren;
  • regels 83-89: worden uitgevoerd wanneer alle asynchrone taken zijn voltooid. Hier wordt de knop [Annuler] verborgen en vervangen door de knop [Exécuter];

2.8.3.8. De tests

De lezer wordt verzocht de volgende tests uit te voeren:

  • fouten te veroorzaken en het apparaat te laten draaien: de foutmeldingen moeten behouden blijven;
  • willekeurige getallen genereren en het apparaat laten draaien: de gegenereerde willekeurige getallen moeten op het scherm blijven staan;
  • een wachttijd van enkele seconden instellen en het apparaat tijdens het wachten laten draaien: de taken moeten zijn geannuleerd (dit is te zien in de logbestanden);

2.8.4. Voorbeeld-22B

We nemen hier voorbeeld 22 opnieuw ter hand om het te herstructureren volgens het model van het project [client-android-skel]. Ter herinnering: het project [Exemple-22] beheert de cyclus van opslaan en herstellen van fragmenten tijdens een rotatie correct en dit project diende als basis voor het project [client-android-skel].

We dupliceren het project [client-android-skel] naar [exemples/Exemple-22B] en laden dit laatste project:

  

Vervolgens kopiëren we diverse elementen uit het project [Exemple-22] naar het project [Exemple-22B].

Allereerst kopiëren we elementen uit de map [res]:

  • [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, de map [values];
  

We passen de bovenmarge van beide weergaven aan naar 120 dp:

[vue1.xml]:


  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginTop="120dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>

[fragment_main]:


  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>

Vervolgens kopiëren we de elementen [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

In dit stadium kunnen we een eerste compilatie proberen. Er verschijnt een eerste soort fouten: die van onjuiste imports-elementen omdat klassen van pakket zijn veranderd. We corrigeren deze imports-elementen. Een tweede soort fouten is te wijten aan het feit dat de fragmenten niet alle methoden van hun bovenliggende klasse [AbstractFragment] implementeren. We corrigeren dit met (Alt+Enter).

De resterende fouten zijn het gevolg van verschillen tussen de oude en de nieuwe klasse [AbstractFragment]. Deze worden voorlopig genegeerd.

2.8.4.1. Het project aanpassen

  

In de map [custom] bevinden zich de architectuurelementen die door de ontwikkelaar kunnen worden aangepast.

Via de interface [IMainActivity] kunnen bepaalde kenmerken van het project worden gespecificeerd:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // toegang tot de sessie
  ISession getSession();

  // weergave wijzigen
  void navigateToView(int position, ISession.Action action);

  // wachtrijbeheer
  void beginWaiting();

  void cancelWaiting();

  // debugmodus
  boolean IS_DEBUG_ENABLED = true;

  // maximale wachttijd voor het antwoord van de server
  int TIMEOUT = 1000;

  // wachttijd voordat de clientverzoek wordt uitgevoerd
  int DELAY = 0;

  // basisauthenticatie
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // aaneenschakeling van fragmenten
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // tabbladbalk
  boolean ARE_TABS_NEEDED = true;

  // laadafbeelding
  boolean IS_WAITING_ICON_NEEDED = false;

  // aantal fragmenten
  int FRAGMENTS_COUNT = 5;

}
  • regels 23, 26, 29, 38: kenmerken van de laag [DAO]. Hier zijn er geen;
  • regel 41: hier zijn vijf fragmenten;
  • regel 32: aangrenzendheid van de fragmenten. Deze constante kan hier een waarde aannemen in [1,4]. De lezer wordt aangemoedigd om deze waarde te variëren om te zien of de toepassing blijft functioneren;
  • regel 35: dit is een applicatie met tabbladen;

De klasse [CoreState] die de status van de fragmenten opslaat, ziet er als volgt uit:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
  // al dan niet bezocht fragment
  protected boolean hasBeenVisited = false;
  // status van het eventuele menu van het fragment
  protected MenuItemState[] menuOptionsState;

  // getters en setters
...
}
  • regel 12: we definiëren de klasse voor de status van het fragment [PlaceHolderFragment]. Het fragment [Vue1Fragment] heeft geen status;

De klasse [Session] is als volgt:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // gegevens die tussen fragmenten onderling en tussen fragmenten en de activiteit moeten worden gedeeld
  // elementen die niet in jSON kunnen worden geserialiseerd, moeten de annotatie @JsonIgnore hebben
  // vergeet niet de getters en setters die nodig zijn voor serialisatie/deserialisatie in jSON

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

  // getters en setters
...
}

Dit is de sessie van het project [Exemple-22].

2.8.4.2. De activiteit [MainActivity]

  

De activiteit [MainActivity] is als volgt:


package client.android.activity;

import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // laag [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessie
  private Session session;

  // menubeheer-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

  private void showFragment(int i) {
...
  }

  // implementatie van methoden van de bovenliggende klasse ---------------------------------------------------
  ...
}

Hier is de klasse [MainActivity] om twee redenen omvangrijker dan die in de voorgaande voorbeelden:

  • er moeten tabbladen worden beheerd;
  • er moet een menu worden beheerd;

2.8.4.2.1. Implementatie van de methoden van de bovenliggende klasse

// methoden van de bovenliggende klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // we gaan verder met de initialisaties die door de bovenliggende klasse zijn gestart
    // sessie
    this.session = (Session) super.session;
    ...
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // fragmentnummer
    final String ARG_SECTION_NUMBER = "section_number";
    // initialisatie van de fragmentenlijst
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; 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);
    }
    // een fragment van +
    fragments[i] = new Vue1Fragment_();
    // resultaat
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // hier geen titels
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
...
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • regels 2-12: de methode [onCreateActivity] wordt aangeroepen door de bovenliggende klasse [AbstractActivity] wanneer de activiteit voor het eerst wordt aangemaakt of opnieuw wordt aangemaakt tijdens een opslag-/herstelcyclus. Wanneer deze methode wordt aangeroepen, heeft de bovenliggende klasse de sessie al hersteld;
  • regel 10: er wordt een lokale verwijzing naar de sessie opgehaald. De typewijziging is te wijten aan het feit dat de sessie van de bovenliggende klasse van het type [AbstractSession] is;
  • regels 19-38: de methode [getFragments] moet de array met de door de applicatie beheerde fragmenten teruggeven aan de bovenliggende klasse. Hier zijn er [FRAGMENTS_COUNT], een aantal dat is gedefinieerd in [IMainActivity]. De eerste [FRAGMENTS_COUNT-1] fragmenten zijn van het type [PlaceHolderFragment] en het laatste van het type [Vue1Fragment];
  • regels 41-45: de methode [getFragmentTitle] moet de titels van de fragmenten weergeven wanneer deze informatie nuttig kan zijn. Dat is hier niet het geval;
  • regels 47-50: deze methode wordt aangeroepen door de bovenliggende klasse wanneer de gebruiker op een tabblad klikt. We komen hier in de volgende paragraaf op terug;
  • regels 52-55: geeft het nummer van de eerste weergave die moet worden getoond wanneer de applicatie opstart. Hier is het fragment [Vue1Fragment] dat als eerste moet worden weergegeven. De methode [getFirstView] zou met voordeel kunnen worden vervangen door een constante in [IMainActivity];

2.8.4.2.2. Beheer van tabbladen

De tabbladen worden beheerd door de volgende methoden:


@Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // we gaan verder met de initialisaties die door de bovenliggende klasse zijn gestart
    // sessie
    this.session = (Session) super.session;
    // 1e tabblad
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // tweede 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);
    }
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // nummer van het weer te geven fragment
    int numFragment;
    switch (position) {
      case 0:
        // fragmentnummer [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // fragmentnummer [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // fragment weergeven
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • regels 1-20: de methode [onCreateActivity] wordt aangeroepen door de bovenliggende klasse [AbstractActivity] wanneer de activiteit voor het eerst wordt aangemaakt of opnieuw wordt aangemaakt tijdens een opslag-/herstelcyclus. Wanneer deze methode wordt aangeroepen, heeft de bovenliggende klasse de sessie al hersteld;
  • regel 9: er wordt een lokale verwijzing naar de sessie opgehaald. De typewijziging is te wijten aan het feit dat de sessie van de bovenliggende klasse van het type [AbstractSession] is;
  • regels 11-13: het eerste tabblad wordt aangemaakt;
  • regels 15-20: het tweede tabblad wordt aangemaakt als er een fragmentnummer is opgeslagen in de sessie (regel 15). Dit nummer is aanvankelijk -1 bij de eerste aanmaak van de activiteit;
  • regels 23-39: deze methode wordt door de bovenliggende klasse aangeroepen wanneer de gebruiker op een tabblad klikt;
  • regels 28-31: als er op tabblad 0 wordt geklikt, moet [Vue1Fragment] worden weergegeven. We weten dat dit het eerste scherm is dat bij het opstarten van de applicatie werd weergegeven;
  • regels 32-35: als er op tabblad 1 wordt geklikt, moet het fragment worden weergegeven waarvan het nummer in de sessie is opgeslagen;
  • regels 37-39: we navigeren naar het gekozen fragment. De bijbehorende actie is [SUBMIT]. Zou dit ook [NAVIGATION] kunnen zijn geweest? In dit document wordt [NAVIGATION] alleen gebruikt wanneer voor het weergeven van het nieuwe fragment alleen de vorige status ervan bekend hoeft te zijn. Dat is hier niet het geval, aangezien de weergave van het getoonde fragment moet veranderen ten opzichte van de vorige status om nog een bezoek weer te geven;

2.8.4.2.3. Beheer van het menu

De activiteit is gekoppeld aan het volgende menu [menu_main.xml]:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment1"
        android:title="@string/fragment1"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment2"
        android:title="@string/fragment2"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment3"
        android:title="@string/fragment3"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment4"
        android:title="@string/fragment4"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>

dat het volgende weergeeft:

  

Het menu wordt beheerd via de volgende methoden:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // logboek
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // verwerking van menuopties
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d(className, "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
    }
    // verwerkt item
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // geen navigatie bij het selecteren van een tabblad via de software
      session.setNavigationOnTabSelectionNeeded(false);
      // we maken de twee tabbladen opnieuw aan vanwege een probleem met het lettertype van de titels
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // het nummer van het weer te geven fragment wordt in de sessie opgeslagen
      session.setNumFragment(i);
      // tabblad nr. 2 wordt geselecteerd met navigatie
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • regels 16-31: verwerking van een klik op een menuoptie van het type [Fragmenti];
  • regels 37-50: geven fragment nr. i weer (dit zijn fragmenten van het type PlaceHolderFragment) in tabblad nr. 1 (2e tabblad);
  • regels 42-44: er wordt besloten de bestaande tabbladen te verwijderen om twee nieuwe aan te maken. Deze beslissing is genomen om het volgende probleem te omzeilen: wanneer men het fragment alleen weergeeft in het bestaande tabblad 1 (zonder het dus te verwijderen), ziet de titel ervan vreemd genoeg er anders uit (lettertype, grootte) dan de titel van tabblad 0;
  • regels 43-44: de twee tabbladen worden aangemaakt maar niet geselecteerd (laatste parameter bij false);
  • regel 40: de bewerkingen in de regels 42-44 kunnen [select]-bewerkingen op de tabbladen uitvoeren, waardoor de handler [onTabSelected] wordt aangeroepen. Als er niets wordt gedaan, vindt er navigatie naar een fragment plaats. Dit wordt voorkomen door de booleaanse waarde [navigationOnTabSelectionNeeded] in de sessie in te stellen op faux. Deze booleaanse waarde wordt automatisch teruggezet naar vrai door de klasse [AbstractFragment] wanneer een fragment zichtbaar wordt;
  • regel 46: het nummer van het weer te geven fragment wordt in de sessie opgeslagen;
  • regels 48-50: tabblad nr. 2 met navigatie wordt geselecteerd (regel 48). Dit activeert de procedure [onTabSelected], die:
    • het fragment weergeeft waarvan het nummer in de sessie is opgeslagen;
    • het nummer van het geselecteerde tabblad in de sessie opslaat;

2.8.4.3. Het fragment [Vue1Fragment]

Hier volgt de definitieve versie van het fragment:


package client.android.fragments.behavior;

import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {

  // de 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(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // levenscyclus van het fragment -----------------------------------------------
  private void initFragment() {
    // niets te doen
  }

  // status van het fragment opslaan
  @Override
  public CoreState saveFragment() {
    // status van de weergave – niets om op te slaan
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // niets te doen
  }

  @Override
  protected void initView(CoreState previousState) {
    // Eerste bezoek?
    if (previousState == null) {
      // het bezoeknummer wordt weergegeven
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // het bezoeknummer wordt weergegeven
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // privé-methoden -------------------------------------
  // bezoeknummer weergeven
  private void showNumVisit() {
    // bezoeknummer verhogen
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // het bezoeknummer wordt weergegeven
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

De klasse is vrijwel leeg.

  • regels 35-39: worden aangeroepen door de bovenliggende klasse wanneer het fragment zijn status moet opslaan. Het fragment [Vue1Fragment] heeft geen status die moet worden opgeslagen. Er wordt simpelweg een instantie van de basisklasse [CoreState] teruggegeven (let op: null mag niet worden teruggegeven);
  • regels 41-44: moeten het nummer van het fragment retourneren. Het fragment [Vue1Fragment] heeft per definitie het nummer [FRAGMENTS_COUNT-1];
  • regels 51-59: worden aangeroepen door de bovenliggende klasse wanneer het fragment voor de eerste keer wordt aangemaakt (previousState==null) of bij volgende keren (previousState!=null);
    • regels 54-57: als het het eerste bezoek is, wordt het bezoeknummer verhoogd en weergegeven (regels 85-92);
  • regels 61-65: worden aangeroepen wanneer het fragment wordt weergegeven in combinatie met een actie [SUBMIT]. Het bezoeknummer wordt verhoogd en weergegeven. Hier is het niet mogelijk dat het bezoeknummer tijdens de levenscyclus twee keer wordt verhoogd. Het eerste bezoek aan het fragment [Vue1Fragment] vindt namelijk plaats bij het opstarten van de applicatie, wanneer de actie per definitie in de sessie de waarde [NONE] heeft. Dit zorgt ervoor dat de methode [updateOnSubmit] niet wordt aangeroepen. Vervolgens zal het nooit meer het eerste bezoek zijn en zal de methode [initView] niets doen;
  • regels 68-71: worden aangeroepen in een cyclus van opslaan/herstellen. Aangezien het fragment geen status heeft, valt hier niets te herstellen;
  • regels 73-76: worden aangeroepen wanneer alle voorgaande updates zijn uitgevoerd. Hier valt niets meer te doen;
  • regels 78-81: worden aangeroepen wanneer alle gestarte asynchrone taken zijn voltooid. Hier zijn er geen asynchrone taken;

2.8.4.4. De status [PlaceHolderFragmentState]

De status van het fragment [PlaceHolderFragment] zal als volgt zijn:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class PlaceHolderFragmentState extends CoreState {
  // tekst
  private String text;

  // constructors
  public PlaceHolderFragmentState() {

  }

  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }

  // getters en setters
 ...
}
  • wanneer de status van het fragment moet worden opgeslagen, wordt de tekst opgeslagen die het weergeeft (regel 7);

2.8.4.5. Het fragment [PlaceHolderFragment]

Het fragment [PlaceHolderFragment] zal er als volgt uitzien:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {

  // componenten van de visuele interface
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;

  // gegevens
  private String text;

  // fragmentnummer
  private static final String ARG_SECTION_NUMBER = "section_number";

  // implementatie van methoden van de bovenliggende klasse ----------------------------
  @Override
  public CoreState saveFragment() {
    // de status van het fragment wordt opgeslagen
    PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
    placeHolderFragmentState.setText(textViewInfo.getText().toString());
    return placeHolderFragmentState;
  }

  @Override
  protected int getNumView() {
    return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // oorspronkelijke tekst
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // de weergegeven tekst wordt bijgewerkt
    // bezoeknummer verhogen
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // gewijzigde tekst
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // logboek
    if (isDebugEnabled) {
      Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
    }
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // de weergegeven tekst wordt teruggezet
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • regels 30-36: wanneer de bovenliggende klasse het fragment vraagt om zijn status op te slaan, wordt de door het fragment weergegeven tekst opgeslagen (regel 34);
  • regels 38-41: geven het fragmentnummer terug. Dit is afhankelijk van het sectienummer dat bij de aanmaak als argument is doorgegeven;
  • regels 43-47: worden aangeroepen bij de eerste aanmaak van het fragment (previousState==null) of bij de volgende aanmaakpogingen (previousState !=null);
    • regel 46: hier wordt geen gebruik gemaakt van de vorige status. De oorspronkelijke tekst [text] (regel 24), die bij het eerste bezoek wordt weergegeven, wordt telkens opnieuw berekend. Dit is discutabel. Men had ervoor kunnen kiezen om deze informatie ook in de status van het fragment op te nemen;
  • regels 49-51: worden aangeroepen bij de eerste opbouw van de weergave die aan het fragment is gekoppeld (previousState==null) of bij de volgende keren (previousState!=null). Hier valt niets aan te doen;
  • regels 53-56: worden aangeroepen wanneer het fragment wordt weergegeven in combinatie met een actie [SUBMIT]. Dit is altijd het geval, behalve bij de cyclus opslaan/herstellen, waarbij de actie [RESTORE] is. We verhogen dus het bezoeknummer en geven dit weer;
  • regels 68-74: worden aangeroepen in een back-up-/herstelcyclus. We herstellen de tekst die in de status van het fragment was opgeslagen;
  • regels 76-79: worden aangeroepen wanneer alle voorgaande updates zijn uitgevoerd. Hier valt verder niets meer te doen;
  • regels 82-83: worden aangeroepen wanneer alle gestarte asynchrone taken zijn voltooid. Hier zijn er geen asynchrone taken;

2.8.4.6. Tests

De lezer wordt uitgenodigd om de applicatie te testen door het apparaat te draaien om te controleren of het weergegeven fragment zijn status behoudt. We zullen ook de logbestanden bekijken.

2.9. Conclusion

Aan het einde van dit hoofdstuk beschikken we over een voorbeeldproject [client-android-skel] van een Android-client die communiceert met een webservice / jSON met de volgende kenmerken:

  • de asynchrone communicatie met de webserver / jSON verloopt via de bibliotheek RxJava;
  • de levenscyclus van een fragment (update, save, restore) wordt beheerd door de bovenliggende klasse [AbstractFragment], die op specifieke momenten bepaalde methoden van de onderliggende klassen aanroept. Het onderliggende fragment hoeft zich dus geen zorgen te maken over de fasen van de levenscyclus, maar hoeft alleen bepaalde methoden te implementeren die door de bovenliggende klasse worden opgelegd;
  • de levenscyclus van de activiteit (opslaan / herstellen) wordt beheerd door een abstracte klasse [AbstractActivity], die op haar beurt de dochteractiviteit verplicht bepaalde methoden te implementeren;
  • de klasse [AbstractActivity] kan een applicatie beheren met of zonder tabbladen, met of zonder laadbeeld, met of zonder basisauthenticatie bij de webserver / jSON. De aanwezigheid of afwezigheid van deze elementen wordt bepaald via de configuratie;

We gaan nu een casestudy presenteren die complexer is dan de voorgaande voorbeelden. De nieuwe applicatie zal gebaseerd zijn op het modelproject [client-android-skel].