2. Estrutura de um cliente Android que comunica com um serviço web / jSON
Apresentamos agora um esboço de 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:
![]() |
A análise desta aplicação-esqueleto será uma oportunidade para rever alguns pontos que abordámos nos exemplos anteriores. Esta aplicação servirá de estrutura para todas as aplicações futuras. Foi construída após inúmeras iterações. O seu objetivo é factorizar, em classes abstratas, o maior número possível de elementos das aplicações que iremos construir em breve, de modo a evitar ter de escrever sempre o mesmo tipo de código, que se diferencia apenas em pormenores. As suas características são as seguintes:
- a comunicação assíncrona com o servidor web / jSON é feita através da biblioteca RxJava;
- o ciclo de vida de um fragmento (update, save, restore) é gerido pela sua classe pai [AbstractFragment], que invoca, em momentos específicos, determinados métodos das suas classes filhas. Assim, a classe filha não tem de se preocupar com as etapas do ciclo de vida, mas apenas com a implementação de determinados métodos impostos pela sua classe pai;
- o ciclo de vida da atividade (guardar/restaurar) é gerido por uma classe abstrata, [AbstractActivity], que também impõe à atividade filha a implementação de determinados métodos;
- a classe [AbstractActivity] é capaz de gerir uma aplicação com ou sem separadores, com ou sem imagem de espera, com ou sem autenticação básica junto do servidor web / jSON. A presença ou ausência destes elementos é definida por configuração;
Este esqueleto foi utilizado em todos os exemplos seguintes. Devido à diversidade destes, o que funcionava num exemplo podia não funcionar no exemplo seguinte. Como o esqueleto foi utilizado num total de sete exemplos, ocorreram numerosas iterações. Se o utilizássemos para um oitavo exemplo, é possível que voltássemos a constatar que a especificidade desse novo exemplo geraria novos erros. No entanto, a utilização deste esqueleto simplificará consideravelmente a redação dos exemplos que se seguem. Com efeito, a gestão do ciclo de vida de um fragmento (update, save, restore), aliada ao conceito de adjacência dos fragmentos, é particularmente complexa. Aqui, está totalmente oculta na classe [AbstractFragment].
2.1. Arquitetura do cliente Android
O cliente Android proposto baseia-se na seguinte arquitetura:
![]() |
- a camada [DAO] implementa uma interface [IDao]. É esta que comunica 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:
![]() |
Vamos apresentar, um a um, os diferentes elementos deste projeto.
2.2. A configuração do Gradle
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Desde a versão 0.11 do plugin Gradle do Android, é necessário utilizar o 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'
}
}
// opções de empacotamento necessárias para conseguir produzir o 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, podemos partir dos números atuais se configurarmos o Android Studio para que estas versões das ferramentas Android (linhas 15-16, 47-48) estejam efetivamente presentes (ver parágrafo 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: alteraremos o pacote da aplicação;
- linhas 10, 15: definiremos o valor do item [app_name] no ficheiro [res / values / strings.xml]. Por enquanto, este é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- nome da aplicação -->
<string name="app_name">[Donnez un nom à votre application]</string>
</resources>
2.4. A organização do código Java
![]() |
- [architecture] agrupa os principais elementos de organização do código;
- [activity] contém a única atividade da aplicação;
- [fragments] agrupa os fragmentos ou vistas da aplicação;
- [dao] agrupa os elementos de 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>
<!-- contentor de fragmentos -->
<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: utiliza-se um contentor de fragmentos específico;
A atividade também possui um menu [res / menu / menu_main.xml] para a sua vista:
<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, se necessário.
2.5.2. O contentor 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 {
// controla o deslizar
private boolean isSwipeEnabled;
// controla a rolagem
private boolean isScrollingEnabled;
// construtores
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// métodos a redefinir para gerir o deslize
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// deslizar autorizado?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// deslizar autorizado?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// controlo da rolagem
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setter
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
}
Esta classe estende a classe padrão do Android [ViewPager] apenas para gerir o deslize (linha 11) e a rolagem (linha 13) entre vistas.
- linhas 26-43: os métodos que inibem o deslizar se este tiver sido desativado;
- linhas 46-49: redefinição do método [setCurrentItem], que serve para alterar a vista exibida. Se a rolagem tiver sido desativada, a mudança de vista será efetuada sem rolagem. Note-se que o programador pode contornar este modo de funcionamento utilizando o método [setCurrentItem(int position, boolean smoothScrolling)], que lhe permite especificar a rolagem que deseja;
2.5.3. A classe [CoreState]
![]() |
A classe [CoreState] é a classe pai dos estados dos diferentes 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)
// tarefa pendente: adicionar aqui as subclasses de [CoreState]
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragmento visitado ou não
protected boolean hasBeenVisited = false;
// estado do eventual menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
...
}
- linha 16: cada fragmento tem no seu estado um valor booleano [hasBeenVisited] que indica se já foi visitado ou não. Isto é necessário porque, por vezes, aquando da primeira exibição de um fragmento, há ações específicas a realizar;
- linha 18: o projeto [client-android-skel] guarda e restaura automaticamente os menus dos fragmentos, caso estes tenham algum. Na tabela MenuItemState[] menuOptionsState, armazena-se o estado de visibilidade de todas as opções do menu;
- linhas 10-13: tal como foi feito em [Exemple-22], o estado da atividade e dos seus fragmentos será guardado na sessão, que, por sua vez, será guardada sob a forma de uma cadeia jSON. Veremos que a sessão memoriza um tabuleiro de elementos do tipo [CoreState]. Se não fizermos nada, será então a cadeia jSON, de um tipo [CoreState], que será guardada. No entanto, pretendemos guardar os estados dos fragmentos, ou seja, os estados derivados de [CoreState]. Para que seja gerada a cadeia jSON do tipo derivado e não a do tipo pai, é necessário declarar os tipos derivados conforme indicado nas linhas 10 a 13. A classe [CoreState] é uma das classes da 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 {
// acesso à sessão
ISession getSession();
// mudança de vista
void navigateToView(int position, ISession.Action action);
// gestão da espera
void beginWaiting();
void cancelWaiting();
// constantes da aplicação (a modificar) -------------------------------------
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
// tempo máximo de espera pela resposta do servidor
int TIMEOUT = 1000;
// tempo de espera antes da execução do pedido do cliente
int DELAY = 0;
// autenticação básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adjacência dos fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de separadores
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = false;
// número de fragmentos da aplicação
int FRAGMENTS_COUNT = 0;
// tarefa: adicione aqui as suas constantes e outros métodos
}
- linha 6: a interface [IMainActivity] estende a interface [IDao] da camada [DAO];
- linha 9: é a atividade que dá acesso à sessão sob a forma de uma instância da interface [ISession];
- linha 12: é através desta atividade que se muda de vista. O segundo parâmetro é a ação que provoca essa mudança de vista, um dos valores SUBMIT, NAVIGATION, RESTORE;
- linhas 15-17: é a atividade que gere a imagem de espera;
- linha 22: para a depuração da aplicação;
- linha 25: para não esperar demasiado tempo caso o servidor deixe de responder;
- linha 28: durante a depuração, definiremos um valor de alguns segundos para termos tempo de cancelar a operação com o servidor e ver o que acontece;
- linha 31: para true se o serviço jSON solicitar uma autenticação básica;
- linha 34: adjacência de fragmentos;
- linha 37: para vrai se a aplicação tiver separadores;
- linha 39: para vrai se a aplicação comunicar com um servidor web / jSON e se se pretender mostrar uma imagem de espera durante as trocas de dados;
- linha 43: o número de fragmentos geridos pela aplicação;
A interface [IMainActivity] é o segundo elemento da arquitetura que o programador deve preencher (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 {
// URL do serviço web
void setUrlServiceWebJson(String url);
// utilizador
void setUser(String user, String mdp);
// tempo limite do cliente
void setTimeout(int timeout);
// autenticação básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
// tempo de espera do cliente, em milissegundos, antes da solicitação
void setDelay(int delay);
// tarefa pendente: declare aqui a sua interface
}
- linha 24: o programador irá completar a interface aqui;
2.5.6. A sessão
![]() |
A classe [Session] encapsula os 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 {
// número da última vista apresentada
int getPreviousView();
void setPreviousView(int numView);
// último estado de uma vista
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// ação em curso
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// estados de todas as vistas -
// não é utilizado pelo código, mas é necessário para a serialização/deserialização jSON
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// n.º do último separador selecionado
int getPreviousTab();
void setPreviousTab(int position);
// navegação ao selecionar um separador
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
Introduzimos a interface [ISession] para impor a presença de determinados métodos na sessão:
- linhas 7-10: o número da última vista (fragmento) apresentada;
- linhas 12-15: o estado de uma vista específica;
- linhas 17-24: introduzimos o conceito de ação em curso. Existem quatro (linha 17):
- RESTORE: está em curso um processo de gravação/restauração. Não há mudança de vista;
- NAVIGATION: está em curso uma navegação. Denominaremos aqui «navegação» uma mudança de vista em que a nova vista pode ser restaurada a partir do seu último estado guardado na sessão;
- SUBMIT: atribuir-se-á o tipo [SUBMIT] a uma ação em curso, quando houver uma mudança de vista e a nova vista depender do estado da atividade em geral e não apenas do seu próprio estado. Por vezes, é difícil distinguir entre NAVIGATION e SUBMIT. Nesse caso, considera-se o caso mais geral, o SUBMIT;
- NONE: 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 do tipo CoreState[]. Para que esta seja gerida corretamente durante as serializações/deserializações jSON, é necessário que possua um getter e um setter;
- linhas 32-35: número do último separador selecionado. É utilizado durante o ciclo de gravação/restauração para voltar a selecionar o separador que estava selecionado antes da rotação do dispositivo;
- linhas 37-40: gestão de um valor booleano que indica se a seleção de um separador deve ser acompanhada por uma mudança 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 {
// n.º da vista anterior
private int preViousView;
// estado das vistas
private CoreState[] coreStates = new CoreState[0];
// ação em curso
private Action action = Action.NONE;
// separador selecionado anteriormente
private int previousTab;
// navegação ao selecionar o separador
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// construtor
public AbstractSession() {
// inicialização da tabela de estados dos fragmentos
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 número da vista que estava a ser apresentada antes da que está atualmente a ser apresentada. Esta informação é útil quando é possível aceder a uma vista a partir de vários locais. É tipicamente o caso na navegação por separadores. A vista apresentada pode, assim, saber qual era a vista anterior;
- linha 12: a tabela de estados de todos os fragmentos exibidos pela atividade;
- linha 18: o número da aba selecionada anteriormente. Desempenha um papel semelhante ao do número da vista anterior da linha 9. Esta informação é útil quando ocorre uma rotação do dispositivo e é necessário voltar à aba que estava selecionada antes da rotação;
- linha 22: um valor booleano que indica se a seleção de uma aba deve ser acompanhada por uma alteração do fragmento exibido. É importante saber que o projeto [client-android-skel] faz uma gestão separada das abas e dos fragmentos, para poder ser utilizado em casos em que o número de abas é inferior ao número de fragmentos. Existem dois tipos de seleção:
- uma seleção efetuada pelo utilizador ao clicar numa separador. Neste caso, geralmente o fragmento exibido deve mudar;
- uma seleção por software através do método [Tablayout.Tab.select()]. Neste caso, a alteração do fragmento exibido nem sempre é desejável. Eis dois exemplos:
- durante uma rotação do dispositivo, a atividade é recriada, assim como as separadores. No entanto, quando a primeira separador é criada, esta é automaticamente submetida a uma operação de software [select]. Nessa altura, não é desejável alterar o fragmento exibido, uma vez que nos encontramos numa fase de recriação da atividade em que o fragmento que acabará por ser exibido não será necessariamente aquele associado ao primeiro separador;
- uma vez que a gestão dos separadores é independente da dos fragmentos, pode ser necessário atualizar os separadores (eliminação, adição) sem interferir com os fragmentos a eles associados. No entanto, algumas destas operações podem, mais uma vez, desencadear uma operação de software [select] implícita numa das separadores. Esta seleção não tem, portanto, de se traduzir necessariamente numa 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 as serializações/deserializações jSON;
- linhas 25-31: o construtor inicializa a matriz de estados dos fragmentos [FRAGMENTS_COUNT] da aplicação. Os elementos desta matriz são inicializados com o campo [hasBeeenVisited=false]. Esta informação é utilizada para determinar se se trata ou não da 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 {
// dados a partilhar entre os próprios fragmentos e entre fragmentos e atividades
// os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
// não se esqueça dos getters e setters necessários para a serialização/deserialização em jSON
}
- linha 5: a classe [Session] estende a classe [AbstractSession] que acabámos de ver. O programador irá colocar aqui os elementos a partilhar entre os próprios fragmentos e entre os fragmentos e a atividade. Note-se que a classe [Session] já não está anotada pela anotação AA [@EBean]. Tornou-se uma classe normal;
2.5.7. A classe abstrata [AbstractActivity]
![]() |
2.5.7.1. Squelette
A classe [AbstractActivity] é uma classe com mais de 300 linhas. Vamos analisá-la por etapas. A sua estrutura básica é 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 {
// camada [DAO]
private IDao dao;
// a sessão
protected Session session;
// o contentor de fragmentos
protected MyPager mViewPager;
// a barra de ferramentas
private Toolbar toolbar;
// a imagem de espera
private ProgressBar loadingPanel;
// barra de separadores
protected TabLayout tabLayout;
// o gestor de fragmentos ou secções
private FragmentPagerAdapter mSectionsPagerAdapter;
// nome da classe
protected String className;
// mapeador jSON
private ObjectMapper jsonMapper;
// construtor
public AbstractActivity() {
// nome da classe
className = getClass().getSimpleName();
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructeur");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// implementação IMainActivity --------------------------------------------------------------------
...
// ciclo de vida - gravação/restauração da atividade ------------------------------------
...
// gestão da imagem de espera ---------------------------------
...
// interface IDao -----------------------------------------------------
...
// o gestor de fragmentos --------------------------------
...
// classes filhas
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);
- gere o armazenamento e a restauração da atividade e dos seus fragmentos durante uma rotação do dispositivo (linha 58);
- gere a imagem de espera durante uma troca de dados 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 incluam seis métodos (linhas 71-81);
2.5.7.2. Implementar a interface [IMainActivity]
A implementação da interface [IMainActivity] (ver parágrafo 2.5.4) é a seguinte:
// implementação 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));
}
// exibição de novo fragmento
mViewPager.setCurrentItem(position);
// regista-se a ação em curso durante esta mudança de vista
session.setAction(action);
}
2.5.7.3. Gravação do estado da atividade e dos seus fragmentos
O estado da atividade e dos seus fragmentos encontra-se inteiramente na sessão. Trata-se, portanto, de guardar essa sessão. Retomamos aqui o que foi feito no projeto [Exemple-22] (ver parágrafo 1.23):
// gestão do armazenamento e restauração da atividade ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// pai
super.onSaveInstanceState(outState);
// guardar sessão sob a forma de uma cadeia jSON
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// registo
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. Restauração do estado da atividade e dos seus fragmentos
Trata-se de restaurar a sessão. Procedemos tal como foi demonstrado em [Exemple-22]:
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
// alguma coisa para restaurar?
if (savedInstanceState != null) {
// recuperação de sessão
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// registo
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
} else {
// sessão
session = new Session();
}
...
- linhas 10-26: se o parâmetro [Bundle savedInstanceState] da linha 2 não for null, então a sessão é restaurada (linhas 12-17);
- linhas 26-29: o caso em que o parâmetro [Bundle savedInstanceState] da linha 2 é null corresponde ao primeiro arranque da atividade. É então criada uma sessão vazia;
2.5.7.5. Inicialização da camada [DAO]
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// camada [DAO]
dao = getDao();
if (dao != null) {
// configuração da camada [DAO]
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// classes filhas
protected abstract IDao getDao();
....
}
- linha 11: é solicitada à atividade filha (linha 21) uma referência à camada [DAO];
- linhas 14-17: se a camada [DAO] existir, esta é configurada a partir das informações contidas na interface [IMainActivity];
2.5.7.6. Inicialização da vista associada à atividade
A vista associada à atividade foi apresentada no parágrafo 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>
<!-- recipiente de fragmentos -->
<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) {
// pai
super.onCreate(savedInstanceState);
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// vista associada
setContentView(R.layout.activity_main);
// componentes da vista ---------------------
// barra de ferramentas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// imagem de espera?
if (IS_WAITING_ICON_NEEDED) {
// adiciona-se a imagem de espera
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// criação de ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// adição do ProgressBar à barra de ferramentas
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: eventual adição de uma imagem de espera: se o valor booleano [IS_WAITING_ICON_NEEDED] for verdadeiro na interface [IMainActivity];
- linha 23: criação da imagem de espera 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 requerer uma barra de separadores. Esta é adicionada e gerida da seguinte forma:
// barra de separadores
protected TabLayout tabLayout;
...
// barra de separadores?
if (ARE_TABS_NEEDED) {
// adiciona-se a barra de separadores
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// sem navegação por seleção até à exibição de um fragmento
session.setNavigationOnTabSelectionNeeded(false);
// criação da barra de separadores
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// adição da barra de separadores à barra de aplicações
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// gestor de eventos da barra de separadores
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// foi selecionado um separador
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()) {
// posição da aba
int position = tab.getPosition();
// memória
session.setPreviousTab(position);
// exibição do fragmento associado?
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// classes filhas
protected abstract void navigateOnTabSelected(int position);
...
- linhas 12-48: adição e gestão de uma barra de separadores;
- linha 6: a barra de separadores é adicionada se a constante [ARE_TABS_NEEDED] estiver definida como vrai na interface [IMainActivity];
- linha 12: durante a criação da barra de separadores, podem ocorrer operações [Tablayout.Tab.select] implícitas (não são provocadas pelo utilizador). Define-se o valor booleano [session.navigationOnTabSelectionNeeded] como faux para evitar qualquer navegação durante estas seleções falsas. Caberá ao programador selecionar o fragmento a apresentar com o método [navigateToView]. A variável booleana [session.navigationOnTabSelectionNeeded] será reposto para vrai 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], à qual voltaremos mais tarde;
- linha 15: definimos as cores dos títulos dos separadores. 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 aba quando esta está selecionada;
- linha (d): a cor do título do separador quando este não está selecionado;
Este ficheiro é, naturalmente, editável. Os códigos hexadecimais das cores podem ser consultados, por exemplo, aqui.
- linhas 17-18: adição desta barra de separadores à barra de aplicações presente na vista XML [activity_main];
- linhas 20-47: gestor de eventos da barra de separadores;
- linhas 22-36: apenas o evento [onTabSelected] é gerido. Corresponde a um clique na aba [Tab tab] passada como parâmetro para o método ou a uma operação de software [TabLayout.Tab.select];
- linha 30: posição do separador selecionado;
- linha 32: esta posição é guardada durante a sessão;
- linha 34: trata-se agora de apresentar o fragmento associado a esta guia. Apenas a classe filha (linha 52) pode efetuar esta associação. Note-se que não se associa a barra de guias ao contentor de fragmentos [mViewPager], tal como foi feito em alguns exemplos analisados. Aqui, separa-se totalmente a gestão da barra de separadores da gestão dos fragmentos. É por isso que, quando se clica num separador, é necessário indicar qual a vista que se pretende ver apresentada;
- linha 28: distingue-se a seleção de separador com ou sem navegação. Em geral, quando o utilizador clica numa aba, pretende-se que haja navegação, enquanto que, numa seleção por software, não se pretende que haja. É o programador que distingue estes dois casos com o elemento [session.navigationOnTabSelectionNeeded]. Quando a navegação não é efetuada, o número da última aba selecionada não é registado na sessão. Caberá ao programador fazê-lo;
2.5.7.8. O gestor de separadores [CustomTabLayout]
![]() |
Utilizamos um gestor de separadores personalizado para poder apresentar o título dos separadores com 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 personalização do tipo de letra dos títulos das separadores é feita nas linhas 30 e 44;
O ficheiro [fonts] é o seguinte:
![]() |
Fontes:
- o código da classe [CustomTabLayout] foi encontrado em URL e [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
- as fontes foram encontradas em URL e [https://www.fontsquirrel.com/fonts/roboto];
2.5.7.9. Últimas inicializações
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// instanciação do gestor de fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// o contentor de fragmentos está associado ao gestor de fragmentos
// ou seja, o fragmento n.º i do contentor de fragmentos é o fragmento n.º i fornecido pelo gestor de fragmentos
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// inibe-se o deslize entre fragmentos
mViewPager.setSwipeEnabled(false);
// adjacência dos fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// é apresentada a primeira vista
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// passa-se o controlo para a atividade filha
onCreateActivity();
}
...
// classes filhas
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- linhas 10-19: aqui encontramos código frequentemente presente nos exemplos analisados;
- 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, para a primeira vista, o valor da ação que provoca a mudança de vista ser NONE;
- linha 22: não fazemos qualquer suposição sobre o primeiro fragmento a apresentar. Nos nossos exemplos, este foi frequentemente o fragmento n.º 0, mas nem sempre (ver Exemplo-22). Por isso, pediremos à atividade filha (linha 30) que nos indique qual é essa primeira vista;
- linha 25: aqui, fatorizámos tudo o que era possível. Agora, a classe filha tem as suas próprias inicializações a efetuar (linha 29);
2.5.7.10. Gestão da imagem de espera
Na classe [AbstractActivity], a imagem de espera é gerida pelos dois métodos seguintes:
// gestão da imagem de espera ---------------------------------
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 parágrafo 2.5.5) é implementada da seguinte forma:
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// camada [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 filha 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:
...
// o gestor de fragmentos --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// construtor
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// fragmentos da classe filha
fragments = getFragments();
}
// deve apresentar o fragmento na posição n.º
@Override
public AbstractFragment getItem(int position) {
// renderiza-se o fragmento
return fragments[position];
}
// indica o número de fragmentos a gerir
@Override
public int getCount() {
return fragments.length;
}
// apresenta o título do fragmento na posição n.º
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// classes filhas
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
...
}
- linha 5: o array dos fragmentos associados à atividade. Todos os fragmentos serão derivados da classe [AbstractFragment];
- linhas 8-12: trata-se do construtor que inicializa a matriz de fragmentos. Este solicita os fragmentos à classe filha da atividade (linha 35);
- linhas 28-31: os títulos dos fragmentos podem ser utilizados numa aplicação em que haja tantos separadores quantos os fragmentos. Neste caso, é possível atribuir ao separador o título do fragmento. Aqui, esses 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 um processo de gravação/restauração:
@Override
public void onResume() {
// pai
super.onResume();
if (IS_DEBUG_ENABLED) {
Log.d(className, "onResume");
}
// se for uma restauração, então é necessário restaurar o último separador selecionado
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 lembrar 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, neste caso, há seleção de um separador, mas não há mudança de fragmento;
2.5.7.14. Résumé
A classe abstrata [AbstractActivity] será a classe pai da única atividade da aplicação.
A atividade filha deverá implementar os seis métodos seguintes:
// classes filhas
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();
Além disso, a atividade filha tem acesso aos seguintes elementos protegidos da sua classe pai:
// a sessão
protected ISession session;
// o contentor dos fragmentos
protected MyPager mViewPager;
// barra de separadores
protected CustomTabLayout tabLayout;
// nome da classe
protected String className;
2.5.8. A atividade [MainActivity]
![]() |
A classe [MainActivity] pode ter um nome diferente. A sua única restrição é implementar a interface [IMainActivity]. A classe fornecida por predefinição é 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 {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// sessão
private Session session;
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sessão
this.session = (Session) super.session;
// tarefa pendente: continuamos as inicializações iniciadas pela classe pai
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// tarefa pendente: definir os fragmentos aqui
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// a fazer: definir aqui os títulos dos fragmentos
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// tarefa: navegação por separadores — definir a vista a apresentar
}
@Override
protected int getFirstView() {
// tarefa: navegação por separadores — definir a primeira vista a apresentar
return 0;
}
}
- linha 14: para que a notação AA [@Bean] da linha 19 seja compreendida, é necessário que a atividade tenha a notaçã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, caso seja 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 implica que o bean AA [Dao] deve existir. É sempre esse o caso com a aplicação-esqueleto que fornecemos. Mesmo numa aplicação sem a camada [DAO], pode-se deixar o pacote [dao] existir. Isso não acarreta complicações;
- linha 22: a sessão como instância do tipo [Session]. A sessão existe na classe pai [AbstractActivity], mas como instância da interface [ISession] (linha 32);
- linhas 24-63: os seis métodos impostos pela classe pai [AbstractActivity];
- linhas 36-39: o método [getDao] devolve uma referência à camada [DAO]. Aqui, essa referência nunca é null. No entanto, na classe pai [AbstractActivity], previu-se o caso em que a classe filha devolvesse uma referência null para indicar que não existia a camada [DAO]. Se se quiser utilizar esta possibilidade (que, na minha opinião, não é muito útil), é aqui que se deve atribuir o ponteiro null;
2.6. A camada [DAO]

![]() |
2.6.1. A interface IDao
Foi apresentada no parágrafo 2.5.5:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// URL do serviço web
void setUrlServiceWebJson(String url);
// utilizador
void setUser(String user, String mdp);
// tempo limite do cliente
void setTimeout(int timeout);
// autenticação básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
// tempo de espera do cliente, em milissegundos, antes da solicitação
void setDelay(int delay);
// tarefa pendente: declare aqui a sua interface
}
O programador irá adicionar os métodos da 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);
// tarefa pendente: declare aqui os URL a atingir
}
O programador irá adicionar os métodos que comunicam com o URL, expostos 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 {
// utilizador
private String user;
// palavra-passe
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// cabeçalhos HTTP da solicitação HTTP interceptada
HttpHeaders headers = request.getHeaders();
// o cabeçalho HTTP de autenticação básica
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
// adição aos cabeçalhos HTTP
headers.setAuthorization(auth);
// prossegue-se o ciclo de vida da solicitação HTTP
return execution.execute(request, body);
}
// elementos da autenticação
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
Esta classe gera o cabeçalho de autenticação HTTP a seguir:
onde [code] é o código Base64 da cadeia «user:mp». Esta classe só é utilizada se o servidor jSON esperar este tipo de autenticação. Existem outras.
Nota: a utilização desta classe é ilustrada no parágrafo 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 {
// mapeador jSON
private ObjectMapper mapper = new ObjectMapper();
// modo de depuração
protected boolean isDebugEnabled;
// nome da classe
protected String className;
// tempo de espera antes da execução da consulta
private int delay;
// construtor
public AbstractDao() {
// nome da classe
className = getClass().getName();
Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
}
// métodos protegidos ----------------------------------------------------------
// interface genérica
protected interface IRequest<T> {
T getResponse();
}
// solicitação genérica a um serviço web / jSON
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// registo
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("delay=%s", delay));
}
// execução do serviço — aguarda-se uma única resposta
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// execução do serviço
try {
// em espera?
if (delay > 0) {
Thread.sleep(delay);
}
// a execução da consulta síncrona está a decorrer
T response = request.getResponse();
// registo
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()));
}
// envia-se a resposta ao observador
subscriber.onNext(response);
// a indicação do fim do observável
subscriber.onCompleted();
} catch (InterruptedException | JsonProcessingException | RuntimeException e) {
// registo
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"));
}
}
// lança-se uma exceção
subscriber.onError(new DaoException(e, 100));
}
}
});
}
// modo de depuração
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 renderizar um tipo [Observable<T>]. Ao contrário de alguns exemplos vistos anteriormente, não se renderiza um tipo [Response<T>], que é um tipo proprietário, mas sim um tipo T qualquer;
- 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.getReponse()] obtém o tipo T através de uma operação HTTP síncrona;
- linhas 48-50: artificialmente, aguarda-se [delay] milissegundos. Em produção, utilizar-se-á [delay=0]. Na fase de depuração, utilizar-se-á [delay=qqs secondes] para dar ao utilizador a oportunidade de cancelar a operação assíncrona e, assim, ver como o código se comporta nessa situação;
- linha 52: a resposta esperada é solicitada através de uma requisição síncrona;
- linha 64: assim que a resposta é recebida, é passada para o observador;
- linha 66: indica-se que não haverá mais emissões. Estamos aqui no caso específico de uma ação assíncrona que devolve apenas um elemento;
- linhas 67-78: em caso de exceção, a exceção é emitida 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 {
// cliente do serviço web
@RestService
protected WebClient webClient;
// segurança
@Bean
protected MyAuthInterceptor authInterceptor;
// o RestTemplate
private RestTemplate restTemplate;
// fábrica do RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// registo
Log.d(className, "afterInject");
// construímos o restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// fixa-se o conversor jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// define-se o restTemplate do cliente web
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// define-se o URL do serviço web
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// regista-se o utilizador no 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));
}
// configuração de fábrica
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));
}
// interceptor de autenticação?
if (isBasicAuthentificationNeeded) {
// adiciona-se o interceptor de autenticação
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// métodos privados -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// tarefa pendente: implementação IDao
}
- linhas 21-22: injeção do bean AA [WebClient], que irá assegurar as comunicações com o servidor web / jSON;
- linhas 24-25: injeção do interceptor de autenticação;
- linhas 31-42: método executado após a inserção dos campos das linhas 21-25;
- linha 37: o objeto [RestTemplate], que assegura as trocas cliente/servidor, é criado a partir de um factory. Não é imprescindível, mas é através do factory que se podem configurar os tempos de espera das comunicações. É por isso que não utilizamos 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 cliente [WebClient] receber uma cadeia jSON do servidor, esta será automaticamente deserializada no objeto que o método deve devolver;
- linha 41: o objeto [RestTemplate] assim configurado é passado para o cliente web, que irá assegurar as trocas cliente/servidor através dele;
- linhas 44-48: define-se o URL como raiz do servidor web / jSON. Todas as URL declaradas na classe [WebClient] são URL relativas a esta URL raiz;
- linhas 50-54: este método permite especificar o proprietário da ligação quando esta é controlada por uma autorização de tipo básico (ver parágrafo 2.6.3);
- linhas 56-64: definem os timeouts das trocas cliente/servidor. Isto é feito através do factory do objeto [RestTemplate] que rege as trocas;
- linhas 66-78: este método permite indicar que o servidor é protegido por uma autenticação do tipo básico;
- linhas 72-77: se for solicitada uma autenticação do tipo básico, o interceptor de autenticação inserido na linha 25 é adicionado aos interceptores do objeto [RestTemplate]. Este interceptor irá adicionar automaticamente a todas as solicitações do cliente web a linha HTTP de autenticação básica esperada pelo servidor;
- o programador implementará a interface [IDao] a partir da linha 87;
2.7. Os 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 {
// identificador da opção do menu
private int menuItemId;
// visibilidade da opção
private boolean isVisible;
// construtores
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters e setters
...
}
2.7.2. A classe [Utils]
A classe [Utils] reúne métodos estáticos utilitários:
package client.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// lista de mensagens de uma exceção - versão 1
static public List<String> getMessagesFromException(Throwable ex) {
// cria-se uma lista com as mensagens de erro da pilha de exceções
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// lista de mensagens de uma exceção - versão 2
static public String getMessageForAlert(Throwable th) {
// construção do texto a apresentar
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--;
}
// resultado
return texte.toString();
}
// lista de mensagens de uma exceção - versão 3
static public String getMessageForAlert(List<String> messages) {
// construção do texto a apresentar
StringBuilder texte = new StringBuilder();
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// resultado
return texte.toString();
}
}
2.7.3. A classe pai [AbstractFragment]
A classe [AbstractFragment] reúne o que é comum a todos os fragmentos da aplicação. Tal como na classe [AbstractActivity], o seu código é complexo. Também aqui vamos analisá-lo por etapas.
2.7.3.1. A estrutura
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 {
// dados privados ------------------------------------------------------------
// as subscrições aos observáveis
private List<Subscription> abonnements = new ArrayList<>();
// menu do fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// ciclo de vida do fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// estado do fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida do fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// tarefas assíncronas
private boolean runningTasksHaveBeenCanceled;
// dados acessíveis às classes filhas ---------------------------------------
// modo de depuração
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// nome da classe
protected String className;
// tarefas assíncronas
protected int numberOfRunningTasks;
// atividade
protected IMainActivity mainActivity;
protected Activity activity;
// sessão
protected Session session;
// atualização do fragmento ----------------------------------------------------------------------------------
...
// gestão do menu ------------------------------------------
...
// gestão da espera -------------------------------------------------------------
...
// gestão de operações assíncronas --------------------------------------------------------------------
...
// gestão de exceções -------------------------------------------------------------------
....
// gestão do ciclo de vida do fragmento --------------------------------------------------------
...
// classes filhas -----------------------------------------------------
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: os dados protegidos acessíveis pelas classes filhas;
- linhas 61-62: código que atualiza o fragmento a ser exibido;
- linhas 64-65: código utilitário para gerir o eventual menu;
- 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 do fragmento com a camada [DAO];
- linhas 73-74: código utilitário para gerir qualquer exceção 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:
// nome da classe
protected String className;
// ciclo de vida do fragmento
private boolean fragmentHasToBeInitialized = false;
...
// construtor ----------------------
public AbstractFragment() {
// inicialização
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// registo
if (isDebugEnabled) {
Log.d(className, "constructeur");
}
}
- linha 9: regista-se o nome da classe filha que aqui é instanciada. Este nome é utilizado em todos os registos da classe pai;
- linha 10: regista-se 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 do menu
Na nossa arquitetura, todos os fragmentos devem ter um menu, mesmo que este esteja vazio. Os registos demonstraram, de facto, que quando o método [onCreateOptionsMenu] — executado quando o fragmento possui um menu — é executado, o fragmento já foi associado à sua atividade, à sua vista e ao seu menu, e está prestes a tornar-se visível. Trata-se, portanto, de um momento em que a atualização da interface visual e do menu pode ser efetuada. É neste método [onCreateOptionsMenu] que solicitamos ao fragmento filho que se atualize.
A gestão do menu reúne métodos utilitários que permitem ao fragmento filho exibir ou não elementos do menu:
// menu do fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// gestão do menu ------------------------------------------
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// percorre todos os itens do menu
for (int i = 0; i < menu.size(); i++) {
// item n.º i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// se o item n.º i for um submenu, então recomeça-se
if (menuItem.hasSubMenu()) {
// recursividade
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// resultado
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// recuperam-se os identificadores das opções do menu
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transferimos as opções do menu para uma matriz
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// identificador da opção
int id = menuOptionsIds.get(i);
// estado da opção
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// resultado
if (isDebugEnabled) {
Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
}
}
// estados das opções do menu
private MenuItemState[] getMenuOptionsStates() {
MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
for (int i = 0; i < menuOptionsStates.length; i++) {
// estado
MenuItemState state = this.menuOptionsStates[i];
// ID do menu
int id = state.getMenuItemId();
// inicialização do estado
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// resultado
return menuOptionsStates;
}
// exibição das opções do menu -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// atualizam-se todas as opções do menu
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// atualizam-se algumas opções do menu
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
- linhas 6-18: este método permite obter os identificadores numéricos de todas as opções do menu;
- linha 6: o método [getMenuOptions] recebe dois parâmetros:
- [Menu menu]: o menu do fragmento;
- [List<Integer> menuOptionsIds]: a lista de identificadores Android das opções do menu. Inicialmente, esta lista está vazia. É preenchida posteriormente através de uma percorrida recursiva (linha 15) da árvore do menu;
- linhas 20-40: a partir do menu, constrói a matriz de estados (identificador, visibilidade) das opções do menu. Esta matriz é armazenada na linha 3. A classe [MenuItemState] foi descrita no parágrafo 2.7.1;
- linhas 43-55: uma variante do método anterior. Faz o mesmo, mas em vez de recalcular os identificadores de todas as opções do menu — o que já foi feito —, utiliza os identificadores da tabela de estados da linha 3;
- linhas 58-63: o método [setAllMenuOptionsStates] permite ocultar ou mostrar todas as opções do menu do fragmento;
- linhas 65-69: o método [setMenuOptionsStates] permite, de forma seletiva, exibir ou ocultar algumas das opções do menu;
- os métodos [getMenuOptions, getMenuOptionsStates] são declarados privados, uma vez que são utilizados exclusivamente no [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 pelo fim de uma tarefa assíncrona
// as subscrições dos observáveis
private List<Subscription> abonnements = new ArrayList<>();
// tarefas assíncronas
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// gestão da espera pelo fim de uma operação assíncrona -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// regista-se o número de tarefas que serão executadas
this.numberOfRunningTasks = numberOfRunningTasks;
// coloca-se a imagem de espera
mainActivity.beginWaiting();
// esvazia-se a lista de subscrições
abonnements.clear();
// ainda não houve cancelamento
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// oculta-se a imagem de espera
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: o parâmetro do método é armazenado;
- linha 13: a imagem de espera é tornada visível;
- linha 15: limpa-se a lista de subscrições às operações assíncronas. Estas ainda não foram criadas pelo fragmento filho;
- linha 17: mantém-se um valor booleano para indicar que as tarefas assíncronas solicitadas pelo fragmento filho foram canceladas. Inicialmente, esse valor 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. Gestão de exceções
// gestão de exceções -------------------------------------------------------------------
// exibição de alerta sobre exceção
protected void showAlert(Throwable th) {
// exibindo as mensagens da pilha de exceções do Throwable th
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
}
// exibição da lista de mensagens
protected void showAlert(List<String> messages) {
// exibe a lista de mensagens
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 numa janela as mensagens da pilha de exceções do Throwable passado como parâmetro;
- linhas 10-13: o método [showAlert(List<String>] permite que um fragmento filho exiba numa janela a lista de mensagens passada como parâmetro;
- a classe [Utils] utilizada nas linhas 6 e 12 foi descrita no parágrafo 2.7.2;
2.7.3.6. Gestão de operações assíncronas
...
// subscrições de observáveis
private List<Subscription> abonnements = new ArrayList<>();
// tarefas assíncronas
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// execução de uma tarefa assíncrona com RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// processo: o observável a ser executado/observado
// consumeResult: o método que processa a resposta obtida
//
// só se criam novas subscrições se não tiver havido qualquer cancelamento
if (!runningTasksHaveBeenCanceled) {
// execução no thread de E/S e observação no thread da interface do utilizador
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// executa-se o observável
try {
abonnements.add(process.subscribe(
// consumo do resultado
consumeResult,
// consumo de exceção
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
consumeThrowable(th);
}
},
// fim da tarefa
new Action0() {
@Override
public void call() {
endOfTask();
}
}));
} catch (Throwable th) {
consumeThrowable(th);
}
}
}
private void endOfTask() {
...
}
// uma operação assíncrona lançou uma exceção
// ou ocorreu uma exceção durante a execução de uma operação assíncrona
private void consumeThrowable(Throwable th) {
...
}
- linhas 9-41: executam uma tarefa assíncrona;
- linha 9: o método [executeInBackground] espera dois parâmetros:
- [Observable<T> process]: o processo assíncrono a executar;
- [Action1<T> consumeResult]: o método do fragmento filho a ser chamado para lhe transmitir 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 ocorrido uma anulação pelo utilizador ou pelo programa (devido a uma exceção);
- linha 16: o processo está configurado para ser executado num thread de E/S e é observado no thread da interface do utilizador;
- linha 16: a instrução [process.subscribe] inicia a execução do processo na thread de E/S. Dentro desta thread, as operações são executadas de forma síncrona, porque utilizamos uma biblioteca HTTP que é síncrona;
- linha 19: o método [process.subscribe] tem três parâmetros:
- linha 21: [consumeResult]: o método do fragmento filho que irá 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 processamento é delegado ao método [consumeThrowable] da linha 49;
- linhas 29-36: o método executado quando a tarefa emite a notificação de fim de emissão. O processamento é delegado ao método [endOfTask] da linha 43;
- linha 19: a tarefa assíncrona que acaba de ser iniciada é registada no campo [abonnements], que regista todas as tarefas assíncronas iniciadas. Isto permitirá cancelá-las, se necessário;
- linhas 37-39: método executado quando ocorre uma exceção durante o processamento da tarefa assíncrona. O processamento é delegado ao método [consumeThrowable] da linha 49;
O método [endOfTask] é o seguinte:
// tarefas assíncronas
protected int numberOfRunningTasks;
...
private void endOfTask() {
// uma tarefa a menos para esperar
numberOfRunningTasks--;
// terminado?
if (numberOfRunningTasks == 0) {
// fim da espera
cancelWaitingTasks();
// a conclusão das tarefas é comunicada à classe filha
notifyEndOfTasks(false);
}
}
...
// classes filhas -----------------------------------------------------
...
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 já não houver tarefas ativas, então o fragmento filho obteve todas as suas respostas;
- linha 10: a espera é cancelada;
- linha 12: notifica-se o fragmento filho de que todas as tarefas que lançou estão concluídas, chamando o seu método [notifyEndOfTasks]. O parâmetro deste método indica como as tarefas terminaram: normalmente, por cancelamento do utilizador ou do código, ou devido a uma exceção. Na linha 12, é sinalizado um fim normal. Note-se que o fragmento filho não precisa de se preocupar em manter um registo das tarefas ainda ativas. A sua classe pai faz isso por ele;
O método [consumeThrowable] é o seguinte:
// tarefas assíncronas
protected int numberOfRunningTasks;
private boolean runningTasksHaveBeenCanceled;
...
// uma operação assíncrona gerou uma exceção
// ou ocorreu uma exceção durante a execução de uma operação assíncrona
private void consumeThrowable(Throwable th) {
// th: a exceção a tratar
//
// registo
if (isDebugEnabled) {
Log.d(className, "Exception reçue");
}
// cancelam-se as tarefas já iniciadas
cancelRunningTasks();
// são apresentadas as mensagens de erro
showAlert(th);
}
// cancelamento das tarefas
protected void cancelRunningTasks() {
// registo
if (isDebugEnabled) {
Log.d(className, "Annulation des tâches lancées");
}
// cancela-se todas as tarefas assíncronas registadas
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
// regista-se o cancelamento
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// fim da espera
cancelWaitingTasks();
// notifica-se o cancelamento das tarefas ao fragmento filho
notifyEndOfTasks(true);
}
...
// classes filhas -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- linha 3: o método [consumeThrowable] recebe a exceção que ocorreu;
- linha 15: todas as tarefas ainda ativas são canceladas;
- linha 17: é exibido o texto da exceção;
- linhas 21-37: cancelamento de todas as tarefas;
- linhas 27-29: todas as subscrições são canceladas;
- linha 31: regista-se que houve uma anulação;
- linha 32: o contador de tarefas é repostado a zero;
- linha 34: a espera é cancelada;
- linha 36: notifica-se o fragmento filho do fim das tarefas devido ao cancelamento;
2.7.3.7. Gestão do ciclo de vida do fragmento
// ciclo de vida --------------------------------------------------------
@Override
public void onDestroyView() {
// pai
super.onDestroyView();
// registo
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// pai
super.onDestroy();
// registo
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] existem apenas para fins de registo. Estes permitem ao programador compreender melhor o ciclo de vida dos fragmentos;
O armazenamento do fragmento durante uma rotação do dispositivo é realizado pelos seguintes métodos [setUserVisibleHint, onSaveInstanceState, saveState]:
// ciclo de vida do fragmento
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// pai
super.setUserVisibleHint(isVisibleToUser);
// guardar?
if (this.isVisibleToUser && !isVisibleToUser) {
// o fragmento vai ser ocultado — estamos a guardá-lo
if (!saveFragmentDone) {
saveState();
}
}
// memória
this.isVisibleToUser = isVisibleToUser;
}
private void saveState() {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// registo
if (isDebugEnabled) {
Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
}
// pai
super.onSaveInstanceState(outState);
// guardar o fragmento apenas se estiver visível
if (isVisibleToUser) {
// talvez o fragmento já tenha sido guardado
if (!saveFragmentDone) {
saveState();
}
// restauração a efetuar em todos os casos
session.setAction(ISession.Action.RESTORE);
}
}
- linhas 6-19: o fragmento é guardado se passar do estado «exibido» para o estado «oculto» (linha 11). É o método [setUserVisibleHint] que nos fornece esta informação;
- linha 14: o salvamento é efetuado pelo método privado das linhas 21-23;
- linhas 25-41: durante uma rotação do dispositivo, o método [onSaveInstanceState] será chamado. O fragmento é guardado sob duas condições:
- se estiver visível (linha 34);
- ainda não tenha sido guardado (linha 36). É possível que os métodos [setUserVisibleHint, onSaveInstanceState] não possam ser executados ambos quando o fragmento está visível e que, por isso, a gestão do valor booleano [saveFragmentDone] seja desnecessária. Em caso de dúvida, optei por utilizar este;
- linha 40: após o salvamento, seguirá-se a restauração. Note-se que, da próxima vez que o fragmento tiver de ser atualizado, tal deverá ser feito através de uma operação [RESTORE];
Deve-se tomar nota dos dois momentos em que é solicitada uma gravação do fragmento:
- quando este passa do estado visível para o estado oculto;
- quando ocorre uma rotação do dispositivo;
O método privado [saveState] é o seguinte:
...
private void saveState() {
// tarefas a cancelar?
if (numberOfRunningTasks != 0) {
// cancelam-se as tarefas
cancelRunningTasks();
}
// guardamos o estado do fragmento
CoreState currentState = saveFragment();
// o fragmento foi visitado
currentState.setHasBeenVisited(true);
// guardar o estado do menu
currentState.setMenuOptionsState(getMenuOptionsStates());
// início de sessão
session.setCoreState(getNumView(), currentState);
// gravação efetuada
saveFragmentDone = true;
// registo
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// classes filhas -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
- linhas 4-7: a rotação do dispositivo pode ocorrer enquanto estão a decorrer operações assíncronas. Nesta altura, decide-se cancelar todas elas. Esta não é uma boa decisão para o utilizador, que terá de voltar a efetuar um novo pedido, potencialmente demorado, quando apenas moveu o telemóvel ou o tablet, ou recebeu uma chamada telefónica. É possível manter as ligações de rede ao longo de um ciclo de cópia de segurança/restauração. No entanto, as soluções não são óbvias e decidi não as abordar neste curso para principiantes. O caminho a seguir é estabelecer essas ligações de rede através de um fragmento sem interface visual associada e que não seja destruído durante o ciclo de cópia de segurança/restauração. Para tal, basta utilizar a instrução [Fragment.setRetainInstance(true)];
- linha 9: solicita-se ao fragmento filho que guarde o seu estado num tipo derivado de [CoreState] (linha 31);
- linha 11: regista-se que o fragmento foi visitado. Esta informação é útil. Quando um fragmento é visitado pela primeira vez, a sua atualização pode ser diferente das seguintes, uma vez que, nessa altura, não possui um estado anterior na sessão;
- linha 13: guarda-se o estado do menu, o que nos permitirá restaurá-lo automaticamente;
- linha 15: este estado atual é guardado na sessão. Nesta, os estados são agrupados por vista/fragmento, tendo cada um deles um estado. O número da vista é fornecido pelo fragmento filho (linha 33);
- linha 17: verifica-se que o fragmento foi guardado. Isto porque dois métodos podem chamar o método [saveState] e não faz sentido efetuar dois guardamentos;
A regeneração da vista associada ao fragmento é assegurada pelo seguinte método:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// classe pai
super.onActivityCreated(savedInstanceState);
// registo
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// a vista deve ser restaurada
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 registar isso 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 ficar visível e ficar à espera das ações do utilizador. É assegurada pelo código seguinte:
// menu do fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida do fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// estados do fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida do fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// Atualização do fragmento ----------------------------------------------------------------------------------
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// registo
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// memória
this.menu = menu;
// recuperam-se as # opções do menu, caso ainda não tenha sido feito
if (fragmentHasToBeInitialized) {
// recuperam-se as # opções do menu
getMenuOptionsStates(menu);
// atividade
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = (Session) this.mainActivity.getSession();
}
// recupera-se o estado anterior do fragmento (na primeira vez, apenas o valor booleano hasBeenVisited tem significado)
previousState = session.getCoreState(getNumView());
// atualização do fragmento filho em várias etapas
// etapa 1 — trata-se da primeira visita?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// não é a primeira visita
// etapa 2: o fragmento deve ser inicializado?
...
// etapa 3: a vista deve ser inicializada?
...
}
// Etapa 4: um envio, uma navegação, uma restauração?
...
// Etapa 5: atualizações finais ----------------------
...
}
...
// classes filhas -----------------------------------------------------
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: é utilizado o método [onCreateOptionsMenu] para atualizar o fragmento. Por este motivo, o fragmento deve ter um menu, vazio se necessário. Quando este método é executado, o fragmento já foi associado à sua vista e à sua atividade e, além disso, está visível;
- linha 25: guarda-se o menu que foi passado como parâmetro (linha 22) ao método;
- linhas 27-34: se o fragmento tiver de ser inicializado:
- linha 29: os estados das opções do menu são colocados na tabela [menuOptionsStates] da linha 3;
- linha 31: a atividade é armazenada como uma instância do tipo Android [Activity];
- linha 32: a atividade é armazenada como uma instância da interface [IMainActivity];
- linha 33: a sessão é guardada. A alteração do tipo é necessária, pois o método [mainActivity.getSession()] devolve um tipo [ISession];
- linha 36: recupera-se da sessão o estado anterior do fragmento. Se for a primeira visita ao fragmento, apenas o valor booleano [previousState.hasBeenVisited] tem significado;
- linhas 39-44: código executado quando se trata da primeira visita ao fragmento. Neste caso, o seu estado anterior não é significativo;
- linhas 44-50: código executado quando não se trata da 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 de acordo com a ação (SUBMIT, NAVIGATION, RESTORE) em curso;
- linhas 54-55: código sempre executado;
As cinco etapas da atualização são as seguintes:
etapa 1
// menu do fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida do fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// estados do fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida do fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// recupera-se o estado anterior do fragmento (na primeira vez, apenas o valor booleano hasBeenVisited tem significado)
previousState = session.getCoreState(getNumView());
// atualização do fragmento filho em várias etapas
// etapa 1 — trata-se da primeira visita?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// inicialização do fragmento e da vista
initFragment(null);
initView(null);
// reinicialização de previousState para a continuação
previousState = null;
} else {
// não é a primeira visita
...
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: solicita-se à classe filha que inicialize o fragmento. O parâmetro do método [initFragment] da linha 35 é o estado anterior do fragmento. Aqui, passa-se null para indicar ao fragmento filha que se trata da primeira visita;
- linha 28: solicita-se à classe filha que inicialize a vista associada ao fragmento. O parâmetro do método [initView] da linha 37 é o estado anterior do fragmento. Aqui, passa-se null para indicar ao fragmento filha que se trata da primeira visita;
- linha 30: define-se o estado anterior como null para as etapas seguintes;
etapas 2 e 3
// menu do fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida do fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// estados do fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida do fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// recupera-se o estado anterior do fragmento (na primeira vez, apenas o valor booleano hasBeenVisited tem significado)
previousState = session.getCoreState(getNumView());
// atualização do fragmento filho em várias etapas
// etapa 1 — trata-se da primeira visita?
if (!previousState.getHasBeenVisited()) {
...
} else {
// não é a primeira visita
// etapa 2: o fragmento deve ser inicializado?
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation fragment");
}
// fragmento filho
initFragment(previousState);
}
// Etapa 3: a vista deve ser inicializada?
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation vue");
}
// fragmento filho
initView(previousState);
}
}
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- linhas 24-42: executadas quando não se trata da primeira visita ao fragmento;
- linhas 27-33: se o fragmento acabou de ser reconstruído, reinicializa-se chamando o método [initFragment] da classe filha (linhas 32, 46). Passa-se-lhe o estado anterior do fragmento;
- linhas 35-51: se a vista associada ao fragmento tiver de ser inicializada ou reinicializada, solicita-se ao fragmento filho que o faça (linhas 40, 48). Mais uma vez, passa-se-lhe o último estado conhecido do fragmento;
etapa 4
// menu do fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida do fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// Estados do fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida do fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// recupera-se o estado anterior do fragmento (na primeira vez, apenas o valor booleano hasBeenVisited tem significado)
previousState = session.getCoreState(getNumView());
// atualização do fragmento filho em várias etapas
...
// etapa 4: um envio, uma navegação, uma restauração?
// registo
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();
}
}
// ação em curso
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// fragmento filho
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// restauração do menu
setMenuOptionsStates(previousState.getMenuOptionsState());
// fragmento filho
updateOnRestore(previousState);
} else {
// trata-se de uma primeira visita – nada a fazer
}
break;
case RESTORE:
// restauração
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// restauração do menu (previousState não pode ser nulo)
setMenuOptionsStates(previousState.getMenuOptionsState());
// fragmento filho
updateOnRestore(previousState);
break;
}
....
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
- linhas 34-66: processa-se a ação em curso, que pode ser uma das três seguintes:
- RESTORE: está a ser efetuada uma restauração do fragmento após uma rotação do dispositivo;
- NAVIGATION: regressa-se ao fragmento com o objetivo de o encontrar no estado em que foi deixado da última vez que foi utilizado;
- SUBMIT: todos os outros casos;
- linha 34: recupera-se a ação em curso;
- linhas 36-42: para uma ação do tipo SUBMIT, chama-se 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: pretendemos restabelecer o fragmento no seu último estado conhecido. A operação NAVIGATION pode ocorrer em conjunto com uma primeira visita. Seria o caso, por exemplo, numa aplicação com separadores: se eu passar do separador 1 para o separador 4:
- tenho de inicializar o fragmento da aba 4, se for a primeira visita;
- restaurar o fragmento da aba 4 ao seu estado anterior, caso não se trate da primeira visita;
- linhas 52-54: não se faz nada se for a primeira visita. Caberá ao método filho [initView(CoreState previousState)] efetuar essa inicialização. A primeira visita é caracterizada pela condição [previousState==null];
- linha 49: se não for a primeira visita ao fragmento, restaura-se o seu menu;
- linha 51: solicita-se à classe filha que se atualize, chamando o método da linha 70. Passa-se-lhe o estado anterior do fragmento para que possa realizar o seu trabalho;
- linhas 56-66: no caso de uma operação de restauração do fragmento, faz-se o mesmo que no caso de uma navegação fora da primeira visita;
etapa 5
// menu do fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida do fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// estados do fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida do fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// etapa 5: atualizações finais ----------------------
// mudámos de perspetiva
session.setPreviousView(getNumView());
// não há mais ações em curso
session.setAction(ISession.Action.NONE);
// quando sairmos deste fragmento, este deverá ser guardado
saveFragmentDone = false;
// enquanto o fragmento não for reconstruído, não precisa de ser inicializado
fragmentHasToBeInitialized = false;
// enquanto a vista não for reconstruída, não precisa de ser inicializada
viewHasToBeInitialized = false;
// volta-se ao funcionamento normal da seleção de separadores
session.setNavigationOnTabSelectionNeeded(true);
// é comunicado ao fragmento que a vista está pronta
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- linhas 18-30: quando se chega aqui, o fragmento já foi inicializado e está pronto para ser apresentado. Repor-se, então, todos os indicadores utilizados na gestão do ciclo de vida do fragmento ao seu estado inicial;
- linha 20: mudámos de vista: registamos isso na sessão;
- linha 22: já não há nenhuma ação em curso;
- linha 24: quando se sair do fragmento agora exibido, será necessário guardá-lo ao sair;
- linha 26: o fragmento já não precisa de ser reconstruído. Este indicador será reposto para vrai quando o construtor do fragmento for novamente executado;
- linha 28: a vista associada ao fragmento já não precisa de ser inicializada. Este indicador será reposto para vrai quando o método [onActivityCreated] for executado novamente;
- linha 30: o fragmento é apresentado, possivelmente numa aplicação com separadores. Neste caso, quando o utilizador clicar num deles, deve ocorrer uma mudança de fragmento;
- linha 36: indica-se à classe filha que o fragmento está pronto. Esta pode incluir no método [notifyEndOfUpdates] as atualizações que devam ser feitas em todos os casos, iniciar uma operação assíncrona para obter novos dados, ...
2.7.4. Um exemplo de fragmento
![]() |
Incluímos no projeto [client-android-skel] um exemplo de fragmento para mostrar ao leitor a estrutura típica de um fragmento de uma 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 {
// campos herdados da classe pai -------------------------------------------------------
// modo de depuração
//-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// nome da classe
//-- String protegida className;
// tarefas assíncronas
//-- protegido int numberOfRunningTasks;
// atividade
//-- protegido IMainActivity mainActivity;
//-- atividade protegida;
// sessão
//-- protegido Session session;
// métodos herdados da classe pai -------------------------------------------------------
// exibição das opções do menu
//-- protegido void setAllMenuOptionsStates(boolean isVisible) {
//-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// gestão da espera pelo fim de uma série de tarefas assíncronas
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// execução de uma tarefa assíncrona com RxAndroid
//-- protegido <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// cancelamento de tarefas
//-- protegido void cancelRunningTasks() {
// exibição de alerta em caso de exceção
//-- protected void showAlert(Throwable th) {
// exibição da lista de mensagens
//-- protected void showAlert(List<String> mensagens) {
// métodos impostos pela classe pai -------------------------------------------------------
@Override
public CoreState saveFragment() {
// é necessário guardar o fragmento
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// senão houver nada para guardar, execute [return new CoreState();] e elimine a classe [DummyFragmentState]
}
@Override
protected int getNumView() {
// é necessário devolver o n.º do fragmento na tabela de fragmentos geridos pela atividade (ver MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// o fragmento torna-se visível e foi construído nesta etapa ou numa etapa anterior
// isto ocorre no arranque da aplicação e a cada rotação do dispositivo Android
// é necessariamente seguida pela execução de [initView]
// é necessário inicializar os campos do fragmento que foi reconstruído
// previousState é o último registo do fragmento — tem o valor null se for a primeira visita ao fragmento
}
@Override
protected void initView(CoreState previousState) {
// o fragmento torna-se visível e a vista associada foi reconstruída nesta etapa ou numa etapa anterior
// isto ocorre sempre que [initFragment] é executado e sempre que o fragmento sai da adjacência do fragmento exibido
// é necessário inicializar os componentes da vista que foi reconstruída
// previousState é o último registo do fragmento — tem o valor null se for a primeira visita ao fragmento
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// é executado após [initFragment, initView] se estes métodos forem executados
// a vista será apresentada após uma operação do tipo SUBMIT
// em geral, é necessário inicializar o fragmento e a vista associada a partir da sessão
// previousState é o último registo do fragmento — tem o valor null se for a primeira visita ao fragmento
// não há nada a fazer se não for possível aceder ao fragmento através de uma operação SUBMIT
// se for possível aceder ao fragmento através de operações SUBMIT a partir de fragmentos diferentes, é possível conhecer a vista anterior através de [session.getPreviousView]
// se for possível chegar ao fragmento através de várias operações SUBMIT a partir do mesmo fragmento, então é necessário ativar um indicador para diferenciar os diferentes tipos de SUBMIT a partir desse fragmento
}
@Override
protected void updateOnRestore(CoreState previousState) {
// é executada após [initFragment, initView], caso estes métodos sejam executados
// a vista será apresentada após uma operação do tipo RESTORE ou NAVIGATION
// previousState é o último registo do fragmento — nunca é nulo
// é necessário restaurar a vista ao seu estado anterior
}
@Override
protected void notifyEndOfUpdates() {
// ocorre após os métodos [updateOnSubmit, updateOnRestore]
// nesta altura, a vista já foi construída e inicializada
// muitas vezes não há nada a fazer aqui, mas também é possível agrupar aqui as ações que teriam de ser realizadas independentemente da forma como se acede a esta vista
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// chamada quando as tarefas assíncronas iniciadas pelo fragmento estão concluídas ou foram canceladas
// estes dois casos podem ser diferenciados graças ao parâmetro runningTasksHaveBeenCanceled
// em geral, é necessário restabelecer a vista num estado diferente daquele em que se encontrava enquanto aguardava as respostas das tarefas assíncronas
}
}
A classe [DummyFragment] pode não ter um estado. Aqui, incluímos um para recordar o que se espera que contenha:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class DummyFragmentState extends CoreState {
// estado do fragmento [DummyFragment]
// incluir apenas campos serializáveis em jSON
// colocar a anotação @JsonIgnore nos restantes, mas não se percebe bem para que poderiam servir
// não se esqueça dos getters/setters — são utilizados para a serialização/deserialização
}
Para ilustrar a utilização do projeto [client-android-skel], vamos começar por utilizar exemplos simples antes de passarmos a um estudo de caso mais completo.
2.8. Exercícios ilustrativos
Vamos começar por refatorar exemplos já escritos.
2.8.1. Exemplo-17B
Retomamos o exemplo 17 analisado no parágrafo 1.18. Trata-se de uma aplicação com um único fragmento, sem tarefas assíncronas e sem separadores. Analisamo-la para ver como se comporta durante uma rotação do dispositivo. Introduzimos os seguintes dados:

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

