2. Esqueleto de um cliente Android que comunica com um serviço web / JSON
Apresentamos agora um esqueleto para uma aplicação Android que comunica com um ou mais serviços web / JSON. Trata-se do projeto [client-android-skel], que se encontra na pasta [architecture] dos exemplos:
![]() |
O estudo desta aplicação-esqueleto proporcionará uma oportunidade para rever alguns pontos que abordámos nos exemplos anteriores. Esta aplicação servirá de esqueleto para todas as aplicações futuras. Foi desenvolvida após inúmeras iterações. Tem como objetivo abstrair o maior número possível de elementos das aplicações que iremos construir em breve, transformando-os em classes abstratas, para evitar ter de escrever repetidamente o mesmo tipo de código, diferindo apenas em pormenores. As suas funcionalidades são as seguintes:
- a comunicação assíncrona com o servidor web/JSON é gerida utilizando a biblioteca RxJava;
- o ciclo de vida de um fragmento (atualizar, guardar, restaurar) é gerido pela sua classe pai [AbstractFragment], que chama determinados métodos das suas classes filhas em momentos específicos. A classe filha, assim, não precisa de se preocupar com as fases do ciclo de vida, mas apenas precisa de implementar determinados métodos exigidos pela sua classe pai;
- o ciclo de vida da atividade (guardar/restaurar) é gerido por uma classe abstrata [AbstractActivity], que também exige que a atividade filha implemente determinados métodos;
- A classe [AbstractActivity] é capaz de gerir uma aplicação com ou sem separadores, com ou sem uma imagem de carregamento e com ou sem autenticação básica no servidor web/JSON. A presença ou ausência destes elementos é determinada pela configuração;
Este esqueleto foi utilizado em todos os exemplos subsequentes. Devido à sua diversidade, o que funcionou num exemplo pode não funcionar no seguinte. Uma vez que o esqueleto foi utilizado num total de sete exemplos, ocorreram inúmeras iterações. Se o utilizássemos para um oitavo exemplo, é possível que descobríssemos novamente que a natureza específica deste novo exemplo gera novos erros. No entanto, a utilização deste esqueleto simplificará consideravelmente a escrita de exemplos futuros. De facto, gerir o ciclo de vida de um fragmento (atualizar, guardar, restaurar) combinado com o conceito de adjacência de fragmentos é particularmente complexo. Aqui, está completamente oculto dentro da classe [AbstractFragment].
2.1. Arquitetura do Cliente Android
O cliente Android proposto baseia-se na seguinte arquitetura:
![]() |
- a camada [DAO] implementa uma interface [IDao]. É responsável pela comunicação com o servidor web/JSON;
- existe apenas uma atividade que também implementa a interface [IDao]. As vistas recorrem a ela para aceder ao servidor;
- as vistas são implementadas por fragmentos;
O projeto Android reflete esta arquitetura:
![]() |
Apresentaremos os vários elementos deste projeto um por um.
2.2. A configuração do Gradle
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// options de packaging nécessaires pour être capable de produire l'APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile 'io.reactivex:rxandroid:1.2.0'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
- Todos os números de versão estão sujeitos a alterações. No entanto, pode começar com os números atuais se configurar o Android Studio para garantir que estas versões das ferramentas Android (linhas 15–16, 47–48) estejam presentes (consulte a secção 6.11);
2.3. O manifesto da aplicação
![]() |
<?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>
- Linha 3: Altere o pacote da aplicação;
- Linhas 10, 15: Vamos definir o valor do item [app_name] no ficheiro [res/values/strings.xml]. Por enquanto, é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- application name -->
<string name="app_name">[Donnez un nom à votre application]</string>
</resources>
2.4. A organização do código Java
![]() |
- [arquitetura] agrupa os principais elementos da organização do código;
- [actividade] contém a única actividade da aplicação;
- [fragments] agrupa os fragmentos ou vistas da aplicação;
- [dao] agrupa os elementos para comunicação com o servidor web / JSON;
2.5. Elementos da Atividade
![]() | ![]() |

2.5.1. A vista associada à atividade
A vista [activity_main.xml] associada à atividade é a seguinte:
<?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>
<!-- fragment container -->
<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>
- Linha 29: É utilizado um contentor de fragmentos específico;
A atividade também possui um menu [res/menu/menu_main.xml] para a sua visualização:
<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>
Por enquanto, está vazio. O programador irá preenchê-lo conforme necessário.
2.5.2. O contêiner de fragmentos [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 {
// swipe control
private boolean isSwipeEnabled;
// controls scrolling
private boolean isScrollingEnabled;
// manufacturers
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// methods to be redefined to manage swiping
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// scrolling control
@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;
}
}
Esta classe estende a classe padrão [ViewPager] do Android exclusivamente para lidar com o deslizar (linha 11) e a rolagem (linha 13) entre as vistas.
- linhas 26–43: métodos que desativam o deslizar se este tiver sido desativado;
- linhas 46–49: redefinição do método [setCurrentItem], que é utilizado para alterar a vista apresentada. Se a rolagem tiver sido desativada, a vista será alterada sem rolagem. Note-se que o programador pode substituir este comportamento utilizando o método [setCurrentItem(int position, boolean smoothScrolling)], que permite especificar o comportamento de rolagem pretendido;
2.5.3. A classe [CoreState]
![]() |
A classe [CoreState] é a classe pai dos estados dos vários fragmentos:
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: add subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- linha 16: Cada fragmento tem um valor booleano [hasBeenVisited] no seu estado que indica se já foi visitado ou não. Isto é necessário porque, por vezes, quando um fragmento é apresentado pela primeira vez, há ações específicas que precisam de ser realizadas;
- linha 18: o projeto [client-android-skel] guarda e restaura automaticamente os menus dos fragmentos, caso estes tenham um. Na matriz MenuItemState[] menuOptionsState, guardamos o estado visível ou oculto de todas as opções do menu;
- Linhas 10–13: Tal como feito no [Exemplo-22], o estado da atividade e dos seus fragmentos será guardado na sessão, que por sua vez será guardada como uma cadeia JSON. Veremos que a sessão armazena uma matriz de elementos do tipo [CoreState]. Se não fizermos nada, a cadeia JSON do tipo [CoreState] será guardada. No entanto, queremos guardar os estados dos fragmentos, que são derivados de [CoreState]. Para garantir que a cadeia JSON do tipo derivado é gerada em vez da do tipo pai, os tipos derivados devem ser declarados conforme mostrado nas linhas 10–13. A classe [CoreState] é uma das classes de arquitetura que o programador deve modificar para cada nova aplicação (linhas 10–13);
2.5.4. A interface [IMainActivity]
![]() |
A interface [IMainActivity] define o que os fragmentos podem solicitar à atividade na seguinte arquitetura:

