2. Estrutura de um cliente Android que se comunica com um serviço web / jSON
Apresentamos agora um esboço de aplicativo Android que se comunica com um ou mais serviços web / jSON. Trata-se do projeto [client-android-skel], que pode ser encontrado na pasta [architecture] de exemplos:
![]() |
O estudo desse esqueleto de aplicativo será uma oportunidade para revisar alguns pontos que abordamos nos exemplos anteriores. Este aplicativo servirá de estrutura para todos os aplicativos futuros. Ele foi construído após inúmeras iterações. Seu objetivo é fatorar, em classes abstratas, o maior número possível de elementos dos aplicativos que construiremos em breve, a fim de evitar ter que escrever sempre o mesmo tipo de código, que se diferencia apenas por detalhes. Suas características são as seguintes:
- a comunicação assíncrona com o servidor web / jSON é feita por meio da biblioteca RxJava;
- o ciclo de vida de um fragmento (atualização, salvamento, restauração) é gerenciado por sua classe pai [AbstractFragment], que chama, em momentos específicos, determinados métodos de suas classes filhas. Assim, a classe filha não precisa se preocupar com as etapas do ciclo de vida, mas apenas em implementar determinados métodos exigidos por sua classe pai;
- o ciclo de vida da atividade (salvar/restaurar) é gerenciado por uma classe abstrata [AbstractActivity], que também exige que a atividade filha implemente determinados métodos;
- a classe [AbstractActivity] é capaz de gerenciar um aplicativo com ou sem abas, com ou sem imagem de espera, com ou sem autenticação básica no servidor web / jSON. A presença ou ausência desses elementos é definida por configuração;
Esse esqueleto foi utilizado em todos os exemplos a seguir. Devido à diversidade deles, o que funcionava em um exemplo poderia não funcionar no exemplo seguinte. Como o esqueleto foi utilizado em sete exemplos no total, ocorreram inúmeras iterações. Se fosse utilizado para um oitavo exemplo, é possível que, mais uma vez, a especificidade desse novo exemplo gerasse novos erros. No entanto, o uso desse esqueleto simplificará consideravelmente a redação dos exemplos a seguir. De fato, o gerenciamento do ciclo de vida de um fragmento (update, save, restore), aliado ao conceito de adjacência entre fragmentos, é particularmente complexo. Aqui, ele está totalmente oculto 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]. É ela que se comunica com o servidor web / jSON;
- há apenas uma atividade que também implementa a interface [IDao]. As visualizações se comunicam com ela para acessar o servidor;
- as visualizações são implementadas por fragmentos;
O projeto Android reflete essa arquitetura:
![]() |
Apresentaremos, um por 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 usar 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 poder gerar 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, é possível partir dos números atuais se configurarmos o Android Studio para que essas versões das ferramentas do Android (linhas 15-16, 47-48) estejam presentes (ver parágrafo 6.11);
2.3. O manifesto do aplicativo
![]() |
<?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 do aplicativo;
- linhas 10, 15: definiremos o valor do item [app_name] no arquivo [res / values / strings.xml]. Por enquanto, ele é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- nome do aplicativo -->
<string name="app_name">[Donnez un nom à votre application]</string>
</resources>
2.4. A organização do código Java
![]() |
- [architecture] reúne os principais elementos de organização do código;
- [activity] contém a única atividade do aplicativo;
- [fragments] agrupa os fragmentos ou visualizações do aplicativo;
- [dao] agrupa os elementos de comunicação com o servidor web / jSON;
2.5. Elementos da atividade
![]() | ![]() |