Se compararmos as visualizações, tudo foi mantido, exceto a lista [2], que agora está vazia.
Além disso, se clicarmos no botão [Valider], surge uma caixa de diálogo que mostra os dados introduzidos no formulário. Se, nesse momento, rodarmos o dispositivo, a caixa de diálogo desaparece.
Por isso, durante uma rotação, teremos de regenerar:
- a lista suspensa e o seu elemento selecionado;
- a caixa de diálogo, caso estivesse a ser apresentada no momento da rotação;
2.8.1.1. O projeto [Exemple-17B]
Duplicamos o projeto [client-android-skel] em exemplos/Exemplo-17B. Em seguida, carregamos o novo projeto [1]:
![]() | ![]() | ![]() |
- no [2-3], na pasta [behavior], colamos o fragmento [Vue1Fragment] do projeto [Exemple-17];
![]() | ![]() | ![]() |
- em [4-5], na pasta [layout] de [Exemple-17B], colamos a vista [vue1.xml] de [Exemple-17]. Esta é a vista associada ao fragmento;
- em [6], a pasta [values] de [Exemple-17B] é substituída pela pasta [values] de [Exemple-17];
A margem superior da vista [vue1.xml] será alterada 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 fase, pode-se tentar uma primeira compilação para verificar os erros. Os primeiros erros assinalados provêm do ficheiro imports, devido a pacotes que mudaram de localização. Corrigem-se esses erros (Ctrl-Shift-O). Outros erros devem-se ao facto de a vista [Vue1Fragment] não implementar todos os métodos exigidos pela sua classe pai [AbstractParent]:

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