package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants (to be modified) -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of application fragments
int FRAGMENTS_COUNT = 0;
// todo add your constants and other methods here
}
- linha 6: a interface [IMainActivity] estende a interface [IDao] da camada [DAO];
- Linha 9: Esta é a atividade que fornece acesso à sessão na forma de uma instância da interface [ISession];
- linha 12: esta é a atividade utilizada para alternar entre vistas. O segundo parâmetro é a ação que desencadeia esta mudança de vista, um dos valores SUBMIT, NAVIGATION ou RESTORE;
- linhas 15–17: esta é a atividade que gere o ecrã de carregamento;
- linha 22: para depuração da aplicação;
- linha 25: para evitar esperar demasiado tempo se o servidor deixar de responder;
- linha 28: durante a depuração, defina este valor para alguns segundos para dar tempo de cancelar a operação com o servidor e ver o que acontece;
- linha 31: defina como true se o serviço JSON exigir autenticação básica;
- linha 34: adjacência de fragmentos;
- linha 37: defina como true se a aplicação tiver separadores;
- linha 39: defina como true se a aplicação comunicar com um servidor web/JSON e pretender apresentar uma imagem de carregamento durante as trocas;
- linha 43: o número de fragmentos geridos pela aplicação;
A interface [IMainActivity] é o segundo elemento da arquitetura que o programador deve implementar (linha 45).
2.5.5. A interface [IDao]
A interface [IMainActivity] estende a seguinte interface [IDao]:
![]() |
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// todo: declare your interface here
}
- linha 24: o programador irá completar a interface aqui;
2.5.6. A sessão
![]() |
A classe [Session] encapsula elementos partilhados pela atividade e pelos fragmentos. Ela implementa a seguinte interface [ISession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
public interface ISession {
// number of last view displayed
int getPreviousView();
void setPreviousView(int numView);
// last state of a view
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// action in progress
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// status of all views -
// not used by code but required for serialization / deserialization jSON
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// last selected tab number
int getPreviousTab();
void setPreviousTab(int position);
// tab selection navigation
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
Introduzimos a interface [ISession] para exigir a presença de determinados métodos na sessão:
- linhas 7–10: o número da última vista (fragmento) exibida;
- linhas 12–15: o estado de uma vista específica;
- linhas 17–24: introduzimos o conceito de uma ação em curso. Existem quatro (linha 17):
- RESTORE: uma operação de guardar/restaurar está em curso. Não há alteração de vista;
- NAVIGATION: a navegação está em curso. Aqui, definimos a navegação como uma alteração de vista em que a nova vista pode ser restaurada a partir do seu último estado guardado durante a sessão;
- SUBMIT: atribuímos o tipo [SUBMIT] a uma ação pendente quando há uma alteração de vista e a nova vista depende do estado geral da atividade, não apenas do seu próprio estado. Por vezes, é difícil distinguir entre NAVIGATION e SUBMIT. Nesses casos, utilizaremos o caso mais geral de SUBMIT;
- NONE: o valor da ação quando esta ainda não recebeu o seu primeiro valor;
- linhas 26–30: os estados da atividade e dos fragmentos serão armazenados numa matriz CoreState[]. Para garantir que isto seja tratado corretamente durante a serialização/desserialização JSON, deve ter um getter e um setter;
- linhas 32-35: número da última guia selecionada. Utilizado durante o ciclo de gravação/restauração para voltar a selecionar a guia que estava selecionada antes de o dispositivo ter sido rodado;
- linhas 37–40: gere um valor booleano que indica se a seleção de uma guia deve ser acompanhada por uma alteração de fragmento;
A interface [ISession] é implementada pela seguinte classe abstrata [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 {
// previous view no
private int preViousView;
// view status
private CoreState[] coreStates = new CoreState[0];
// action in progress
private Action action = Action.NONE;
// previously selected tab
private int previousTab;
// tab selection navigation
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// manufacturer
public AbstractSession() {
// initialize the fragment status table
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;
}
}
- linha 9: o ID da vista que foi exibida antes da vista atualmente exibida. Esta informação é útil quando uma vista pode ser acedida a partir de vários locais. Este é normalmente o caso na navegação baseada em separadores. A vista exibida pode então determinar qual a vista que foi exibida anteriormente;
- linha 12: a matriz de estados para todos os fragmentos exibidos pela atividade;
- linha 18: o ID da guia selecionada anteriormente. Desempenha um papel semelhante ao do ID da vista anterior na linha 9. Esta informação é útil quando o dispositivo é rodado e é necessário regressar à guia que estava selecionada antes da rotação;
- linha 22: um valor booleano que indica se a seleção de uma guia deve resultar numa alteração do fragmento exibido. Note-se que o projeto [client-android-skel] gere guias e fragmentos separadamente, para que possa ser utilizado em casos em que o número de guias é inferior ao número de fragmentos. Existem dois tipos de seleção:
- uma seleção feita pelo utilizador ao clicar numa guia. Neste caso, o fragmento exibido geralmente precisa de ser alterado;
- uma seleção orientada por software através do método [Tablayout.Tab.select()]. Neste caso, alterar o fragmento exibido nem sempre é desejável. Aqui estão dois exemplos:
- quando o dispositivo é rodado, a atividade é recriada, assim como as separadores. No entanto, quando a primeira separador é criada, esta é automaticamente submetida a uma operação de [select] por software. Por conseguinte, não é desejável alterar o fragmento apresentado, uma vez que nos encontramos numa fase de recriação da atividade em que o fragmento que será finalmente apresentado não será necessariamente aquele associado à primeira separador;
- uma vez que a gestão de separadores é separada da gestão de fragmentos, poderá querer atualizar os separadores (eliminar, adicionar) sem interferir com os fragmentos associados. No entanto, algumas destas operações podem, mais uma vez, desencadear uma operação de software [select] implícita num dos separadores. Esta seleção não resulta necessariamente na navegação para o fragmento associado;
- linha 21: o campo [navigationOnTabSelectionNeeded] não se destina a ser guardado durante as operações de gravação da atividade e dos seus fragmentos. A anotação [@JsonIgnore] faz com que o campo seja ignorado durante a serialização/desserialização JSON;
- linhas 25–31: O construtor inicializa a matriz de estados para os [FRAGMENTS_COUNT] fragmentos da aplicação. Os elementos desta matriz são inicializados com o campo [hasBeenVisited=false]. Esta informação é utilizada para determinar se esta é ou não a primeira visita ao fragmento;
A classe [Session] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// data to be shared between fragments themselves and between fragments and activities
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
// don't forget the getters and setters required for serialization / deserialization jSON
}
- linha 5: a classe [Session] estende a classe [AbstractSession] que acabámos de ver. O programador irá colocar aqui os elementos a serem partilhados entre os próprios fragmentos e entre os fragmentos e a atividade. Note-se que a classe [Session] já não está anotada com a anotação [@EBean]. Tornou-se uma classe normal;
2.5.7. A classe abstrata [AbstractActivity]
![]() |
2.5.7.1. Estrutura
A classe [AbstractActivity] é uma classe com mais de 300 linhas. Vamos analisá-la passo a passo. A sua estrutura é a seguinte:
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 {
// layer [DAO]
private IDao dao;
// the session
protected Session session;
// the fragment container
protected MyPager mViewPager;
// the toolbar
private Toolbar toolbar;
// the waiting image
private ProgressBar loadingPanel;
// tab bar
protected TabLayout tabLayout;
// the fragment or section manager
private FragmentPagerAdapter mSectionsPagerAdapter;
// class name
protected String className;
// mapper jSON
private ObjectMapper jsonMapper;
// manufacturer
public AbstractActivity() {
// class name
className = getClass().getSimpleName();
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructeur");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// implémentation IMainActivity --------------------------------------------------------------------
...
// life cycle - backup / restore ------------------------------------
...
// hold image management ---------------------------------
...
// interface IDao -----------------------------------------------------
...
// the fragment manager --------------------------------
...
// girls' classes
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();
}
A classe [AbstractActivity]:
- implementa a interface [IMainActivity] (linhas 21, 55);
- lida com o salvamento e a restauração da atividade e dos seus fragmentos quando o dispositivo gira (linha 58);
- lida com o ecrã de carregamento durante a comunicação com o servidor web / JSON (linha 61);
- implementa a interface IDao da camada [DAO] (linha 64);
- implementa o gestor de fragmentos (linha 67);
- exige que as suas classes filhas tenham seis métodos (linhas 71–81);
2.5.7.2. Implementação da interface [IMainActivity]
A implementação da interface [IMainActivity] (ver secção 2.5.4) é a seguinte:
// implémentation 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));
}
// display new fragment
mViewPager.setCurrentItem(position);
// we note the action in progress when the view changes
session.setAction(action);
}
2.5.7.3. Guardar o estado da atividade e dos seus fragmentos
O estado da atividade e dos seus fragmentos está inteiramente contido na sessão. Por isso, precisamos de guardar a sessão. Aqui, reutilizamos o que foi feito no projeto [Exemplo-22] (ver secção 1.23):
// backup / restore management ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// save session as jSON string
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.5.7.4. Restaurar o estado da atividade e dos seus fragmentos
Isto envolve a restauração da sessão. Procedemos conforme mostrado no [Exemplo-22]:
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
// something to restore?
if (savedInstanceState != null) {
// session recovery
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 {
// session
session = new Session();
}
...
- linhas 10–26: se o parâmetro [Bundle savedInstanceState] na linha 2 não for nulo, a sessão é restaurada (linhas 12–17);
- linhas 26–29: se o parâmetro [Bundle savedInstanceState] na linha 2 for nulo, isso corresponde à primeira vez que a atividade é iniciada. É então criada uma sessão vazia;
2.5.7.5. Inicialização da camada [DAO]
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// layer [DAO]
dao = getDao();
if (dao != null) {
// layer configuration [DAO]
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// girls' classes
protected abstract IDao getDao();
....
}
- linha 11: é solicitada uma referência à camada [DAO] à atividade filha (linha 21);
- linhas 14–17: se a camada [DAO] existir, é configurada utilizando a informação contida na interface [IMainActivity];
2.5.7.6. Inicialização da vista associada à atividade
A vista associada à atividade foi apresentada na Secção 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>
<!-- fragment container -->
<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>
Esta vista é inicializada com o seguinte código:
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// associated view
setContentView(R.layout.activity_main);
// view components ---------------------
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// waiting image?
if (IS_WAITING_ICON_NEEDED) {
// we add the waiting image
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// creation ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// added ProgressBar to toolbar
toolbar.addView(loadingPanel);
}
...
- linha 11: a vista XML [activity_main] está associada à atividade;
- linhas 14-15: a barra de ferramentas está integrada e é suportada;
- linhas 17-27: adição opcional de um ícone de carregamento: se o booleano [IS_WAITING_ICON_NEEDED] for verdadeiro na interface [IMainActivity];
- linha 23: criação da imagem de carregamento do tipo [ProgressBar] referenciada pelo campo [loadingPanel];
- linha 24: inicialmente, esta imagem está oculta;
- linha 26: é adicionada à barra de ferramentas;
2.5.7.7. Gestão de separadores
A interface [IMainActivity] pode solicitar uma barra de separadores. Esta é adicionada e gerida da seguinte forma:
// tab bar
protected TabLayout tabLayout;
...
// tab bar?
if (ARE_TABS_NEEDED) {
// add the tab bar
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// no selection navigation until a fragment is displayed
session.setNavigationOnTabSelectionNeeded(false);
// tab bar creation
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// tab bar added to application bar
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// tab bar event manager
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// a tab has been selected
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()) {
// miter position
int position = tab.getPosition();
// memory
session.setPreviousTab(position);
// associated fragment display?
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// girls' classes
protected abstract void navigateOnTabSelected(int position);
...
- linhas 12–48: adicionar e gerir uma barra de separadores;
- linha 6: a barra de separadores é adicionada se a constante [ARE_TABS_NEEDED] estiver definida como true na interface [IMainActivity];
- linha 12: ao criar a barra de separadores, podem ocorrer operações implícitas [Tablayout.Tab.select] (estas não são acionadas pelo utilizador). Definimos o booleano [session.navigationOnTabSelectionNeeded] como false para impedir qualquer navegação durante essas seleções falsas. Caberá ao programador selecionar o fragmento a ser exibido usando o método [navigateToView]. O booleano [session.navigationOnTabSelectionNeeded] será redefinido como true quando este fragmento for exibido (ver classe AbstractFragment);
- linha 14: criação de uma barra de separadores referenciada pelo campo [tabLayout]. Utilizamos uma barra de separadores personalizada [CustomTabLayout], que discutiremos mais adiante;
- linha 15: definimos as cores dos títulos das guias. Estas encontram-se no seguinte ficheiro [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>
- linha (c): a cor do título da guia quando a guia está selecionada;
- linha (d): a cor do título da guia quando esta não está selecionada;
Este ficheiro é, naturalmente, editável. Pode encontrar os códigos de cor hexadecimais aqui, por exemplo.
- linhas 17-18: adicionar esta barra de separadores à barra de aplicações na vista XML [activity_main];
- linhas 20–47: manipulador de eventos para a barra de separadores;
- linhas 22–36: apenas o evento [onTabSelected] é tratado. Corresponde a um clique na [guia Tab] passada como parâmetro para o método ou a uma operação de software [TabLayout.Tab.select];
- linha 30: posição da guia selecionada;
- linha 32: esta posição é armazenada na sessão;
- linha 34: o fragmento associado a esta guia deve agora ser exibido. Apenas a classe filha (linha 52) pode fazer esta associação. Note-se que não associamos a barra de guias ao contêiner de fragmentos [mViewPager], como foi feito em alguns dos exemplos estudados. Aqui, separamos completamente a gestão da barra de guias da dos fragmentos. É por isso que, quando uma guia é clicada, devemos especificar qual vista queremos ver exibida;
- linha 28: distinguimos entre a seleção de separadores com ou sem navegação. Geralmente, quando o utilizador clica num separador, espera-se que haja navegação, ao passo que durante uma seleção programática, isso não acontece. O programador distingue entre estes dois casos utilizando o elemento [session.navigationOnTabSelectionNeeded]. Quando a navegação não é realizada, o número do último separador selecionado não é guardado na sessão. Cabe ao programador fazê-lo;
2.5.7.8. O gestor de separadores [CustomTabLayout]
![]() |
Utilizamos um gestor de separadores personalizado para apresentar os títulos dos separadores em diferentes tipos de letra. A classe [CustomTabLayout] é a seguinte:
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);
}
}
}
}
- A fonte dos títulos dos separadores é personalizada nas linhas 30 e 44;
A pasta [fonts] tem o seguinte conteúdo:
![]() |
Fontes:
- O código da classe [CustomTabLayout] foi encontrado no URL [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
- As fontes foram encontradas no URL [https://www.fontsquirrel.com/fonts/roboto];
2.5.7.9. Inicializações mais recentes
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// fragment manager instantiation
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// fragment adjacency
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// display 1st view
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// we hand over to the daughter activity
onCreateActivity();
}
...
// girls' classes
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- linhas 10–19: Este código é comum nos exemplos que estudámos;
- linhas 21–23: Exibição da primeira vista. Existem, sem dúvida, várias formas de distinguir este caso. Aqui, utilizámos o facto de que, para a primeira vista, o valor da ação que desencadeia a mudança de vista é NONE;
- linha 22: não fazemos suposições sobre o primeiro fragmento a ser exibido. Nos nossos exemplos, este tem sido frequentemente o fragmento #0, mas nem sempre (ver Exemplo-22). Por isso, pediremos à atividade filha (linha 30) que nos diga qual é esta vista;
- linha 25: separámos tudo o que podíamos aqui. Agora, a classe filha tem as suas próprias inicializações para realizar (linha 29);
2.5.7.10. Tratamento da imagem de carregamento
Na classe [AbstractActivity], a imagem de espaço reservado é gerida pelos dois métodos seguintes:
// hold image management ---------------------------------
public void cancelWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.INVISIBLE);
}
}
public void beginWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.VISIBLE);
}
}
2.5.7.11. Implementação da interface [IDao]
Na classe [AbstractActivity], a interface [IDao] (ver secção 2.5.5) é implementada da seguinte forma:
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// layer [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);
}
- Linha 3: Recorde-se que o valor deste campo foi fornecido pela atividade secundária no método [onCreate];
2.5.7.12. Implementação do gestor de fragmentos
Na classe [AbstractActivity], o gestor de fragmentos é implementado da seguinte forma:
...
// the fragment manager --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// daughter class fragments
fragments = getFragments();
}
// must render fragment no. position
@Override
public AbstractFragment getItem(int position) {
// we return the fragment
return fragments[position];
}
// makes the number of fragments to manage
@Override
public int getCount() {
return fragments.length;
}
// makes the title of fragment no. position
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// girls' classes
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
...
}
- linha 5: a matriz de fragmentos associados à atividade. Todos os fragmentos serão derivados da classe [AbstractFragment];
- linhas 8–12: este é o construtor que inicializa a matriz de fragmentos. Ele solicita estes à classe filha da atividade (linha 35);
- linhas 28–31: os títulos dos fragmentos podem ser utilizados numa aplicação onde existam tantas separadores quantos os fragmentos. Neste caso, pode atribuir-se ao separador o título do fragmento. Aqui, estes títulos são solicitados à classe filha (linha 37);
2.5.7.13. O método [onResume]
O método [onResume] é executado pouco antes de a vista associada à atividade se tornar visível. É utilizado aqui para selecionar um separador após uma operação de guardar/restaurar:
@Override
public void onResume() {
// parent
super.onResume();
if (IS_DEBUG_ENABLED) {
Log.d(className, "onResume");
}
// if restore, then restore last selected tab
if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
tabLayout.getTabAt(session.getPreviousTab()).select();
}
}
- Linha 10: Seleção do separador que estava selecionado antes do processo de gravação/restauração. É importante notar aqui que no método [onCreate] — que, no ciclo de vida da atividade, é executado antes do método [onResume] — a navegação ao selecionar um separador foi desativada. Portanto, aqui, um separador é selecionado, mas não há mudança de fragmento;
2.5.7.14. Resumo
A classe abstrata [AbstractActivity] será a classe pai da única atividade da aplicação.
A atividade filha deve implementar os seguintes seis métodos:
// girls' classes
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();
A atividade filha também tem acesso aos seguintes membros protegidos da sua classe pai:
// the session
protected ISession session;
// the fragment container
protected MyPager mViewPager;
// tab bar
protected CustomTabLayout tabLayout;
// class name
protected String className;
2.5.8. A atividade [MainActivity]
![]() |
A classe [MainActivity] pode ter um nome diferente. O único requisito é que implemente a interface [IMainActivity]. A classe padrão fornecida é a seguinte:
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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: we continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define fragments here
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// todo: define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// todo: tabbed browsing - define the view to be displayed
}
@Override
protected int getFirstView() {
// todo: tabbed browsing - define the first view to be displayed
return 0;
}
}
- linha 14: para que a anotação AA [@Bean] na linha 19 seja válida, a atividade deve ter a anotação AA [@EActivity];
- linha 15: a atividade está associada ao menu XML [menu_main]. Atualmente, este menu está vazio. O programador terá de o preencher, se necessário;
- linha 16: a classe estende a classe [AbstractActivity];
- Linhas 19–20: uma referência à camada [DAO]. Esta será instanciada pela biblioteca AA antes de este campo ser inicializado. Isto significa que o bean AA [Dao] deve existir. Este é sempre o caso com a aplicação esqueleto que fornecemos. Mesmo numa aplicação sem uma camada [DAO], pode deixar o pacote [dao] no lugar. Isto não causa quaisquer complicações;
- linha 22: a sessão como uma instância do tipo [Session]. A sessão existe na classe pai [AbstractActivity], mas como uma instância da interface [ISession] (linha 32);
- linhas 24–63: os seis métodos exigidos pela classe pai [AbstractActivity];
- Linhas 36–39: O método [getDao] devolve uma referência à camada [DAO]. Aqui, esta referência nunca é nula. No entanto, na classe pai [AbstractActivity], previmos o caso em que a classe filha devolve uma referência nula para indicar que não existe camada [DAO]. Se desejar utilizar esta opção (na minha opinião, não muito útil), é aqui que deve definir o ponteiro como nulo;
2.6. A camada [DAO]