2.5.1. A visualização associada à atividade
A visualização [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>
<!-- contêiner 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 contêiner de fragmentos específico;
A atividade também possui um menu [res / menu / menu_main.xml] para sua visualização:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
</menu>
Por enquanto, ele está vazio. O desenvolvedor o preencherá, se necessário.
2.5.2. O contêiner de fragmentos [MyPager]
![]() |
package client.android.architecture;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// controla o deslize
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 serem redefinidos para gerenciar o deslize
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// deslize autorizado?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// deslize permitido?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// controle 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;
}
}
Essa classe estende a classe padrão do Android [ViewPager] apenas para gerenciar o deslize (linha 11) e a rolagem (linha 13) entre as visualizações.
- linhas 26-43: os métodos que desativam o deslize caso ele tenha sido desativado;
- linhas 46-49: redefinição do método [setCurrentItem], que serve para alterar a visualização exibida. Se a rolagem tiver sido desativada, a mudança de visualização ocorrerá sem rolagem. Vale ressaltar que o desenvolvedor pode contornar esse modo de funcionamento utilizando o método [setCurrentItem(int position, boolean smoothScrolling)], que permite especificar a rolagem desejada;
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: 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 possível menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
...
}
- linha 16: cada fragmento possui, em seu estado, um valor booleano [hasBeenVisited] que indica se já foi visitado ou não. Isso é necessário porque, às vezes, na primeira exibição de um fragmento, há ações específicas a serem realizadas;
- linha 18: o projeto [client-android-skel] salva e restaura automaticamente os menus dos fragmentos, caso eles tenham um. Na tabela MenuItemState[] menuOptionsState, armazena-se o estado de visibilidade de todas as opções do menu;
- linhas 10-13: assim como foi feito em [Exemple-22], o estado da atividade e de seus fragmentos será salvo na sessão, que, por sua vez, será salva na forma de uma cadeia jSON. Veremos que a sessão armazena um array de elementos do tipo [CoreState]. Se nada for feito, será salva a cadeia jSON de um tipo [CoreState]. No entanto, queremos salvar 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 desenvolvedor deve modificar para cada nova aplicação (linhas 10 a 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 visualização
void navigateToView(int position, ISession.Action action);
// gerenciamento da espera
void beginWaiting();
void cancelWaiting();
// constantes do aplicativo (a serem modificadas) -------------------------------------
// 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 da solicitação 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 abas
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = false;
// número de fragmentos do aplicativo
int FRAGMENTS_COUNT = 0;
// tarefa: adicione aqui suas constantes e outros métodos
}
- linha 6: a interface [IMainActivity] estende a interface [IDao] da camada [DAO];
- linha 9: é a atividade que fornece acesso à sessão na forma de uma instância da interface [ISession];
- linha 12: é por meio dessa atividade que se alterna de visualização. O segundo parâmetro é a ação que provoca essa mudança de visualização, um dos valores SUBMIT, NAVIGATION, RESTORE;
- linhas 15-17: é a atividade que gerencia a imagem de espera;
- linha 22: para a depuração do aplicativo;
- linha 25: para não esperar muito tempo caso o servidor não responda mais;
- linha 28: durante a depuração, definiremos um valor de alguns segundos para ter tempo de cancelar a operação com o servidor e verificar o que está acontecendo;
- linha 31: para true caso o serviço jSON solicite autenticação básica;
- linha 34: adjacência de fragmentos;
- linha 37: para vrai se o aplicativo tiver abas;
- linha 39: para vrai se o aplicativo se comunicar com um servidor web / jSON e se desejar exibir uma imagem de espera durante as trocas de dados;
- linha 43: o número de fragmentos gerenciados pelo aplicativo;
A interface [IMainActivity] é o segundo elemento da arquitetura que o desenvolvedor 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);
// usuário
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: declare sua interface aqui
}
- linha 24: o desenvolvedor preencherá a interface aqui;
2.5.6. A sessão
![]() |
A classe [Session] encapsula os elementos compartilhados 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 visualização exibida
int getPreviousView();
void setPreviousView(int numView);
// último estado de uma visualização
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// ação em andamento
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// estados de todas as visualizações -
// não é utilizado pelo código, mas é necessário para serialização/desserialização jSON
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// nº da última aba selecionada
int getPreviousTab();
void setPreviousTab(int position);
// navegação ao selecionar a aba
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
Introduzimos a interface [ISession] para exigir a presença de determinados métodos na sessão:
- linhas 7-10: o número da última visualização (fragmento) exibida;
- linhas 12-15: o estado de uma visualização específica;
- linhas 17-24: introduzimos o conceito de ação em andamento. São quatro (linha 17):
- RESTORE: um salvamento/restauração está em andamento. Não há mudança de visualização;
- NAVIGATION: uma navegação está em andamento. Chamaremos aqui de navegação uma mudança de visualização em que a nova visualização pode ser restaurada a partir de seu último estado armazenado na sessão;
- SUBMIT: atribuiremos o tipo [SUBMIT] a uma ação em andamento quando houver mudança de visualização e a nova visualização depender do estado da atividade em geral, e não apenas de seu próprio estado. Às 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 seu primeiro valor;
- linhas 26-30: os estados da atividade e dos fragmentos serão armazenados em um array do tipo CoreState[]. Para que ele seja gerenciado corretamente durante as serializações/desserializações jSON, é necessário que ele possua um getter e um setter;
- linhas 32-35: número da última aba selecionada. É usado durante o ciclo de salvamento/restauração para selecionar novamente a aba que estava selecionada antes da rotação do dispositivo;
- linhas 37-40: gerenciamento de um valor booleano que indica se a seleção de uma aba 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 visualização anterior
private int preViousView;
// status das visualizações
private CoreState[] coreStates = new CoreState[0];
// ação em andamento
private Action action = Action.NONE;
// guia selecionado anteriormente
private int previousTab;
// navegação ao selecionar a aba
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// construtor
public AbstractSession() {
// inicializando a 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 visualização que estava exibida antes da atual. Essa informação é útil quando é possível acessar uma visualização a partir de vários locais. Esse é tipicamente o caso na navegação por abas. A visualização exibida pode, então, saber qual era a visualização 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 visualização anterior da linha 9. Essa informação é útil quando há rotação do dispositivo e é necessário retornar à aba que estava selecionada antes da rotação;
- linha 22: um valor booleano indicando se a seleção de uma aba deve ser acompanhada por uma mudança no fragmento exibido. É importante saber que o projeto [client-android-skel] faz um gerenciamento separado das abas e dos fragmentos para poder ser utilizado em casos em que o número de abas é menor que o número de fragmentos. Existem dois tipos de seleção:
- uma seleção feita pelo usuário ao clicar em uma aba. Nesse caso, geralmente o fragmento exibido deve mudar;
- uma seleção por meio do método [Tablayout.Tab.select()]. Nesse caso, a alteração do fragmento exibido nem sempre é desejável. Aqui estão dois exemplos:
- ao girar o dispositivo, a atividade é recriada, assim como as abas. No entanto, quando a primeira aba é criada, ela passa automaticamente por uma operação de software [select]. Nesse momento, não é desejável alterar o fragmento exibido, pois estamos em uma fase de recriação da atividade em que o fragmento exibido no final não será necessariamente aquele associado à primeira aba;
- uma vez que o gerenciamento das abas é separado do gerenciamento dos fragmentos, pode-se querer atualizar as abas (exclusão, adição) sem interferir nos fragmentos a elas associados. No entanto, algumas dessas operações podem, mais uma vez, acionar uma operação de software [select] implícita em uma das abas. Essa seleção não deve, portanto, necessariamente resultar em uma navegação para o fragmento associado;
- linha 21: o campo [navigationOnTabSelectionNeeded] não deve ser salvo durante as operações de salvamento da atividade e de seus fragmentos. A anotação [@JsonIgnore] faz com que o campo seja ignorado durante as serializações/desserializações jSON;
- linhas 25-31: o construtor inicializa a matriz de estados dos fragmentos [FRAGMENTS_COUNT] do aplicativo. Os elementos dessa matriz são inicializados com o campo [hasBeeenVisited=false]. Essa informação é usada 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 serem compartilhados 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/desserialização em jSON
}
- linha 5: a classe [Session] estende a classe [AbstractSession] que acabamos de ver. O desenvolvedor colocará nela os elementos a serem compartilhados entre os próprios fragmentos e entre os fragmentos e a atividade. Observe-se que a classe [Session] não é mais anotada pela anotação AA [@EBean]. Ela se tornou uma classe normal;
2.5.7. A classe abstrata [AbstractActivity]
![]() |
2.5.7.1. Squelette
A classe [AbstractActivity] tem mais de 300 linhas. Vamos analisá-la em etapas. 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 contêiner de fragmentos
protected MyPager mViewPager;
// a barra de ferramentas
private Toolbar toolbar;
// a imagem de espera
private ProgressBar loadingPanel;
// barra de abas
protected TabLayout tabLayout;
// o gerenciador de fragmentos ou seções
private FragmentPagerAdapter mSectionsPagerAdapter;
// nome da classe
protected String className;
// mapeador jSON
private ObjectMapper jsonMapper;
// construtor
public AbstractActivity() {
// nome da classe
className = getClass().getSimpleName();
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructeur");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// implementação IMainActivity --------------------------------------------------------------------
...
// ciclo de vida — backup/restauração da atividade ------------------------------------
...
// gerenciamento da imagem de espera ---------------------------------
...
// interface IDao -----------------------------------------------------
...
// gerenciador de fragmentos --------------------------------
...
// classes derivadas
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);
- gerencia o salvamento e a restauração da atividade e de seus fragmentos durante uma rotação do dispositivo (linha 58);
- gerencia 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 gerenciador de fragmentos (linha 67);
- exige que suas classes filhas possuam 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);
// registra-se a ação em andamento durante essa mudança de visualização
session.setAction(action);
}
2.5.7.3. Salvamento do estado da atividade e de seus fragmentos
O estado da atividade e de seus fragmentos está inteiramente na sessão. Portanto, trata-se de salvar essa sessão. Repetimos aqui o que foi feito no projeto [Exemple-22] (ver parágrafo 1.23):
// gerenciamento de salvamento/restauração da atividade ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// pai
super.onSaveInstanceState(outState);
// salvamento da sessão na forma de uma string jSON
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.5.7.4. Restauração do estado da atividade e de seus fragmentos
Trata-se de restaurar a sessão. Procedemos conforme demonstrado em [Exemple-22]:
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// log
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();
}
// log
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 início da atividade. Nesse caso, é criada uma sessão vazia;
2.5.7.5. Inicialização da camada [DAO]
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// log
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 uma referência à camada [DAO] à atividade filha (linha 21);
- linhas 14-17: se a camada [DAO] existir, ela é configurada a partir das informações contidas na interface [IMainActivity];
2.5.7.6. Inicialização da visualização associada à atividade
A visualização 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>
<!-- contêiner 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>
Essa visualização é inicializada com o código a seguir:
@Override
protected void onCreate(Bundle savedInstanceState) {
// pai
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// visualização associada
setContentView(R.layout.activity_main);
// componentes da visualização ---------------------
// barra de ferramentas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// imagem de espera?
if (IS_WAITING_ICON_NEEDED) {
// adicionamos 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 visualização XML [activity_main] está associada à atividade;
- linhas 14-15: a barra de ferramentas é integrada e suportada;
- linhas 17-27: possível adição de uma imagem de espera: se o booleano [IS_WAITING_ICON_NEEDED] estiver definido como verdadeiro na interface [IMainActivity];
- linha 23: criação da imagem de espera do tipo [ProgressBar], referenciada pelo campo [loadingPanel];
- linha 24: inicialmente, essa imagem está oculta;
- linha 26: ela é adicionada à barra de ferramentas;
2.5.7.7. Gerenciamento de abas
A interface [IMainActivity] pode solicitar uma barra de abas. Ela é adicionada e gerenciada da seguinte maneira:
// barra de abas
protected TabLayout tabLayout;
...
// barra de abas?
if (ARE_TABS_NEEDED) {
// adiciona-se a barra de abas
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// sem navegação por seleção até a exibição de um fragmento
session.setNavigationOnTabSelectionNeeded(false);
// criação da barra de abas
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// adição da barra de abas à barra de aplicativos
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// gerenciador de eventos da barra de abas
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// uma aba foi selecionada
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 gerenciamento de uma barra de abas;
- linha 6: a adição da barra de abas ocorre se a constante [ARE_TABS_NEEDED] estiver definida como vrai na interface [IMainActivity];
- linha 12: durante a criação da barra de abas, podem ocorrer operações [Tablayout.Tab.select] implícitas (não são provocadas pelo usuário). Define-se o booleano [session.navigationOnTabSelectionNeeded] como faux para evitar qualquer navegação durante essas seleções falsas. Caberá ao desenvolvedor selecionar o fragmento a ser exibido com o método [navigateToView]. A variável booleana [session.navigationOnTabSelectionNeeded] será redefinida para vrai quando esse fragmento for exibido (consulte a classe AbstractFragment);
- linha 14: criação de uma barra de abas referenciada pelo campo [tabLayout]. Utilizamos uma barra de abas personalizada [CustomTabLayout], à qual voltaremos mais adiante;
- linha 15: definimos as cores dos títulos das abas. Elas estão no seguinte arquivo [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 ela está selecionada;
- linha (d): a cor do título da aba quando ela não está selecionada;
Esse arquivo é, obviamente, editável. Os códigos hexadecimais das cores podem ser encontrados, por exemplo, aqui.
- linhas 17-18: adição dessa barra de abas à barra de aplicativos presente na visualização XML [activity_main];
- linhas 20-47: gerenciador de eventos da barra de abas;
- linhas 22-36: apenas o evento [onTabSelected] é gerenciado. Ele 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 da aba selecionada;
- linha 32: essa posição é armazenada na sessão;
- linha 34: trata-se agora de exibir o fragmento associado a essa aba. Apenas a classe filha (linha 52) pode fazer essa associação. Observe-se que não se associa a barra de abas ao contêiner de fragmentos [mViewPager], como foi feito em alguns exemplos estudados. Aqui, separamos totalmente o gerenciamento da barra de abas do gerenciamento dos fragmentos. É por isso que, quando uma aba é clicada, somos obrigados a indicar qual visualização queremos que seja exibida;
- linha 28: distingue-se a seleção de aba com ou sem navegação. Em geral, quando o usuário clica em uma aba, deseja-se que haja navegação, e durante uma seleção programada, não se deseja isso. Cabe ao desenvolvedor distinguir esses dois casos com o elemento [session.navigationOnTabSelectionNeeded]. Quando a navegação não é realizada, o número da última aba selecionada não é registrado na sessão. Caberá ao desenvolvedor fazer isso;
2.5.7.8. O gerenciador de abas [CustomTabLayout]
![]() |
Utilizamos um gerenciador de abas personalizado para poder exibir o título das abas com diferentes fontes. 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 da fonte dos títulos das abas é feita nas linhas 30 e 44;
O arquivo [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);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// instanciação do gerenciador de fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// o contêiner de fragmentos está associado ao gerenciador de fragmentos
// ou seja, o fragmento nº i do contêiner de fragmentos é o fragmento nº i fornecido pelo gerenciador de fragmentos
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// desativa-se o deslize entre fragmentos
mViewPager.setSwipeEnabled(false);
// adjacência dos fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// exibe-se a primeira visualização
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// passa o controle para a atividade filha
onCreateActivity();
}
...
// classes filhas
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- linhas 10-19: aqui encontramos um código frequentemente encontrado nos exemplos estudados;
- linhas 21-23: exibição da primeira visualização. Sem dúvida, há várias maneiras de distinguir esse caso. Aqui, utilizamos o fato de que, para a primeira visualização, o valor da ação que provoca a mudança de visualização é NONE;
- linha 22: não fazemos nenhuma suposição sobre o primeiro fragmento a ser exibido. Em nossos exemplos, esse fragmento costumava ser o nº 0, mas nem sempre (ver Exemplo-22). Portanto, solicitaremos à atividade filha (linha 30) que nos informe qual é essa primeira visualização;
- linha 25: aqui, fatoramos tudo o que foi possível. Agora, a classe filha precisa realizar suas próprias inicializações (linha 29);
2.5.7.10. Gerenciamento da imagem de espera
Na classe [AbstractActivity], a imagem de espera é gerenciada pelos dois métodos a seguir:
// gerenciamento 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 maneira:
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: vale lembrar que o valor desse campo foi fornecido pela atividade filha no método [onCreate];
2.5.7.12. Implementação do gerenciador de fragmentos
Na classe [AbstractActivity], o gerenciador de fragmentos é implementado da seguinte forma:
...
// o gerenciador de fragmentos --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// construtor
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// fragmentos da classe filha
fragments = getFragments();
}
// deve renderizar o fragmento na posição n.º
@Override
public AbstractFragment getItem(int position) {
// exibe-se o fragmento
return fragments[position];
}
// retorna o número de fragmentos a serem gerenciados
@Override
public int getCount() {
return fragments.length;
}
// retorna 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: a matriz dos fragmentos associados à atividade. Todos os fragmentos serão derivados da classe [AbstractFragment];
- linhas 8-12: este é o construtor que inicializa a matriz de fragmentos. Ele solicita esses fragmentos à classe filha da atividade (linha 35);
- linhas 28-31: os títulos dos fragmentos podem ser usados em um aplicativo onde há tantas abas quanto fragmentos. Nesse caso, é possível atribuir à aba 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 um pouco antes de a visualização associada à atividade se tornar visível. Ele é utilizado aqui para selecionar uma aba após um salvamento/restauração:
@Override
public void onResume() {
// pai
super.onResume();
if (IS_DEBUG_ENABLED) {
Log.d(className, "onResume");
}
// se for restauração, então é preciso restaurar a última aba selecionada
if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
tabLayout.getTabAt(session.getPreviousTab()).select();
}
}
- linha 10: seleção da aba que estava selecionada antes do processo de salvamento/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 uma aba foi desativada. Portanto, neste caso, há seleção de uma aba, 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 do aplicativo.
A atividade filha deverá implementar os seis métodos a seguir:
// 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 de sua classe pai:
// a sessão
protected ISession session;
// o contêiner de fragmentos
protected MyPager mViewPager;
// barra de abas
protected CustomTabLayout tabLayout;
// nome da classe
protected String className;
2.5.8. A atividade [MainActivity]
![]() |
A classe [MainActivity] pode ter um nome diferente. Sua única restrição é implementar a interface [IMainActivity]. A classe básica fornecida é a seguinte:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// sessão
private Session session;
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// log
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: definir os fragmentos aqui
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// a fazer: definir os títulos dos fragmentos aqui
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// tarefa: navegação por abas — definir a visualização a ser exibida
}
@Override
protected int getFirstView() {
// tarefa: navegação por abas — definir a primeira visualização a ser exibida
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, esse menu está vazio. O desenvolvedor deverá preenchê-lo, se necessário;
- linha 16: a classe estende a classe [AbstractActivity];
- linhas 19-20: uma referência à camada [DAO]. Ela será instanciada pela biblioteca AA antes que este campo seja inicializado. Isso implica que o bean AA [Dao] deve existir. Esse é sempre o caso com o esqueleto de aplicativo que fornecemos. Mesmo em um aplicativo sem a camada [DAO], é possível deixar o pacote [dao] existir. Isso não causa 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] retorna uma referência à camada [DAO]. Aqui, essa referência nunca é null. No entanto, na classe pai [AbstractActivity], foi previsto o caso em que a classe filha retornasse uma referência null para indicar que não havia uma camada [DAO]. Se quisermos utilizar essa possibilidade (que, na minha opinião, não é muito útil), é aqui que devemos retornar o ponteiro null;
2.6. A camada [DAO]