Corrige-se isto no ficheiro [build.gradle] do módulo (linha 20 abaixo):
![]() |
Nesta fase, pode-se recompilar para verificar os erros restantes. O único erro assinalado diz respeito ao método [Vue1Fragment.updateFragment]:
![]() |
É necessário eliminar a anotação [@Override] da linha 135. Já não existem erros. Vamos partir daqui para modificar o projeto.
2.8.1.2. O estado do fragmento [Vue1Fragment]
O fragmento [Vue1Fragment] precisa de guardar informações durante a rotação do dispositivo, para que possa ser restaurado na íntegra. Para tal, criamos uma classe [Vue1FragmentState]:
![]() |
Por enquanto, esta classe 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
![]() |
Na pasta [custom] encontram-se os elementos de arquitetura personalizáveis pelo programador.
As constantes da 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 {
// acesso à sessão
ISession getSession();
// mudança de vista
void navigateToView(int position, ISession.Action action);
// gestão da espera
void beginWaiting();
void cancelWaiting();
// constantes da aplicação -------------------------------------
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
// tempo máximo de espera pela resposta do servidor
int TIMEOUT = 1000;
// tempo de espera antes da execução do pedido do cliente
int DELAY = 0;
// autenticação básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adjacência dos fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de separadores
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = false;
// número de fragmentos da aplicação
int FRAGMENTS_COUNT = 1;
}
- linhas 24-31: a aplicação não utiliza aqui a sua camada [DAO]. Estas constantes não serão utilizadas;
- linha 34: uma adjacência de fragmentos igual a 1, que é o valor por predefinição. Como a aplicação tem apenas um fragmento (linha 43), este valor não tem importância;
- linhas 39-40: como não há operações com a camada [DAO], é desnecessário ter uma imagem de espera;
- linha 37: não se trata de 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 {
// os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
}
Está vazia. Com efeito, como existe apenas um fragmento, não há necessidade de prever uma comunicação entre fragmentos com 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 {
// fragmento visitado ou não
protected boolean hasBeenVisited = false;
// estado do eventual menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
...
}
- linhas 11-13: temos de incluir todas as classes derivadas de [CoreState] que memorizam o estado dos diferentes fragmentos. Aqui, existe apenas uma (linha 12);
2.8.1.4. A atividade [MainActivity]
A atividade [MainActivity] apresenta-se atualmente 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 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 {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// sessão
private Session session;
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sessão
this.session = (Session) super.session;
// tarefa pendente: continuamos as inicializações iniciadas pela classe pai
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// tarefa pendente: definir os fragmentos aqui
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// a fazer: definir aqui os títulos dos fragmentos
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// tarefa pendente: navegação por separadores — definir a vista a apresentar quando o separador n.º [position] for selecionado
}
@Override
protected int getFirstView() {
// tarefa: definir o n.º da primeira vista (fragmento) a apresentar
return 0;
}
}
Os comentários [//todo] indicam o que o programador deve 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 {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// sessão
private Session session;
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sessão
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 das linhas 41-44 deve ser alterado. Este deve devolver o array dos fragmentos da aplicação. Na linha 43, não se deve esquecer de colocar 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 [Exemple-17], decide-se memorizar os seguintes elementos do fragmento:
- a lista de valores da lista suspensa;
- a posição do elemento selecionado nessa lista;
- a mensagem exibida pela caixa de diálogo, caso esta 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 {
// os valores da lista suspensa
private List<String> list;
// o elemento selecionado na lista suspensa
private int listSelectedPosition;
// a mensagem apresentada na caixa de diálogo
private String message;
// getters e setters
...
}
2.8.1.6. O fragmento [AbstractFragment]
Atualmente, o ciclo de vida do fragmento é gerido por dois métodos (linhas 6 e 32):
// lista suspensa
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// marca-se o primeiro botão
radioButton1.setChecked(true);
// o calendário
datePicker1.setCalendarViewShown(false);
// o 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));
}
});
// a lista suspensa
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
...
protected void updateFragment() {
// inicialização do adaptador da lista suspensa
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á migrado para os métodos definidos pela classe [AbstractFragment] da seguinte forma:
// gestão do ciclo de vida do fragmento ---------------------------------------------------------------------
@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) {
// 1.ª visita?
if (previousState == null) {
// criam-se os valores da lista suspensa
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// recuperação dos valores da lista suspensa
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// e a mensagem da caixa de diálogo
message = state.getMessage();
}
// inicialização do adaptador da lista suspensa
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) {
// o calendário
datePicker1.setCalendarViewShown(false);
// o 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));
}
});
// inicialização do adaptador da lista suspensa
dropDownList.setAdapter(dataAdapter);
// Primeira visita?
if (previousState == null) {
// marca-se o primeiro botão
radioButton1.setChecked(true);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// valor da barra de deslocamento
seekBarValue.setText(String.valueOf(seekBar.getProgress()));
// elemento selecionado na lista suspensa
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// caixa de diálogo visível?
if (message != null) {
// é apresentada
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 memorizar numa classe derivada de [CoreState] e devolver a instância desta;
- linhas 11-14: o método [getNumView] deve devolver o número do fragmento. Neste caso, 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 igual a null, então trata-se da primeira visita;
- linhas 19-25: na primeira visita, criam-se os valores da lista suspensa;
- linhas 26-30: se não se tratar da 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 da lista suspensa;
- linhas 37-62: o método [initView] serve para inicializar os componentes da interface visual. Recebe como parâmetro o estado anterior [previousState]. Se for [previousState==null], então trata-se da primeira visita;
- encontramos aqui o que existia anteriormente no método [@AfterViews];
- linhas 57-61: na primeira visita, verifica-se se o primeiro botão de opção está marcado;
- linhas 64-67: o método [updateOnSubmit] é executado quando a ação em curso é [SUBMIT]. Aqui, não há navegação entre fragmentos e, por isso, não há ação em curso;
- linhas 69-81: o método [updateOnRestore] é executado quando a ação em curso é [NAVIGATION] ou [RESTORE]. Aqui, não há navegação entre fragmentos e, por isso, não é possível realizar a ação [NAVIGATION];
- linha 72: recalcula-se (não se restaura) o valor de TextView seekBarValue. Com efeito, durante as rotações, por vezes perdia-se o seu valor;
- linhas 74-75: posiciona-se a lista no elemento que estava selecionado antes da rotação. Sem isso, a lista posicionava-se no seu primeiro elemento;
- linhas 76-80: volta a exibir-se a caixa de diálogo se a mensagem do estado anterior não for null. 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 em paz. 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 restauração da caixa de diálogo é feita da seguinte forma:
// a mensagem da caixa de diálogo
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// lista de mensagens a exibir
List<String> messages = new ArrayList<>();
...
// exibição
doAfficher(messages);
}
private void doAfficher(final List<String> messages) {
// construção do texto a exibir
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// armazenar a mensagem
message = texte.toString();
// exibe-se
showMessage();
}
private void showMessage() {
// exibe-se
new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// reinicialização da mensagem
message = null;
}
}).show();
}
Quando o utilizador valida o formulário, o método [doValider] (linha 5) cria uma lista de mensagens que, em seguida, exibe (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 mensagem que a caixa de diálogo exibe e é esta mesma mensagem que o método [updateOnRestore] faz exibir;
- linha 27: o segundo parâmetro do método [setNeutralButton] é o método executado quando o utilizador clica no botão [Fermer] da caixa de diálogo;
- linha 31: ao fechar a caixa de diálogo, a mensagem é devolvida a null para indicar que a caixa de diálogo já não está presente;
2.8.1.7. Tests
Convida-se o leitor a testar este projeto e a verificar se o fragmento é efetivamente mantido após uma ou várias rotações sucessivas.
2.8.2. Exemplo 23: cliente meteorológico
Alguns sites permitem obter informações meteorológicas sob a forma de cadeias de caracteres como jSON. Eis um exemplo:

O URL tem o seguinte formato: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID}, sendo que:
- city: a cidade cuja previsão meteorológica se pretende obter, neste caso Angers;
- country: o país da cidade, neste caso a França (fr);
- APPID: uma chave obtida ao registar-se no site [https://home.openweathermap.org/users/sign_up];
2.8.2.1. O projeto
![]() |
O projeto foi desenvolvido a partir do projeto [client-android-skel]. Apresenta as seguintes características:
- tem apenas um fragmento cujo estado não é necessário manter;
- efectua pedidos assíncronos;
2.8.2.2. 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 {
// acesso à sessão
ISession getSession();
// alteração da visualização
void navigateToView(int position, ISession.Action action);
// gestão da espera
void beginWaiting();
void cancelWaiting();
// constantes da aplicação -------------------------------------
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
// tempo máximo de espera pela resposta do servidor
int TIMEOUT = 1000;
// tempo de espera antes da execução do pedido do cliente
int DELAY = 5000;
// autenticação básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adjacência dos fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de separadores
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = true;
// número de fragmentos da aplicação
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 dos fragmentos. Neste caso, esta constante não tem importância, uma vez que existe apenas um fragmento;
- linha 37: não se trata de 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)
// tarefa: adicionar aqui as subclasses de [CoreState]
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragmento visitado ou não
protected boolean hasBeenVisited = false;
// estado do eventual menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
...
}
- linhas 10-13: não há nada a declarar, uma vez que nesta aplicação existe 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 {
// os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
}
Está vazia, uma vez que, nesta aplicação, não existe comunicação entre fragmentos.
2.8.2.3. A camada [DAO]
![]() |
Na camada [DAO], três classes devem ser personalizadas:
- a interface IDao;
- a sua implementação Dao;
- a interface WebClient de 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);
// serviço meteorológico
@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. Recorde-se que esta está relacionada com o URL raiz (RestClientRootUrl, linha 12) do cliente. Aqui, este URL raiz será o [http://api.openweathermap.org/];
A interface [IDao] será a seguinte:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// URL do serviço web
void setUrlServiceWebJson(String url);
// utilizador
void setUser(String user, String mdp);
// tempo limite do cliente
void setTimeout(int timeout);
// autenticação básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
// tempo de espera do cliente, em milissegundos, antes da solicitação
void setDelay(int delay);
// serviço meteorológico
Observable<String> getWeatherForecast(String city, String country, String APPID);
}
- Recorde-se que os métodos das linhas 6-22 estão presentes por predefinição na interface IDao do projeto [client-android-skel];
- linha 25: o método [getWeatherForecast] permite obter a cadeia jSON relativa à previsão meteorológica da cidade [city] do país [country]. O terceiro parâmetro é a chave obtida no 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 {
// cliente do serviço web
@RestService
protected WebClient webClient;
// segurança
@Bean
protected MyAuthInterceptor authInterceptor;
// o RestTemplate
private RestTemplate restTemplate;
// fábrica do RestTemplate
private SimpleClientHttpRequestFactory factory;
// tempo limite
private int timeout;
@AfterInject
public void afterInject() {
// registo
Log.d(className, "afterInject");
// constrói-se o restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// define-se o conversor jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// define-se o restTemplate do cliente web
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// define-se o URL do serviço web
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// regista-se o utilizador no 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));
}
// memória
this.timeout = timeout;
// fábrica de configuração
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));
}
// interceptor de autenticação?
if (isBasicAuthentificationNeeded) {
// adiciona-se o interceptor de autenticação
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// métodos privados -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// serviço meteorológico ---------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
// registo
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));
}
// resultado
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- Recorde-se que as linhas 17-90 estão presentes por predefinição na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos de implementação da interface [IDao], específicos da aplicação (linha 92);
- linhas 93-105: implementação do método [getWeatherForecast]. Esta é muito simples e é realizada em 6 linhas, linhas 100-105;
- linha 100: o método [getResponse] é um método da classe pai [AbstractDao]. Este método espera um parâmetro do tipo [IRequest<T>], em que T é o tipo da resposta esperada do servidor; neste caso, um String, uma vez que se espera uma cadeia de caracteres 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. Este tem como função fornecer a resposta de tipo T que o método [Observable<T> getWeatherForecast] deve devolver;
- linha 103: é a interface [WebClient] que fornece esta resposta. São-lhe passados os três parâmetros recebidos na linha 94. Por este motivo, estes devem ter o atributo final;
2.8.2.4. A atividade [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 {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// registo
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);
}
}
- Recorde-se que as linhas 15-55 estão presentes por predefinição no projeto [client-android-skel]. Basta personalizá-las;
- linhas 37 a 40: a tabela de fragmentos. Aqui existe apenas um;
- 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 apresentar é a vista n.º 0, a de [MeteoFragment];
- linhas 58-61: implementação da interface [IDao]. Aqui, não há nada a fazer além de delegar o trabalho à camada [DAO] da linha 21;
2.8.2.5. O fragmento [MeteoFragment]
![]() |
O fragmento [MeteoFragment] consulta o serviço web / jSON de meteorologia. 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 apresenta 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 serve para consultar a previsão meteorológica de uma cidade;
- linhas 14-15: esta opção do menu serve para cancelar este pedido, caso esteja em curso;
- linhas 16-18: esta opção do menu encerra a aplicação;
O código completo do 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 {
// dados locais
private int nbReponsesRecues;
// gestão de eventos ---------------------------------------------------------------------------------------
// cidades cuja previsão meteorológica se pretende obter
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
@OptionsItem(R.id.actionMeteo)
protected void doMeteo() {
// o seu país
String country = "fr";
// obtenha um identificador API ao criar uma conta [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// URL do serviço web / jSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// início da espera pelas tarefas assíncronas [paysDeLoire.length]
beginWaiting(paysDeLoire.length);
// número de respostas recebidas
nbReponsesRecues = 0;
// as chamadas assíncronas são efetuadas em paralelo
for (String city : paysDeLoire) {
// previsão meteorológica
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// análise da resposta
consumeResponse(response);
// uma resposta positiva
nbReponsesRecues++;
}
});
}
}
// análise da resposta do servidor
private void consumeResponse(String response) {
// registo
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// início da espera
protected void beginWaiting(int numberOfRunningTasks) {
// registo
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// pai
beginRunningTasks(numberOfRunningTasks);
// é apresentada a opção [Annuler]
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{
new MenuItemState(R.id.menuActions, true),
new MenuItemState(R.id.actionAnnuler, true)});
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// exibição de resultados
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();
}
// métodos privados -----------------------------------
private void initMenu() {
if (isDebugEnabled) {
Log.d(className, "initMenu");
}
// menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
// gestão do ciclo de vida ---------------------------------------------------------------------------------------
...
}
- linhas 25-50: gestão do clique na opção de menu [Météo];
- linha 32: construção do URL do serviço web / jSON do serviço meteorológico. Este é depois passado para a camada [DAO] através da atividade;
- linha 34: inicia-se a espera. Passa-se o número de tarefas que vão ser lançadas, para que a classe pai nos possa sinalizar o fim das mesmas. Aqui, há cinco tarefas, pois vamos solicitar a previsão meteorológica das cinco cidades da linha 23;
- linha 16: vamos contar o número de respostas recebidas para poder exibi-lo;
- linhas 38-50: percorremos as cidades cuja previsão meteorológica pretendemos obter;
- linha 40: vamos efetuar 5 pedidos HTTP em paralelo;
- linha 40: solicitamos à classe pai [AbstractParent] que interroga o serviço web /jSON;
- linhas 40-48: o método [executeInBackground] espera dois parâmetros:
- linha 40: o processo a observar e a executar é fornecido pelo método [mainActivity.getWeatherForecast];
- linhas 40-48: a instância [Action1] a ser executada quando se receber a resposta do serviço assíncrono. 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] da linha 53;
- linha 46: incrementa-se o contador de respostas recebidas;
- linhas 53-56: utilização de uma resposta jSON do serviço meteorológico;
- linha 55: limita-se a registar a cadeia jSON;
- linhas 59-72: código executado antes do lançamento das tarefas assíncronas;
- linha 65: passa-se o número de tarefas a executar para a classe pai [AbstractParent]. É isto que permite que esta nos avise quando todas estiverem concluídas;
- linhas 67-70: preparação do menu para um período de espera. Mantém-se apenas a opção [Actions/Annuler], que permitirá ao utilizador cancelar as tarefas iniciadas;
- linhas 74-92: código executado quando a classe pai nos avisa que todas as tarefas iniciadas estão concluídas;
- linha 77: repõe-se o menu no seu estado inicial. O método [initMenu] (linhas 95-102) apresenta o menu com todas as suas opções, exceto a opção [Actions/Annuler], que fica oculta;
- linhas 80-91: exibe-se o número de respostas recebidas;
O clique na opção de menu [Annuler] é gerido pelo código seguinte:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancelar tarefas assíncronas
cancelRunningTasks();
}
- linha 7: solicita-se à classe pai que cancele as tarefas ainda ativas;
O clique na opção de menu [Terminer] é gerido pelo código seguinte:
@OptionsItem(R.id.actionTerminer)
protected void doTerminer() {
// parar tudo
System.exit(0);
}
A gestão do ciclo de vida do fragmento é assegurada pelos seguintes métodos:
// gestão do ciclo de vida ---------------------------------------------------------------------------------------
@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) {
// 1.ª visita?
if (previousState == null) {
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
- linhas 3-6: servem para memorizar o estado do fragmento numa classe derivada de [CoreState]. Se o fragmento não tiver um estado a memorizar, como neste caso, basta devolver uma instância de [CoreState]. Não se deve devolver null, pois isso provocaria posteriormente uma falha do sistema;
- linhas 8-11: devem devolver o número da vista. Aqui, o fragmento [MeteoFragment] tem o número 0;
- linhas 13-16: servem para inicializar o fragmento depois de este ter sido construído (previousState==null) ou reconstruído (previousState!=null). Aqui, não há nada a fazer. O único campo passível de inicialização é o seguinte:
// cidades cuja previsão meteorológica se pretende obter
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
mas este inicializa-se automaticamente;
- linhas 18-24: servem 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 for a primeira visita ao fragmento, inicializa-se o seu menu para ocultar a opção [Annuler];
- linhas 27-30: chamadas se, para chegar ao fragmento, tiver havido navegação com uma ação do tipo [SUBMIT]. Aqui, não há navegação entre fragmentos, uma vez que existe apenas um fragmento;
- linhas 32-35: chamadas durante um ciclo de gravação/restauração devido a uma rotação do dispositivo ou a outro motivo. Aqui, como não foi gravado nenhum estado, não há nada a fazer;
- linhas 37-40: chamadas quando todas as atualizações anteriores tiverem sido efetuadas. Aqui, não há nada a fazer;
2.8.2.6. Tests
Vamos agora executar o exemplo:


Os registos são entã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 no thread da interface do utilizador;
Agora, fazemos a consulta com um identificador 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. Irá, então, cancelar todas as tarefas;
Agora, vamos definir um tempo de espera de 5 segundos para [IMainActivity.DELAY] e cancelar a operação. Os registos ficam então assim:
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: a anulação das tarefas provoca uma exceção em cada uma das threads das cinco tarefas. 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)], que as faz esperar artificialmente [delay] milissegundos;
2.8.3. Exemplo 16B
Reestruturamos aqui o exemplo 16 do parágrafo 1.17. Este apresenta um fragmento que efetua chamadas assíncronas a um servidor de números aleatórios. Vejamos como se comporta durante uma rotação do dispositivo:

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