![]() |
2.6.1. A interface IDao
Foi apresentada na secção 2.5.5:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// todo: declare your interface here
}
O programador irá adicionar os métodos para a sua camada [DAO] a partir da linha 24.
2.6.2. A interface [WebClient]
![]() |
A interface [WebClient] é a seguinte:
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 : declare here the URL to be reached
}
O programador irá adicionar os métodos que comunicam com as URLs expostas pelo servidor JSON a partir da linha 17.
2.6.3. O interceptor de autenticação [MyAuthInterceptor]
![]() |
A classe [MyAuthInterceptor] é a seguinte:
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 {
// user
private String user;
// password
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// headers HTTP of the HTTP request intercepted
HttpHeaders headers = request.getHeaders();
// the HTTP basic authentication header
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
// added to HTTP headers
headers.setAuthorization(auth);
// continue the query life cycle HTTP
return execution.execute(request, body);
}
// authentication elements
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
Esta classe gera o seguinte cabeçalho de autenticação HTTP:
onde [code] é a cadeia de caracteres codificada em Base64 «user:mp». Esta classe só é utilizada se o servidor JSON esperar esta forma de autenticação. Existem outras formas.
Nota: A utilização desta classe é ilustrada na secção 3.6.3.1.
2.6.4. A classe [AbstractDao]
![]() |
A classe [AbstractDao] é a seguinte:
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();
// debug mode
protected boolean isDebugEnabled;
// class name
protected String className;
// timeout before request execution
private int delay;
// manufacturer
public AbstractDao() {
// class name
className = getClass().getName();
Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
}
// méthodes protégées ----------------------------------------------------------
// generic interface
protected interface IRequest<T> {
T getResponse();
}
// generic request to a web service / jSON
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("delay=%s", delay));
}
// service execution - a single response is expected
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// service execution
try {
// waiting?
if (delay > 0) {
Thread.sleep(delay);
}
// execute the synchronous request
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()));
}
// the response is sent to the observer
subscriber.onNext(response);
// we signal the end of the observable
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"));
}
}
// an exception is issued
subscriber.onError(new DaoException(e, 100));
}
}
});
}
// debug mode
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
public void setDelay(int delay) {
this.delay = delay;
}
}
- linhas 35–81: o método [getResponse] utiliza a biblioteca RxAndroid para devolver um tipo [Observable<T>]. Ao contrário de alguns exemplos vistos anteriormente, não devolve um tipo [Response<T>] — que é um tipo proprietário — mas sim qualquer tipo T;
- linha 35: o método [getResponse] recebe como parâmetro uma instância do tipo [IRequest<T>] das linhas 30–32, cujo método [IRequest.getResponse()] obtém o tipo T através de uma operação HTTP síncrona;
- linhas 48–50: artificialmente, esperamos [delay] milissegundos. Em produção, definiremos [delay=0]. Durante a depuração, definiremos [delay=alguns segundos] para dar ao utilizador a oportunidade de cancelar a operação assíncrona e, assim, ver como o código se comporta nesse caso;
- linha 52: a resposta esperada é solicitada com um pedido síncrono;
- linha 64: assim que a resposta é recebida, é passada ao observador;
- linha 66: indicamos que não haverá mais emissões. Este é o caso específico de uma ação assíncrona que retorna apenas um elemento;
- linhas 67–78: no caso de uma exceção, esta é propagada para o observador (linha 77);
2.6.5. A classe [Dao]
![]() |
A classe [Dao] é a seguinte:
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 {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the 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));
}
// factory configuration
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));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// todo: implementation IDao
}
- linhas 21-22: injeção do bean AA [WebClient], que irá tratar da comunicação com o servidor web / JSON;
- linhas 24-25: injeção do interceptor de autenticação;
- linhas 31-42: método executado após a injeção dos campos das linhas 21-25;
- linha 37: o objeto [RestTemplate], que lida com a comunicação cliente/servidor, é criado a partir de uma fábrica. Isto não é estritamente necessário, mas a fábrica permite-nos configurar os tempos limite de comunicação. É por isso que não usamos o construtor sem parâmetros [RestTemplate()];
- linha 39: adicionamos um conversor JSON aos conversores do [RestTemplate]. Este será o único conversor. Assim, quando um método do [WebClient] receber uma string JSON do servidor, ela será automaticamente deserializada no objeto que o método deve devolver;
- linha 41: o objeto [RestTemplate] configurado desta forma é passado para o cliente web, que irá gerir a comunicação cliente/servidor utilizando-o;
- Linhas 44–48: definimos a URL raiz do servidor web/JSON. Todas as URLs declaradas na classe [WebClient] são relativas a esta URL raiz;
- linhas 50–54: este método permite especificar o proprietário da ligação quando a ligação é controlada por autenticação básica (ver secção 2.6.3);
- linhas 56–64: definem os tempos de espera para as trocas cliente/servidor. Isto é feito através da fábrica do objeto [RestTemplate], que controla as trocas;
- linhas 66–78: este método especifica que o servidor está protegido por autenticação básica;
- linhas 72–77: se for necessária autenticação básica, o interceptor de autenticação injetado na linha 25 é adicionado aos interceptores do objeto [RestTemplate]. Este interceptor irá adicionar automaticamente o cabeçalho HTTP de autenticação básica esperado pelo servidor a todos os pedidos do cliente web;
- O programador irá implementar a interface [IDao] a partir da linha 87;
2.7. Fragmentos
![]() |
2.7.1. A classe [MenuItemState]
A classe [MenuItemState] encapsula o estado de uma opção de menu:
package client.android.architecture;
public class MenuItemState {
// menu option identify
private int menuItemId;
// option visibility
private boolean isVisible;
// manufacturers
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters and setters
...
}
2.7.2. A classe [Utils]
A classe [Utils] contém métodos utilitários estáticos:
package client.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// list of exception messages - version 1
static public List<String> getMessagesFromException(Throwable ex) {
// create a list of error msgs from the exception stack
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// exception message list - version 2
static public String getMessageForAlert(Throwable th) {
// build the text to be displayed
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--;
}
// result
return texte.toString();
}
// list of exception messages - version 3
static public String getMessageForAlert(List<String> messages) {
// build the text to be displayed
StringBuilder texte = new StringBuilder();
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return texte.toString();
}
}
2.7.3. A classe pai [AbstractFragment]
A classe [AbstractFragment] contém os elementos comuns a todos os fragmentos da aplicação. Tal como na classe [AbstractActivity], o seu código é complexo. Iremos também analisá-lo passo a passo.
2.7.3.1. O esqueleto
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 {
// données privées ------------------------------------------------------------
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment status
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
// data accessible to daughter classes ---------------------------------------
// debug mode
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// class name
protected String className;
// asynchronous tasks
protected int numberOfRunningTasks;
// activity
protected IMainActivity mainActivity;
protected Activity activity;
// session
protected Session session;
// update Fragment ----------------------------------------------------------------------------------
...
// menu management ------------------------------------------
...
// wait management -------------------------------------------------------------
...
// asynchronous operation management --------------------------------------------------------------------
...
// gestion exception -------------------------------------------------------------------
....
// fragment lifecycle management --------------------------------------------------------
...
// classes filles -----------------------------------------------------
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);
}
- linhas 28–45: os dados privados da classe;
- linhas 47–58: dados protegidos acessíveis pelas classes derivadas;
- linhas 61–62: código que atualiza o fragmento a ser exibido;
- linhas 64-65: código utilitário para gerir o menu, se existir;
- linhas 67–68: código utilitário para gerir a espera durante uma operação assíncrona;
- linhas 70–71: código para facilitar a comunicação entre o fragmento e a camada [DAO];
- linhas 73-74: código utilitário para lidar com quaisquer exceções de forma padrão;
- linhas 76-77: código que gere o ciclo de vida do fragmento;
- linhas 80–94: a classe pai impõe 8 métodos às suas classes filhas;
2.7.3.2. O construtor
O construtor da classe é o seguinte:
// class name
protected String className;
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
...
// constructeur ----------------------
public AbstractFragment() {
// init
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// log
if (isDebugEnabled) {
Log.d(className, "constructeur");
}
}
- Linha 9: O nome da classe filha que está a ser instanciada aqui é registado. Este nome é utilizado em todos os registos da classe pai;
- linha 10: registamos que o fragmento está a ser construído. Esta informação será utilizada quando for solicitado ao fragmento filho que se atualize;
2.7.3.3. Gestão de menus
Na nossa arquitetura, todos os fragmentos devem ter um menu, mesmo que este esteja vazio. Os registos mostraram, de facto, que quando o método [onCreateOptionsMenu] — que é executado quando o fragmento tem um menu — é executado, o fragmento já foi associado à sua atividade, vista e menu e está prestes a tornar-se visível. Este é, portanto, o momento em que a interface visual e o menu podem ser atualizados. É dentro deste método [onCreateOptionsMenu] que instruímos o fragmento filho a atualizar-se.
A gestão do menu inclui métodos utilitários que permitem ao fragmento filho mostrar ou ocultar itens do menu:
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// menu management ------------------------------------------
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// scroll through all menu items
for (int i = 0; i < menu.size(); i++) {
// item n° i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// if item n° i is a sub-menu, then start again
if (menuItem.hasSubMenu()) {
// recursivity
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// result
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// we retrieve the identifiers of the menu options
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transfer menu options to a table
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// identify option
int id = menuOptionsIds.get(i);
// status option
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
if (isDebugEnabled) {
Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
}
}
// menu option status
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();
// initialization status
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
return menuOptionsStates;
}
// display menu options -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// update all menu options
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// update certain menu options
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
- linhas 6–18: este método recupera os identificadores numéricos de todas as opções do menu;
- linha 6: o método [getMenuOptions] aceita dois parâmetros:
- [Menu menu]: o menu do fragmento;
- [List<Integer> menuOptionsIds]: a lista de IDs Android para as opções do menu. Inicialmente, esta lista está vazia. É então preenchida por uma percussão recursiva (linha 15) da árvore do menu;
- linhas 20–40: com base no menu, constrói a matriz de estados (ID, visibilidade) para as opções do menu. Esta matriz é armazenada na linha 3. A classe [MenuItemState] foi descrita na secção 2.7.1;
- linhas 43–55: uma variante do método anterior. Faz o mesmo, mas em vez de recalcular os identificadores para todas as opções do menu — o que já foi feito — utiliza os identificadores da matriz de estados na linha 3;
- linhas 58–63: o método [setAllMenuOptionsStates] permite ocultar ou mostrar todas as opções de menu do fragmento;
- linhas 65–69: o método [setMenuOptionsStates] permite mostrar ou ocultar seletivamente determinadas opções de menu;
- Os métodos [getMenuOptions, getMenuOptionsStates] são declarados como privados porque são utilizados apenas dentro de [AbstractFragment]. Os métodos [setAllMenuOptionsStates] (linha 58) e [setMenuOptionsStates] (linha 65) são declarados como protegidos para que estejam disponíveis para as classes filhas;
2.7.3.4. Gestão da espera pela conclusão de uma tarefa assíncrona
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// asynchronous tasks
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// management of waiting for the end of an asynchronous operation -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// the number of tasks to be executed is noted
this.numberOfRunningTasks = numberOfRunningTasks;
// we put the image on hold
mainActivity.beginWaiting();
// empty the subscription list
abonnements.clear();
// no cancellations yet
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// we hide the waiting image
mainActivity.cancelWaiting();
}
- linhas 9–18: Para iniciar uma ou mais operações assíncronas, o fragmento filho chamará o método pai [beginRunningTasks]. O parâmetro deste método é o número de tarefas assíncronas que o fragmento filho irá iniciar;
- linha 11: armazenamos o parâmetro do método;
- linha 13: o ecrã de carregamento é exibido;
- linha 15: a lista de subscrições para operações assíncronas é limpa. Estas ainda não foram criadas pelo fragmento filho;
- linha 17: é mantido um booleano para indicar que as tarefas assíncronas solicitadas pelo fragmento filho foram canceladas. Inicialmente, o booleano tem o valor false;
- linhas 20–25: o fragmento filho chama o método pai [cancelWaitingTasks] para indicar que pretende cancelar as tarefas que iniciou;
- linha 22: a imagem de espera é ocultada;
2.7.3.5. Tratamento de exceções
// gestion exception -------------------------------------------------------------------
// exception alert display
protected void showAlert(Throwable th) {
// display messages from the Throwable th exception stack
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
}
// message list display
protected void showAlert(List<String> messages) {
// the message list is displayed
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
- linhas 4-7: o método [showAlert(Throwable)] permite que um fragmento filho exiba as mensagens da pilha de exceções do Throwable passado como parâmetro numa janela;
- linhas 10–13: o método [showAlert(List<String>)] permite que um fragmento filho exiba a lista de mensagens passadas como parâmetro numa janela;
- A classe [Utils] utilizada nas linhas 6 e 12 foi descrita na secção 2.7.2;
2.7.3.6. Tratamento de operações assíncronas
...
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// asynchronous task execution with RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// process: the observable to be executed/observed
// consumeResult: the method that uses the response obtained
//
// new subscriptions are only created if they have not been cancelled
if (!runningTasksHaveBeenCanceled) {
// execution on I/O thread and observation on Ui thread
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// the observable is executed
try {
abonnements.add(process.subscribe(
// consumption result
consumeResult,
// consumption exception
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
consumeThrowable(th);
}
},
// end of task
new Action0() {
@Override
public void call() {
endOfTask();
}
}));
} catch (Throwable th) {
consumeThrowable(th);
}
}
}
private void endOfTask() {
...
}
// an asynchronous operation has thrown an exception
// or an exception has occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
...
}
- linhas 9–41: executar uma tarefa assíncrona;
- linha 9: o método [executeInBackground] espera dois parâmetros:
- [Observable<T> process]: o processo assíncrono a ser executado;
- [Action1<T> consumeResult]: o método do fragmento filho a ser chamado para lhe passar os elementos emitidos pelo processo. Nos nossos exemplos anteriores, os processos emitiram sempre apenas um elemento. O tipo T de [Action1<T>] é o tipo T do resultado devolvido pelo processo observado;
- linha 14: a tarefa assíncrona só é iniciada se ainda não tiver sido cancelada pelo utilizador ou pelo programa (devido a uma exceção);
- linha 16: o processo está configurado para ser executado numa thread de E/S e observado na thread da interface do utilizador;
- linha 16: a instrução [process.subscribe] inicia o processo na thread de E/S. Dentro desta thread, as operações são executadas de forma síncrona porque estamos a utilizar uma biblioteca HTTP síncrona;
- linha 19: o método [process.subscribe] tem três parâmetros:
- linha 21: [consumeResult]: o método do fragmento filho que consumirá os elementos emitidos pelo processo;
- linhas 22–28: o método executado quando ocorre uma exceção durante o processamento da tarefa assíncrona. O tratamento é delegado ao método [consumeThrowable] na linha 49;
- linhas 29–36: o método executado quando a tarefa emite a notificação de fim de emissão. O tratamento é delegado ao método [endOfTask] na linha 43;
- linha 19: a tarefa assíncrona que acaba de ser iniciada é registada no campo [subscriptions], que mantém um registo de todas as tarefas assíncronas iniciadas. Isto permitirá que sejam canceladas, se necessário;
- linhas 37–39: método executado quando ocorre uma exceção durante o processamento da tarefa assíncrona. O tratamento é delegado ao método [consumeThrowable] na linha 49;
O método [endOfTask] é o seguinte:
// asynchronous tasks
protected int numberOfRunningTasks;
...
private void endOfTask() {
// one less job to wait for
numberOfRunningTasks--;
// finished?
if (numberOfRunningTasks == 0) {
// end waiting
cancelWaitingTasks();
// the end of tasks is signalled to the daughter class
notifyEndOfTasks(false);
}
}
...
// classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- linha 6: uma tarefa assíncrona acaba de ser concluída. O contador de tarefas ativas é decrementado;
- linha 8: se não houver mais tarefas ativas, então a sub-thread recebeu todas as suas respostas;
- linha 10: a espera é cancelada;
- linha 12: notificamos o fragmento filho de que todas as tarefas que ele iniciou foram concluídas, chamando o seu método [notifyEndOfTasks]. O parâmetro deste método indica como as tarefas terminaram — normalmente, ou devido a cancelamento pelo utilizador ou pelo código, porque ocorreu uma exceção. Na linha 12, sinalizamos um fim normal. Note-se que o fragmento filho não precisa de manter um registo das tarefas que ainda estão ativas. A sua classe pai faz isso por ele;
O método [consumeThrowable] é o seguinte:
// asynchronous tasks
protected int numberOfRunningTasks;
private boolean runningTasksHaveBeenCanceled;
...
// an asynchronous operation has thrown an exception
// or an exception has occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
// th: the exception to be dealt with
//
// log
if (isDebugEnabled) {
Log.d(className, "Exception reçue");
}
// cancel tasks already started
cancelRunningTasks();
// error messages are displayed
showAlert(th);
}
// cancel tasks
protected void cancelRunningTasks() {
// log
if (isDebugEnabled) {
Log.d(className, "Annulation des tâches lancées");
}
// cancel all registered asynchronous tasks
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
// we note the cancellation
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// end of wait
cancelWaitingTasks();
// the cancellation of tasks is reported to the daughter fragment
notifyEndOfTasks(true);
}
...
// classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- Linha 3: O método [consumeThrowable] captura a exceção que ocorreu;
- linha 15: todas as tarefas que ainda estão ativas são canceladas;
- linha 17: o texto da exceção é exibido;
- linhas 21–37: todas as tarefas são canceladas;
- linhas 27–29: todas as subscrições são canceladas;
- linha 31: é registada uma nota indicando que ocorreu um cancelamento;
- linha 32: o contador de tarefas é reiniciado para zero;
- linha 34: a espera é cancelada;
- linha 36: o fragmento filho é notificado de que as tarefas terminaram após o cancelamento;
2.7.3.7. Gestão do ciclo de vida do fragmento
// life cycle --------------------------------------------------------
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// parent
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) {
...
}
- Linhas 2–20: Os métodos [onDestroyView, onDestroy] são incluídos exclusivamente para fins de registo. Estes permitem ao programador compreender melhor o ciclo de vida do fragmento;
O salvamento do fragmento quando o dispositivo gira é tratado pelos seguintes métodos: [setUserVisibleHint, onSaveInstanceState, saveState]:
// fragment life cycle
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// backup?
if (this.isVisibleToUser && !isVisibleToUser) {
// the fragment will be hidden - save it
if (!saveFragmentDone) {
saveState();
}
}
// memory
this.isVisibleToUser = isVisibleToUser;
}
private void saveState() {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
}
// parent
super.onSaveInstanceState(outState);
// save fragment only if visible
if (isVisibleToUser) {
// perhaps the backup has already been made
if (!saveFragmentDone) {
saveState();
}
// restoration to be carried out in all cases
session.setAction(ISession.Action.RESTORE);
}
}
- linhas 6-19: o fragmento é guardado se passar do estado visível para o estado oculto (linha 11). O método [setUserVisibleHint] fornece esta informação;
- linha 14: o salvamento é realizado pelo método privado nas linhas 21–23;
- linhas 25–41: quando o dispositivo gira, o método [onSaveInstanceState] é chamado. O fragmento é guardado em duas condições:
- se estiver visível (linha 34);
- ainda não foi guardado (linha 36). É possível que os métodos [setUserVisibleHint] e [onSaveInstanceState] não sejam ambos executados quando o fragmento está visível e que, por isso, a gestão do booleano [saveFragmentDone] seja desnecessária. Em caso de dúvida, optei por usá-lo;
- linha 40: após o salvamento vem a restauração. Note que da próxima vez que o fragmento precisar de se atualizar, ele o fará através de uma operação [RESTORE];
Observe os dois momentos em que é solicitado o salvamento de um fragmento:
- quando ele faz a transição do estado visível para o estado oculto;
- quando o dispositivo gira;
O método privado [saveState] é o seguinte:
...
private void saveState() {
// tasks to cancel?
if (numberOfRunningTasks != 0) {
// cancel tasks
cancelRunningTasks();
}
// save fragment state
CoreState currentState = saveFragment();
// the fragment has been visited
currentState.setHasBeenVisited(true);
// save menu status
currentState.setMenuOptionsState(getMenuOptionsStates());
// session setting
session.setCoreState(getNumView(), currentState);
// backup done
saveFragmentDone = true;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// classes filles -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
- linhas 4-7: Pode ocorrer uma rotação do dispositivo enquanto operações assíncronas estão em curso. Aqui, decide-se cancelar todas elas. Esta não é uma boa decisão para o utilizador, que terá de fazer um novo pedido, potencialmente demorado, simplesmente porque moveu o seu telemóvel ou tablet ou recebeu uma chamada. É possível manter as ligações de rede através de um ciclo de gravação/restauração. No entanto, as soluções não são simples, e decidi não as abordar neste curso para principiantes. O caminho a seguir é estabelecer estas ligações de rede através de um fragmento que não tenha nenhuma interface de utilizador associada e que não seja destruído durante o ciclo de guardar/restaurar. Para o fazer, basta utilizar a instrução [Fragment.setRetainInstance(true)];
- linha 9: pedimos ao fragmento filho para guardar o seu estado num tipo derivado de [CoreState] (linha 31);
- linha 11: registamos que o fragmento foi visitado. Esta informação é útil. Quando um fragmento é visitado pela primeira vez, a sua atualização pode diferir das subsequentes, uma vez que não possui um estado anterior na sessão;
- linha 13: guardamos o estado do menu, o que nos permitirá restaurá-lo automaticamente;
- linha 15: este estado atual é guardado na sessão. Na sessão, os estados são agrupados por vista/fragmento, cada um com um estado. O número da vista é fornecido pelo fragmento filho (linha 33);
- linha 17: registamos que o fragmento foi guardado. Isto porque dois métodos podem chamar o método [saveState], e não é necessário realizar dois guardados;
A vista associada ao fragmento é regenerada pelo seguinte método:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// parent
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// the view must be restored
viewHasToBeInitialized = true;
}
No ciclo de vida, o método [onActivityCreated] é executado imediatamente após o método [onCreateView]. A chamada deste último método indica que a vista associada ao fragmento deve ser reconstruída. Basta anotar isto na linha 10.
2.7.3.8. Atualização do fragmento
A atualização do fragmento é a última operação realizada no fragmento antes de este se tornar visível e aguardar a entrada do utilizador. É tratada pelo seguinte código:
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// update Fragment ----------------------------------------------------------------------------------
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// log
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// memory
this.menu = menu;
// retrieve # menu options if not already done
if (fragmentHasToBeInitialized) {
// retrieve the # menu options
getMenuOptionsStates(menu);
// activity
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = (Session) this.mainActivity.getSession();
}
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// this is not the 1st visit
// step 2: does the fragment need to be initialized?
...
// step 3: should the view be initialized?
...
}
// step 4: a submit, a browse, a restore?
...
// step 5: terminal updates ----------------------
...
}
...
// classes filles -----------------------------------------------------
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();
- linha 19: o método [onCreateOptionsMenu] é utilizado para atualizar o fragmento. Por este motivo, o fragmento deve ter um menu, mesmo que este esteja vazio. Quando este método é executado, o fragmento já foi associado à sua vista e atividade e também está visível;
- linha 25: o menu passado como parâmetro (linha 22) para o método é armazenado;
- linhas 27–34: se o fragmento precisar de ser inicializado:
- linha 29: os estados das opções do menu são armazenados na matriz [menuOptionsStates] da linha 3;
- linha 31: a atividade é armazenada como uma instância do tipo [Activity] do Android;
- linha 32: a atividade é armazenada como uma instância da interface [IMainActivity];
- linha 33: a sessão é armazenada. A conversão de tipo é necessária porque o método [mainActivity.getSession()] retorna um tipo [ISession];
- linha 36: o estado anterior do fragmento é recuperado da sessão. Se esta for a primeira visita ao fragmento, apenas o booleano [previousState.hasBeenVisited] é relevante;
- linhas 39–44: código executado quando esta é a primeira visita ao fragmento. Neste caso, o seu estado anterior não é relevante;
- linhas 44–50: código executado quando esta não é a primeira visita ao fragmento;
- linhas 46–47: código executado se o construtor do fragmento tiver sido chamado (fragmentHasToBeInitialized == true);
- linhas 48-49: código executado se a vista associada ao fragmento tiver sido reconstruída (viewHasToBeInitialized==true);
- linhas 51-52: código executado dependendo da ação atual (SUBMIT, NAVIGATION, RESTORE);
- linhas 54-55: código sempre executado;
Os cinco passos da atualização são os seguintes:
passo 1
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// fragment and view initialization
initFragment(null);
initView(null);
// raz previousState for the suite
previousState = null;
} else {
// this is not the 1st visit
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- linha 19: o estado anterior do fragmento é recuperado da sessão;
- linhas 22–31: código executado se o fragmento nunca tiver sido visitado;
- linha 27: a classe filha é solicitada a inicializar o fragmento. O parâmetro do método [initFragment] na linha 35 é o estado anterior do fragmento. Aqui, é passado null para indicar ao fragmento filho que esta é a primeira visita;
- linha 28: a classe filha é solicitada a inicializar a vista associada ao fragmento. O parâmetro do método [initView] na linha 37 é o estado anterior do fragmento. Aqui, é passado null para indicar ao fragmento filho que esta é a primeira visita;
- linha 30: definimos o estado anterior como null para os passos que se seguem;
Passos 2 e 3
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// we retrieve the previous fragment state (the very 1st time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
...
} else {
// ce n'is not the 1st visit
// step 2: does the fragment need to be initialized?
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation fragment");
}
// girl fragment
initFragment(previousState);
}
// step 3: should the view be initialized?
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation vue");
}
// girl fragment
initView(previousState);
}
}
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- linhas 24–42: executadas quando esta não é a primeira visita ao fragmento;
- linhas 27–33: se o fragmento acabou de ser reconstruído, ele é reinicializado chamando o método [initFragment] da classe filha (linhas 32, 46). O estado anterior do fragmento é passado para ele;
- linhas 35–51: se a vista associada ao fragmento precisar de ser inicializada ou reiniciada, o fragmento filho é solicitado a fazê-lo (linhas 40, 48). Mais uma vez, o último estado conhecido do fragmento é-lhe passado;
Passo 4
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
...
// step 4: a submit, a browse, a restore?
// 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();
}
}
// action in progress
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// girl fragment
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// catering menu
setMenuOptionsStates(previousState.getMenuOptionsState());
// girl fragment
updateOnRestore(previousState);
} else {
// 1st visit - nothing to do
}
break;
case RESTORE:
// restoration
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// menu restoration (previousState cannot be null)
setMenuOptionsStates(previousState.getMenuOptionsState());
// girl fragment
updateOnRestore(previousState);
break;
}
....
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
- linhas 34–66: processamos a ação atual, que pode ser uma das três seguintes:
- RESTORE: Estamos a restaurar o fragmento após uma rotação do dispositivo;
- NAVIGATION: Estamos a regressar ao fragmento, com a intenção de o encontrar no estado em que o deixámos da última vez que o utilizámos;
- SUBMIT: todos os outros casos;
- linha 34: recuperamos a ação atual;
- linhas 36–42: para uma ação do tipo SUBMIT, chamamos o método [updateOnSubmit] do fragmento filho (linhas 41, 68), passando-lhe o último estado conhecido do fragmento;
- linhas 43–55: para uma ação do tipo NAVIGATION;
- linhas 47–54: queremos restaurar o fragmento para o seu último estado conhecido. A operação NAVIGATION pode coincidir com uma primeira visita. Este seria o caso, por exemplo, numa aplicação com separadores: se eu mudar do separador 1 para o separador 4:
- devo inicializar o fragmento para a guia 4 se esta for a primeira visita;
- restaurar o fragmento da guia 4 para o seu estado anterior, se não for a primeira visita;
- linhas 52–54: não fazer nada se for a primeira visita. O método filho [initView(CoreState previousState)] tratará desta inicialização. A primeira visita é identificada pela condição [previousState == null];
- linha 49: se esta não for a primeira visita ao fragmento, restaurar o seu menu;
- linha 51: solicitamos à classe filha que se atualize chamando o método na linha 70. Passamos-lhe o estado anterior do fragmento para que possa realizar a sua tarefa;
- linhas 56–66: no caso de uma operação de restauração do fragmento, fazemos o mesmo que no caso de navegação fora da primeira visita;
Passo 5
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// step 5: terminal updates ----------------------
// we've changed our view
session.setPreviousView(getNumView());
// more action in progress
session.setAction(ISession.Action.NONE);
// when leaving this fragment, it must be saved
saveFragmentDone = false;
// as long as the fragment has not been rebuilt, it does not need to be initialized
fragmentHasToBeInitialized = false;
// as long as the view has not been rebuilt, it does not need to be initialized
viewHasToBeInitialized = false;
// returns to normal tab selection operation
session.setNavigationOnTabSelectionNeeded(true);
// the fragment is notified that the view is ready
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- linhas 18–30: quando chegamos a este ponto, o fragmento já foi inicializado e está pronto para ser exibido. Em seguida, reiniciamos todos os indicadores utilizados na gestão do ciclo de vida do fragmento para o seu estado inicial;
- linha 20: a vista mudou; isto é registado na sessão;
- linha 22: não há mais ações em andamento;
- linha 24: quando sairmos do fragmento atualmente exibido, teremos de o guardar ao sair;
- linha 26: o fragmento já não precisa de ser reconstruído. Este sinalizador será definido como verdadeiro quando o construtor do fragmento for executado novamente;
- linha 28: a vista associada ao fragmento já não precisa de ser inicializada. Este sinalizador será definido como verdadeiro novamente quando o método [onActivityCreated] for executado novamente;
- linha 30: o fragmento pode ser exibido numa aplicação com separadores. Neste caso, quando o utilizador clica num dos separadores, deve ocorrer uma mudança de fragmento;
- linha 36: a classe filha é notificada de que o fragmento está pronto. Pode utilizar o método [notifyEndOfUpdates] para realizar atualizações que teriam de ser feitas de qualquer forma, iniciar uma operação assíncrona para obter novos dados, etc.
2.7.4. Um exemplo de um fragmento
![]() |
Incluímos um exemplo de fragmento no projeto [client-android-skel] para mostrar ao leitor a estrutura típica de um fragmento numa aplicação baseada neste projeto.
A classe [DummyFragment] é a seguinte:
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 {
// fields inherited from parent class -------------------------------------------------------
// debug mode
//-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// class name
//-- protected String className;
// asynchronous tasks
//-- protected int numberOfRunningTasks;
// activity
//-- protected IMainActivity mainActivity;
//-- protected Activity activity;
// session
//-- protected Session session;
// methods inherited from the parent class -------------------------------------------------------
// display menu options
//-- protected void setAllMenuOptionsStates(boolean isVisible) {
//-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// management of waiting for the end of a series of asynchronous tasks
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// asynchronous task execution with RxAndroid
//-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// cancel tasks
//-- protected void cancelRunningTasks() {
// exception alert display
//-- protected void showAlert(Throwable th) {
// message list display
//-- protected void showAlert(List<String> messages) {
// methods imposed by the parent class -------------------------------------------------------
@Override
public CoreState saveFragment() {
// save the fragment
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
}
@Override
protected int getNumView() {
// return the fragment number in the table of fragments managed by the activity (cf MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// the fragment becomes visible and has undergone construction in this or a previous stage
// this happens on application startup and every time the Android device is rotated
// is necessarily followed by the execution of [initView]
// the fields of the fragment that has been rebuilt must be initialized
// previousState is the fragment's last save - is null if this is the fragment's 1st visit
}
@Override
protected void initView(CoreState previousState) {
// the fragment becomes visible and the associated view has been reconstructed in this or a previous step
// this happens every time [initFragment] is executed and every time the fragment leaves the adjacency of the displayed fragment
// initialize the components of the view that has been rebuilt
// previousState is the fragment's last save - is null if it's the fragment's 1st visit
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after an operation of type SUBMIT
// the fragment and the associated view usually have to be initialized from the session
// previousState is the fragment's last save - is null if it's the fragment's 1st visit
// there's nothing to be done if the fragment can't be reached by a SUBMIT operation
// if the fragment can be reached by SUBMIT operations from different fragments, the previous view can be known by [session.getPreviousView]
// if the fragment can be reached by several SUBMIT operations from the same fragment, then a flag must be set to differentiate between the different types of SUBMIT from this fragment
}
@Override
protected void updateOnRestore(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after an operation of type RESTORE or NAVIGATION
// previousState is the fragment's last backup - never null
// restore the view to its previous state
}
@Override
protected void notifyEndOfUpdates() {
// comes after methods [updateOnSubmit, updateOnRestore]
// when we're there, the view has been built and initialized
// there's often nothing to do here, but you can also factor in actions that need to be done no matter how you arrive at this view
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// called when asynchronous tasks launched by the fragment are either completed or cancelled
// these two cases can be differentiated using parameter runningTasksHaveBeenCanceled
// the view generally needs to be reset to a different state from the one it had while waiting for responses from asynchronous tasks
}
}
A classe [DummyFragment] pode não ter um estado. Aqui, incluímos um para nos lembrar do que se espera dela:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class DummyFragmentState extends CoreState {
// fragment status [DummyFragment]
// set only serializable fields to jSON
// put the annotation @JsonIgnore on the others, but it's hard to see what use they could be
// don't forget the getters/setters - they are used for serialization/deserialization
}
Para ilustrar a utilização do projeto [client-android-skel], vamos primeiro utilizar exemplos simples antes de passarmos a um estudo de caso mais abrangente.
2.8. Exercícios ilustrativos
Começaremos por refatorar exemplos que já foram escritos.
2.8.1. Exemplo-17B
Vamos revisitar o Exemplo 17 da Secção 1.18. Trata-se de uma aplicação com um único fragmento, sem tarefas assíncronas e sem separadores. Vamos analisá-la para ver como se comporta quando o dispositivo é rodado. Vamos introduzir o seguinte:

Em seguida, em [1], rodamos o dispositivo duas vezes. A nova visualização é a seguinte:

Se compararmos as vistas, tudo foi preservado, exceto a lista [2], que agora está vazia.
Além disso, se clicar no botão [Submit], aparece uma caixa de diálogo a mostrar as entradas feitas no formulário. Se rodar o dispositivo nesse momento, a caixa de diálogo desaparece.
Portanto, durante uma rotação, teremos de regenerar:
- a lista suspensa e o seu item selecionado;
- a caixa de diálogo, caso tenha sido exibida durante a rotação;
2.8.1.1. O projeto [Exemplo-17B]
Duplicamos o projeto [client-android-skel] para examples/Example-17B. Em seguida, carregamos o novo projeto [1]:
![]() | ![]() | ![]() |
- em [2-3], na pasta [behavior], colamos o fragmento [Vue1Fragment] do projeto [Example-17];
![]() | ![]() | ![]() |
- em [4-5], na pasta [layout] de [Example-17B], colamos a vista [vue1.xml] de [Example-17]. Esta é a vista associada ao fragmento;
- em [6], a pasta [values] de [Exemplo-17B] é substituída pela pasta [values] de [Exemplo-17];
Vamos alterar a margem superior da vista [vue1.xml] para 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"/>
Nesta altura, podemos tentar uma compilação inicial para verificar se existem erros. Os primeiros erros reportados resultam de importações de pacotes que foram movidas. Corrigimo-los (Ctrl-Shift-O). Outros erros, tais como , surgem porque a vista [Vue1Fragment] não implementa todos os métodos exigidos pela sua classe pai [AbstractParent]:

Gerar os métodos em falta (Alt-Enter).
Outro erro de compilação relatado é o seguinte:

Corrigimos isto no ficheiro [build.gradle] do módulo (linha 20 abaixo):
![]() |
Nesta altura, podemos recompilar para ver os erros restantes. O único erro relatado está no método [Vue1Fragment.updateFragment]:
![]() |
Deve remover a anotação [@Override] da linha 135. Já não existem mais erros. Vamos usar isto como ponto de partida para modificar o projeto.
2.8.1.2. O estado do fragmento [Vue1Fragment]
O fragmento [Vue1Fragment] precisa de guardar informações quando o dispositivo roda, para que possa ser totalmente restaurado. Criamos uma classe [Vue1FragmentState] para este efeito:
![]() |
Por enquanto, esta turma está vazia:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.1.3. Personalização do projeto
![]() |
A pasta [custom] contém elementos de arquitetura que podem ser personalizados pelo programador.
As constantes para a interface [IMainActivity] serão as seguintes:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- linhas 24–31: A aplicação não utiliza a sua camada [DAO] aqui. Estas constantes não serão utilizadas;
- linha 34: uma adjacência de fragmentos igual a 1, que é o valor padrão. Como a aplicação tem apenas um fragmento (linha 43), este valor é irrelevante;
- linhas 39-40: uma vez que não há operações com a camada [DAO], não há necessidade de uma imagem de espaço reservado;
- linha 37: esta não é uma aplicação com separadores;
- linha 43: existe apenas um fragmento;
A classe [Session] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
Está vazio. De facto, uma vez que existe apenas um fragmento, não há necessidade de prever a comunicação entre fragmentos utilizando uma sessão.
Por fim, a classe [CoreState] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Linhas 11–13: Temos de listar todas as classes derivadas de [CoreState] que armazenam o estado dos vários fragmentos. Aqui, existe apenas uma (linha 12);
2.8.1.4. A [MainActivity]
A atividade [MainActivity] tem atualmente o seguinte aspeto:
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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: we continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define fragments here
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// todo: define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// todo: tabbed navigation - define the view to be displayed when tab no. [position] is selected
}
@Override
protected int getFirstView() {
// todo: define the number of the first view (fragment) to be displayed
return 0;
}
}
Os comentários [//todo] indicam o que o programador precisa de fazer. A classe [MainActivity] evolui da seguinte forma:
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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
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;
}
}
Apenas o método nas linhas 41–44 precisa de ser modificado. Deve devolver a matriz dos fragmentos da aplicação. Na linha 43, não se esqueça de adicionar o sublinhado após o nome do fragmento.
2.8.1.5. O estado do fragmento [FragmentState]
Na sequência dos testes de rotação realizados no projeto [Exemplo-17], decidimos armazenar os seguintes elementos do fragmento:
- a lista de valores na lista suspensa;
- a posição do item selecionado nesta lista;
- a mensagem exibida pela caixa de diálogo, caso esteja presente no momento da rotação;
A classe [Vue1FragmentState] será a seguinte:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// drop-down list values
private List<String> list;
// the item selected from the drop-down list
private int listSelectedPosition;
// the message displayed in the
private String message;
// getters and setters
...
}
2.8.1.6. O fragmento [AbstractFragment]
Atualmente, o ciclo de vida do fragmento é gerido por dois métodos (linhas 6 e 32):
// drop-down list
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// check the first button
radioButton1.setChecked(true);
// the calendar
datePicker1.setCalendarViewShown(false);
// on 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));
}
});
// the drop-down list
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
...
protected void updateFragment() {
// initialize drop-down list adapter
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
O código destes dois métodos será movido para os métodos definidos pela classe [AbstractFragment] da seguinte forma:
// fragment lifecycle management ---------------------------------------------------------------------
@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) {
// 1st visit?
if (previousState == null) {
// create drop-down list values
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// returns values from the drop-down list
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// and the
message = state.getMessage();
}
// initialize drop-down list adapter
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) {
// the calendar
datePicker1.setCalendarViewShown(false);
// on 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));
}
});
// initialize drop-down list adapter
dropDownList.setAdapter(dataAdapter);
// 1st visit?
if (previousState == null) {
// check the first button
radioButton1.setChecked(true);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// seekbar value
seekBarValue.setText(String.valueOf(seekBar.getProgress()));
// item selected from drop-down list
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// visible dialogue?
if (message != null) {
// we display it
showMessage();
}
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
- linhas 2–9: o método [saveFragment] deve colocar os elementos do fragmento a serem guardados numa classe derivada de [CoreState] e devolver uma instância dessa classe;
- linhas 11–14: o método [getNumView] deve devolver o número do fragmento. Aqui, existe apenas um fragmento, cujo número é 0;
- linhas 16–34: O método [initFragment] deve inicializar os campos do fragmento. Recebe o estado anterior do fragmento. Se [previousState] for nulo, então esta é a primeira visita;
- linhas 19–25: Na primeira visita, os valores para a lista suspensa são criados;
- linhas 26–30: se esta não for a primeira visita, os campos [list, message] do fragmento são restaurados a partir do estado anterior;
- linhas 33-34: inicialização do campo [dataAdapter] do fragmento. Esta é a fonte de dados para a lista suspensa;
- linhas 37–62: o método [initView] é utilizado para inicializar os componentes da interface visual. Recebe o estado anterior [previousState] como parâmetro. Se [previousState == null], significa que esta é a primeira visita;
- Aqui, vemos o que estava anteriormente no método [@AfterViews];
- linhas 57–61: na primeira visita, garantimos que o primeiro botão de opção esteja selecionado;
- linhas 64–67: o método [updateOnSubmit] é executado quando a ação atual é [SUBMIT]. Aqui, não há navegação entre fragmentos e, portanto, não há ação atual;
- linhas 69–81: o método [updateOnRestore] é executado quando a ação atual é [NAVIGATION] ou [RESTORE]. Aqui, não há navegação entre fragmentos e, portanto, nenhuma ação [NAVIGATION] possível;
- linha 72: recalculamos (não restauramos) o valor do TextView seekBarValue. Isto porque, durante as rotações, o seu valor por vezes era perdido;
- linhas 74–75: A lista é posicionada no item que estava selecionado antes da rotação. Sem isto, a lista voltaria por padrão para o seu primeiro item;
- linhas 76-80: a caixa de diálogo é exibida novamente se a mensagem do estado anterior não for nula. Voltaremos ao método [showMessage] (linha 79);
- linhas 83–86: o método [notifyEndOfUpdates] é o último método chamado pela classe pai antes de deixar o fragmento filho sozinho. Aqui, não há nada a fazer;
- linhas 88–91: o método [notifyEndOfTasks] sinaliza o fim das tarefas assíncronas iniciadas pelo fragmento. Aqui, não há nenhuma;
A caixa de diálogo é restaurada da seguinte forma:
// dialog box message
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// list of messages to display
List<String> messages = new ArrayList<>();
...
// display
doAfficher(messages);
}
private void doAfficher(final List<String> messages) {
// poster text is created
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// the message is memorized
message = texte.toString();
// we display it
showMessage();
}
private void showMessage() {
// we display it
new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// message reset
message = null;
}
}).show();
}
Quando o utilizador envia o formulário, o método [doValider] (linha 5) cria uma lista de mensagens, que depois apresenta (linha 10) na caixa de diálogo.
- linhas 14–20: A lista de mensagens é concatenada numa única mensagem, que é armazenada na linha 2;
- linhas 25–33: esta é a mensagem exibida pela caixa de diálogo e é a mesma mensagem que o método [updateOnRestore] exibe;
- linha 27: o segundo parâmetro do método [setNeutralButton] é o método executado quando o utilizador clica no botão [Fechar] na caixa de diálogo;
- linha 31: quando a caixa de diálogo fecha, a mensagem é definida como nula para indicar que a caixa de diálogo já não está presente;
2.8.1.7. Testes
Convidamos os leitores a testar este projeto e a verificar se o fragmento é preservado após uma ou mais rotações sucessivas.
2.8.2. Exemplo-23: Cliente Meteorológico
Alguns sites fornecem informações meteorológicas na forma de cadeias JSON. Aqui está um exemplo:

O URL tem o seguinte formato: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID}, em que:
- cidade: a cidade para a qual pretende obter a previsão meteorológica, neste caso Angers;
- country: o país da cidade, neste caso França (fr);
- APPID: uma chave obtida através do registo no site [https://home.openweathermap.org/users/sign_up];
2.8.2.1. O Projeto
![]() |
O projeto foi desenvolvido com base no projeto [client-android-skel]. Apresenta as seguintes características:
- tem apenas um fragmento cujo estado não precisa de ser mantido;
- efectua pedidos assíncronos;
2.8.2.2. Personalização do projeto
![]() |
A interface [IMainActivity] permite-lhe especificar determinadas características do projeto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 5000;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- Linhas 25, 28, 31, 40: características da camada [DAO]. Linha 31: Não é necessária autenticação básica;
- linha 34: adjacência de fragmentos. Aqui, esta constante é irrelevante, uma vez que existe apenas um fragmento;
- linha 37: esta não é uma aplicação com separadores;
- linha 43: existe apenas um fragmento;
A classe [CoreState] que armazena o estado dos fragmentos será a seguinte:
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: add subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- linhas 10–13: não há nada a declarar, uma vez que esta aplicação tem apenas um fragmento cujo estado não é guardado;
A classe [Session] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
Está vazio porque não há comunicação entre fragmentos nesta aplicação.
2.8.2.3. A camada [DAO]
![]() |
Na camada [DAO], é necessário personalizar três classes:
- a interface IDao;
- a sua implementação Dao;
- a interface WebClient para comunicação com o servidor web / JSON;
A interface [WebClient] será a seguinte:
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);
// weather service
@Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
- linhas 18-19: o URL do serviço meteorológico. Note que este é relativo ao URL raiz do cliente (RestClientRootUrl, linha 12). Aqui, este URL raiz será [http://api.openweathermap.org/];
A interface [IDao] será a seguinte:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// weather service
Observable<String> getWeatherForecast(String city, String country, String APPID);
}
- Note que os métodos nas linhas 6–22 estão incluídos por predefinição na interface IDao do projeto [client-android-skel];
- Linha 25: O método [getWeatherForecast] recupera a cadeia JSON relativa ao tempo na cidade [city] do país [country]. O terceiro parâmetro é a chave obtida a partir do site [https://home.openweathermap.org/users/sign_up];
A interface [IDao] é implementada pela seguinte classe [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 {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
// timeout
private int timeout;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the 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));
}
// memory
this.timeout = timeout;
// factory configuration
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));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// service météo ---------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
}
// result
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- Note que as linhas 17–90 estão incluídas por predefinição na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos de implementação para a interface [IDao], específicos da aplicação (linha 92);
- linhas 93–105: implementação do método [getWeatherForecast]. Isto é muito simples e ocupa 6 linhas, linhas 100–105;
- linha 100: o método [getResponse] é um método da classe pai [AbstractDao]. Espera um parâmetro do tipo [IRequest<T>], onde T é o tipo da resposta esperada do servidor; aqui, é um String, uma vez que esperamos uma string JSON. O tipo T de [IRequest<T>] deve ser o tipo T do método [Observable<T> getWeatherForecast];
- a interface [IRequest<T>] tem apenas um método: getResponse. A sua função é fornecer a resposta do tipo T que o método [Observable<T> getWeatherForecast] deve devolver;
- linha 103: é a interface [WebClient] que fornece esta resposta. Passamos-lhe os três parâmetros recebidos na linha 94. Por este motivo, estes devem ter o atributo final;
2.8.2.4. A [MainActivity]
![]() |
A atividade [MainActivity] é a seguinte:
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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// methods parent class -----------------------
@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);
}
}
- Note que as linhas 15–55 estão incluídas por predefinição no projeto [client-android-skel]. Basta personalizá-las;
- Linhas 37–40: a matriz de fragmentos. Aqui existe apenas uma;
- Linhas 43–46: Não são necessários títulos de fragmentos;
- Linhas 48–50: não há separadores aqui;
- Linhas 52–55: A primeira vista a ser exibida é a vista n.º 0, a do [MeteoFragment];
- linhas 58–61: implementação da interface [IDao]. Aqui, não há nada a fazer além de delegar o trabalho à camada [DAO] na linha 21;
2.8.2.5. O fragmento [MeteoFragment]
![]() |
O [MeteoFragment] consulta o serviço web meteorológico / JSON. A sua estrutura é a seguinte:
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 {
...
}
- Linha 14: A vista [res/layout/meteo_fragment.xml] é a seguinte:
<?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>
A vista exibe apenas o texto da linha 10;
- linha 15: o menu [res / menu / menu_meteo.xml] é o seguinte:
<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>
- linhas 10-12: esta opção do menu é utilizada para solicitar a previsão meteorológica de uma cidade;
- linhas 14-15: esta opção do menu é utilizada para cancelar o pedido, caso este esteja em curso;
- linhas 16-18: esta opção do menu fecha a aplicação;
O código completo para o fragmento é o seguinte:
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 {
// local data
private int nbReponsesRecues;
// gestion des événements ---------------------------------------------------------------------------------------
// cities whose weather we want
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
@OptionsItem(R.id.actionMeteo)
protected void doMeteo() {
// his country
String country = "fr";
// get a API login by creating an account [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// URL web service / jSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// start waiting for [paysDeLoire.length] asynchronous tasks
beginWaiting(paysDeLoire.length);
// number of responses received
nbReponsesRecues = 0;
// asynchronous calls are made in parallel
for (String city : paysDeLoire) {
// weather
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// exploiting the answer
consumeResponse(response);
// a + response
nbReponsesRecues++;
}
});
}
}
// exploitation server response
private void consumeResponse(String response) {
// log
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// start of waiting
protected void beginWaiting(int numberOfRunningTasks) {
// log
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// parent
beginRunningTasks(numberOfRunningTasks);
// the [Cancel] option is displayed
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();
// displaying results
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();
}
// private methods -----------------------------------
private void initMenu() {
if (isDebugEnabled) {
Log.d(className, "initMenu");
}
// menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
// life cycle management ---------------------------------------------------------------------------------------
...
}
- linhas 25-50: tratamento do clique na opção de menu [Weather];
- linha 32: construção do URL do serviço web / JSON para o serviço meteorológico. Este é então passado para a camada [DAO] através da atividade;
- linha 34: iniciamos a espera. Passamos o número de tarefas a serem lançadas para que a classe pai nos possa notificar quando estiverem concluídas. Aqui, existem cinco tarefas porque estamos a solicitar a previsão meteorológica para as cinco cidades listadas na linha 23;
- linha 16: contamos o número de respostas recebidas para que possamos exibi-lo;
- linhas 38–50: percorremos as cidades para as quais queremos a previsão meteorológica;
- linha 40: faremos 5 pedidos HTTP em paralelo;
- linha 40: pedimos à classe pai [AbstractParent] para consultar o serviço web / JSON;
- linhas 40–48: o método [executeInBackground] espera dois parâmetros:
- linha 40: o processo a ser observado e executado é fornecido pelo método [mainActivity.getWeatherForecast];
- linhas 40–48: a instância [Action1] a ser executada quando a resposta do serviço assíncrono for recebida. O tipo T de [Action1<T>] deve ser o tipo T do resultado do método [getWeatherForecast];
- linha 44: foi recebida uma resposta. Esta é passada para o método [consumeResponse] na linha 53;
- linha 46: o contador de respostas recebidas é incrementado;
- linhas 53–56: consumo de uma resposta JSON do serviço meteorológico;
- linha 55: registamos simplesmente a cadeia JSON;
- linhas 59–72: código executado antes do lançamento das tarefas assíncronas;
- linha 65: passamos o número de tarefas a executar para a classe pai [AbstractParent]. Isto permite que ela nos notifique quando todas estiverem concluídas;
- linhas 67–70: preparação do menu para uma espera. Mantemos apenas a opção [Actions/Cancel], que permitirá ao utilizador cancelar as tarefas iniciadas;
- linhas 74–92: código executado quando a classe pai nos notifica que todas as tarefas iniciadas estão concluídas;
- linha 77: reiniciamos o menu para o seu estado inicial. O método [initMenu] (linhas 95-102) exibe o menu com todas as suas opções, exceto a opção [Actions/Cancel], que fica oculta;
- linhas 80–91: o número de respostas recebidas é exibido;
O clique na opção de menu [Cancelar] é tratado pelo seguinte código:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
- linha 7: solicitamos à classe pai que cancele as tarefas que ainda estão ativas;
Clicar na opção de menu [Concluir] é tratado pelo seguinte código:
@OptionsItem(R.id.actionTerminer)
protected void doTerminer() {
// we stop everything
System.exit(0);
}
O ciclo de vida do fragmento é gerido pelos seguintes métodos:
// life cycle management ---------------------------------------------------------------------------------------
@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) {
// 1st visit?
if (previousState == null) {
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
- linhas 3-6: utilizadas para armazenar o estado do fragmento numa classe derivada de [CoreState]. Se o fragmento não tiver nenhum estado para armazenar, como neste caso, devolvemos simplesmente uma instância de [CoreState]. Não devolva null, pois isso acabaria por causar uma falha;
- linhas 8-11: devem devolver o ID da vista. Aqui, o [MeteoFragment] tem o ID 0;
- linhas 13–16: utilizadas para inicializar o fragmento assim que este tiver sido construído (previousState == null) ou reconstruído (previousState != null). Aqui, não há nada a fazer. O único campo que pode ser inicializado é o seguinte:
// villes dont on veut la météo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
mas inicializa-se automaticamente;
- linhas 18–24: são usadas para inicializar a vista associada ao fragmento assim que este tiver sido construído (previousState == null) ou reconstruído (previousState != null);
- linhas 21-23: se esta for a primeira visita ao fragmento, o seu menu é inicializado para ocultar a opção [Cancel];
- linhas 27–30: chamadas se a navegação para o fragmento envolveu uma ação [SUBMIT]. Aqui, não há navegação entre fragmentos, uma vez que existe apenas um fragmento;
- linhas 32-35: chamado durante um ciclo de gravação/restauração devido à rotação do dispositivo ou por outro motivo. Aqui, uma vez que nenhum estado foi guardado, não há nada a fazer;
- linhas 37–40: chamadas quando todas as atualizações anteriores foram concluídas. Aqui, não há nada a fazer;
2.8.2.6. Testes
Executamos agora o exemplo:


Os registos são os seguintes:
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
- linhas 32-36: as respostas JSON são obtidas em threads de E/S
- linhas 37-41: o fragmento recupera as 5 respostas na thread da interface do utilizador;
Agora, fazemos a solicitação com um ID de API incorreto:
String APIID = "";

Os registos são então os seguintes:
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"]]
- linhas 3-6, 10: as 5 chamadas HTTP geraram 5 exceções;
- linha 7: o fragmento [MeteoFragment] recebe a primeira exceção. Em seguida, irá cancelar todas as tarefas;
Agora vamos definir um tempo limite de 5 segundos [IMainActivity.DELAY] e cancelar a operação. Os registos ficam então da seguinte forma:
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]]
- linha 3: pedido de cancelamento;
- linha 4: a espera é cancelada porque ocorreu um cancelamento;
- linhas 6–10: O cancelamento das tarefas provoca uma exceção em cada uma das cinco threads de tarefa. O tipo de exceção depende das aplicações. A exceção aqui é [java.lang.InterruptedException] porque as tarefas foram interrompidas enquanto executavam a instrução [Thread.sleep(delay)], o que as leva a esperar artificialmente por [delay] milissegundos;
2.8.3. Exemplo-16B
Aqui refatoramos o Exemplo 16 da Secção 1.17. Apresenta um fragmento que faz chamadas assíncronas a um servidor de números aleatórios. Vamos ver como se comporta durante uma rotação do dispositivo:

- Em [1], o dispositivo é rodado duas vezes;