![]() |
2.6.1. A interface IDao
Ela 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);
// usuário
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: declare sua interface aqui
}
O desenvolvedor adicionará os métodos de 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: declare aqui os URL a serem alcançados
}
O desenvolvedor adicionará os métodos que se comunicam com o URL, expostos pelo servidor jSON, a partir da linha 17.
2.6.3. O interceptador 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 {
// usuário
private String user;
// senha
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);
// continua-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;
}
}
Essa classe gera o cabeçalho de autenticação HTTP a seguir:
onde [code] é o código Base64 da sequência 'user:mp'. Essa classe só é utilizada se o servidor jSON esperar por essa forma de autenticação. Existem outras.
Observação: o uso desta classe é ilustrado 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) {
// registro
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 de serviço
try {
// em espera?
if (delay > 0) {
Thread.sleep(delay);
}
// executando a consulta síncrona
T response = request.getResponse();
// log
if (isDebugEnabled) {
String log;
if (response instanceof String) {
log = (String) response;
} else {
log = mapper.writeValueAsString(response);
}
Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
}
// enviando a resposta ao observador
subscriber.onNext(response);
// se sinaliza o fim do observável
subscriber.onCompleted();
} catch (InterruptedException | JsonProcessingException | RuntimeException e) {
// registro
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 por meio 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 usuário a oportunidade de cancelar a operação assíncrona e, assim, observar como o código se comporta nessa situação;
- linha 52: a resposta esperada é solicitada por meio de uma consulta síncrona;
- linha 64: assim que a resposta é recebida, ela é 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 retorna 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() {
// registro
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) {
// registra-se o usuário no interceptador
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));
}
// interceptador de autenticação?
if (isBasicAuthentificationNeeded) {
// adiciona-se o interceptador 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 garantirá as trocas com o servidor web / jSON;
- linhas 24-25: injeção do interceptador 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], responsável pelas trocas entre cliente e servidor, é criado a partir de um factory. Isso não é imprescindível, mas é por meio do factory que podemos configurar os tempos de espera das trocas de dados. É por isso que não utilizamos o construtor sem parâmetros [RestTemplate()];
- linha 39: adicionamos um conversor jSON aos conversores do [RestTemplate]. Esse será o único conversor. Assim, quando um método do cliente [WebClient] receber uma string jSON do servidor, ela será automaticamente deserializada no objeto que o método deve retornar;
- linha 41: o objeto [RestTemplate] assim configurado é passado para o cliente web, que garantirá as trocas cliente/servidor por meio 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 essa URL raiz;
- linhas 50-54: este método permite especificar o proprietário da conexão quando esta é controlada por uma autorização do tipo básico (ver parágrafo 2.6.3);
- linhas 56-64: definem os parâmetros das trocas cliente/servidor. Isso é feito por meio do parâmetro do objeto que rege as trocas;
- linhas 66-78: esse 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ásica, o interceptador de autenticação inserido na linha 25 é adicionado aos interceptadores do objeto [RestTemplate]. Esse interceptador adicionará automaticamente a todas as solicitações do cliente web a linha HTTP de autenticação básica esperada pelo servidor;
- o desenvolvedor 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 de 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 ser exibido
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) {
// construindo o texto a ser exibido
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 do aplicativo. Assim como na classe [AbstractActivity], seu código é complexo. Também vamos analisá-lo passo a passo.
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 ------------------------------------------------------------
// assinaturas de 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 ----------------------------------------------------------------------------------
...
// gerenciamento do menu ------------------------------------------
...
// gerenciamento de espera -------------------------------------------------------------
...
// gerenciamento de operações assíncronas --------------------------------------------------------------------
...
// gerenciamento de exceções -------------------------------------------------------------------
....
// gerenciamento do ciclo de vida do fragmento --------------------------------------------------------
...
// classes derivadas -----------------------------------------------------
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 gerenciar o possível menu;
- linhas 67-68: código utilitário para gerenciar 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 gerenciar qualquer exceção de maneira padrão;
- linhas 76-77: código que gerencia 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;
// log
if (isDebugEnabled) {
Log.d(className, "constructeur");
}
}
- linha 9: observa-se o nome da classe filha que está sendo instanciada aqui. Esse nome é utilizado em todos os logs da classe pai;
- linha 10: observa-se que o fragmento está sendo construído. Essa informação será utilizada quando for solicitado que o fragmento filho seja atualizado;
2.7.3.3. Gerenciamento do menu
Em nossa arquitetura, todo fragmento deve ter um menu, mesmo que vazio. Os logs mostraram, de fato, que quando o método [onCreateOptionsMenu] — executado quando o fragmento possui um menu — é executado, o fragmento já foi associado à sua atividade, à sua visualização e ao seu menu e se tornará visível. Portanto, esse é o momento em que a atualização da interface visual e do menu pode ser realizada. É nesse método [onCreateOptionsMenu] que solicitamos que o fragmento filho seja atualizado.
O gerenciamento do menu reúne métodos utilitários que permitem ao fragmento filho exibir ou não os elementos do menu:
// menu do fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// gerenciamento 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)");
}
// recuperamos 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++) {
// status
MenuItemState state = this.menuOptionsStates[i];
// ID do menu
int id = state.getMenuItemId();
// inicialização do relatório
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// resultado
return menuOptionsStates;
}
// exibição das opções do menu -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// atualiza-se todas as opções do menu
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// atualiza-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, essa lista está vazia. Em seguida, ela é preenchida por meio 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. Essa 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. Ela faz a mesma coisa, 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 exibir 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 como privados, pois são utilizados exclusivamente no [AbstractFragment]. Os métodos [setAllMenuOptionsStates] (linha 58) e [setMenuOptionsStates] (linha 65) são declarados como protegidos para que fiquem disponíveis para as classes filhas;
2.7.3.4. Gerenciamento da espera pelo término de uma tarefa assíncrona
// assinaturas de observáveis
private List<Subscription> abonnements = new ArrayList<>();
// tarefas assíncronas
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// gerenciamento da espera pelo término de uma operação assíncrona -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// registra-se o número de tarefas que serão executadas
this.numberOfRunningTasks = numberOfRunningTasks;
// insere-se a imagem de espera
mainActivity.beginWaiting();
// esvazia-se a lista de assinaturas
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 desse 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 é exibida;
- linha 15: limpa-se a lista de assinaturas das operações assíncronas. Estas ainda não foram criadas pelo fragmento filho;
- linha 17: mantém-se um booleano para indicar que as tarefas assíncronas solicitadas pelo fragmento filho foram canceladas. Inicialmente, o booleano tem o valor false;
- linhas 20-25: o fragmento filho chama o método pai [cancelWaitingTasks] para indicar que deseja cancelar as tarefas que iniciou;
- linha 22: a imagem de espera é ocultada;
2.7.3.5. Gerenciamento de exceções
// gestão de exceção -------------------------------------------------------------------
// 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 em uma 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 em uma 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. Gerenciamento de operações assíncronas
...
// assinaturas 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) {
// process: o observável a ser executado/observado
// consumeResult: o método que processa a resposta obtida
//
// só se criam novas assinaturas se não tiver havido cancelamento
if (!runningTasksHaveBeenCanceled) {
// execução na thread de E/S e observação na thread da interface do usuário
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// o observável é executado
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 gerou 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 ser executado;
- [Action1<T> consumeResult]: o método do fragmento filho a ser chamado para transmitir a ele os elementos emitidos pelo processo. Em nossos exemplos anteriores, os processos sempre emitiram apenas um elemento. O tipo T de [Action1<T>] é o tipo T do resultado retornado pelo processo observado;
- linha 14: a tarefa assíncrona só é iniciada se ainda não tiver ocorrido uma cancelamento pelo usuário ou pelo programa (devido a uma exceção);
- linha 16: o processo está configurado para ser executado em um thread de E/S e é observado no thread da interface do usuário;
- linha 16: a instrução [process.subscribe] inicia a execução do processo na thread de E/S. Dentro dessa thread, as operações são executadas de forma síncrona, pois 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 é registrada no campo [abonnements], que armazena todas as tarefas assíncronas iniciadas. Isso 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 aguardar
numberOfRunningTasks--;
// terminou?
if (numberOfRunningTasks == 0) {
// fim da espera
cancelWaitingTasks();
// notifica-se a conclusão das tarefas à 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 não houver mais tarefas ativas, então o fragmento filho obteve todas as suas respostas;
- linha 10: a espera é cancelada;
- linha 12: avisa-se o fragmento filho de que todas as tarefas que ele iniciou foram concluídas, chamando seu método [notifyEndOfTasks]. O parâmetro desse método indica como as tarefas foram concluídas: normalmente, por cancelamento do usuário ou do código devido a uma exceção. Na linha 12, é sinalizado um término normal. Observe que o fragmento filho não precisa se preocupar em manter uma contagem das tarefas ainda ativas. 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 ser tratada
//
// log
if (isDebugEnabled) {
Log.d(className, "Exception reçue");
}
// as tarefas já iniciadas são canceladas
cancelRunningTasks();
// exibindo as mensagens de erro
showAlert(th);
}
// cancelamento das tarefas
protected void cancelRunningTasks() {
// log
if (isDebugEnabled) {
Log.d(className, "Annulation des tâches lancées");
}
// cancela todas as tarefas assíncronas registradas
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
// registra-se o cancelamento
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// fim da espera
cancelWaitingTasks();
// a cancelamento das tarefas é comunicado 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: exibe-se o texto da exceção;
- linhas 21-37: cancelamento de todas as tarefas;
- linhas 27-29: todas as assinaturas são canceladas;
- linha 31: registra-se que houve cancelamento;
- linha 32: o contador de tarefas é zerado;
- linha 34: a espera é cancelada;
- linha 36: notifica-se o fragmento filho sobre o término das tarefas após o cancelamento;
2.7.3.7. Gerenciamento do ciclo de vida do fragmento
// ciclo de vida --------------------------------------------------------
@Override
public void onDestroyView() {
// pai
super.onDestroyView();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// pai
super.onDestroy();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroy");
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
private void saveState() {
...
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
...
}
- linhas 2-20: os métodos [onDestroyView, onDestroy] existem apenas para fins de registro. Eles permitem que o desenvolvedor compreenda melhor o ciclo de vida dos fragmentos;
O salvamento 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);
// salvar?
if (this.isVisibleToUser && !isVisibleToUser) {
// o fragmento será ocultado — estamos salvando-o
if (!saveFragmentDone) {
saveState();
}
}
// memória
this.isVisibleToUser = isVisibleToUser;
}
private void saveState() {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
}
// pai
super.onSaveInstanceState(outState);
// salvar o fragmento somente se ele estiver visível
if (isVisibleToUser) {
// talvez o backup já tenha sido feito
if (!saveFragmentDone) {
saveState();
}
// restauração a ser feita em todos os casos
session.setAction(ISession.Action.RESTORE);
}
}
- linhas 6-19: o fragmento é salvo se ele passar do estado exibido para o estado oculto (linha 11). É o método [setUserVisibleHint] que nos fornece essa informação;
- linha 14: o salvamento é feito 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 é salvo sob duas condições:
- ele esteja visível (linha 34);
- ainda não tenha sido salvo (linha 36). É possível que os métodos [setUserVisibleHint, onSaveInstanceState] não possam ser executados ambos quando o fragmento estiver visível e que, portanto, o gerenciamento do booleano [saveFragmentDone] seja desnecessário. Por precaução, optei por utilizá-lo;
- linha 40: após o salvamento, virá a restauração. Observe que, na próxima vez que o fragmento precisar ser atualizado, isso deverá ser feito por meio de uma operação [RESTORE];
Observe-se os dois momentos em que é solicitado um salvamento do fragmento:
- quando ele 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 serem canceladas?
if (numberOfRunningTasks != 0) {
// cancelamos as tarefas
cancelRunningTasks();
}
// salvamos o estado do fragmento
CoreState currentState = saveFragment();
// o fragmento foi visitado
currentState.setHasBeenVisited(true);
// salvar o estado do menu
currentState.setMenuOptionsState(getMenuOptionsStates());
// início da sessão
session.setCoreState(getNumView(), currentState);
// salvamento concluído
saveFragmentDone = true;
// registro
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 operações assíncronas estão em andamento. Nesse caso, decide-se cancelar todas elas. Essa não é uma boa decisão para o usuário, que terá de refazer uma nova solicitação, possivelmente demorada, quando na verdade apenas moveu seu celular ou tablet ou recebeu uma ligação. É possível manter as conexões de rede durante um ciclo de backup/restauração. No entanto, as soluções não são óbvias e decidi não abordá-las neste curso para iniciantes. O caminho a seguir é estabelecer essas conexões de rede por meio de um fragmento sem interface visual associada e que não seja destruído durante o ciclo de backup/restauração. Para isso, basta usar a instrução [Fragment.setRetainInstance(true)];
- linha 9: solicita-se ao fragmento filho que salve seu estado em um tipo derivado de [CoreState] (linha 31);
- linha 11: registra-se que o fragmento foi visitado. Essa informação é útil. Quando um fragmento é visitado pela primeira vez, sua atualização pode ser diferente das seguintes, pois, nesse caso, ele não possui um estado anterior na sessão;
- linha 13: salvamos o estado do menu, o que nos permitirá restaurá-lo automaticamente;
- linha 15: esse estado atual é salvo na sessão. Nela, os estados são agrupados por visualização/fragmento, sendo que cada um deles possui um estado. O número da visualização é fornecido pelo fragmento filho (linha 33);
- linha 17: observa-se que o fragmento foi salvo. Isso porque dois métodos podem chamar o método [saveState] e não faz sentido realizar dois salvamentos;
A regeneração da visualização associada ao fragmento é garantida pelo seguinte método:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// pai
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// a visualização deve ser restaurada
viewHasToBeInitialized = true;
}
No ciclo de vida, o método [onActivityCreated] é executado logo após o método [onCreateView]. A chamada desse último método indica que a visualização associada ao fragmento deve ser reconstruída. Basta observar 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 que ele se torne visível e fique aguardando as ações do usuário. Ela é realizada pelo código a seguir:
// 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) {
// registro
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// memória
this.menu = menu;
// recuperam-se as # opções do menu, caso isso ainda não tenha sido feito
if (fragmentHasToBeInitialized) {
// recupera-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 booleano hasBeenVisited tem significado)
previousState = session.getCoreState(getNumView());
// atualização do fragmento filho em várias etapas
// etapa 1 — é a 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 visualização 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: o método [onCreateOptionsMenu] é utilizado para atualizar o fragmento. Por esse motivo, o fragmento deve possuir um menu, vazio se necessário. Quando esse método é executado, o fragmento já foi associado à sua visualização e à sua atividade e, além disso, está visível;
- linha 25: armazena-se o menu que foi passado como parâmetro (linha 22) ao método;
- linhas 27-34: se o fragmento precisar ser inicializado:
- linha 29: os estados das opções do menu são inseridos na matriz [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 é armazenada. A mudança de tipo é necessária, pois o método [mainActivity.getSession()] retorna um tipo [ISession];
- linha 36: recupera-se da sessão o estado anterior do fragmento. Se for a primeira visita ao fragmento, apenas o booleano [previousState.hasBeenVisited] tem significado;
- linhas 39-44: código executado quando se trata da primeira visita ao fragmento. Nesse caso, seu estado anterior não é significativo;
- linhas 44-50: código executado quando não é a primeira visita ao fragmento;
- linhas 46-47: código executado se o construtor do fragmento tiver sido chamado (fragmentHasToBeInitialized==true);
- linhas 48-49: código executado se a visualização 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 andamento;
- 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 — é a primeira visita?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// inicialização do fragmento e da visualização
initFragment(null);
initView(null);
// zerar 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 esta é a primeira visita;
- linha 28: solicita-se à classe filha que inicialize a visualização 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 esta é a 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 — é a 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 visualização 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, ele é reinicializado chamando o método [initFragment] da classe filha (linhas 32, 46). Passa-se a ele o estado anterior do fragmento;
- linhas 35-51: se a visualização associada ao fragmento precisar ser inicializada ou reinicializada, solicita-se ao fragmento filho que faça isso (linhas 40, 48). Mais uma vez, passa-se a ele 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?
// registro
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 andamento
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 andamento, que pode ser uma das três seguintes:
- RESTORE: estamos restaurando o fragmento após uma rotação do dispositivo;
- NAVIGATION: retorna-se ao fragmento com o objetivo de encontrá-lo no estado em que foi deixado na última vez em que foi utilizado;
- SUBMIT: todos os outros casos;
- linha 34: recupera-se a ação em andamento;
- 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: queremos restaurar o fragmento ao seu último estado conhecido. A operação NAVIGATION pode ocorrer em uma primeira visita. Esse seria o caso, por exemplo, em um aplicativo com abas: se eu passar da aba 1 para a aba 4:
- preciso inicializar o fragmento da aba 4 se for a primeira visita;
- restaurar o fragmento da aba 4 ao seu estado anterior, caso não seja a primeira visita;
- linhas 52-54: não se faz nada se for a primeira visita. Caberá ao método filho [initView(CoreState previousState)] realizar 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 menu dele;
- linha 51: solicita-se que a classe filha seja atualizada chamando o método da linha 70. Passa-se a ela o estado anterior do fragmento para que ela possa realizar 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 ----------------------
// mudamos de visualização
session.setPreviousView(getNumView());
// não há mais nenhuma ação em andamento
session.setAction(ISession.Action.NONE);
// ao sair deste fragmento, ele deverá ser salvo
saveFragmentDone = false;
// enquanto o fragmento não for reconstruído, ele não precisa ser inicializado
fragmentHasToBeInitialized = false;
// enquanto a visualização não for reconstruída, ela não precisa ser inicializada
viewHasToBeInitialized = false;
// retornamos ao funcionamento normal da seleção de abas
session.setNavigationOnTabSelectionNeeded(true);
// é sinalizado ao fragmento que a visualização está pronta
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- linhas 18-30: ao chegar aqui, o fragmento já foi inicializado e está pronto para ser exibido. Em seguida, todos os indicadores utilizados no gerenciamento do ciclo de vida do fragmento são repostos em seu estado inicial;
- linha 20: mudamos de visualização: registramos isso na sessão;
- linha 22: não há mais nenhuma ação em andamento;
- linha 24: ao sair do fragmento agora exibido, será necessário salvá-lo ao sair;
- linha 26: o fragmento não precisa mais ser reconstruído. Esse indicador será redefinido para vrai quando o construtor do fragmento for executado novamente;
- linha 28: a visualização associada ao fragmento não precisa mais ser inicializada. Esse indicador será redefinido para vrai quando o método [onActivityCreated] for executado novamente;
- linha 30: o fragmento pode ser exibido em um aplicativo com abas. Nesse caso, quando o usuário clicar em uma delas, deve ocorrer uma troca de fragmento;
- linha 36: indica-se à classe filha que o fragmento está pronto. Ela pode incluir no método [notifyEndOfUpdates] as atualizações que devem ser feitas em todos os casos, iniciar uma operação assíncrona para obter novos dados, etc.
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 um aplicativo baseado nesse 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;
//-- protegido Atividade atividade;
// 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) {
// gerenciamento da espera pelo término 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 salvar o fragmento
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// senão houver nada para salvar, execute [return new CoreState();] e remova a classe [DummyFragmentState]
}
@Override
protected int getNumView() {
// é necessário retornar o número do fragmento na tabela de fragmentos gerenciados pela atividade (cf. MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// o fragmento fica visível e passou por uma construção nesta etapa ou em uma etapa anterior
// isso ocorre ao iniciar o aplicativo 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 registro do fragmento — vale null se for a primeira visita ao fragmento
}
@Override
protected void initView(CoreState previousState) {
// o fragmento se torna visível e a visualização associada foi reconstruída nesta etapa ou em uma etapa anterior
// isso ocorre sempre que [initFragment] é executado e sempre que o fragmento sai da adjacência do fragmento exibido
// é necessário inicializar os componentes da visualização que foi reconstruída
// previousState é o último salvamento do fragmento — vale null se for a primeira visita ao fragmento
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// é executado após [initFragment, initView] se esses métodos forem executados
// a visualização será exibida após uma operação do tipo SUBMIT
// geralmente é necessário inicializar o fragmento e a visualização associada a partir da sessão
// previousState é o último salvamento do fragmento — vale null se for a primeira visita ao fragmento
// não há nada a fazer se não for possível acessar o fragmento por meio de uma operação SUBMIT
// se for possível chegar ao fragmento por meio de operações SUBMIT a partir de fragmentos diferentes, é possível conhecer a visualização anterior por meio de [session.getPreviousView]
// se for possível chegar ao fragmento por meio de várias operações SUBMIT a partir do mesmo fragmento, então é necessário definir um indicador na sessão para diferenciar os diferentes tipos de SUBMIT a partir desse fragmento
}
@Override
protected void updateOnRestore(CoreState previousState) {
// é executada após [initFragment, initView], caso esses métodos sejam executados
// a visualização será exibida após uma operação do tipo RESTORE ou NAVIGATION
// previousState é o último salvamento do fragmento — nunca é nulo
// é necessário restaurar a visualização ao seu estado anterior
}
@Override
protected void notifyEndOfUpdates() {
// ocorre após os métodos [updateOnSubmit, updateOnRestore]
// neste ponto, a visualização já foi construída e inicializada
// geralmente não há nada a fazer aqui, mas também é possível agrupar aqui as ações que precisariam ser realizadas independentemente da forma como se chega a essa visualização
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// chamada quando as tarefas assíncronas iniciadas pelo fragmento são concluídas ou canceladas
// esses dois casos podem ser diferenciados graças ao parâmetro runningTasksHaveBeenCanceled
// geralmente, é necessário redefinir a visualização para um 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 lembrar o que é esperado nela:
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 demais, mas não fica claro para que eles poderiam servir
// não se esqueça dos getters/setters — eles são usados para serialização/desserialização
}
Para ilustrar o uso do projeto [client-android-skel], vamos primeiro utilizar exemplos simples antes de passarmos para um estudo de caso mais completo.
2.8. Exercícios ilustrativos
Começaremos refatorando exemplos já escritos.
2.8.1. Exemplo-17B
Retomamos o exemplo 17 estudado no parágrafo 1.18. Trata-se de um aplicativo com um único fragmento, sem tarefas assíncronas e sem abas. Vamos examiná-lo para ver como ele se comporta durante uma rotação do dispositivo. Fazemos as seguintes entradas:

Em seguida, em [1], giramos o dispositivo duas vezes. A nova visualização fica assim:

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], aparece uma caixa de diálogo mostrando os dados inseridos no formulário. Se, nesse momento, girarmos o dispositivo, a caixa de diálogo desaparece.
Portanto, durante uma rotação, precisaremos regenerar:
- a lista suspensa e seu item selecionado;
- a caixa de diálogo, caso estivesse exibida 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 visualização [vue1.xml] de [Exemple-17]. Essa é a visualização associada ao fragmento;
- em [6], a pasta [values] de [Exemple-17B] é substituída pela pasta [values] de [Exemple-17];
A margem superior da visualização [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 sinalizados provêm do imports, devido a pacotes que mudaram de local. Corrige-se esses erros (Ctrl-Shift-O). Outros erros decorrem do fato de que a vista [Vue1Fragment] não implementa todos os métodos exigidos por sua classe pai [AbstractParent]:

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

Corrige-se isso no arquivo [build.gradle] do módulo (linha 20 abaixo):
![]() |
Nesta fase, é possível recompilar para verificar os erros restantes. O único erro relatado está no método [Vue1Fragment.updateFragment]:
![]() |
É preciso remover a anotação [@Override] da linha 135. Agora não há mais erros. Vamos partir daqui para modificar o projeto.
2.8.1.2. O estado do fragmento [Vue1Fragment]
O fragmento [Vue1Fragment] precisa salvar informações durante a rotação do dispositivo para que possa ser restaurado completamente. Criamos uma classe [Vue1FragmentState] para isso:
![]() |
Por enquanto, essa 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 desenvolvedor.
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 visualização
void navigateToView(int position, ISession.Action action);
// gerenciamento de espera
void beginWaiting();
void cancelWaiting();
// constantes do aplicativo -------------------------------------
// 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 da solicitação 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 abas
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = false;
// número de fragmentos do aplicativo
int FRAGMENTS_COUNT = 1;
}
- linhas 24-31: o aplicativo não utiliza aqui sua camada [DAO]. Essas constantes não serão utilizadas;
- linha 34: uma adjacência de fragmentos igual a 1, que é o valor padrão. Como o aplicativo possui apenas um fragmento (linha 43), esse 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 um aplicativo com abas;
- linha 43: há 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
}
Ela está vazia. De fato, como há 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: precisamos incluir todas as classes derivadas de [CoreState] que armazenam o estado dos diferentes fragmentos. Aqui, há apenas uma (linha 12);
2.8.1.4. A atividade [MainActivity]
A atividade [MainActivity] está 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() {
// log
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: definir os fragmentos aqui
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// a fazer: definir os títulos dos fragmentos aqui
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// tarefa: navegação por abas — definir a visualização a ser exibida quando a aba nº [position] for selecionada
}
@Override
protected int getFirstView() {
// tarefa: definir o número da primeira visualização (fragmento) a ser exibida
return 0;
}
}
Os comentários [//todo] indicam o que o desenvolvedor 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() {
// log
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 a 44 deve ser alterado. Ele deve retornar a matriz de fragmentos do aplicativo. Na linha 43, não se esqueça de colocar o sublinhado após o nome do fragmento.
2.8.1.5. O estado do fragmento [FragmentState]
Após os testes de rotação realizados no projeto [Exemple-17], decidiu-se armazenar 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 ela 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 item selecionado na lista suspensa
private int listSelectedPosition;
// a mensagem exibida 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 é gerenciado 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 desses dois métodos será migrado para os métodos definidos pela classe [AbstractFragment] da seguinte maneira:
// gerenciamento 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) {
// Primeira visita?
if (previousState == null) {
// são criados 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 rolagem
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) {
// é exibida
showMessage();
}
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
- linhas 2-9: o método [saveFragment] deve colocar os elementos do fragmento a serem armazenados em uma classe derivada de [CoreState] e retornar a instância dessa classe;
- linhas 11-14: o método [getNumView] deve retornar o número do fragmento. Aqui, há apenas um fragmento, cujo número é 0;
- linhas 16-34: o método [initFragment] deve inicializar os campos do fragmento. Ele 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 for a primeira visita, os campos [list, message] do fragmento são restaurados a partir do estado anterior;
- linhas 33-34: inicialização do campo [dataAdapter] do fragmento. Essa é a fonte de dados da lista suspensa;
- linhas 37-62: o método [initView] serve para inicializar os componentes da interface visual. Ele recebe como parâmetro o estado anterior [previousState]. Se for [previousState==null], então trata-se da primeira visita;
- aqui encontramos o que havia 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 andamento é [SUBMIT]. Aqui, não há navegação entre fragmentos e, portanto, não há ação em andamento;
- linhas 69-81: o método [updateOnRestore] é executado quando a ação em andamento é [NAVIGATION] ou [RESTORE]. Aqui, não há navegação entre fragmentos e, portanto, não é possível realizar a ação [NAVIGATION];
- linha 72: recalcula-se (não se restaura) o valor de TextView seekBarValue. De fato, durante as rotações, esse valor às vezes era perdido;
- linhas 74-75: posiciona-se a lista no elemento que estava selecionado antes da rotação. Sem isso, a lista se posicionava no primeiro elemento;
- linhas 76-80: exibimos novamente a caixa de diálogo se a mensagem do estado anterior for diferente de 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 maneira:
// a mensagem da caixa de diálogo
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// lista de mensagens a serem exibidas
List<String> messages = new ArrayList<>();
...
// exibição
doAfficher(messages);
}
private void doAfficher(final List<String> messages) {
// construímos o texto a ser exibido
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// armazenamento da mensagem
message = texte.toString();
// exibição
showMessage();
}
private void showMessage() {
// exibição
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 usuário confirma 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 em uma única mensagem, que é armazenada na linha 2;
- linhas 25-33: é essa mensagem que a caixa de diálogo exibe e é essa mesma mensagem que o método [updateOnRestore] faz com que seja exibida;
- linha 27: o segundo parâmetro do método [setNeutralButton] é o método executado quando o usuário clica no botão [Fermer] da caixa de diálogo;
- linha 31: ao fechar a caixa de diálogo, a mensagem é repassada para null para indicar que a caixa de diálogo não está mais presente;
2.8.1.7. Tests
Sugere-se ao leitor que teste este projeto e verifique se o fragmento é mantido após uma ou mais rotações sucessivas.
2.8.2. Exemplo 23: cliente de previsão do tempo
Alguns sites disponibilizam informações meteorológicas na forma de sequências como jSON. Veja 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 do tempo se deseja, neste caso Angers;
- country: o país da cidade, neste caso a França (fr);
- APPID: uma chave obtida ao se cadastrar 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]. Ele apresenta as seguintes características:
- possui apenas um fragmento cujo estado não precisa ser mantido;
- ele realiza consultas assíncronas;
2.8.2.2. Personalização do projeto
![]() |
A interface [IMainActivity] permite especificar certas 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);
// gerenciamento da fila de espera
void beginWaiting();
void cancelWaiting();
// constantes do aplicativo -------------------------------------
// 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 da solicitação 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 abas
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = true;
// número de fragmentos do aplicativo
int FRAGMENTS_COUNT = 1;
}
- linhas 25, 28, 31, 40: características da camada [DAO]. Linha 31: não há necessidade de autenticação básica;
- linha 34: adjacência dos fragmentos. Aqui, essa constante não tem importância, pois há apenas um fragmento;
- linha 37: não se trata de um aplicativo com abas;
- linha 43: há 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, pois, neste aplicativo, há apenas um fragmento cujo estado não é mantido;
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
}
Ela está vazia, pois, nesta aplicação, não há comunicação entre fragmentos.
2.8.2.3. A camada [DAO]
![]() |
Na camada [DAO], três classes devem ser personalizadas:
- a interface IDao;
- sua implementação Dao;
- a interface WebClient para comunicação com o servidor web / jSON;
A interface [WebClient] será a seguinte:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// 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. Vale lembrar que esta está relacionada ao URL raiz (RestClientRootUrl, linha 12) do cliente. Aqui, esse URL raiz será [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);
// usuário
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);
}
- vale lembrar que os métodos das linhas 6 a 22 estão presentes por padrão na interface IDao do projeto [client-android-skel];
- linha 25: o método [getWeatherForecast] permite obter a sequência jSON com a previsão do tempo 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() {
// log
Log.d(className, "afterInject");
// construímos 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) {
// registra-se o usuário no interceptador
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));
}
// interceptador de autenticação?
if (isBasicAuthentificationNeeded) {
// adiciona-se o interceptador 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) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
}
// resultado
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- vale lembrar que as linhas 17 a 90 estão presentes por padrão na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos de implementação da interface [IDao], específicos para a aplicação (linha 92);
- linhas 93-105: implementação do método [getWeatherForecast]. Essa implementação é muito simples e ocupa 6 linhas, das linhas 100 à 105;
- linha 100: o método [getResponse] é um método da classe pai [AbstractDao]. Ele espera um parâmetro do tipo [IRequest<T>], em que T é o tipo da resposta esperada do servidor; neste caso, um String, já que se espera uma string jSON. O tipo T de [IRequest<T>] deve ser o tipo T do método [Observable<T> getWeatherForecast];
- a interface [IRequest<T>] possui apenas um método: getResponse. Este tem a função de fornecer a resposta do tipo T que o método [Observable<T> getWeatherForecast] deve retornar;
- linha 103: é a interface [WebClient] que fornece essa resposta. São passados a ela os três parâmetros recebidos na linha 94. Por esse motivo, esses parâmetros 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() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new MeteoFragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
// interface IDao ---------------------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(String city, String country, String APPID) {
return dao.getWeatherForecast(city, country, APPID);
}
}
- Lembre-se de que as linhas 15 a 55 estão presentes por padrão no projeto [client-android-skel]. Basta personalizá-las;
- linhas 37 a 40: a tabela de fragmentos. Há apenas um aqui;
- linhas 43-46: não são necessários títulos de fragmentos;
- linhas 48-50: não há guias aqui;
- linhas 52-55: a primeira visualização a ser exibida é a visualização 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 previsão do tempo. 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 visualização [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 visualização exibe apenas o texto da linha 10;
- linha 15: o menu [res / menu / menu_meteo.xml] é o seguinte:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionMeteo"
android:title="@string/actionMeteo"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
<item
android:id="@+id/actionTerminer"
android:title="@string/actionTerminer"/>
</menu>
</item>
</menu>
- linhas 10-12: essa opção do menu serve para consultar a previsão do tempo de uma cidade;
- linhas 14-15: esta opção do menu serve para cancelar essa consulta, caso ela esteja em andamento;
- linhas 16-18: esta opção do menu encerra o aplicativo;
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;
// gerenciamento de eventos ---------------------------------------------------------------------------------------
// cidades cuja previsão do tempo se deseja
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
@OptionsItem(R.id.actionMeteo)
protected void doMeteo() {
// seu país
String country = "fr";
// obtenha um identificador API criando 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 feitas em paralelo
for (String city : paysDeLoire) {
// previsão do tempo
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) {
// registro
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// início da espera
protected void beginWaiting(int numberOfRunningTasks) {
// registro
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// pai
beginRunningTasks(numberOfRunningTasks);
// é exibida 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: tratamento 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. Em seguida, ela é passada para a camada [DAO] por meio da atividade;
- linha 34: iniciamos a espera. Passamos o número de tarefas que serão iniciadas, para que a classe pai possa nos notificar quando elas forem concluídas. Aqui, há cinco tarefas, pois vamos solicitar a previsão do tempo 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 do tempo queremos obter;
- linha 40: vamos fazer 5 solicitações HTTP em paralelo;
- linha 40: solicitamos à classe pai [AbstractParent] que consulte o serviço web /jSON;
- linhas 40-48: o método [executeInBackground] espera dois parâmetros:
- linha 40: o processo a ser observado e executado é fornecido pelo método [mainActivity.getWeatherForecast];
- linhas 40-48: a instância [Action1] a ser executada ao 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. Ela é passada para o método [consumeResponse] da linha 53;
- linha 46: o contador de respostas recebidas é incrementado;
- linhas 53-56: processamento de uma resposta jSON do serviço meteorológico;
- linha 55: apenas se registra a string 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 serem executadas para a classe pai [AbstractParent]. É isso que permite que ela nos avise quando todas estiverem concluídas;
- linhas 67-70: preparação do menu para uma espera. Mantemos apenas a opção [Actions/Annuler], que permitirá ao usuário cancelar as tarefas iniciadas;
- linhas 74-92: código executado quando a classe pai nos avisa que todas as tarefas iniciadas foram concluídas;
- linha 77: o menu é restaurado ao seu estado inicial. O método [initMenu] (linhas 95-102) exibe 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] é processado pelo código a seguir:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancelamento de tarefas assíncronas
cancelRunningTasks();
}
- linha 7: solicita-se à classe pai que cancele as tarefas ainda ativas;
O clique na opção de menu [Terminer] é gerenciado pelo código a seguir:
@OptionsItem(R.id.actionTerminer)
protected void doTerminer() {
// paramos tudo
System.exit(0);
}
O gerenciamento do ciclo de vida do fragmento é feito 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) {
// Primeira 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 armazenar o estado do fragmento em uma classe derivada de [CoreState]. Se o fragmento não tiver um estado a ser armazenado, como neste caso, basta retornar uma instância de [CoreState]. Não se deve retornar null, pois isso causaria uma falha posteriormente;
- linhas 8-11: devem retornar o número da vista. Aqui, o fragmento [MeteoFragment] tem o número 0;
- linhas 13-16: servem para inicializar o fragmento depois que ele for construído (previousState==null) ou reconstruído (previousState!=null). Aqui, não há nada a ser feito. O único campo passível de inicialização é o seguinte:
// cidades cuja previsão do tempo se deseja
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
mas ele se inicializa sozinho;
- linhas 18-24: servem para inicializar a visualização associada ao fragmento assim que ela for construída (previousState==null) ou reconstruída (previousState!=null);
- linhas 21-23: se for a primeira visita ao fragmento, inicializa-se seu menu para ocultar a opção [Annuler];
- linhas 27-30: chamadas se, para chegar ao fragmento, houve navegação com uma ação do tipo [SUBMIT]. Aqui, não há navegação entre fragmentos, pois há apenas um fragmento;
- linhas 32-35: chamadas durante um ciclo de salvamento/restauração devido a uma rotação do dispositivo ou outro motivo. Aqui, como não foi salvo nenhum estado, não há nada a ser feito;
- linhas 37-40: chamadas quando todas as atualizações anteriores foram realizadas. Aqui, não há nada a ser feito;
2.8.2.6. Tests
Agora vamos executar o exemplo:


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

Os logs ficam então assim:
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. Ele, 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 logs ficam 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: solicitação de cancelamento;
- linha 4: a espera é cancelada porque ocorreu um cancelamento;
- linhas 6-10: o cancelamento das tarefas gera uma exceção em cada uma das threads das cinco tarefas. O tipo de exceção depende dos aplicativos. A exceção aqui é [java.lang.InterruptedException] porque as tarefas foram interrompidas enquanto executavam a instrução [Thread.sleep(delay)], que as faz aguardar artificialmente [delay] milissegundos;
2.8.3. Exemplo 16B
Reestruturamos aqui o exemplo 16 do parágrafo 1.17. Ele apresenta um fragmento que faz chamadas assíncronas a um servidor de números aleatórios. Vejamos como ele se comporta durante uma rotação do dispositivo:

- em [1], giramos o dispositivo duas vezes;

Percebe-se que todas as mensagens de erro foram perdidas. Vamos tentar melhorar isso.
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 arquivo [res/layout/vue1.xml], a pasta [res/values]:
![]() |
Alteraremos a margem superior da visualização [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 relacionado a imports. Algumas classes mudaram de pacote na migração para [Exemple-16B]. Começamos corrigindo esse tipo de erro;
- um segundo tipo de erro é sinalizado na classe [Vue1Fragment] porque ela não implementa os métodos exigidos pela classe pai [AbstractParent]. Fazemos uma 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 sofrerá mais alterações;
2.8.3.2. Criação de um relatório para o fragmento [Vue1Fragment]
Vimos que algumas informações do fragmento precisariam ser salvas durante uma rotação, a fim de restaurar o fragmento como estava antes da rotação. Portanto, 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 certas 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);
// gerenciamento da fila de espera
void beginWaiting();
void cancelWaiting();
// constantes do aplicativo -------------------------------------
// 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 da solicitação 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 abas
boolean ARE_TABS_NEEDED = false;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = true;
// número de fragmentos do aplicativo
int FRAGMENTS_COUNT = 1;
}
- linhas 25, 28, 31, 40: características da camada [DAO]. Não há necessidade de autenticação básica;
- linha 34: adjacência dos fragmentos. Aqui, essa constante não tem importância, pois há apenas um fragmento;
- linha 37: não se trata de um aplicativo com abas;
- linha 43: há 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
}
Ela está vazia, pois, nesta aplicação, não há comunicação entre fragmentos.
2.8.3.4. A camada [DAO]
![]() |
Na camada [DAO], três classes devem ser personalizadas:
- a interface IDao;
- sua implementação Dao;
- a interface WebClient para 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
// status 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. Vale lembrar que esta está relacionada ao URL raiz (RestClientRootUrl, linha 12) do cliente. Aqui, esse 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);
// usuário
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);
}
- vale lembrar que os métodos das linhas 6 a 22 estão presentes por padrã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]. Esse número é obtido em uma resposta do tipo [Response<Integer>], na qual o número aleatório está 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() {
// registro
Log.d(className, "afterInject");
// fabricação do 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) {
// registra-se o usuário no interceptador
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));
}
// interceptador de autenticação?
if (isBasicAuthentificationNeeded) {
// adiciona-se o interceptador 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);
}
});
}
}
- vale lembrar que as linhas 17 a 85 estão presentes por padrão na classe [Dao] do projeto [client-android-skel]. Basta adicionar os métodos de implementação da interface [IDao];
- linhas 88 a 97: implementação do método [getAlea]. Essa implementação é muito simples e ocupa 6 linhas, das linhas 91 a 96;
- linha 91: o método [getResponse] é um método da classe pai [AbstractDao]. Ele 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>] possui apenas um método: getResponse. Esse método tem a função de fornecer a resposta do tipo T que deve ser retornada pelo método [Observable<T> getAlea];
- linha 94: é a interface [WebClient] que fornece essa resposta. São passados a ela os dois parâmetros recebidos na linha 89. Por esse motivo, esses parâmetros 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() {
// log
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 os títulos dos fragmentos aqui
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// navegação por abas — definir a visualização a ser exibida
}
@Override
protected int getFirstView() {
return 0;
}
// interface IDao ------------------------------------------
@Override
public Observable<Response<Integer>> getAlea(int a, int b) {
return dao.getAlea(a, b);
}
}
- Lembre-se de que as linhas 15 a 61 estão presentes por padrão no projeto [client-android-skel]. Basta personalizá-las;
- linhas 40 a 44: a tabela de fragmentos. Há apenas um aqui;
- linhas 47-51: não são necessários títulos de fragmentos;
- linhas 53-56: não há guias aqui;
- linhas 58-61: a primeira visualização a ser exibida é a visualização 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 visualização ------------------------
// mensagem de erro sobre o número de números aleatórios solicitados
private boolean txtErrorAleasVisible = false;
// mensagem de erro sobre o intervalo [a,b] de geração
private boolean txtErrorIntervalleVisible = false;
// mensagem de erro sobre o URL do serviço web
private boolean txtMsgErreurUrlServiceWebVisible = false;
// mensagem de erro sobre o tempo de espera
private boolean textViewErreurDelayVisible = false;
// status visível ou não do botão Executar
private boolean btnExecuterVisible = true;
// getters e setters
...
}
Para determinar o que precisava ser armazenado no fragmento, realizamos rotações do dispositivo em diversas situações e observamos o que havia desaparecido durante a restauração. Chegamos à conclusão de que era necessário armazenar as informações das linhas 10 a 23.
2.8.3.7. O fragmento [Vue1Fragment]
![]() |
Atualmente, a visualização [Vue1Fragment] apresenta diversos erros devido ao fato de que a classe pai [AbstractFragment], da qual ela deriva, foi alterada. Em vez de descrever uma a uma as alterações a serem feitas, 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, lembramos que todo fragmento deve ter um menu, mesmo que vazio. É o caso aqui.
2.8.3.7.1. Gerenciamento do clique no botão [Exécuter]
@Click(R.id.btn_Executer)
protected void doExecuter() {
// verifica-se os dados inseridos
if (!isPageValid()) {
return;
}
// as respostas anteriores são apagadas
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);
// solicita-se os números aleatórios
getAleasInBackground(nbAleas, a, b);
}
void getAleasInBackground(int nbAleas, int a, int b) {
// cria-se o processo a ser observado
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getAlea(a, b));
}
// solicitando os 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) {
// registro
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();
// retorno à interface do usuário
return;
}
// adiciona-se a informação à lista de respostas
reponses.add(0, String.valueOf(response.getBody()));
// atualização das respostas
dataAdapterReponses.notifyDataSetChanged();
}
// cancelamento ----------
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancela-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 as entradas são válidas. Podem então aparecer mensagens de erro;
- linhas 8-9: a lista de respostas é esvaziada. Essa alteração é refletida no ListView, que as exibe;
- linhas 11-12: o número de respostas recebidas é zerado;
- linha 14: define-se o URL do serviço de números aleatórios. Essa informação será transmitida à camada [DAO];
- linha 15: define-se o tempo de espera antes de enviar a solicitação ao serviço de números aleatórios. Essa informação será transmitida à camada [DAO];
- linha 17: prepara-se o lançamento de 1 tarefa assíncrona (e não N, veremos o motivo);
- linhas 24-27: das N tarefas assíncronas, transformamos cada uma em uma sequência de operações [merge];
- linhas 29-36: solicita-se à classe pai [AbstractParent] que consulte 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 ser observado e executado é aquele que foi calculado nas linhas anteriores;
- linhas 29-36: a instância [Action1] a ser executada ao 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), ela é processada no método da linha 39;
- linhas 49-50: registra-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 esse 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: o método das linhas 68-75 é chamado. Ele cancelará as tarefas ainda ativas (linha 74);
- linha 62: a resposta é adicionada à lista de respostas, que é a fonte de dados do ListView;
- linha 64: o ListView é atualizado;
- linhas 77-83: o método [beginWaiting(int nbRunningTasks)] prepara a visualização para a espera (linhas 81-82) e informa à 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 é garantido 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 visualização
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) {
// primeira 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úmero de respostas
nbReponses = reponses.size();
}
@Override
protected void initView(CoreState previousState) {
// link entre a lista de visualização e o adaptador
listReponses.setAdapter(dataAdapterReponses);
// Primeira visita?
if (previousState == null) {
// ocultamos as 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 visualização
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 salvamento do fragmento quando a classe pai solicita que isso seja feito;
- linha 11: exibe a mensagem de erro sobre o tempo de espera;
- linha 12: exibe a mensagem de erro sobre o número de números aleatórios solicitados;
- linha 13: exibe a mensagem de erro sobre o URL do serviço web / jSON;
- linha 14: exibição 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 retornar o número da visualização. O número do fragmento aqui é 0, pois há apenas um;
- linhas 25-38: inicialização dos campos do fragmento, seja na primeira visita (previousState==null), seja em uma 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 visualização associada ao fragmento, seja na primeira visita (previousState==null), seja em uma visita posterior;
- linha 43: associa-se o ListView do fragmento à fonte de dados que acaba de ser construída no método [initFragment];
- linhas 45-54: se for a primeira visita, prepara-se a visualização para sua primeira exibição;
- linhas 57-60: executadas durante uma navegação entre fragmentos associada a uma ação do tipo [SUBMIT]. Aqui, há apenas um fragmento e, portanto, 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 salvamento/restauração devido a uma rotação do dispositivo ou outro motivo. Aqui, apenas este último caso pode ocorrer. É preciso 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 visualização;
- linhas 78-81: chamadas quando todas as atualizações anteriores foram realizadas. Aqui, não há nada a ser feito;
- linhas 83-89: executadas quando todas as tarefas assíncronas estiverem concluídas. Aqui, oculta-se o botão [Annuler] para substituí-lo pelo botão [Exécuter];
2.8.3.8. Os testes
O leitor é convidado a realizar os seguintes testes:
- criar erros e acionar o dispositivo: as mensagens de erro devem permanecer exibidas;
- obter números aleatórios e executar o dispositivo: os números aleatórios obtidos devem permanecer exibidos;
- definir uma espera de vários segundos e executar o dispositivo durante a espera: as tarefas devem ter sido canceladas (isso pode ser verificado nos logs);
2.8.4. Exemplo-22B
Retomamos aqui o exemplo 22 para refatorá-lo de acordo com o modelo do projeto [client-android-skel]. Vale lembrar que o projeto [Exemple-22] gerencia corretamente o ciclo de salvamento/restauração dos fragmentos durante uma rotação e que foi ele que serviu de base para o projeto [client-android-skel].
Duplicamos o projeto [client-android-skel] em [exemples/Exemple-22B] e carregamos este último projeto:
![]() |
Em seguida, copiamos diversos elementos do projeto [Exemple-22] para o projeto [Exemple-22B].
Primeiramente, 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 visualizações 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, copiaremos os elementos [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
Nesta fase, podemos tentar uma primeira compilação. Surge um primeiro tipo de erros: os imports incorretos, pois as classes mudaram de pacote. Corrigimos esses imports. Um segundo tipo de erro se deve ao fato de que os fragmentos não implementam todos os métodos de sua classe pai [AbstractFragment]. Corrigimos pressionando (Alt+Enter).
Os erros restantes decorrem das diferenças existentes entre a classe antiga e a nova [AbstractFragment]. Por enquanto, eles são ignorados.
2.8.4.1. Personalização do projeto
![]() |
Na pasta [custom] encontram-se os elementos de arquitetura personalizáveis pelo desenvolvedor.
A interface [IMainActivity] permite especificar certas 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);
// gerenciamento da fila de 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 da solicitação 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 abas
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]. Não há nenhuma aqui;
- linha 41: há aqui cinco fragmentos;
- linha 32: adjacência dos fragmentos. Essa constante pode assumir aqui um valor em [1,4]. Recomenda-se ao leitor que altere esse valor para verificar se o aplicativo continua funcionando;
- linha 35: trata-se de um aplicativo com abas;
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 possui estado;
A classe [Session] é a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// dados a serem compartilhados 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/desserialização em jSON
// número de fragmentos visitados
private int numVisit;
// Número do fragmento do tipo [PlaceholderFragment] exibido na segunda aba
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;
// gerenciamento 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 dois motivos:
- há abas para gerenciar;
- há um menu a ser gerenciado;
2.8.4.2.1. Implementação dos métodos da classe pai
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// log
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 matriz 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 para o 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 salvamento/restauração. Quando esse método é chamado, a classe pai já restaurou a sessão;
- linha 10: recupera-se uma referência local da sessão. A mudança de tipo se deve ao fato de que a sessão da classe pai é do tipo [AbstractSession];
- linhas 19-38: o método [getFragments] deve devolver à classe pai a matriz dos fragmentos gerenciados pela aplicação. Aqui, há [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 exibir os títulos dos fragmentos quando essa informação for útil. Esse não é o caso aqui;
- linhas 47-50: esse método é chamado pela classe pai quando o usuário clica em uma aba. Voltaremos a isso no parágrafo seguinte;
- linhas 52-55: retorna o número da primeira visualização a ser exibida quando o aplicativo é iniciado. Aqui, é o fragmento [Vue1Fragment] que deve ser exibido primeiro. O método [getFirstView] poderia ser substituído, com vantagem, por uma constante em [IMainActivity];
2.8.4.2.2. Gerenciamento de abas
As abas são gerenciadas pelos seguintes métodos:
@Override
protected void onCreateActivity() {
// log
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ª aba?
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 ser exibido
int numFragment;
switch (position) {
case 0:
// nº do fragmento [Vue1Fragment]
numFragment = getFirstView();
break;
default:
// Número do fragmento [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// exibiçã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 salvamento/restauração. Quando esse método é chamado, a classe pai já restaurou a sessão;
- linha 9: recupera-se uma referência local da sessão. A mudança de tipo se deve ao fato de que a sessão da classe pai é do tipo [AbstractSession];
- linhas 11-13: cria-se a primeira aba;
- linhas 15-20: cria-se a segunda aba se um número de fragmento estiver registrado na sessão (linha 15). Esse número é inicialmente igual a -1 na primeira construção da atividade;
- linhas 23-39: este método é chamado pela classe pai quando o usuário clica em uma aba;
- linhas 28-31: se a aba 0 for clicada, deve-se exibir [Vue1Fragment]. Sabe-se que essa é a primeira visualização exibida ao iniciar o aplicativo;
- linhas 32-35: se a aba 1 for clicada, deve-se exibir o fragmento cujo número está registrado na sessão;
- linhas 37-39: navegamos até o fragmento escolhido. A ação associada é [SUBMIT]. Seria possível que fosse [NAVIGATION]? Neste documento, utiliza-se [NAVIGATION] apenas quando a exibição do novo fragmento requer apenas o conhecimento de seu estado anterior. Aqui, esse não é o caso, pois a exibição do fragmento deve mudar em relação ao seu estado anterior para mostrar mais uma visita;
2.8.4.2.3. Gerenciamento do menu
A atividade está associada ao seguinte menu [menu_main.xml]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
que exibe o seguinte:
![]() |
A gestão do menu é realizada pelos seguintes métodos:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// registro
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 ao selecionar uma aba por meio do software
session.setNavigationOnTabSelectionNeeded(false);
// recriamos as duas abas devido a um problema com a fonte 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 ser exibido é definido na sessão
session.setNumFragment(i);
// seleciona-se a aba nº 2 com navegação
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- linhas 16-31: gerenciamento do clique em uma opção de menu do tipo [Fragmenti];
- linhas 37-50: exibem o fragmento nº i (trata-se dos fragmentos do tipo PlaceHolderFragment) na aba nº 1 (2ª aba);
- linhas 42-44: decide-se excluir as abas existentes para recriar duas novas. Essa decisão foi tomada para contornar o seguinte problema: quando se limita a exibir o fragmento na aba 1 existente (sem, portanto, excluí-la), curiosamente seu título apresenta uma aparência (fonte, tamanho) diferente daquela do título da aba 0;
- linhas 43-44: as duas abas 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 abas, o que chamará o gerenciador [onTabSelected]. Se nada for feito, haverá navegação para um fragmento. Isso é evitado definindo a variável booleana [navigationOnTabSelectionNeeded] como faux na sessão. Essa variável booleana é automaticamente redefinida como vrai pela classe [AbstractFragment] quando um fragmento se torna visível;
- linha 46: registra-se o número do fragmento a ser exibido na sessão;
- linhas 48-50: seleciona-se a aba nº 2 com navegação (linha 48). Isso acionará o procedimento [onTabSelected], que irá:
- exibir o fragmento cujo número foi inserido na sessão;
- armazenar na sessão o número da aba selecionada;
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;
// gerenciador de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// exibe-se o nome digitado
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
}
// salvar estado do fragmento
@Override
public CoreState saveFragment() {
// estado da visualização — nada a salvar
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) {
// primeira visita?
if (previousState == null) {
// exibe o número da visita
showNumVisit();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// exibe-se o número 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úmero 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 precisa salvar seu estado. O fragmento [Vue1Fragment] não tem estado a ser salvo. Simplesmente retorna-se uma instância da classe base [CoreState] (lembrete: não se deve retornar null);
- linhas 41-44: devem retornar o número do fragmento. O fragmento [Vue1Fragment] tem, por definição, o número [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 ele é exibido (linhas 85-92);
- linhas 61-65: chamadas quando o fragmento for exibido associado a uma ação [SUBMIT]. Incrementa-se o número de visita e ele é exibido. Aqui, não é possível que o número de visita seja incrementado duas vezes no ciclo de vida. De fato, a primeira visita ao fragmento [Vue1Fragment] ocorre no início da aplicação, quando a ação é [NONE] por definição na sessão. Isso garante que o método [updateOnSubmit] não será chamado. Depois disso, nunca mais será a primeira visita e o método [initView] não fará nada;
- linhas 68-71: chamadas em um ciclo de salvamento/restauração. Como o fragmento não possui estado, não há nada a ser restaurado aqui;
- linhas 73-76: chamadas quando todas as atualizações anteriores tiverem sido realizadas. 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 salvar o estado do fragmento, será salvo o texto que ele 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 exibido
// incremento do número de visitas
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texto modificado
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
// registro
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 exibido
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 que o fragmento salve seu estado, salva-se o texto exibido pelo fragmento (linha 34);
- linhas 38-41: retornam o número do fragmento. Esse número depende do número da seção que foi passado como argumento durante sua criação;
- linhas 43-47: chamadas na 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), exibido na primeira visita, é recalculado a cada vez. Isso é questionável. Teria sido possível optar por incluir essa informação também no estado do fragmento;
- linhas 49-51: chamadas na primeira construção da visualização associada ao fragmento (previousState==null) ou nas seguintes (previousState!=null). Não há nada a ser feito;
- linhas 53-56: chamadas quando o fragmento for exibido associado a uma ação [SUBMIT]. Isso sempre ocorre, exceto no ciclo de salvamento/restauração, em que a ação é [RESTORE]. Portanto, incrementamos o número da visita e o exibimos;
- linhas 68-74: chamadas em um ciclo de salvamento/restauração. Restaura-se o texto que havia sido salvo no estado do fragmento;
- linhas 76-79: chamadas quando todas as atualizações anteriores foram realizadas. 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
Recomenda-se ao leitor que teste o aplicativo girando o dispositivo para verificar se o fragmento exibido não perde seu estado. Também será necessário verificar os logs.
2.9. Conclusion
Ao final deste capítulo, dispomos de um projeto modelo [client-android-skel] de cliente Android que se comunica com um serviço web / jSON com as seguintes características:
- a comunicação assíncrona com o servidor web / jSON é feita por meio da biblioteca RxJava;
- o ciclo de vida de um fragmento (atualização, salvamento, restauração) é gerenciado por sua classe pai [AbstractFragment], que chama, em momentos específicos, determinados métodos de suas classes filhas. Assim, o fragmento filho não precisa se preocupar com as etapas do ciclo de vida, mas apenas implementar determinados métodos exigidos por sua classe pai;
- o ciclo de vida da atividade (salvar/restaurar) é gerenciado por uma classe abstrata [AbstractActivity], que também exige que a atividade filha implemente determinados métodos;
- a classe [AbstractActivity] é capaz de gerenciar um aplicativo com ou sem abas, com ou sem imagem de espera, com ou sem autenticação básica no servidor web / jSON. A presença ou ausência desses elementos é definida por configuração;
Apresentaremos agora um estudo de caso mais complexo do que os exemplos anteriores. A nova aplicação será baseada no projeto modelo [client-android-skel].



















