Vemos 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 [exemples/Exemple-16B] e, em seguida, carregamos o novo projeto:
![]() |
Do projeto inicial [Exemple-16], copiamos para o [Exemple-16B] os seguintes elementos:
- o ficheiro [res/layout/vue1.xml], a pasta [res/values]:
![]() |
Alteraremos 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 [Vue1Fragment]:
![]() |
- a classe [dao / service / Response]:
![]() |
Nesta fase, podemos tentar uma primeira compilação:
- um primeiro tipo de erros é o das classes imports. Algumas classes mudaram de pacote na migração para [Exemple-16B]. Começamos por corrigir este tipo de erros;
- um segundo tipo de erro é sinalizado na classe [Vue1Fragment] porque esta não implementa os métodos exigidos pela classe pai [AbstractParent]. Procedemos à geração automática desses métodos;
Tentamos uma segunda compilação:
- todos os erros restantes estão agora concentrados na classe [Vue1Fragment], a classe que irá sofrer mais alterações;
2.8.3.2. Criação de um estado para o fragmento [Vue1Fragment]
Vimos que algumas informações do fragmento teriam de ser guardadas durante uma rotação, a fim de restaurar o fragmento tal como estava antes da rotação. Por isso, criamos um estado [Vue1FragmentState], 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 {
// acesso à sessão
ISession getSession();
// alteração da visualização
void navigateToView(int position, ISession.Action action);
// gestão da espera
void beginWaiting();
void cancelWaiting();
// constantes da aplicação -------------------------------------
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
// tempo máximo de espera pela resposta do servidor
int TIMEOUT = 1000;
// tempo de espera antes da execução do pedido do cliente
int DELAY = 5000;
// autenticação básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adjacência dos fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de separadores
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = true;
// número de fragmentos da aplicação
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 dos fragmentos. Neste caso, esta constante não tem importância, uma vez que existe apenas um fragmento;
- linha 37: não se trata de 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 {
// fragmento visitado ou não
protected boolean hasBeenVisited = false;
// estado do eventual menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
...
}
- linha 12: declaramos a classe do 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 {
// os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
}
Está vazia, uma vez que, nesta aplicação, não existe comunicação entre fragmentos.
2.8.3.4. A camada [DAO]
![]() |
Na camada [DAO], três classes devem ser personalizadas:
- a interface IDao;
- a sua implementação Dao;
- a interface WebClient de comunicação com o servidor web / jSON;
A classe [Response] provém do projeto [Exemple-16], que a utiliza:
package client.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- propriedades
// estado da operação
private int status;
// eventuais mensagens de erro
private List<String> messages;
// o corpo da resposta
private T body;
// construtores
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters e 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 número aleatório no intervalo [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- linhas 18-19: o URL do serviço de números aleatórios. Recorde-se que esta está relacionada com o URL raiz (RestClientRootUrl, linha 12) do cliente. Aqui, este URL raiz será [http://localhost:8080];
A interface [IDao] será a seguinte:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// URL do serviço web
void setUrlServiceWebJson(String url);
// utilizador
void setUser(String user, String mdp);
// tempo limite do cliente
void setTimeout(int timeout);
// autenticação básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
// tempo de espera do cliente, em milissegundos, antes da solicitação
void setDelay(int delay);
// serviço de números aleatórios
Observable<Response<Integer>> getAlea(int a, int b);
}
- Recorde-se que os métodos das linhas 6-22 estão presentes por predefinição na interface IDao do projeto [client-android-skel];
- linha 25: o método [getAlea] permite obter um número aleatório no intervalo [a,b]. Este número é obtido numa resposta do tipo [Response<Integer>], em que o número aleatório se encontra 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 {
// cliente do serviço web
@RestService
protected WebClient webClient;
// segurança
@Bean
protected MyAuthInterceptor authInterceptor;
// o RestTemplate
private RestTemplate restTemplate;
// fábrica do RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// registo
Log.d(className, "afterInject");
// fabrica-se o restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// fixa-se o conversor jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// define-se o restTemplate do cliente web
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// define-se o URL do serviço web
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// regista-se o utilizador no 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));
}
// configuração de fábrica
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));
}
// interceptor de autenticação?
if (isBasicAuthentificationNeeded) {
// adiciona-se o interceptor de autenticação
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// métodos privados -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// serviço de números aleatórios
@Override
public Observable<Response<Integer>> getAlea(final int a, final int b) {
// execução do cliente web
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getAlea(a, b);
}
});
}
}
- Recorde-se que as linhas 17-85 estão presentes por predefinição na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos de implementação da interface [IDao];
- linhas 88-97: implementação do método [getAlea]. Esta é muito simples e é realizada em 6 linhas, linhas 91-96;
- linha 91: o método [getResponse] é um método da classe pai [AbstractDao]. Este método espera um parâmetro do tipo [IRequest<T>], em que 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. Este tem como 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 atividade [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 {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continuamos as inicializações iniciadas pela classe pai
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// definir os fragmentos aqui
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
// definir aqui os títulos dos fragmentos
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// navegação por separadores - definir a vista a apresentar
}
@Override
protected int getFirstView() {
return 0;
}
// interface IDao ------------------------------------------
@Override
public Observable<Response<Integer>> getAlea(int a, int b) {
return dao.getAlea(a, b);
}
}
- Recorde-se que as linhas 15-61 estão presentes por predefinição no projeto [client-android-skel]. Basta personalizá-las;
- linhas 40-44: a tabela 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 apresentar é 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] da 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 {
// estado do fragmento ------------------------
// lista de respostas
private List<String> reponses = new ArrayList<>();
// estado da vista ------------------------
// mensagem de erro relativa ao número de números aleatórios solicitados
private boolean txtErrorAleasVisible = false;
// mensagem de erro sobre o intervalo de geração [a,b]
private boolean txtErrorIntervalleVisible = false;
// mensagem de erro relativa ao URL do serviço web
private boolean txtMsgErreurUrlServiceWebVisible = false;
// mensagem de erro relativa ao tempo de espera
private boolean textViewErreurDelayVisible = false;
// estado visível ou não do botão Executar
private boolean btnExecuterVisible = true;
// getters e setters
...
}
Para determinar o que era necessário memorizar no fragmento, realizámos rotações do dispositivo em várias situações e observámos o que desaparecia durante a restauração. Chegámos à conclusão de que era necessário memorizar as informações das linhas 10 a 23.
2.8.3.7. O fragmento [Vue1Fragment]
![]() |
Atualmente, a vista [Vue1Fragment] apresenta vários erros devido ao facto de a classe pai [AbstractFragment], da qual deriva, ter sido alterada. Em vez de descrever uma a uma as alterações a efetuar, vamos 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 {
...
}
- na linha 26, recorde-se que qualquer fragmento deve ter um menu, mesmo que esteja vazio. É o que acontece aqui.
2.8.3.7.1. Gestão do clique no botão [Exécuter]
@Click(R.id.btn_Executer)
protected void doExecuter() {
// verifica-se os dados introduzidos
if (!isPageValid()) {
return;
}
// limpa-se as respostas anteriores
reponses.clear();
dataAdapterReponses.notifyDataSetChanged();
// zerar o contador de respostas
nbReponses = 0;
infoReponses.setText("Liste des réponses (0)");
// inicialização da atividade
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// prepara-se a tarefa aleatória
beginWaiting(1);
// solicitam-se os números aleatórios
getAleasInBackground(nbAleas, a, b);
}
void getAleasInBackground(int nbAleas, int a, int b) {
// cria-se o processo a observar
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getAlea(a, b));
}
// solicitação de números aleatórios
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// a resposta é processada
consumeAleaResponse(response);
}
});
}
protected void consumeAleaResponse(Response<Integer> response) {
// registo
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// uma resposta de +
nbReponses++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
// analisamos a resposta
// erro?
if (response.getStatus() != 0) {
// exibição
showAlert(response.getMessages());
// cancelamento
doAnnuler();
// regresso à interface do utilizador
return;
}
// adiciona-se a informação à lista de respostas
reponses.add(0, String.valueOf(response.getBody()));
// atualizar as respostas
dataAdapterReponses.notifyDataSetChanged();
}
// cancelamento ----------
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancelam-se as tarefas assíncronas
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// ativa-se a ampulheta
beginRunningTasks(nbRunningTasks);
// o botão [Annuler] substitui o botão [Exécuter]
btnExecuter.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
- linhas 4-6: verifica-se primeiro se os dados introduzidos são válidos. Podem então surgir mensagens de erro;
- linhas 8-9: a lista de respostas é esvaziada. Esta alteração é refletida no ListView, que as apresenta;
- linhas 11-12: o número de respostas recebidas é zerado;
- linha 14: define-se o URL do serviço de números aleatórios. Esta informação será transmitida à camada [DAO];
- linha 15: define-se o tempo de espera antes de efetuar o pedido ao serviço de números aleatórios. Esta informação será transmitida à camada [DAO];
- linha 17: prepara-se o lançamento de 1 tarefa assíncrona (e não N, veremos porquê);
- linhas 24-27: das N tarefas assíncronas, transformamos cada uma numa sequência de operações [merge];
- linhas 29-36: solicita-se à classe pai [AbstractParent] que interroga o serviço web / jSON para obter números aleatórios;
- linhas 29-36: o método [executeInBackground] espera dois parâmetros:
- linha 29: o processo a observar e a executar é aquele que foi calculado nas linhas anteriores;
- linhas 29-36: a instância [Action1] a ser executada quando se receber a resposta do serviço assíncrono. 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 é processada no método da linha 39;
- linhas 49-50: regista-se e sinaliza-se que foi recebida uma nova resposta;
- 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, significa que o servidor encontrou um problema;
- linha 55: é exibida uma mensagem de erro. O método [showAlert] pertence à classe pai;
- linha 57: é chamado o método das linhas 68-75. Este irá cancelar as tarefas ainda ativas (linha 74);
- linha 62: a resposta é adicionada à lista de respostas, que constitui a fonte de dados do 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 comunica à classe pai que as tarefas do [nbRunningTasks] serão executadas em breve (linha 79);
2.8.3.7.2. O ciclo de vida do fragmento
O ciclo de vida do fragmento é assegurado pelos seguintes métodos:
// dados locais
private List<String> reponses;
private ArrayAdapter<String> dataAdapterReponses;
private int nbReponses = 0;
...
// gestão do ciclo de vida ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// estado atual da vista
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) {
// 1.ª visita?
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
reponses = state.getReponses();
} else {
reponses = new ArrayList<>();
}
// fonte de dados do listView
dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
// N.º de respostas
nbReponses = reponses.size();
}
@Override
protected void initView(CoreState previousState) {
// ligação entre a lista de visualização e o adaptador
listReponses.setAdapter(dataAdapterReponses);
// Primeira visita?
if (previousState == null) {
// ocultar mensagens de erro
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// os botões
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// estado anterior da vista
Vue1FragmentState state = (Vue1FragmentState) previousState;
// mostrar/ocultar mensagens de erro
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);
// botões
btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
// número de respostas
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// o botão [Exécuter] substitui o botão [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
- linhas 7-18: garantem o armazenamento do fragmento quando a classe pai o solicita;
- linha 11: exibe a mensagem de erro relativa ao tempo de espera;
- 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: visibilidade da mensagem de erro relativa ao intervalo [a,b] de geração de números aleatórios;
- linha 15: visibilidade do botão [Exécuter];
- linha 16: a lista de respostas recebidas;
- linhas 20-23: devem indicar o número da vista. O número do fragmento é aqui 0, uma vez que só existe um;
- linhas 25-38: inicialização dos campos do fragmento, quer numa primeira visita (previousState==null), quer numa visita posterior;
- linhas 29-30: se não for a primeira visita, o campo [reponses] é restaurado a partir do estado anterior do fragmento;
- linhas 31-33: se for a primeira visita, o campo [reponses] é inicializado com uma lista vazia;
- linhas 34-37: a partir do campo [reponses], é possível construir a fonte de dados do 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, quer numa primeira visita (previousState==null), quer numa visita posterior;
- linha 43: associa-se o ListView do fragmento à fonte de dados que acabou de ser criada no método [initFragment];
- linhas 45-54: se for a primeira visita, prepara-se a vista para a sua primeira exibição;
- linhas 57-60: executadas durante uma navegação entre fragmentos associada a uma ação do tipo [SUBMIT]. Neste caso, existe apenas um fragmento e, por isso, não há navegação entre fragmentos;
- linhas 63-76: executadas durante uma navegação entre fragmentos associada a uma ação do tipo [NAVIGATION] ou durante um ciclo de gravação/restauração devido a uma rotação do dispositivo ou a outro motivo. Aqui, apenas este último caso pode ocorrer. É importante lembrar que, neste contexto, em todos os casos, [previousState] é sempre diferente de null;
- linha 65: converte-se o estado anterior para o tipo do estado do fragmento;
- linhas 66-75: utiliza-se o conteúdo do estado anterior para restaurar a vista;
- linhas 78-81: chamadas quando todas as atualizações anteriores tiverem sido efetuadas. Aqui, não há nada a fazer;
- linhas 83-89: executadas quando todas as tarefas assíncronas estiverem concluídas. Aqui, oculta-se o botão [Annuler] para o substituir pelo botão [Exécuter];
2.8.3.8. Os testes
O leitor é convidado a realizar os seguintes testes:
- criar erros e colocar o dispositivo em funcionamento: as mensagens de erro devem permanecer visíveis;
- obter números aleatórios e executar o dispositivo: os números aleatórios obtidos 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 é visível nos registos);
2.8.4. Exemplo-22B
Retomamos aqui o exemplo 22 para o refatorar de acordo com o modelo do projeto [client-android-skel]. Recorde-se que o projeto [Exemple-22] gere corretamente o ciclo de gravação/restauração dos fragmentos durante uma rotação e que foi este que serviu de base ao projeto [client-android-skel].
Duplicamos o projeto [client-android-skel] para [exemples/Exemple-22B] e carregamos este último projeto:
![]() |
Em seguida, copiamos vários elementos do projeto [Exemple-22] para o projeto [Exemple-22B].
Em primeiro lugar, copiamos elementos da pasta [res]:
- [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, a pasta [values];
![]() |
Alteraremos a margem superior das duas vistas para 120 dp:
[vue1.xml]:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="120dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
[fragment_main]:
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>
Em seguida, copiamos os elementos [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
Nesta fase, podemos tentar uma primeira compilação. Surge um primeiro tipo de erros: os imports incorretos, porque as classes mudaram de pacote. Corrigimos esses imports. Um segundo tipo de erros deve-se ao facto de os fragmentos não implementarem todos os métodos da sua classe pai [AbstractFragment]. Corrige-se premindo (Alt+Enter).
Os erros restantes resultam das diferenças existentes entre a classe antiga e a nova [AbstractFragment]. Por enquanto, ignoramo-los.
2.8.4.1. Personalização do projeto
![]() |
Na pasta [custom] encontram-se os elementos de arquitetura personalizáveis 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 {
// acesso à sessão
ISession getSession();
// mudança de visualização
void navigateToView(int position, ISession.Action action);
// gestão da espera
void beginWaiting();
void cancelWaiting();
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
// tempo máximo de espera pela resposta do servidor
int TIMEOUT = 1000;
// tempo de espera antes da execução do pedido do cliente
int DELAY = 0;
// autenticação básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adjacência dos fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de separadores
boolean ARE_TABS_NEEDED = true;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = false;
// número de fragmentos
int FRAGMENTS_COUNT = 5;
}
- linhas 23, 26, 29, 38: características da camada [DAO]. Aqui não existem;
- linha 41: existem aqui cinco fragmentos;
- linha 32: adjacência dos fragmentos. Esta constante pode assumir aqui um valor em [1,4]. Recomenda-se ao leitor que altere este valor para verificar se a aplicação continua a funcionar;
- linha 35: trata-se de 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 {
// fragmento visitado ou não
protected boolean hasBeenVisited = false;
// estado do eventual menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
...
}
- linha 12: declaramos a classe do estado do fragmento [PlaceHolderFragment]. O fragmento [Vue1Fragment], por sua vez, 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 {
// dados a partilhar entre os próprios fragmentos e entre fragmentos e a atividade
// os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
// não se esqueça dos getters e setters necessários para a serialização/deserialização em jSON
// número de fragmentos visitados
private int numVisit;
// n.º do fragmento do tipo [PlaceholderFragment] apresentado no segundo separador
private int numFragment = -1;
// getters e setters
...
}
Esta é a sessão do projeto [Exemple-22].
2.8.4.2. A atividade [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 {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// sessão
private Session session;
// gestão do menu-----------------------
@Override
public boolean onOptionsItemSelected(MenuItem item) {
...
}
private void showFragment(int i) {
...
}
// implementação de métodos da classe pai ---------------------------------------------------
...
}
Aqui, a classe [MainActivity] é mais extensa do que a dos 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
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continuamos as inicializações iniciadas pela classe pai
// sessão
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// n.º do fragmento
final String ARG_SECTION_NUMBER = "section_number";
// inicialização da tabela de fragmentos
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// cria-se um fragmento
fragments[i] = new PlaceholderFragment_();
// é possível passar argumentos ao fragmento
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// um fragmento de +
fragments[i] = new Vue1Fragment_();
// resultado
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
// sem títulos aqui
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: recupera-se uma referência local da sessão. A alteração do tipo deve-se ao facto de a sessão da classe pai ser do tipo [AbstractSession];
- linhas 19-38: o método [getFragments] deve devolver à classe pai a matriz dos fragmentos geridos pela aplicação. Aqui, existem [FRAGMENTS_COUNT], número definido em [IMainActivity]. Os primeiros fragmentos [FRAGMENTS_COUNT-1] são do tipo [PlaceHolderFragment] e o último é do tipo [Vue1Fragment];
- linhas 41-45: o método [getFragmentTitle] deve apresentar os títulos dos fragmentos quando essa informação for útil. Não é esse o caso aqui;
- linhas 47-50: este método é chamado pela classe pai quando o utilizador clica num separador. Voltaremos a este assunto no parágrafo seguinte;
- linhas 52-55: devolve o número da primeira vista a apresentar quando a aplicação é iniciada. Neste caso, é o fragmento [Vue1Fragment] que 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 dos separadores
Os separadores são geridos pelos seguintes métodos:
@Override
protected void onCreateActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continuamos as inicializações iniciadas pela classe pai
// sessão
this.session = (Session) super.session;
// 1.º separador
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// 2.º separador?
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) {
// n.º do fragmento a apresentar
int numFragment;
switch (position) {
case 0:
// n.º do fragmento [Vue1Fragment]
numFragment = getFirstView();
break;
default:
// N.º do fragmento [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// visualização do fragmento
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: recupera-se uma referência local da sessão. A alteração do tipo deve-se ao facto de a sessão da classe pai ser do tipo [AbstractSession];
- linhas 11-13: cria-se o primeiro separador;
- linhas 15-20: cria-se o segundo separador se estiver registado um número de fragmento na sessão (linha 15). Este número tem inicialmente o valor -1 aquando da primeira construção da atividade;
- linhas 23-39: este método é chamado pela classe pai quando o utilizador clica numa aba;
- linhas 28-31: se for clicado no separador 0, deve ser apresentado [Vue1Fragment]. Sabe-se que esta é a primeira vista que foi apresentada ao iniciar a aplicação;
- linhas 32-35: se for clicada a aba 1, deve ser exibido o fragmento cujo número está registado na sessão;
- linhas 37-39: navega-se para o fragmento escolhido. A ação associada é [SUBMIT]. Teria podido ser [NAVIGATION]? Neste documento, utiliza-se [NAVIGATION] apenas quando a exibição do novo fragmento requer apenas o conhecimento do seu estado anterior. Aqui, não é esse o caso, uma vez que a exibição do fragmento em causa tem de mudar em relação ao seu estado anterior para apresentar 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 apresenta o seguinte:
![]() |
A gestão do menu é assegurada pelos seguintes métodos:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onOptionsItemSelected");
}
// processamento das opções do menu
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d(className, "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// item processado
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// sem navegação na seleção programática de um separador
session.setNavigationOnTabSelectionNeeded(false);
// recriamos os dois separadores devido a um problema com o tipo de letra dos títulos
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
// o número do fragmento a apresentar é definido na sessão
session.setNumFragment(i);
// seleciona-se o separador n.º 2 com navegação
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- linhas 16-31: gestão do clique numa opção de menu do tipo [Fragmenti];
- linhas 37-50: exibem o fragmento n.º i (trata-se de fragmentos do tipo PlaceHolderFragment) no separador n.º 1 (2.º separador);
- linhas 42-44: decide-se eliminar os separadores existentes para recriar dois novos. Esta decisão foi tomada para contornar o seguinte problema: quando nos limitamos a apresentar o fragmento no separador 1 existente (sem, portanto, o eliminar), curiosamente o seu título tem um aspeto (tipo de letra, tamanho) diferente do título do separador 0;
- linhas 43-44: as duas separadores são criadas, mas não selecionadas (último parâmetro em false);
- linha 40: as operações das linhas 42-44 podem realizar operações [select] nas guias, o que irá chamar o gestor [onTabSelected]. Se não se fizer nada, haverá então uma navegação para um fragmento. Isto evita-se definindo o valor booleano [navigationOnTabSelectionNeeded] para faux na sessão. Este valor booleano é automaticamente reposto para vrai pela classe [AbstractFragment] quando um fragmento se torna visível;
- linha 46: regista-se o n.º do fragmento a apresentar na sessão;
- linhas 48-50: seleciona-se o separador n.º 2 com navegação (linha 48). Isto irá acionar o procedimento [onTabSelected], que irá:
- exibir o fragmento cujo número foi colocado na sessão;
- armazenar na sessão o número do separador selecionado;
2.8.4.3. O fragmento [Vue1Fragment]
Apresentamos aqui 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 {
// os elementos da interface visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// gestor de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// é apresentado o nome introduzido
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// ciclo de vida do fragmento -----------------------------------------------
private void initFragment() {
// nada a fazer
}
// guardar estado do fragmento
@Override
public CoreState saveFragment() {
// estado da vista - nada para guardar
return new CoreState();
}
@Override
protected int getNumView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// nada a fazer
}
@Override
protected void initView(CoreState previousState) {
// 1.ª visita?
if (previousState == null) {
// exibe-se o n.º da visita
showNumVisit();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// exibe-se o n.º da visita
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// métodos privados -------------------------------------
// exibição do n.º da visita
private void showNumVisit() {
// incrementar o número da visita
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// exibe o número da visita
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
A classe está praticamente vazia.
- linhas 35-39: chamadas pela classe pai quando o fragmento tem de guardar o seu estado. O fragmento [Vue1Fragment] não tem nenhum estado para guardar. Basta devolver uma instância da classe base [CoreState] (lembre-se: não se deve devolver null);
- linhas 41-44: devem devolver o n.º do fragmento. O fragmento [Vue1Fragment] tem, por definição, o n.º [FRAGMENTS_COUNT-1];
- linhas 51-59: chamadas pela classe pai quando o fragmento é construído pela primeira vez (previousState==null) ou nas vezes seguintes (previousState!=null);
- linhas 54-57: se for a primeira visita, incrementa-se o número de visitas e este é apresentado (linhas 85-92);
- linhas 61-65: chamadas quando o fragmento vai ser apresentado associado a uma ação [SUBMIT]. Incrementa-se o número de visita e exibe-se o mesmo. Aqui, não é possível que o número de visita seja incrementado duas vezes durante o ciclo de vida. Com efeito, a primeira visita ao fragmento [Vue1Fragment] ocorre no arranque da aplicação, quando a ação é, por definição, [NONE] na sessão. Isto garante que o método [updateOnSubmit] não será chamado. Posteriormente, já não será nunca mais a primeira visita e o método [initView] não fará nada;
- linhas 68-71: chamadas num ciclo de gravação/restauração. Como o fragmento não tem estado, não há aqui nada para restaurar;
- linhas 73-76: chamadas quando todas as atualizações anteriores tiverem sido efetuadas. Aqui, não há mais nada a fazer;
- linhas 78-81: chamadas quando todas as tarefas assíncronas iniciadas estiverem 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 {
// texto
private String text;
// construtores
public PlaceHolderFragmentState() {
}
public PlaceHolderFragmentState(String text) {
super();
this.text = text;
}
// getters e setters
...
}
- quando for necessário guardar o estado do fragmento, será guardado o texto que este exibia (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 {
// componentes da interface visual
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// dados
private String text;
// n.º do fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
// implementação dos métodos da classe pai ----------------------------
@Override
public CoreState saveFragment() {
// salva-se o estado do fragmento
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) {
// texto original
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// atualiza-se o texto apresentado
// incrementar o número de visitas
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texto alterado
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
// registo
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) {
// restaura-se o texto apresentado
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, guarda-se o texto exibido pelo fragmento (linha 34);
- linhas 38-41: devolvem o número do fragmento. Este depende do número da secção que lhe foi passado como argumento aquando da sua criação;
- linhas 43-47: chamadas durante a primeira construção do fragmento (previousState==null) ou nas seguintes (previousState !=null);
- linha 46: aqui, não se utiliza o estado anterior. O texto inicial [text] (linha 24), apresentado na primeira visita, é recalculado de cada vez. Isto é discutível. Poderia ter-se optado por incluir também esta informação no estado do fragmento;
- linhas 49-51: chamadas durante a primeira construção da vista associada ao fragmento (previousState==null) ou nas seguintes (previousState!=null). Não há nada a fazer;
- linhas 53-56: chamadas quando o fragmento vai ser apresentado associado a uma ação [SUBMIT]. É sempre este o caso, exceto no ciclo de gravação/restauração, em que a ação é [RESTORE]. Assim, incrementa-se o número da visita e este é apresentado;
- linhas 68-74: chamadas num ciclo de gravação/restauração. Restaura-se o texto que tinha sido guardado no estado do fragmento;
- linhas 76-79: chamadas quando todas as atualizações anteriores tiverem sido efetuadas. Aqui, não há mais nada a fazer;
- linhas 82-83: chamadas quando todas as tarefas assíncronas iniciadas estiverem concluídas. Aqui, não há tarefas assíncronas;
2.8.4.6. Tests
Convida-se o leitor a testar a aplicação, rodando o dispositivo para verificar se o fragmento apresentado mantém o seu estado. Analisaremos também os registos.
2.9. Conclusion
No final deste capítulo, dispomos de um projeto modelo [client-android-skel] de um cliente Android que comunica com um serviço web / jSON com as seguintes características:
- a comunicação assíncrona com o servidor web / jSON é feita através da biblioteca RxJava;
- o ciclo de vida de um fragmento (atualização, gravação, restauração) é gerido pela sua classe pai [AbstractFragment], que invoca, em momentos específicos, determinados métodos das suas classes filhas. Assim, o fragmento filho não precisa de se preocupar com as etapas do ciclo de vida, mas apenas de implementar determinados métodos impostos pela sua classe pai;
- o ciclo de vida da atividade (guardar/restaurar) é gerido por uma classe abstrata, [AbstractActivity], que também impõe à atividade filha a implementação de determinados métodos;
- a classe [AbstractActivity] é capaz de gerir uma aplicação com ou sem separadores, com ou sem imagem de espera, com ou sem autenticação básica junto do servidor web / jSON. A presença ou ausência destes elementos é definida por configuração;
Vamos agora apresentar um estudo de caso mais complexo do que os exemplos anteriores. A nova aplicação basear-se-á no projeto modelo [client-android-skel].



















