Podemos ver que perdemos todas as mensagens de erro. Vamos tentar melhorar isto.
2.8.3.1. O Projeto Exemplo-16B
Copiamos o projeto [client-android-skel] para o projeto [examples/Example-16B] e, em seguida, carregamos o novo projeto:
![]() |
A partir do projeto inicial [Exemplo-16], copiamos os seguintes elementos para [Exemplo-16B]:
- o ficheiro [res/layout/vue1.xml], a pasta [res/values]:
![]() |
Vamos alterar a margem superior da vista [vue1.xml] para 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" />
- o fragmento [View1Fragment]:
![]() |
- a classe [DAO / serviço / Resposta]:
![]() |
Nesta fase, podemos tentar uma compilação inicial:
- O primeiro tipo de erro diz respeito às importações. Algumas classes foram movidas para pacotes diferentes durante a migração para [Exemplo-16B]. Começamos por corrigir estes erros;
- Um segundo tipo de erro é relatado na classe [Vue1Fragment] porque esta não implementa os métodos exigidos pela classe pai [AbstractParent]. Geramos automaticamente esses métodos;
Tentamos uma segunda compilação:
- todos os erros restantes estão agora concentrados na classe [Vue1Fragment], a classe que sofrerá mais alterações;
2.8.3.2. Criar um estado para o fragmento [Vue1Fragment]
Vimos que certas informações do fragmento precisarão de ser guardadas durante uma rotação, a fim de restaurar o fragmento ao seu estado anterior à rotação. Por isso, criamos um estado [Vue1FragmentState], que está vazio por enquanto:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.3.3. Personalização do projeto
![]() |
A interface [IMainActivity] permite especificar determinadas características do projeto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 5000;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- linhas 25, 28, 31, 40: características da camada [DAO]. Não é necessária autenticação básica;
- linha 34: adjacência de fragmentos. Aqui, esta constante é irrelevante, uma vez que existe apenas um fragmento;
- linha 37: esta não é uma aplicação com separadores;
- linha 43: existe apenas um fragmento;
A classe [CoreState] que armazena o estado dos fragmentos será a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Linha 12: Declaramos a classe de estado do fragmento [Vue1Fragment];
A classe [Session] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
Está vazio porque não há comunicação entre fragmentos nesta aplicação.
2.8.3.4. A camada [DAO]
![]() |
Na camada [DAO], é necessário personalizar três classes:
- a interface IDao;
- a sua implementação Dao;
- a interface WebClient para comunicação com o servidor web / JSON;
A classe [Response] provém do projeto [Exemplo-16], que a utiliza:
package client.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
A interface [WebClient] será a seguinte:
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 random number in the range [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- Linhas 18–19: A URL do serviço de números aleatórios. Note que esta URL é relativa à URL raiz do cliente (RestClientRootUrl, linha 12). Aqui, a URL raiz é [http://localhost:8080];
A interface [IDao] será a seguinte:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// random number service
Observable<Response<Integer>> getAlea(int a, int b);
}
- Note que os métodos nas linhas 6–22 estão presentes por predefinição na interface IDao do projeto [client-android-skel];
- Linha 25: O método [getAlea] devolve um número aleatório no intervalo [a,b]. Este número é devolvido numa resposta [Response<Integer>], em que o número aleatório está contido no campo [body] desse tipo;
A interface [IDao] é implementada pela seguinte classe [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 {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the 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));
}
// factory configuration
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));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// random number service
@Override
public Observable<Response<Integer>> getAlea(final int a, final int b) {
// web client execution
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getAlea(a, b);
}
});
}
}
- Note que as linhas 17–85 estão incluídas por predefinição na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos para implementar a interface [IDao];
- linhas 88–97: implementação do método [getAlea]. Isto é muito simples e ocupa 6 linhas, linhas 91–96;
- linha 91: o método [getResponse] é um método da classe pai [AbstractDao]. Espera um parâmetro do tipo [IRequest<T>], onde T é o tipo da resposta esperada, neste caso um tipo Response<Integer>. O tipo T de [IRequest<T>] (linha 91) deve ser o tipo T do método [Observable<T> getAlea] (linha 89);
- a interface [IRequest<T>] tem apenas um método: getResponse. A sua função é fornecer a resposta do tipo T que o método [Observable<T> getAlea] deve devolver;
- linha 94: é a interface [WebClient] que fornece esta resposta. São-lhe passados os dois parâmetros recebidos na linha 89. Por este motivo, estes devem ter o atributo final;
2.8.3.5. A [MainActivity]
![]() |
A atividade [MainActivity] é a seguinte:
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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// define fragments here
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
// define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// tabbed browsing - define the view to be displayed
}
@Override
protected int getFirstView() {
return 0;
}
// interface IDao ------------------------------------------
@Override
public Observable<Response<Integer>> getAlea(int a, int b) {
return dao.getAlea(a, b);
}
}
- Note que as linhas 15–61 estão incluídas por predefinição no projeto [client-android-skel]. Basta personalizá-las;
- linhas 40–44: a matriz de fragmentos. Aqui existe apenas um;
- linhas 47–51: não são necessários títulos de fragmentos;
- linhas 53–56: não há separadores aqui;
- linhas 58–61: a primeira vista a ser exibida é a vista n.º 0, a de [Vue1Fragment];
- linhas 64-67: implementação da interface [IDao]. Aqui, não há nada a fazer além de delegar o trabalho à camada [DAO] na linha 23;
2.8.3.6. O estado do fragmento [Vue1Fragment]
![]() |
A classe [Vue1FragmentState] será a seguinte:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.ArrayList;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// fragment status ------------------------
// list of answers
private List<String> reponses = new ArrayList<>();
// condition view ------------------------
// error msg on the number of random numbers requested
private boolean txtErrorAleasVisible = false;
// error msg on generation interval [a,b]
private boolean txtErrorIntervalleVisible = false;
// error msg on the URL of the web service
private boolean txtMsgErreurUrlServiceWebVisible = false;
// waiting time error msg
private boolean textViewErreurDelayVisible = false;
// whether or not the Execute button is visible
private boolean btnExecuterVisible = true;
// getters and setters
...
}
Para determinar o que precisava de ser guardado no fragmento, rodámos o dispositivo em várias situações e observámos o que se perdia após a restauração. Concluímos que a informação nas linhas 10–23 precisava de ser guardada.
2.8.3.7. O fragmento [View1Fragment]
![]() |
Atualmente, a vista [Vue1Fragment] contém vários erros devido ao facto de a classe pai [AbstractFragment], da qual deriva, ter sido alterada. Em vez de descrever as alterações a realizar uma a uma, iremos comentar diretamente a versão final.
A estrutura do fragmento é a seguinte:
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 {
...
}
- Linha 26: Note que cada fragmento deve ter um menu, mesmo que esteja vazio. É o que acontece aqui.
2.8.3.7.1. Tratamento do clique no botão [Executar]
@Click(R.id.btn_Executer)
protected void doExecuter() {
// check the data entered
if (!isPageValid()) {
return;
}
// delete previous answers
reponses.clear();
dataAdapterReponses.notifyDataSetChanged();
// reset the response counter to 0
nbReponses = 0;
infoReponses.setText("Liste des réponses (0)");
// activity initialization
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// prepare the random task
beginWaiting(1);
// we ask for the random numbers
getAleasInBackground(nbAleas, a, b);
}
void getAleasInBackground(int nbAleas, int a, int b) {
// create the process to be observed
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getAlea(a, b));
}
// we ask for the random numbers
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// we consume the answer
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();
}
}
// a + response
nbReponses++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
// we analyze the response
// mistake?
if (response.getStatus() != 0) {
// display
showAlert(response.getMessages());
// cancellation
doAnnuler();
// back to Ui
return;
}
// we add the information to the list of answers
reponses.add(0, String.valueOf(response.getBody()));
// refreshing the answers
dataAdapterReponses.notifyDataSetChanged();
}
// cancellation ----------
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// we set the hourglass
beginRunningTasks(nbRunningTasks);
// the [Cancel] button replaces the [Execute] button
btnExecuter.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
- linhas 4-6: Primeiro, verificamos se as entradas são válidas. Podem então aparecer mensagens de erro;
- linhas 8-9: a lista de respostas é apagada. Esta alteração é refletida no ListView que as apresenta;
- linhas 11-12: O número de respostas recebidas é reiniciado para zero;
- linha 14: Definimos o URL para o serviço de números aleatórios. Esta informação será passada para a camada [DAO];
- linha 15: define-se o período de tempo limite antes de enviar o pedido ao serviço de números aleatórios. Esta informação será passada para a camada [DAO];
- linha 17: preparamo-nos para iniciar 1 tarefa assíncrona (não N; veremos porquê);
- linhas 24–27: Combinamos as N tarefas assíncronas numa única sequência de operações [merge];
- linhas 29–36: solicitamos à classe pai [AbstractParent] que consulte o serviço web de números aleatórios / JSON;
- linhas 29–36: o método [executeInBackground] espera dois parâmetros:
- linha 29: o processo a ser observado e executado é aquele calculado nas linhas anteriores;
- linhas 29–36: a instância [Action1] a ser executada quando a resposta do serviço assíncrono for recebida. O tipo T de [Action1<T>] deve ser o tipo T do resultado do método [getAlea], ou seja, um tipo [Response<Integer>];
- linha 34: quando chega uma resposta (um número aleatório), esta é consumida no método na linha 39;
- linhas 49–50: registamos e sinalizamos que uma nova resposta foi recebida;
- linhas 53–60: o tipo [Response<T>] possui um campo [status] que é um código de erro. Se este código for diferente de zero, então o servidor encontrou um problema;
- linha 55: é exibida uma mensagem de erro. O método [showAlert] pertence à classe pai;
- linha 57: o método nas linhas 68–75 é chamado. Ele cancelará quaisquer tarefas que ainda estejam ativas (linha 74);
- linha 62: a resposta é adicionada à lista de respostas, que é a fonte de dados para o ListView;
- linha 64: o ListView é atualizado;
- linhas 77–83: o método [beginWaiting(int nbRunningTasks)] prepara a vista para a espera (linhas 81–82) e notifica a classe pai de que [nbRunningTasks] tarefas serão executadas em breve (linha 79);
2.8.3.7.2. O ciclo de vida do fragmento
O ciclo de vida do fragmento é gerido pelos seguintes métodos:
// local data
private List<String> reponses;
private ArrayAdapter<String> dataAdapterReponses;
private int nbReponses = 0;
...
// life cycle management ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// current view status
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) {
// 1st visit?
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
reponses = state.getReponses();
} else {
reponses = new ArrayList<>();
}
// listView data source
dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
// nre of responses
nbReponses = reponses.size();
}
@Override
protected void initView(CoreState previousState) {
// listview / adapter link
listReponses.setAdapter(dataAdapterReponses);
// 1st visit?
if (previousState == null) {
// hide error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// buttons
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// previous view status
Vue1FragmentState state = (Vue1FragmentState) previousState;
// show / hide error msg
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);
// buttons
btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
// no. of responses
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// the [Execute] button replaces the [Cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
- linhas 7–18: garantem que o fragmento é guardado quando a classe pai o solicita;
- linha 11: exibe a mensagem de erro relativa ao tempo limite;
- linha 12: exibe a mensagem de erro relativa ao número de números aleatórios solicitados;
- linha 13: exibe a mensagem de erro relativa ao URL do serviço web / JSON;
- linha 14: exibe a mensagem de erro relativa ao intervalo [a,b] para a geração de números aleatórios;
- linha 15: visibilidade do botão [Executar];
- linha 16: a lista de respostas recebidas;
- linhas 20–23: devem devolver o ID da vista. O ID do fragmento aqui é 0, uma vez que existe apenas um;
- linhas 25–38: inicialização dos campos do fragmento, seja na primeira visita (previousState == null) ou numa visita subsequente;
- linhas 29–30: se esta não for a primeira visita, o campo [reponses] é restaurado a partir do estado anterior do fragmento;
- linhas 31–33: se esta for a primeira visita, o campo [reponses] é inicializado com uma lista vazia;
- linhas 34-37: utilizando o campo [reponses], podemos construir a fonte de dados para o ListView do fragmento (linha 35), bem como o número de respostas (linha 37);
- linhas 40–55: executadas para inicializar a vista associada ao fragmento, seja na primeira visita (previousState == null) ou numa visita subsequente;
- linha 43: a ListView do fragmento é vinculada à fonte de dados que acabou de ser construída no método [initFragment];
- linhas 45–54: se esta for a primeira visita, a vista é preparada para a sua primeira exibição;
- linhas 57–60: executadas durante a navegação entre fragmentos associada a uma ação [SUBMIT]. Aqui, existe apenas um fragmento e, por isso, não há navegação entre fragmentos;
- linhas 63–76: executadas durante a navegação entre fragmentos associada a uma ação [NAVIGATION] ou durante um ciclo de gravação/restauração devido à rotação do dispositivo ou a outro motivo. Aqui, apenas o último caso pode ocorrer. Lembre-se de que, aqui, em todos os casos, [previousState] é sempre diferente de nulo;
- linha 65: o estado anterior é convertido para o tipo de estado do fragmento;
- linhas 66–75: o conteúdo do estado anterior é utilizado para restaurar a vista;
- linhas 78–81: chamado quando todas as atualizações anteriores tiverem sido concluídas. Aqui, não há nada a fazer;
- linhas 83–89: executadas quando todas as tarefas assíncronas estiverem concluídas. Aqui, o botão [Cancel] é ocultado e substituído pelo botão [Execute];
2.8.3.8. Testes
O leitor é convidado a realizar os seguintes testes:
- criar erros e executar o dispositivo: as mensagens de erro devem permanecer visíveis;
- Gerar números aleatórios e executar o dispositivo: os números aleatórios gerados devem permanecer visíveis;
- definir uma espera de vários segundos e executar o dispositivo durante a espera: as tarefas devem ter sido canceladas (isto pode ser verificado nos registos);
2.8.4. Exemplo-22B
Aqui, revisamos o Exemplo 22 para o refatorar de acordo com o modelo do projeto [client-android-skel]. Recorde-se que o projeto [Exemplo-22] lida corretamente com o ciclo de gravação/restauração do fragmento durante a rotação e que serviu de base para o projeto [client-android-skel].
Duplicamos o projeto [client-android-skel] para [examples/Exemplo-22B] e carregamos este último projeto:
![]() |
Em seguida, copiamos vários elementos do projeto [Exemplo-22] para o projeto [Exemplo-22B].
Primeiro, copiamos os elementos da pasta [res]:
- [layout/fragment_main.xml, layout/view1.xml, menu/menu_fragment.xml, menu/menu_main.xml, a pasta [values];
![]() |
Vamos alterar a margem superior de ambas as vistas para 120 dp:
[view1.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"/>
Em seguida, copiamos os elementos [View1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
Nesta altura, podemos tentar uma primeira compilação. Surge um primeiro tipo de erro: importações incorretas, porque as classes mudaram de pacote. Corrigimos estas importações. Um segundo tipo de erro deve-se ao facto de os fragmentos não implementarem todos os métodos da sua classe pai [AbstractFragment]. Corrigimos isto premindo (Alt+Enter).
Os erros restantes resultam de diferenças entre as classes antigas e novas [AbstractFragment]. Por enquanto, ignoramo-los.
2.8.4.1. Personalização do projeto
![]() |
A pasta [custom] contém elementos de arquitetura que podem ser personalizados pelo programador.
A interface [IMainActivity] permite especificar determinadas características do projeto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = true;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of fragments
int FRAGMENTS_COUNT = 5;
}
- linhas 23, 26, 29, 38: características da camada [DAO]. Não há nenhuma aqui;
- linha 41: existem cinco fragmentos aqui;
- linha 32: adjacência de fragmentos. Esta constante pode assumir um valor entre [1,4] aqui. Recomenda-se ao leitor que altere este valor para verificar se a aplicação continua a funcionar;
- linha 35: esta é uma aplicação com separadores;
A classe [CoreState] que armazena o estado dos fragmentos será a seguinte:
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 {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Linha 12: Declaramos a classe de estado do fragmento [PlaceHolderFragment]. O próprio fragmento [Vue1Fragment] não tem estado;
A classe [Session] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// data to be shared between fragments themselves and between fragments and activities
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
// don't forget the getters and setters required for serialization / deserialization jSON
// number of fragments visited
private int numVisit;
// n° fragment type [PlaceholderFragment] displayed in second tab
private int numFragment = -1;
// getters and setters
...
}
Esta é a sessão para o projeto [Exemplo-22].
2.8.4.2. A [MainActivity]
![]() |
A atividade [MainActivity] é a seguinte:
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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// menu management-----------------------
@Override
public boolean onOptionsItemSelected(MenuItem item) {
...
}
private void showFragment(int i) {
...
}
// implementation of parent class methods ---------------------------------------------------
...
}
Aqui, a classe [MainActivity] é maior do que nos exemplos anteriores por duas razões:
- há separadores para gerir;
- há um menu para gerir;
2.8.4.2.1. Implementação dos métodos da classe pai
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
// session
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// fragment no
final String ARG_SECTION_NUMBER = "section_number";
// initialization of fragment table
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// a fragment of +
fragments[i] = new Vue1Fragment_();
// result
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
// no titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
...
}
@Override
protected int getFirstView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
- linhas 2–12: O método [onCreateActivity] é chamado pela classe pai [AbstractActivity] quando a atividade é criada pela primeira vez ou recriada durante um ciclo de gravação/restauração. Quando este método é chamado, a classe pai já restaurou a sessão;
- linha 10: é recuperada uma referência local à sessão. A conversão de tipo é necessária porque a sessão da classe pai é do tipo [AbstractSession];
- linhas 19–38: o método [getFragments] deve devolver à classe pai a matriz de fragmentos geridos pela aplicação. Aqui existem [FRAGMENTS_COUNT] fragmentos, um número definido em [IMainActivity]. Os primeiros [FRAGMENTS_COUNT-1] fragmentos são do tipo [PlaceHolderFragment] e o último é do tipo [Vue1Fragment];
- linhas 41–45: o método [getFragmentTitle] deve devolver os títulos dos fragmentos quando esta informação for útil. Não é o caso aqui;
- linhas 47–50: este método é chamado pela classe pai quando o utilizador clica num separador. Voltaremos a este assunto na próxima secção;
- linhas 52–55: devolve o número da primeira vista a apresentar quando a aplicação é iniciada. Aqui, o fragmento [Vue1Fragment] deve ser apresentado em primeiro lugar. O método [getFirstView] poderia ser vantajosamente substituído por uma constante em [IMainActivity];
2.8.4.2.2. Gestão de separadores
Os separadores são geridos utilizando os seguintes métodos:
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
// session
this.session = (Session) super.session;
// 1st tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// 2nd tab ?
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) {
// fragment number to display
int numFragment;
switch (position) {
case 0:
// fragment no. [Vue1Fragment]
numFragment = getFirstView();
break;
default:
// fragment no. [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// fragment display
if (numFragment != mViewPager.getCurrentItem()) {
navigateToView(numFragment, ISession.Action.SUBMIT);
}
}
}
- linhas 1–20: O método [onCreateActivity] é chamado pela classe pai [AbstractActivity] quando a atividade é criada pela primeira vez ou recriada durante um ciclo de gravação/restauração. Quando este método é chamado, a classe pai já restaurou a sessão;
- linha 9: é recuperada uma referência local à sessão. A conversão de tipo é necessária porque a sessão da classe pai é do tipo [AbstractSession];
- linhas 11–13: a primeira guia é criada;
- linhas 15–20: a segunda guia é criada se um ID de fragmento estiver armazenado na sessão (linha 15). Este ID é inicialmente definido como -1 quando a atividade é construída pela primeira vez;
- linhas 23–39: este método é chamado pela classe pai quando o utilizador clica numa guia;
- linhas 28-31: se a guia 0 for clicada, então [Vue1Fragment] deve ser exibido. Sabemos que esta é a primeira vista que foi exibida quando a aplicação foi iniciada;
- linhas 32–35: se a guia 1 for clicada, então o fragmento cujo número está armazenado na sessão deve ser exibido;
- linhas 37–39: navegamos para o fragmento selecionado. A ação associada é [SUBMIT]. Poderia ter sido [NAVIGATION]? Neste documento, usamos [NAVIGATION] apenas quando a exibição do novo fragmento requer apenas o conhecimento do seu estado anterior. Aqui, esse não é o caso, uma vez que a exibição do fragmento deve mudar do seu estado anterior para mostrar mais uma visita;
2.8.4.2.3. Gestão do Menu
A atividade está associada ao seguinte 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>
que exibe o seguinte:
![]() |
O menu é gerido através dos seguintes métodos:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onOptionsItemSelected");
}
// processing menu options
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;
}
}
// item processed
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// no navigation on software tab selection
session.setNavigationOnTabSelectionNeeded(false);
// we recreate the two tabs for a title font issue
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
// the fragment number to be displayed is set in session
session.setNumFragment(i);
// select tab 2 with navigation
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- linhas 16-31: processar um clique numa opção do menu [Fragmenti];
- linhas 37–50: exibem o fragmento #i (estes são fragmentos do tipo PlaceHolderFragment) na guia #1 (a segunda guia);
- linhas 42-44: decidimos remover as separadores existentes para criar duas novas. Esta decisão foi tomada para contornar o seguinte problema: quando simplesmente exibimos o fragmento na separador 1 existente (sem a eliminar), curiosamente o seu título aparece (tipo de letra, tamanho) diferente do título da separador 0;
- linhas 43–44: as duas separadores são criadas, mas não selecionadas (último parâmetro definido como false);
- Linha 40: As operações nas linhas 42–44 podem desencadear operações [select] nas guias, o que chamará o manipulador [onTabSelected]. Se nenhuma ação for tomada, isto resultará na navegação para um fragmento. Evitamos isso definindo o booleano [navigationOnTabSelectionNeeded] como false na sessão. Este booleano é automaticamente redefinido para true pela classe [AbstractFragment] quando um fragmento se torna visível;
- linha 46: armazenamos o número do fragmento a ser exibido na sessão;
- linhas 48–50: Selecione o separador n.º 2 com navegação (linha 48). Isto irá acionar o procedimento [onTabSelected], que irá:
- exibirá o fragmento cujo número foi armazenado na sessão;
- armazenar o número da guia selecionada na sessão;
2.8.4.3. O fragmento [Vue1Fragment]
Aqui está a versão final do fragmento:
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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// fragment life cycle -----------------------------------------------
private void initFragment() {
// nothing to do
}
// save fragment status
@Override
public CoreState saveFragment() {
// view status - nothing to save
return new CoreState();
}
@Override
protected int getNumView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// nothing to do
}
@Override
protected void initView(CoreState previousState) {
// 1st visit?
if (previousState == null) {
// the visit number is displayed
showNumVisit();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// the visit number is displayed
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// private methods -------------------------------------
// display visit no
private void showNumVisit() {
// increment visit no
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// the visit number is displayed
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
A sala está quase vazia.
- linhas 35-39: chamado pela classe pai quando o fragmento precisa de guardar o seu estado. O fragmento [Vue1Fragment] não tem nenhum estado para guardar. Simplesmente devolvemos uma instância da classe base [CoreState] (lembrete: não devemos devolver null);
- linhas 41-44: deve devolver o ID do fragmento. Por definição, o fragmento [Vue1Fragment] tem o ID [FRAGMENTS_COUNT-1];
- linhas 51-59: chamado pela classe pai quando o fragmento é construído pela primeira vez (previousState == null) ou em visitas subsequentes (previousState != null);
- linhas 54-57: se esta for a primeira visita, incremente a contagem de visitas e exiba-a (linhas 85-92);
- linhas 61-65: chamado quando o fragmento está prestes a ser exibido em associação com uma ação [SUBMIT]. A contagem de visitas é incrementada e exibida. Aqui, não é possível que a contagem de visitas seja incrementada duas vezes durante o ciclo de vida. Na verdade, a primeira visita ao fragmento [Vue1Fragment] ocorre no arranque da aplicação, quando a ação é definida como [NONE] por padrão na sessão. Isto garante que o método [updateOnSubmit] não será chamado. Depois disso, nunca mais será a primeira visita, e o método [initView] não fará nada;
- linhas 68–71: chamado durante um ciclo de gravação/restauração. Como o fragmento não tem estado, não há nada para restaurar aqui;
- linhas 73–76: chamado quando todas as atualizações anteriores foram concluídas. Aqui, não há mais nada a fazer;
- linhas 78–81: chamadas quando todas as tarefas assíncronas iniciadas tiverem sido concluídas. Aqui, não há tarefas assíncronas;
2.8.4.4. O estado [PlaceHolderFragmentState]
O estado do fragmento [PlaceHolderFragment] será o seguinte:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class PlaceHolderFragmentState extends CoreState {
// text
private String text;
// manufacturers
public PlaceHolderFragmentState() {
}
public PlaceHolderFragmentState(String text) {
super();
this.text = text;
}
// getters and setters
...
}
- Quando precisarmos de guardar o estado do fragmento, guardaremos o texto que estava a ser apresentado (linha 7);
2.8.4.5. O fragmento [PlaceHolderFragment]
O fragmento [PlaceHolderFragment] será o seguinte:
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 {
// visual interface components
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// data
private String text;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// implementation of parent class methods ----------------------------
@Override
public CoreState saveFragment() {
// save fragment state
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) {
// original text
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// update the text displayed
// increment visit no
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
}
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restore displayed text
PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
textViewInfo.setText(state.getText());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
}
- linhas 30–36: quando a classe pai solicita ao fragmento que guarde o seu estado, o texto apresentado pelo fragmento é guardado (linha 34);
- linhas 38–41: devolvem o ID do fragmento. Isto depende do ID da secção passado como argumento quando o fragmento foi criado;
- linhas 43–47: chamado durante a primeira construção do fragmento (previousState == null) ou durante construções subsequentes (previousState != null);
- linha 46: aqui, o estado anterior não é utilizado. O texto inicial [text] (linha 24) apresentado na primeira visita é recalculado de cada vez. Isto é discutível. Poderíamos ter optado por incluir também esta informação no estado do fragmento;
- linhas 49–51: chamadas durante a primeira renderização da vista associada ao fragmento (previousState == null) ou durante renderizações subsequentes (previousState != null). Não há nada a fazer;
- linhas 53–56: chamadas quando o fragmento está prestes a ser exibido em associação com uma ação [SUBMIT]. Este é sempre o caso, exceto durante o ciclo de gravação/restauração, onde a ação é [RESTORE]. Por isso, incrementamos o número de visitas e exibimo-lo;
- linhas 68–74: chamadas durante um ciclo de gravação/restauração. Restauramos o texto que foi guardado no estado do fragmento;
- linhas 76–79: chamadas quando todas as atualizações anteriores foram concluídas. Aqui, não há mais nada a fazer;
- linhas 82-83: chamadas quando todas as tarefas assíncronas iniciadas estão concluídas. Aqui, não há tarefas assíncronas;
2.8.4.6. Testes
Convidamos o leitor a testar a aplicação, rodando o dispositivo para verificar se o fragmento exibido não perde o seu estado. Iremos também examinar os registos.
2.9. Conclusão
No final deste capítulo, temos um projeto de exemplo [client-android-skel] para um cliente Android que comunica com um serviço web / JSON com as seguintes funcionalidades:
- A comunicação assíncrona com o servidor web/JSON é gerida utilizando a biblioteca RxJava;
- o ciclo de vida do fragmento (atualização, gravação, restauração) é gerido pela sua classe pai [AbstractFragment], que chama métodos específicos das suas classes filhas em momentos precisos. O fragmento filho não precisa, assim, preocupar-se com as fases do ciclo de vida, mas apenas com a implementação de determinados métodos exigidos pela sua classe pai;
- o ciclo de vida da atividade (guardar / restaurar) é gerido por uma classe abstrata [AbstractActivity], que também exige que a atividade filha implemente determinados métodos;
- A classe [AbstractActivity] pode lidar com uma aplicação com ou sem separadores, com ou sem uma imagem de carregamento e com ou sem autenticação básica no servidor web / JSON. A presença ou ausência destes elementos é determinada pela configuração;
Apresentaremos agora um estudo de caso mais complexo do que os exemplos anteriores. A nova aplicação será baseada no projeto modelo [client-android-skel].



















































